WIP: IMEventModel end-to-end chain
[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 creditsSnapshot
79  * @param agreementsSnapshot
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 lastChangeReason
85  *          The [[gr.grnet.aquarium.user.UserStateChangeReason]] for which the usr state has changed.
86  * @param totalEventsProcessedCounter
87  * @param parentUserStateId
88  *          The `ID` of the parent state. The parent state is the one used as a reference point in order to calculate
89  *          this user state.
90  * @param _id
91  *          The unique `ID` given by the store.
92  *
93  * @author Christos KK Loverdos <loverdos@gmail.com>
94  */
95 case class UserState(
96     isInitial: Boolean,
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     imStateSnapshot: IMStateSnapshot,
159     creditsSnapshot: CreditSnapshot,
160     agreementsSnapshot: AgreementSnapshot,
161     ownedResourcesSnapshot: OwnedResourcesSnapshot,
162     newWalletEntries: List[NewWalletEntry],
163     // The last known change reason for this userState
164     lastChangeReason: UserStateChangeReason = NoSpecificChangeReason,
165     totalEventsProcessedCounter: Long = 0L,
166     // The user state we used to compute this one. Normally the (cached)
167     // state at the beginning of the billing period.
168     parentUserStateId: Option[String] = None,
169     _id: ObjectId = new ObjectId()
170 ) extends JsonSupport {
171
172   private[this] def _allSnapshots: List[Long] = {
173     List(
174       imStateSnapshot.snapshotTime,
175       creditsSnapshot.snapshotTime, agreementsSnapshot.snapshotTime,
176       ownedResourcesSnapshot.snapshotTime,
177       implicitlyIssuedSnapshot.snapshotTime,
178       latestResourceEventsSnapshot.snapshotTime
179     )
180   }
181
182   def oldestSnapshotTime: Long = _allSnapshots min
183
184   def newestSnapshotTime: Long  = _allSnapshots max
185
186   def idOpt: Option[String] = _id match {
187     case null ⇒ None
188     case _id  ⇒ Some(_id.toString)
189   }
190
191 //  def userCreationDate = new Date(userCreationMillis)
192 //
193 //  def userCreationFormatedDate = new MutableDateCalc(userCreationMillis).toString
194
195   def maybeDSLAgreement(at: Long): Maybe[DSLAgreement] = {
196     agreementsSnapshot match {
197       case snapshot @ AgreementSnapshot(data, _) ⇒
198         snapshot.getAgreement(at)
199       case _ ⇒
200        Failed(new AquariumException("No agreement snapshot found for user %s".format(userID)))
201     }
202   }
203
204   def findResourceInstanceSnapshot(resource: String, instanceId: String): Maybe[ResourceInstanceSnapshot] = {
205     ownedResourcesSnapshot.findResourceInstanceSnapshot(resource, instanceId)
206   }
207
208   def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
209     ownedResourcesSnapshot.getResourceInstanceAmount(resource, instanceId, defaultValue)
210   }
211
212   def copyForResourcesSnapshotUpdate(resource: String,   // resource name
213                                      instanceId: String, // resource instance id
214                                      newAmount: Double,
215                                      snapshotTime: Long): UserState = {
216
217     val (newResources, _, _) = ownedResourcesSnapshot.computeResourcesSnapshotUpdate(resource, instanceId, newAmount, snapshotTime)
218
219     this.copy(
220       ownedResourcesSnapshot = newResources,
221       stateChangeCounter = this.stateChangeCounter + 1)
222   }
223   
224   def copyForChangeReason(changeReason: UserStateChangeReason) = {
225     this.copy(lastChangeReason = changeReason)
226   }
227
228   def resourcesMap = ownedResourcesSnapshot.toResourcesMap
229
230   def modifyFromIMEvent(imEvent: IMEventModel, snapshotMillis: Long): UserState = {
231     this.copy(
232       isInitial = false,
233       imStateSnapshot = IMStateSnapshot(imEvent, snapshotMillis),
234       lastChangeReason = IMEventArrival(imEvent)
235     )
236   }
237
238 //  def toShortString = "UserState(%s, %s, %s, %s, %s)".format(
239 //    userId,
240 //    _id,
241 //    parentUserStateId,
242 //    totalEventsProcessedCounter,
243 //    calculationReason)
244 }
245
246
247 object UserState {
248   def fromJson(json: String): UserState = {
249     StdConverters.AllConverters.convertEx[UserState](JsonTextFormat(json))
250   }
251
252   object JsonNames {
253     final val _id = "_id"
254     final val userID = "userID"
255   }
256 }
257
258 final class BillingMonthInfo private(val year: Int,
259                                      val month: Int,
260                                      val startMillis: Long,
261                                      val stopMillis: Long) extends Ordered[BillingMonthInfo] {
262
263   def previousMonth: BillingMonthInfo = {
264     BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goPreviousMonth)
265   }
266
267   def nextMonth: BillingMonthInfo = {
268     BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goNextMonth)
269   }
270
271
272   def compare(that: BillingMonthInfo) = {
273     val ds = this.startMillis - that.startMillis
274     if(ds < 0) -1 else if(ds == 0) 0 else 1
275   }
276
277
278   override def equals(any: Any) = any match {
279     case that: BillingMonthInfo ⇒
280       this.year == that.year && this.month == that.month // normally everything else MUST be the same by construction
281     case _ ⇒
282       false
283   }
284
285   override def hashCode() = {
286     31 * year + month
287   }
288
289   override def toString = "%s-%02d".format(year, month)
290 }
291
292 object BillingMonthInfo {
293   def fromMillis(millis: Long): BillingMonthInfo = {
294     fromDateCalc(new MutableDateCalc(millis))
295   }
296
297   def fromDateCalc(mdc: MutableDateCalc): BillingMonthInfo = {
298     val year = mdc.getYear
299     val month = mdc.getMonthOfYear
300     val startMillis = mdc.goStartOfThisMonth.getMillis
301     val stopMillis  = mdc.goEndOfThisMonth.getMillis // no need to `copy` here, since we are discarding `mdc`
302
303     new BillingMonthInfo(year, month, startMillis, stopMillis)
304   }
305 }
306
307 sealed trait UserStateChangeReason {
308   /**
309    * Return `true` if the result of the calculation should be stored back to the
310    * [[gr.grnet.aquarium.store.UserStateStore]].
311    *
312    */
313   def shouldStoreUserState: Boolean
314
315   def shouldStoreCalculatedWalletEntries: Boolean
316
317   def forPreviousBillingMonth: UserStateChangeReason
318
319   def calculateCreditsForImplicitlyTerminated: Boolean
320
321   def code: UserStateChangeReasonCodes.ChangeReasonCode
322 }
323
324 object UserStateChangeReasonCodes {
325   type ChangeReasonCode = Int
326
327   final val InitialSetupCode       = 1
328   final val NoSpecificChangeCode   = 2
329   final val MonthlyBillingCode     = 3
330   final val RealtimeBillingCode    = 4
331   final val IMEventArrivalCode     = 5
332 }
333
334 case object InitialUserStateSetup extends UserStateChangeReason {
335   def shouldStoreUserState = true
336
337   def shouldStoreCalculatedWalletEntries = false
338
339   def forPreviousBillingMonth = this
340
341   def calculateCreditsForImplicitlyTerminated = false
342
343   def code = UserStateChangeReasonCodes.InitialSetupCode
344 }
345 /**
346  * A calculation made for no specific reason. Can be for testing, for example.
347  *
348  */
349 case object NoSpecificChangeReason extends UserStateChangeReason {
350   def shouldStoreUserState = false
351
352   def shouldStoreCalculatedWalletEntries = false
353
354   def forBillingMonthInfo(bmi: BillingMonthInfo) = this
355
356   def forPreviousBillingMonth = this
357
358   def calculateCreditsForImplicitlyTerminated = false
359
360   def code = UserStateChangeReasonCodes.NoSpecificChangeCode
361 }
362
363 /**
364  * An authoritative calculation for the billing period.
365  *
366  * This marks a state for caching.
367  *
368  * @param billingMonthInfo
369  */
370 case class MonthlyBillingCalculation(billingMonthInfo: BillingMonthInfo) extends UserStateChangeReason {
371   def shouldStoreUserState = true
372
373   def shouldStoreCalculatedWalletEntries = true
374
375   def forPreviousBillingMonth = MonthlyBillingCalculation(billingMonthInfo.previousMonth)
376
377   def calculateCreditsForImplicitlyTerminated = true
378
379   def code = UserStateChangeReasonCodes.MonthlyBillingCode
380 }
381
382 /**
383  * Used for the realtime billing calculation.
384  *
385  * @param forWhenMillis The time this calculation is for
386  */
387 case class RealtimeBillingCalculation(forWhenMillis: Long) extends UserStateChangeReason {
388   def shouldStoreUserState = false
389
390   def shouldStoreCalculatedWalletEntries = false
391
392   def forPreviousBillingMonth = this
393
394   def calculateCreditsForImplicitlyTerminated = false
395
396   def code = UserStateChangeReasonCodes.RealtimeBillingCode
397 }
398
399 case class IMEventArrival(imEvent: IMEventModel) extends UserStateChangeReason {
400   def shouldStoreUserState = true
401
402   def shouldStoreCalculatedWalletEntries = false
403
404   def forPreviousBillingMonth = this
405
406   def calculateCreditsForImplicitlyTerminated = false
407
408   def code = UserStateChangeReasonCodes.IMEventArrivalCode
409 }