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