Add one more calculation reason
[aquarium] / src / main / scala / gr / grnet / aquarium / user / UserState.scala
1 /*
2  * Copyright 2011 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.logic.events.WalletEntry
43 import gr.grnet.aquarium.util.date.MutableDateCalc
44 import gr.grnet.aquarium.store.RecordID
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  * @author Christos KK Loverdos <loverdos@gmail.com>
58  */
59
60 case class UserState(
61     userId: String,
62
63     /**
64      * When the user was created in the system (not Aquarium). We use this as a basis for billing periods. Set to
65      * zero if unknown.
66      * 
67      */
68     userCreationMillis: Long,
69
70     /**
71      * Each time the user state is updated, this must be increased.
72      * The counter is used when accessing user state from the cache (user state store)
73      * in order to get the latest value for a particular billing period.
74      */
75     stateChangeCounter: Long,
76
77     /**
78      * True iff this user state refers to a full billing period, that is a full billing month.
79      */
80     isFullBillingMonthState: Boolean,
81
82     /**
83      * The full billing period for which this user state refers to.
84      * This is set when the user state refers to a full billing period (= month)
85      * and is used to cache the user state for subsequent queries.
86      */
87     theFullBillingMonth: BillingMonthInfo,
88
89     /**
90      * If this is a state for a full billing month, then keep here the implicit OFF
91      * resource events or any other whose cost policy demands an implicit event at the end of the billing period.
92      *
93      * The use case is this: A VM may have been started (ON state) before the end of the billing period
94      * and ended (OFF state) after the beginning of the next billing period. In order to bill this, we must assume
95      * an implicit OFF even right at the end of the billing period and an implicit ON event with the beginning of the
96      * next billing period.
97      */
98     implicitlyTerminatedSnapshot: ImplicitlyIssuedResourceEventsSnapshot,
99
100     /**
101      * So far computed wallet entries for the current billing month.
102      */
103     billingMonthWalletEntries: List[WalletEntry],
104
105     /**
106      * Wallet entries that were computed for out of sync events.
107      * (for the current billing month ??)
108      */
109     outOfSyncWalletEntries: List[WalletEntry],
110
111     /**
112      * The latest resource events per resource instance
113      */
114     latestResourceEventsSnapshot: LatestResourceEventsSnapshot,
115
116     /**
117      * Counts the total number of resource events used to produce this user state for
118      * the billing period recorded by `billingPeriodSnapshot`
119      */
120     billingPeriodResourceEventsCounter: Long,
121
122     /**
123      * The out of sync events used to produce this user state for
124      * the billing period recorded by `billingPeriodSnapshot`
125      */
126     billingPeriodOutOfSyncResourceEventsCounter: Long,
127
128     activeStateSnapshot: ActiveStateSnapshot,
129     creditsSnapshot: CreditSnapshot,
130     agreementsSnapshot: AgreementSnapshot,
131     rolesSnapshot: RolesSnapshot,
132     ownedResourcesSnapshot: OwnedResourcesSnapshot,
133     calculationReason: UserStateCalculationReason = NoSpecificCalculationReason,
134     totalEventsProcessedCounter: Long = 0L,
135     // The user state we used to compute this one. Normally the (cached)
136     // state at the beginning of the billing period.
137     parentUserStateId: Option[String] = None,
138     _id: String = ""
139 ) extends JsonSupport {
140
141   private[this] def _allSnapshots: List[Long] = {
142     List(
143       activeStateSnapshot.snapshotTime,
144       creditsSnapshot.snapshotTime, agreementsSnapshot.snapshotTime, rolesSnapshot.snapshotTime,
145       ownedResourcesSnapshot.snapshotTime,
146       implicitlyTerminatedSnapshot.snapshotTime,
147       latestResourceEventsSnapshot.snapshotTime
148     )
149   }
150
151   def oldestSnapshotTime: Long = _allSnapshots min
152
153   def newestSnapshotTime: Long  = _allSnapshots max
154
155   def idOpt: Option[String] = _id match {
156     case null ⇒ None
157     case ""   ⇒ None
158     case _id  ⇒ Some(_id)
159   }
160
161 //  def userCreationDate = new Date(userCreationMillis)
162 //
163 //  def userCreationFormatedDate = new MutableDateCalc(userCreationMillis).toString
164
165   def maybeDSLAgreement(at: Long): Maybe[DSLAgreement] = {
166     agreementsSnapshot match {
167       case snapshot @ AgreementSnapshot(data, _) ⇒
168         snapshot.getAgreement(at)
169       case _ ⇒
170        Failed(new Exception("No agreement snapshot found for user %s".format(userId)))
171     }
172   }
173
174   def findResourceInstanceSnapshot(resource: String, instanceId: String): Maybe[ResourceInstanceSnapshot] = {
175     ownedResourcesSnapshot.findResourceInstanceSnapshot(resource, instanceId)
176   }
177
178   def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
179     ownedResourcesSnapshot.getResourceInstanceAmount(resource, instanceId, defaultValue)
180   }
181
182   def copyForResourcesSnapshotUpdate(resource: String,   // resource name
183                                      instanceId: String, // resource instance id
184                                      newAmount: Double,
185                                      snapshotTime: Long): UserState = {
186
187     val (newResources, _, _) = ownedResourcesSnapshot.computeResourcesSnapshotUpdate(resource, instanceId, newAmount, snapshotTime)
188
189     this.copy(
190       ownedResourcesSnapshot = newResources,
191       stateChangeCounter = this.stateChangeCounter + 1)
192   }
193
194   def resourcesMap = ownedResourcesSnapshot.toResourcesMap
195   
196   def safeCredits = creditsSnapshot match {
197     case c @ CreditSnapshot(_, _) ⇒ c
198     case _ ⇒ CreditSnapshot(0.0, 0)
199   }
200 }
201
202
203 object UserState {
204   def fromJson(json: String): UserState = {
205     JsonHelpers.jsonToObject[UserState](json)
206   }
207
208   def fromJValue(jsonAST: JsonAST.JValue): UserState = {
209     JsonHelpers.jValueToObject[UserState](jsonAST)
210   }
211
212   def fromBytes(bytes: Array[Byte]): UserState = {
213     JsonHelpers.jsonBytesToObject[UserState](bytes)
214   }
215
216   def fromXml(xml: String): UserState = {
217     fromJValue(Xml.toJson(scala.xml.XML.loadString(xml)))
218   }
219
220   object JsonNames {
221     final val _id = "_id"
222     final val userId = "userId"
223   }
224 }
225
226 final class BillingMonthInfo private(val year: Int,
227                                      val month: Int,
228                                      val startMillis: Long,
229                                      val stopMillis: Long) extends Ordered[BillingMonthInfo] {
230
231   def previousMonth: BillingMonthInfo = {
232     BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goPreviousMonth)
233   }
234
235   def nextMonth: BillingMonthInfo = {
236     BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goNextMonth)
237   }
238
239
240   def compare(that: BillingMonthInfo) = {
241     val ds = this.startMillis - that.startMillis
242     if(ds < 0) -1 else if(ds == 0) 0 else 1
243   }
244
245
246   override def equals(any: Any) = any match {
247     case that: BillingMonthInfo ⇒
248       this.year == that.year && this.month == that.month // normally everything else MUST be the same by construction
249     case _ ⇒
250       false
251   }
252
253   override def hashCode() = {
254     31 * year + month
255   }
256
257   override def toString = "%s-%02d".format(year, month)
258 }
259
260 object BillingMonthInfo {
261   def fromMillis(millis: Long): BillingMonthInfo = {
262     fromDateCalc(new MutableDateCalc(millis))
263   }
264
265   def fromDateCalc(mdc: MutableDateCalc): BillingMonthInfo = {
266     val year = mdc.getYear
267     val month = mdc.getMonthOfYear
268     val startMillis = mdc.goStartOfThisMonth.getMillis
269     val stopMillis  = mdc.goEndOfThisMonth.getMillis // no need to `copy` here, since we are discarding `mdc`
270
271     new BillingMonthInfo(year, month, startMillis, stopMillis)
272   }
273 }
274
275 sealed trait UserStateCalculationReason {
276   /**
277    * Return `true` if the result of the calculation should be stored back to the
278    * [[gr.grnet.aquarium.store.UserStateStore]].
279    *
280    */
281   def shouldStoreUserState: Boolean
282
283   def forPreviousBillingMonth: UserStateCalculationReason
284 }
285
286 case object InitialUserStateCalculation extends UserStateCalculationReason {
287   def shouldStoreUserState = true
288
289   def forPreviousBillingMonth = this
290 }
291 /**
292  * A calculation made for no specific reason. Can be for testing, for example.
293  *
294  */
295 case object NoSpecificCalculationReason extends UserStateCalculationReason {
296   def shouldStoreUserState = false
297
298   def forBillingMonthInfo(bmi: BillingMonthInfo) = this
299
300   def forPreviousBillingMonth = this
301 }
302
303 /**
304  * An authoritative calculation for the billing period.
305  *
306  * This marks a state for caching.
307  *
308  * @param billingMonthInfo
309  */
310 case class MonthlyBillingCalculation(billingMonthInfo: BillingMonthInfo) extends UserStateCalculationReason {
311   def shouldStoreUserState = true
312
313   def forPreviousBillingMonth = MonthlyBillingCalculation(billingMonthInfo.previousMonth)
314 }
315
316 /**
317  * Any calculation.
318  *
319  * @param forWhenMillis The time this calculation is for
320  */
321 case class GenericBillingCalculation(forWhenMillis: Long) extends UserStateCalculationReason {
322   def shouldStoreUserState = false
323
324   def forPreviousBillingMonth = this
325 }