Merge branch 'master' into 1852_billing_period_calc
[aquarium] / src / main / scala / gr / grnet / aquarium / user / UserStateComputations.scala
1 /*
2  * Copyright 2011 GRNET S.A. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or
5  * without modification, are permitted provided that the following
6  * conditions are met:
7  *
8  *   1. Redistributions of source code must retain the above
9  *      copyright notice, this list of conditions and the following
10  *      disclaimer.
11  *
12  *   2. Redistributions in binary form must reproduce the above
13  *      copyright notice, this list of conditions and the following
14  *      disclaimer in the documentation and/or other materials
15  *      provided with the distribution.
16  *
17  * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24  * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25  * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28  * POSSIBILITY OF SUCH DAMAGE.
29  *
30  * The views and conclusions contained in the software and
31  * documentation are those of the authors and should not be
32  * interpreted as representing official policies, either expressed
33  * or implied, of GRNET S.A.
34  */
35
36 package gr.grnet.aquarium.user
37
38 import scala.collection.mutable
39
40 import gr.grnet.aquarium.store.ResourceEventStore
41 import com.ckkloverdos.maybe.{Failed, NoVal, Just, Maybe}
42 import gr.grnet.aquarium.logic.accounting.Accounting
43 import gr.grnet.aquarium.logic.events.ResourceEvent
44 import gr.grnet.aquarium.logic.accounting.dsl.{DSLCostPolicy, DSLPolicy, DSLAgreement}
45 import gr.grnet.aquarium.util.date.{TimeHelpers, DateCalculator}
46
47 sealed abstract class CalculationType(_name: String) {
48   def name = _name
49 }
50
51 /**
52  * Normal calculations that are part of the bill generation procedure
53  */
54 case object PeriodicCalculation extends CalculationType("periodic")
55
56 /**
57  * Adhoc calculations, e.g. when computing the state in realtime.
58  */
59 case object AdhocCalculation extends CalculationType("adhoc")
60
61 trait UserPolicyFinder {
62   def findUserPolicyAt(userId: String, whenMillis: Long): DSLPolicy
63 }
64
65 trait FullStateFinder {
66   def findFullState(userId: String, whenMillis: Long): Any
67 }
68
69 trait UserStateCache {
70   def findUserStateAtEndOfPeriod(userId: String, year: Int, month: Int): Maybe[UserState]
71
72   /**
73    * Find the most up-to-date user state for the particular billing period.
74    */
75   def findLatestUserStateForBillingMonth(userId: String, yearOfBillingMonth: Int, billingMonth: Int): Maybe[UserState]
76 }
77
78 /**
79  *
80  * @author Christos KK Loverdos <loverdos@gmail.com>
81  */
82 class UserStateComputations {
83   def createFirstUserState(userId: String, agreementName: String) = {
84     val now = 0L
85     UserState(
86       userId,
87       now,
88       0L,
89       false,
90       null,
91       0L,
92       ActiveSuspendedSnapshot(false, now),
93       CreditSnapshot(0, now),
94       AgreementSnapshot(Agreement(agreementName, now, now) :: Nil, now),
95       RolesSnapshot(List(), now),
96       PaymentOrdersSnapshot(Nil, now),
97       OwnedGroupsSnapshot(Nil, now),
98       GroupMembershipsSnapshot(Nil, now),
99       OwnedResourcesSnapshot(List(), now)
100     )
101   }
102
103   /**
104    * Get the user state as computed up to (and not including) the start of the new billing period.
105    *
106    * Always compute, taking into account any "out of sync" resource events
107    */
108   def computeUserStateAtStartOfBillingPeriod(billingYear: Int,
109                                              billingMonth: Int,
110                                              knownUserState: UserState,
111                                              accounting: Accounting): UserState = {
112
113     val billingDate = new DateCalculator(billingYear, billingMonth, 1)
114     val billingDateMillis = billingDate.toMillis
115
116 //    if(billingDateMillis < knownUserState.startDateMillis) {
117 //      val userId = knownUserState.userId
118 //      val agreementName = knownUserState.agreement match {
119 //        case null      ⇒ "default"
120 //        case agreement ⇒ agreement.data
121 //      }
122 //      createFirstUserState(userId, agreementName)
123 //    } else {
124       // We really need to compute the user state here
125
126       // get all events that
127       // FIXME: Implement
128       knownUserState
129 //    }
130   }
131
132   /**
133    * Find the previous resource event, if needed by the event's cost policy,
134    * in order to use it for any credit calculations.
135    */
136   def findPreviousRCEventOf(previousRCEventsMap: mutable.Map[ResourceEvent.FullResourceType, ResourceEvent],
137                             rcEvent: ResourceEvent,
138                             costPolicy: DSLCostPolicy): Maybe[ResourceEvent] = {
139
140     if(costPolicy.needsPreviousEventForCreditCalculation) {
141       // Get a previous resource only if this is needed by the policy
142       previousRCEventsMap.get(rcEvent.fullResourceInfo) match {
143         case Some(previousRCEvent) ⇒
144           Just(previousRCEvent)
145         case None ⇒
146           queryForPreviousRCEvent(rcEvent)
147       }
148     } else {
149       // No need for previous event. Will return NoVal
150       NoVal
151     }
152   }
153
154   /**
155    * FIXME: implement
156    */
157   def queryForPreviousRCEvent(rcEvent: ResourceEvent): Maybe[ResourceEvent] = {
158     NoVal
159   }
160
161   def updatePreviousRCEventWith(previousRCEventsMap: mutable.Map[ResourceEvent.FullResourceType, ResourceEvent],
162                                 newRCEvent: ResourceEvent): Unit = {
163     previousRCEventsMap(newRCEvent.fullResourceInfo) = newRCEvent
164   }
165
166   /**
167    * Do a full month billing.
168    *
169    * Takes into account "out of sync events".
170    * 
171    */
172   def computeFullMonthlyBilling(yearOfBillingMonth: Int,
173                                 billingMonth: Int,
174                                 userId: String,
175                                 policyFinder: UserPolicyFinder,
176                                 fullStateFinder: FullStateFinder,
177                                 userStateCache: UserStateCache,
178                                 timeUnitInMillis: Long,
179                                 rcEventStore: ResourceEventStore,
180                                 currentUserState: UserState,
181                                 otherStuff: Traversable[Any],
182                                 defaultPolicy: DSLPolicy, // Policy.policy
183                                 accounting: Accounting): Maybe[UserState] = Maybe {
184
185     val billingMonthStartDate = new DateCalculator(yearOfBillingMonth, billingMonth, 1)
186     val prevBillingMonthStartDate = billingMonthStartDate.previousMonth
187     val yearOfPrevBillingMonth = prevBillingMonthStartDate.year
188     val prevBillingMonth = prevBillingMonthStartDate.monthOfYear
189
190     // Check if this value is already cached and valid, otherwise compute the value
191     // TODO : cache it in case of new computation
192     val cachedStartUserStateM = userStateCache.findLatestUserStateForBillingMonth(
193       userId,
194       yearOfPrevBillingMonth,
195       prevBillingMonth)
196
197     val (previousStartUserState, newStartUserState) = cachedStartUserStateM match {
198       case Just(cachedStartUserState) ⇒
199         // So, we do have a cached user state but must check if this is still valid
200
201         // Check how many resource events were used to produce this user state
202         val cachedHowmanyRCEvents = cachedStartUserState.resourceEventsCounter
203
204         // Ask resource event store to see if we had any "out of sync" events for the particular (== previous)
205         // billing period.
206         val prevHowmanyOutOfSyncRCEvents = rcEventStore.countOutOfSyncEventsForBillingMonth(
207           userId,
208           yearOfPrevBillingMonth,
209           prevBillingMonth)
210         
211         val recomputedStartUserState = if(prevHowmanyOutOfSyncRCEvents == 0) {
212           // This is good, there were no "out of sync" resource events, so we can use the cached value
213           cachedStartUserState
214         } else {
215           // Oops, there are "out of sync" resource event. Must compute (potentially recursively)
216           computeUserStateAtStartOfBillingPeriod(
217             yearOfPrevBillingMonth,
218             prevBillingMonth,
219             cachedStartUserState,
220             accounting)
221         }
222
223         (cachedStartUserState, recomputedStartUserState)
224       case NoVal ⇒
225         // We do not even have a cached value, so computate one!
226         val recomputedStartUserState = computeUserStateAtStartOfBillingPeriod(
227           yearOfPrevBillingMonth,
228           prevBillingMonth,
229           currentUserState,
230           accounting)
231
232         (recomputedStartUserState, recomputedStartUserState)
233       case Failed(e, m) ⇒
234         throw new Exception(m, e)
235     }
236
237     // OK. Now that we have a user state to start with (= start of billing period reference point),
238     // let us deal with the events themselves.
239     val billingStartMillis = billingMonthStartDate.toMillis
240     val billingStopMillis = billingMonthStartDate.endOfThisMonth.toMillis
241     val allBillingPeriodRelevantRCEvents = rcEventStore.findAllRelevantResourceEventsForBillingPeriod(userId, billingStartMillis, billingStopMillis)
242
243     type FullResourceType = ResourceEvent.FullResourceType
244     val previousRCEventsMap = mutable.Map[FullResourceType, ResourceEvent]()
245     val impliedRCEventsMap  = mutable.Map[FullResourceType, ResourceEvent]() // those which do not exists but are
246     // implied in order to do billing calculations (e.g. the "off" vmtime resource event)
247     var workingUserState = newStartUserState
248
249     for(newRCEvent <- allBillingPeriodRelevantRCEvents) {
250       // We need to do these kinds of calculations:
251       // 1. Credit state calculations
252       // 2. Resource state calculations
253
254       // How credits are computed:
255       // - "onoff" events (think "vmtime"):
256       //   - need to be considered in on/off pairs
257       //   - just use the time difference of this event to the previous one for the credit computation
258       // - "discrete" events (think "bandwidth"):
259       //   - just use their value, which is a difference already for the credit computation
260       // - "continuous" events (think "bandwidth"):
261       //   - need the previous absolute value
262       //   - need the time difference of this event to the previous one
263       //   - use both the above (previous absolute value, time difference) for the credit computation
264       //
265       // BUT ALL THE ABOVE SHOULD NOT BE CONSIDERED HERE; RATHER THEY ARE POLYMORPHIC BEHAVIOURS
266
267       // We need:
268       // A. The previous event
269
270
271       // The DSLCostPolicy for the resource does not change, so it is safe to use the default DSLPolicy to obtain it.
272       val costPolicyM = newRCEvent.findCostPolicy(defaultPolicy)
273       costPolicyM match {
274         case Just(costPolicy) ⇒
275           val previousRCEventM = findPreviousRCEventOf(previousRCEventsMap, newRCEvent, costPolicy)
276           val previousRCEventValueM = previousRCEventM.map(_.value)
277 //          val previousRCInstanceAmount = workingUserState.ownedResources.
278
279           // 1. Update resource state
280           val newRCInstanceAmountM = costPolicy.computeNewResourceInstanceAmount(previousRCEventValueM, newRCEvent.value)
281           newRCInstanceAmountM match {
282             case Just(newRCInstanceAmount) ⇒
283               workingUserState.ownedResources.addOrUpdateResourceSnapshot(
284                 newRCEvent.resource,
285                 newRCEvent.instanceId,
286                 newRCInstanceAmount,
287                 TimeHelpers.nowMillis)
288             case NoVal ⇒
289               () // ERROR
290             case failed @ Failed(_, _) ⇒
291               () // ERROR
292           }
293
294           // 2. Update credit state
295
296           // 3. Calc wallet entries
297
298         case NoVal ⇒
299           () // ERROR
300         case failed @ Failed(_, _) ⇒
301           () // ERROR
302       }
303
304
305       updatePreviousRCEventWith(previousRCEventsMap, newRCEvent)
306     } // for(newResourceEvent <- allBillingPeriodRelevantRCEvents)
307
308
309     null
310   }
311
312
313   /**
314   * Runs the billing algorithm on the specified period.
315   * By default, a billing period is monthly.
316   * The start of the billing period is midnight of the first day of the month we compute the bill for.
317   *
318   */
319    def doPartialMonthlyBilling(startBillingYear: Int,
320                                startBillingMonth: Int,
321                                stopBillingMillis: Long,
322                                userId: String,
323                                policyFinder: UserPolicyFinder,
324                                fullStateFinder: FullStateFinder,
325                                userStateFinder: UserStateCache,
326                                timeUnitInMillis: Long,
327                                rcEventStore: ResourceEventStore,
328                                currentUserState: UserState,
329                                otherStuff: Traversable[Any],
330                                accounting: Accounting): Maybe[UserState] = Maybe {
331   
332
333      null.asInstanceOf[UserState]
334    }
335 }
336
337 object DefaultUserStateComputations extends UserStateComputations