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