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