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