WIP: Remodeling UserState store mechanics
[aquarium] / src / main / scala / gr / grnet / aquarium / user / UserState.scala
1 /*
2  * Copyright 2011-2012 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 gr.grnet.aquarium.util.json.JsonSupport
39 import gr.grnet.aquarium.logic.accounting.dsl.DSLAgreement
40 import com.ckkloverdos.maybe.{Failed, Maybe}
41 import gr.grnet.aquarium.util.date.MutableDateCalc
42 import gr.grnet.aquarium.event.{NewWalletEntry, WalletEntry}
43 import gr.grnet.aquarium.converter.{JsonTextFormat, StdConverters}
44 import gr.grnet.aquarium.AquariumException
45 import gr.grnet.aquarium.event.im.IMEventModel
46 import org.bson.types.ObjectId
47
48
49 /**
50  * A comprehensive representation of the User's state.
51  *
52  * Note that it is made of autonomous parts that are actually data snapshots.
53  *
54  * The different snapshots need not agree on the snapshot time, ie. some state
55  * part may be stale, while other may be fresh.
56  *
57  * The user state is meant to be partially updated according to relevant events landing on Aquarium.
58  *
59  * @define communicatedByIM
60  *          This is communicated to Aquarium from the `IM` system.
61  *
62  *
63  * @param userID
64  *          The user ID. $communicatedByIM
65  * @param userCreationMillis
66  *          When the user was created.
67  *          $communicatedByIM
68  *          Set to zero if unknown.
69  * @param stateChangeCounter
70  * @param isFullBillingMonthState
71  * @param theFullBillingMonth
72  * @param implicitlyIssuedSnapshot
73  * @param billingMonthWalletEntries
74  * @param outOfSyncWalletEntries
75  * @param latestResourceEventsSnapshot
76  * @param billingPeriodResourceEventsCounter
77  * @param billingPeriodOutOfSyncResourceEventsCounter
78  * @param activeStateSnapshot
79  * @param creditsSnapshot
80  * @param agreementsSnapshot
81  * @param rolesSnapshot
82  * @param ownedResourcesSnapshot
83  * @param newWalletEntries
84  *          The wallet entries computed. Not all user states need to holds wallet entries,
85  *          only those that refer to billing periods (end of billing period).
86  * @param lastChangeReasonCode
87  *          The code for the `lastChangeReason`.
88  * @param lastChangeReason
89  *          The [[gr.grnet.aquarium.user.UserStateChangeReason]] for which the usr state has changed.
90  * @param totalEventsProcessedCounter
91  * @param parentUserStateId
92  *          The `ID` of the parent state. The parent state is the one used as a reference point in order to calculate
93  *          this user state.
94  * @param _id
95  *          The unique `ID` given by the store.
96  *
97  * @author Christos KK Loverdos <loverdos@gmail.com>
98  */
99 case class UserState(
100     userID: String,
101
102     userCreationMillis: Long,
103
104     /**
105      * Each time the user state is updated, this must be increased.
106      * The counter is used when accessing user state from the cache (user state store)
107      * in order to get the latest value for a particular billing period.
108      */
109     stateChangeCounter: Long,
110
111     /**
112      * True iff this user state refers to a full billing period, that is a full billing month.
113      */
114     isFullBillingMonthState: Boolean,
115
116     /**
117      * The full billing period for which this user state refers to.
118      * This is set when the user state refers to a full billing period (= month)
119      * and is used to cache the user state for subsequent queries.
120      */
121     theFullBillingMonth: BillingMonthInfo,
122
123     /**
124      * If this is a state for a full billing month, then keep here the implicit OFF
125      * resource events or any other whose cost policy demands an implicit event at the end of the billing period.
126      *
127      * The use case is this: A VM may have been started (ON state) before the end of the billing period
128      * and ended (OFF state) after the beginning of the next billing period. In order to bill this, we must assume
129      * an implicit OFF even right at the end of the billing period and an implicit ON event with the beginning of the
130      * next billing period.
131      */
132     implicitlyIssuedSnapshot: ImplicitlyIssuedResourceEventsSnapshot,
133
134     /**
135      * So far computed wallet entries for the current billing month.
136      */
137     billingMonthWalletEntries: List[WalletEntry],
138
139     /**
140      * Wallet entries that were computed for out of sync events.
141      * (for the current billing month ??)
142      */
143     outOfSyncWalletEntries: List[WalletEntry],
144
145     /**
146      * The latest (previous) resource events per resource instance.
147      */
148     latestResourceEventsSnapshot: LatestResourceEventsSnapshot,
149
150     /**
151      * Counts the total number of resource events used to produce this user state for
152      * the billing period recorded by `billingPeriodSnapshot`
153      */
154     billingPeriodResourceEventsCounter: Long,
155
156     /**
157      * The out of sync events used to produce this user state for
158      * the billing period recorded by `billingPeriodSnapshot`
159      */
160     billingPeriodOutOfSyncResourceEventsCounter: Long,
161
162     activeStateSnapshot: ActiveStateSnapshot,
163     creditsSnapshot: CreditSnapshot,
164     agreementsSnapshot: AgreementSnapshot,
165     rolesSnapshot: RolesSnapshot,
166     ownedResourcesSnapshot: OwnedResourcesSnapshot,
167     newWalletEntries: List[NewWalletEntry],
168     lastChangeReasonCode: UserStateChangeReasonCodes.ChangeReasonCode,
169     // The last known change reason for this userState
170     lastChangeReason: UserStateChangeReason = NoSpecificChangeReason,
171     totalEventsProcessedCounter: Long = 0L,
172     // The user state we used to compute this one. Normally the (cached)
173     // state at the beginning of the billing period.
174     parentUserStateId: Option[String] = None,
175     _id: ObjectId = new ObjectId()
176 ) extends JsonSupport {
177
178   private[this] def _allSnapshots: List[Long] = {
179     List(
180       activeStateSnapshot.snapshotTime,
181       creditsSnapshot.snapshotTime, agreementsSnapshot.snapshotTime, rolesSnapshot.snapshotTime,
182       ownedResourcesSnapshot.snapshotTime,
183       implicitlyIssuedSnapshot.snapshotTime,
184       latestResourceEventsSnapshot.snapshotTime
185     )
186   }
187
188   def oldestSnapshotTime: Long = _allSnapshots min
189
190   def newestSnapshotTime: Long  = _allSnapshots max
191
192   def idOpt: Option[String] = _id match {
193     case null ⇒ None
194     case _id  ⇒ Some(_id.toString)
195   }
196
197 //  def userCreationDate = new Date(userCreationMillis)
198 //
199 //  def userCreationFormatedDate = new MutableDateCalc(userCreationMillis).toString
200
201   def maybeDSLAgreement(at: Long): Maybe[DSLAgreement] = {
202     agreementsSnapshot match {
203       case snapshot @ AgreementSnapshot(data, _) ⇒
204         snapshot.getAgreement(at)
205       case _ ⇒
206        Failed(new AquariumException("No agreement snapshot found for user %s".format(userID)))
207     }
208   }
209
210   def findResourceInstanceSnapshot(resource: String, instanceId: String): Maybe[ResourceInstanceSnapshot] = {
211     ownedResourcesSnapshot.findResourceInstanceSnapshot(resource, instanceId)
212   }
213
214   def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
215     ownedResourcesSnapshot.getResourceInstanceAmount(resource, instanceId, defaultValue)
216   }
217
218   def copyForResourcesSnapshotUpdate(resource: String,   // resource name
219                                      instanceId: String, // resource instance id
220                                      newAmount: Double,
221                                      snapshotTime: Long): UserState = {
222
223     val (newResources, _, _) = ownedResourcesSnapshot.computeResourcesSnapshotUpdate(resource, instanceId, newAmount, snapshotTime)
224
225     this.copy(
226       ownedResourcesSnapshot = newResources,
227       stateChangeCounter = this.stateChangeCounter + 1)
228   }
229   
230   def copyForChangeReason(changeReason: UserStateChangeReason) = {
231     this.copy(lastChangeReasonCode = changeReason.code, lastChangeReason = changeReason)
232   }
233
234   def resourcesMap = ownedResourcesSnapshot.toResourcesMap
235
236 //  def toShortString = "UserState(%s, %s, %s, %s, %s)".format(
237 //    userId,
238 //    _id,
239 //    parentUserStateId,
240 //    totalEventsProcessedCounter,
241 //    calculationReason)
242 }
243
244
245 object UserState {
246   def fromJson(json: String): UserState = {
247     StdConverters.AllConverters.convertEx[UserState](JsonTextFormat(json))
248   }
249
250   object JsonNames {
251     final val _id = "_id"
252     final val userID = "userID"
253   }
254 }
255
256 final class BillingMonthInfo private(val year: Int,
257                                      val month: Int,
258                                      val startMillis: Long,
259                                      val stopMillis: Long) extends Ordered[BillingMonthInfo] {
260
261   def previousMonth: BillingMonthInfo = {
262     BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goPreviousMonth)
263   }
264
265   def nextMonth: BillingMonthInfo = {
266     BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goNextMonth)
267   }
268
269
270   def compare(that: BillingMonthInfo) = {
271     val ds = this.startMillis - that.startMillis
272     if(ds < 0) -1 else if(ds == 0) 0 else 1
273   }
274
275
276   override def equals(any: Any) = any match {
277     case that: BillingMonthInfo ⇒
278       this.year == that.year && this.month == that.month // normally everything else MUST be the same by construction
279     case _ ⇒
280       false
281   }
282
283   override def hashCode() = {
284     31 * year + month
285   }
286
287   override def toString = "%s-%02d".format(year, month)
288 }
289
290 object BillingMonthInfo {
291   def fromMillis(millis: Long): BillingMonthInfo = {
292     fromDateCalc(new MutableDateCalc(millis))
293   }
294
295   def fromDateCalc(mdc: MutableDateCalc): BillingMonthInfo = {
296     val year = mdc.getYear
297     val month = mdc.getMonthOfYear
298     val startMillis = mdc.goStartOfThisMonth.getMillis
299     val stopMillis  = mdc.goEndOfThisMonth.getMillis // no need to `copy` here, since we are discarding `mdc`
300
301     new BillingMonthInfo(year, month, startMillis, stopMillis)
302   }
303 }
304
305 sealed trait UserStateChangeReason {
306   /**
307    * Return `true` if the result of the calculation should be stored back to the
308    * [[gr.grnet.aquarium.store.UserStateStore]].
309    *
310    */
311   def shouldStoreUserState: Boolean
312
313   def shouldStoreCalculatedWalletEntries: Boolean
314
315   def forPreviousBillingMonth: UserStateChangeReason
316
317   def calculateCreditsForImplicitlyTerminated: Boolean
318
319   def code: UserStateChangeReasonCodes.ChangeReasonCode
320 }
321
322 object UserStateChangeReasonCodes {
323   type ChangeReasonCode = Int
324
325   final val InitialCalculationCode = 1
326   final val NoSpecificChangeCode   = 2
327   final val MonthlyBillingCode     = 3
328   final val RealtimeBillingCode    = 4
329   final val IMEventArrivalCode   = 5
330 }
331
332 case object InitialUserStateCalculation extends UserStateChangeReason {
333   def shouldStoreUserState = true
334
335   def shouldStoreCalculatedWalletEntries = false
336
337   def forPreviousBillingMonth = this
338
339   def calculateCreditsForImplicitlyTerminated = false
340
341   def code = UserStateChangeReasonCodes.InitialCalculationCode
342 }
343 /**
344  * A calculation made for no specific reason. Can be for testing, for example.
345  *
346  */
347 case object NoSpecificChangeReason extends UserStateChangeReason {
348   def shouldStoreUserState = false
349
350   def shouldStoreCalculatedWalletEntries = false
351
352   def forBillingMonthInfo(bmi: BillingMonthInfo) = this
353
354   def forPreviousBillingMonth = this
355
356   def calculateCreditsForImplicitlyTerminated = false
357
358   def code = UserStateChangeReasonCodes.NoSpecificChangeCode
359 }
360
361 /**
362  * An authoritative calculation for the billing period.
363  *
364  * This marks a state for caching.
365  *
366  * @param billingMonthInfo
367  */
368 case class MonthlyBillingCalculation(billingMonthInfo: BillingMonthInfo) extends UserStateChangeReason {
369   def shouldStoreUserState = true
370
371   def shouldStoreCalculatedWalletEntries = true
372
373   def forPreviousBillingMonth = MonthlyBillingCalculation(billingMonthInfo.previousMonth)
374
375   def calculateCreditsForImplicitlyTerminated = true
376
377   def code = UserStateChangeReasonCodes.MonthlyBillingCode
378 }
379
380 /**
381  * Used for the realtime billing calculation.
382  *
383  * @param forWhenMillis The time this calculation is for
384  */
385 case class RealtimeBillingCalculation(forWhenMillis: Long) extends UserStateChangeReason {
386   def shouldStoreUserState = false
387
388   def shouldStoreCalculatedWalletEntries = false
389
390   def forPreviousBillingMonth = this
391
392   def calculateCreditsForImplicitlyTerminated = false
393
394   def code = UserStateChangeReasonCodes.RealtimeBillingCode
395 }
396
397 case class IMEventArrival(imEvent: IMEventModel) extends UserStateChangeReason {
398   def shouldStoreUserState = true
399
400   def shouldStoreCalculatedWalletEntries = false
401
402   def forPreviousBillingMonth = this
403
404   def calculateCreditsForImplicitlyTerminated = false
405
406   def code = UserStateChangeReasonCodes.IMEventArrivalCode
407 }