2 * Copyright 2011-2012 GRNET S.A. All rights reserved.
4 * Redistribution and use in source and binary forms, with or
5 * without modification, are permitted provided that the following
8 * 1. Redistributions of source code must retain the above
9 * copyright notice, this list of conditions and the following
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.
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.
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.
36 package gr.grnet.aquarium.user
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
50 * A comprehensive representation of the User's state.
52 * Note that it is made of autonomous parts that are actually data snapshots.
54 * The different snapshots need not agree on the snapshot time, ie. some state
55 * part may be stale, while other may be fresh.
57 * The user state is meant to be partially updated according to relevant events landing on Aquarium.
59 * @define communicatedByIM
60 * This is communicated to Aquarium from the `IM` system.
64 * The user ID. $communicatedByIM
65 * @param userCreationMillis
66 * When the user was created.
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 activeStateSnapshot
79 * @param creditsSnapshot
80 * @param agreementsSnapshot
81 * @param rolesSnapshot
82 * @param ownedResourcesSnapshot
83 * @param newWalletEntries
84 * The wallet entries computed. Not all user states need to holds wallet entries,
85 * only those that refer to billing periods (end of billing period).
86 * @param lastChangeReasonCode
87 * The code for the `lastChangeReason`.
88 * @param lastChangeReason
89 * The [[gr.grnet.aquarium.user.UserStateChangeReason]] for which the usr state has changed.
90 * @param totalEventsProcessedCounter
91 * @param parentUserStateId
92 * The `ID` of the parent state. The parent state is the one used as a reference point in order to calculate
95 * The unique `ID` given by the store.
97 * @author Christos KK Loverdos <loverdos@gmail.com>
102 userCreationMillis: Long,
105 * Each time the user state is updated, this must be increased.
106 * The counter is used when accessing user state from the cache (user state store)
107 * in order to get the latest value for a particular billing period.
109 stateChangeCounter: Long,
112 * True iff this user state refers to a full billing period, that is a full billing month.
114 isFullBillingMonthState: Boolean,
117 * The full billing period for which this user state refers to.
118 * This is set when the user state refers to a full billing period (= month)
119 * and is used to cache the user state for subsequent queries.
121 theFullBillingMonth: BillingMonthInfo,
124 * If this is a state for a full billing month, then keep here the implicit OFF
125 * resource events or any other whose cost policy demands an implicit event at the end of the billing period.
127 * The use case is this: A VM may have been started (ON state) before the end of the billing period
128 * and ended (OFF state) after the beginning of the next billing period. In order to bill this, we must assume
129 * an implicit OFF even right at the end of the billing period and an implicit ON event with the beginning of the
130 * next billing period.
132 implicitlyIssuedSnapshot: ImplicitlyIssuedResourceEventsSnapshot,
135 * So far computed wallet entries for the current billing month.
137 billingMonthWalletEntries: List[WalletEntry],
140 * Wallet entries that were computed for out of sync events.
141 * (for the current billing month ??)
143 outOfSyncWalletEntries: List[WalletEntry],
146 * The latest (previous) resource events per resource instance.
148 latestResourceEventsSnapshot: LatestResourceEventsSnapshot,
151 * Counts the total number of resource events used to produce this user state for
152 * the billing period recorded by `billingPeriodSnapshot`
154 billingPeriodResourceEventsCounter: Long,
157 * The out of sync events used to produce this user state for
158 * the billing period recorded by `billingPeriodSnapshot`
160 billingPeriodOutOfSyncResourceEventsCounter: Long,
162 activeStateSnapshot: ActiveStateSnapshot,
163 creditsSnapshot: CreditSnapshot,
164 agreementsSnapshot: AgreementSnapshot,
165 rolesSnapshot: RolesSnapshot,
166 ownedResourcesSnapshot: OwnedResourcesSnapshot,
167 newWalletEntries: List[NewWalletEntry],
168 lastChangeReasonCode: UserStateChangeReasonCodes.ChangeReasonCode,
169 // The last known change reason for this userState
170 lastChangeReason: UserStateChangeReason = NoSpecificChangeReason,
171 totalEventsProcessedCounter: Long = 0L,
172 // The user state we used to compute this one. Normally the (cached)
173 // state at the beginning of the billing period.
174 parentUserStateId: Option[String] = None,
175 _id: ObjectId = new ObjectId()
176 ) extends JsonSupport {
178 private[this] def _allSnapshots: List[Long] = {
180 activeStateSnapshot.snapshotTime,
181 creditsSnapshot.snapshotTime, agreementsSnapshot.snapshotTime, rolesSnapshot.snapshotTime,
182 ownedResourcesSnapshot.snapshotTime,
183 implicitlyIssuedSnapshot.snapshotTime,
184 latestResourceEventsSnapshot.snapshotTime
188 def oldestSnapshotTime: Long = _allSnapshots min
190 def newestSnapshotTime: Long = _allSnapshots max
192 def idOpt: Option[String] = _id match {
194 case _id ⇒ Some(_id.toString)
197 // def userCreationDate = new Date(userCreationMillis)
199 // def userCreationFormatedDate = new MutableDateCalc(userCreationMillis).toString
201 def maybeDSLAgreement(at: Long): Maybe[DSLAgreement] = {
202 agreementsSnapshot match {
203 case snapshot @ AgreementSnapshot(data, _) ⇒
204 snapshot.getAgreement(at)
206 Failed(new AquariumException("No agreement snapshot found for user %s".format(userID)))
210 def findResourceInstanceSnapshot(resource: String, instanceId: String): Maybe[ResourceInstanceSnapshot] = {
211 ownedResourcesSnapshot.findResourceInstanceSnapshot(resource, instanceId)
214 def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
215 ownedResourcesSnapshot.getResourceInstanceAmount(resource, instanceId, defaultValue)
218 def copyForResourcesSnapshotUpdate(resource: String, // resource name
219 instanceId: String, // resource instance id
221 snapshotTime: Long): UserState = {
223 val (newResources, _, _) = ownedResourcesSnapshot.computeResourcesSnapshotUpdate(resource, instanceId, newAmount, snapshotTime)
226 ownedResourcesSnapshot = newResources,
227 stateChangeCounter = this.stateChangeCounter + 1)
230 def copyForChangeReason(changeReason: UserStateChangeReason) = {
231 this.copy(lastChangeReasonCode = changeReason.code, lastChangeReason = changeReason)
234 def resourcesMap = ownedResourcesSnapshot.toResourcesMap
236 // def toShortString = "UserState(%s, %s, %s, %s, %s)".format(
239 // parentUserStateId,
240 // totalEventsProcessedCounter,
241 // calculationReason)
246 def fromJson(json: String): UserState = {
247 StdConverters.AllConverters.convertEx[UserState](JsonTextFormat(json))
251 final val _id = "_id"
252 final val userID = "userID"
256 final class BillingMonthInfo private(val year: Int,
258 val startMillis: Long,
259 val stopMillis: Long) extends Ordered[BillingMonthInfo] {
261 def previousMonth: BillingMonthInfo = {
262 BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goPreviousMonth)
265 def nextMonth: BillingMonthInfo = {
266 BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goNextMonth)
270 def compare(that: BillingMonthInfo) = {
271 val ds = this.startMillis - that.startMillis
272 if(ds < 0) -1 else if(ds == 0) 0 else 1
276 override def equals(any: Any) = any match {
277 case that: BillingMonthInfo ⇒
278 this.year == that.year && this.month == that.month // normally everything else MUST be the same by construction
283 override def hashCode() = {
287 override def toString = "%s-%02d".format(year, month)
290 object BillingMonthInfo {
291 def fromMillis(millis: Long): BillingMonthInfo = {
292 fromDateCalc(new MutableDateCalc(millis))
295 def fromDateCalc(mdc: MutableDateCalc): BillingMonthInfo = {
296 val year = mdc.getYear
297 val month = mdc.getMonthOfYear
298 val startMillis = mdc.goStartOfThisMonth.getMillis
299 val stopMillis = mdc.goEndOfThisMonth.getMillis // no need to `copy` here, since we are discarding `mdc`
301 new BillingMonthInfo(year, month, startMillis, stopMillis)
305 sealed trait UserStateChangeReason {
307 * Return `true` if the result of the calculation should be stored back to the
308 * [[gr.grnet.aquarium.store.UserStateStore]].
311 def shouldStoreUserState: Boolean
313 def shouldStoreCalculatedWalletEntries: Boolean
315 def forPreviousBillingMonth: UserStateChangeReason
317 def calculateCreditsForImplicitlyTerminated: Boolean
319 def code: UserStateChangeReasonCodes.ChangeReasonCode
322 object UserStateChangeReasonCodes {
323 type ChangeReasonCode = Int
325 final val InitialCalculationCode = 1
326 final val NoSpecificChangeCode = 2
327 final val MonthlyBillingCode = 3
328 final val RealtimeBillingCode = 4
329 final val IMEventArrivalCode = 5
332 case object InitialUserStateCalculation extends UserStateChangeReason {
333 def shouldStoreUserState = true
335 def shouldStoreCalculatedWalletEntries = false
337 def forPreviousBillingMonth = this
339 def calculateCreditsForImplicitlyTerminated = false
341 def code = UserStateChangeReasonCodes.InitialCalculationCode
344 * A calculation made for no specific reason. Can be for testing, for example.
347 case object NoSpecificChangeReason extends UserStateChangeReason {
348 def shouldStoreUserState = false
350 def shouldStoreCalculatedWalletEntries = false
352 def forBillingMonthInfo(bmi: BillingMonthInfo) = this
354 def forPreviousBillingMonth = this
356 def calculateCreditsForImplicitlyTerminated = false
358 def code = UserStateChangeReasonCodes.NoSpecificChangeCode
362 * An authoritative calculation for the billing period.
364 * This marks a state for caching.
366 * @param billingMonthInfo
368 case class MonthlyBillingCalculation(billingMonthInfo: BillingMonthInfo) extends UserStateChangeReason {
369 def shouldStoreUserState = true
371 def shouldStoreCalculatedWalletEntries = true
373 def forPreviousBillingMonth = MonthlyBillingCalculation(billingMonthInfo.previousMonth)
375 def calculateCreditsForImplicitlyTerminated = true
377 def code = UserStateChangeReasonCodes.MonthlyBillingCode
381 * Used for the realtime billing calculation.
383 * @param forWhenMillis The time this calculation is for
385 case class RealtimeBillingCalculation(forWhenMillis: Long) extends UserStateChangeReason {
386 def shouldStoreUserState = false
388 def shouldStoreCalculatedWalletEntries = false
390 def forPreviousBillingMonth = this
392 def calculateCreditsForImplicitlyTerminated = false
394 def code = UserStateChangeReasonCodes.RealtimeBillingCode
397 case class IMEventArrival(imEvent: IMEventModel) extends UserStateChangeReason {
398 def shouldStoreUserState = true
400 def shouldStoreCalculatedWalletEntries = false
402 def forPreviousBillingMonth = this
404 def calculateCreditsForImplicitlyTerminated = false
406 def code = UserStateChangeReasonCodes.IMEventArrivalCode