/*
- * Copyright 2011 GRNET S.A. All rights reserved.
+ * Copyright 2011-2012 GRNET S.A. All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
package gr.grnet.aquarium.user
-import gr.grnet.aquarium.util.json.{JsonHelpers, JsonSupport}
-import net.liftweb.json.{JsonAST, Xml}
+import gr.grnet.aquarium.util.json.JsonSupport
import gr.grnet.aquarium.logic.accounting.dsl.DSLAgreement
import com.ckkloverdos.maybe.{Failed, Maybe}
-import gr.grnet.aquarium.logic.events.WalletEntry
+import gr.grnet.aquarium.util.date.MutableDateCalc
+import gr.grnet.aquarium.event.{NewWalletEntry, WalletEntry}
+import gr.grnet.aquarium.converter.{JsonTextFormat, StdConverters}
+import gr.grnet.aquarium.AquariumException
+import gr.grnet.aquarium.event.im.IMEventModel
+import org.bson.types.ObjectId
/**
*
* The user state is meant to be partially updated according to relevant events landing on Aquarium.
*
+ * @define communicatedByIM
+ * This is communicated to Aquarium from the `IM` system.
+ *
+ *
+ * @param userID
+ * The user ID. $communicatedByIM
+ * @param userCreationMillis
+ * When the user was created.
+ * $communicatedByIM
+ * Set to zero if unknown.
+ * @param stateChangeCounter
+ * @param isFullBillingMonthState
+ * @param theFullBillingMonth
+ * @param implicitlyIssuedSnapshot
+ * @param billingMonthWalletEntries
+ * @param outOfSyncWalletEntries
+ * @param latestResourceEventsSnapshot
+ * @param billingPeriodResourceEventsCounter
+ * @param billingPeriodOutOfSyncResourceEventsCounter
+ * @param activeStateSnapshot
+ * @param creditsSnapshot
+ * @param agreementsSnapshot
+ * @param rolesSnapshot
+ * @param ownedResourcesSnapshot
+ * @param newWalletEntries
+ * The wallet entries computed. Not all user states need to holds wallet entries,
+ * only those that refer to billing periods (end of billing period).
+ * @param lastChangeReasonCode
+ * The code for the `lastChangeReason`.
+ * @param lastChangeReason
+ * The [[gr.grnet.aquarium.user.UserStateChangeReason]] for which the usr state has changed.
+ * @param totalEventsProcessedCounter
+ * @param parentUserStateId
+ * The `ID` of the parent state. The parent state is the one used as a reference point in order to calculate
+ * this user state.
+ * @param _id
+ * The unique `ID` given by the store.
+ *
* @author Christos KK Loverdos <loverdos@gmail.com>
*/
-
case class UserState(
- userId: String,
+ userID: String,
- /**
- * When the user was created in the system (not Aquarium). We use this as a basis for billing periods. Set to
- * zero if unknown.
- *
- */
userCreationMillis: Long,
/**
* This is set when the user state refers to a full billing period (= month)
* and is used to cache the user state for subsequent queries.
*/
- theFullBillingMonth: BillingMonth,
+ theFullBillingMonth: BillingMonthInfo,
/**
* If this is a state for a full billing month, then keep here the implicit OFF
- * resource events.
+ * resource events or any other whose cost policy demands an implicit event at the end of the billing period.
*
* The use case is this: A VM may have been started (ON state) before the end of the billing period
* and ended (OFF state) after the beginning of the next billing period. In order to bill this, we must assume
* an implicit OFF even right at the end of the billing period and an implicit ON event with the beginning of the
* next billing period.
*/
- implicitOFFs: ImplicitOFFResourceEventsSnapshot,
+ implicitlyIssuedSnapshot: ImplicitlyIssuedResourceEventsSnapshot,
/**
* So far computed wallet entries for the current billing month.
outOfSyncWalletEntries: List[WalletEntry],
/**
- * The latest resource events per resource instance
+ * The latest (previous) resource events per resource instance.
*/
- latestResourceEvents: LatestResourceEventsSnapshot,
+ latestResourceEventsSnapshot: LatestResourceEventsSnapshot,
/**
- * Counts the number of resource events used to produce this user state for
+ * Counts the total number of resource events used to produce this user state for
* the billing period recorded by `billingPeriodSnapshot`
*/
- resourceEventsCounter: Long,
+ billingPeriodResourceEventsCounter: Long,
- active: ActiveStateSnapshot,
- credits: CreditSnapshot,
- agreements: AgreementSnapshot,
- roles: RolesSnapshot,
- ownedResources: OwnedResourcesSnapshot
+ /**
+ * The out of sync events used to produce this user state for
+ * the billing period recorded by `billingPeriodSnapshot`
+ */
+ billingPeriodOutOfSyncResourceEventsCounter: Long,
+
+ activeStateSnapshot: ActiveStateSnapshot,
+ creditsSnapshot: CreditSnapshot,
+ agreementsSnapshot: AgreementSnapshot,
+ rolesSnapshot: RolesSnapshot,
+ ownedResourcesSnapshot: OwnedResourcesSnapshot,
+ newWalletEntries: List[NewWalletEntry],
+ lastChangeReasonCode: UserStateChangeReasonCodes.ChangeReasonCode,
+ // The last known change reason for this userState
+ lastChangeReason: UserStateChangeReason = NoSpecificChangeReason,
+ totalEventsProcessedCounter: Long = 0L,
+ // The user state we used to compute this one. Normally the (cached)
+ // state at the beginning of the billing period.
+ parentUserStateId: Option[String] = None,
+ _id: ObjectId = new ObjectId()
) extends JsonSupport {
private[this] def _allSnapshots: List[Long] = {
List(
- active.snapshotTime,
- credits.snapshotTime, agreements.snapshotTime, roles.snapshotTime,
- ownedResources.snapshotTime)
+ activeStateSnapshot.snapshotTime,
+ creditsSnapshot.snapshotTime, agreementsSnapshot.snapshotTime, rolesSnapshot.snapshotTime,
+ ownedResourcesSnapshot.snapshotTime,
+ implicitlyIssuedSnapshot.snapshotTime,
+ latestResourceEventsSnapshot.snapshotTime
+ )
}
def oldestSnapshotTime: Long = _allSnapshots min
def newestSnapshotTime: Long = _allSnapshots max
+ def idOpt: Option[String] = _id match {
+ case null ⇒ None
+ case _id ⇒ Some(_id.toString)
+ }
+
// def userCreationDate = new Date(userCreationMillis)
//
-// def userCreationFormatedDate = new DateCalculator(userCreationMillis).toString
+// def userCreationFormatedDate = new MutableDateCalc(userCreationMillis).toString
def maybeDSLAgreement(at: Long): Maybe[DSLAgreement] = {
- agreements match {
+ agreementsSnapshot match {
case snapshot @ AgreementSnapshot(data, _) ⇒
snapshot.getAgreement(at)
case _ ⇒
- Failed(new Exception("No agreement snapshot found for user %s".format(userId)))
+ Failed(new AquariumException("No agreement snapshot found for user %s".format(userID)))
}
}
def findResourceInstanceSnapshot(resource: String, instanceId: String): Maybe[ResourceInstanceSnapshot] = {
- ownedResources.findResourceInstanceSnapshot(resource, instanceId)
+ ownedResourcesSnapshot.findResourceInstanceSnapshot(resource, instanceId)
}
def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
- ownedResources.getResourceInstanceAmount(resource, instanceId, defaultValue)
+ ownedResourcesSnapshot.getResourceInstanceAmount(resource, instanceId, defaultValue)
}
def copyForResourcesSnapshotUpdate(resource: String, // resource name
newAmount: Double,
snapshotTime: Long): UserState = {
- val (newResources, _, _) = ownedResources.computeResourcesSnapshotUpdate(resource, instanceId, newAmount, snapshotTime)
+ val (newResources, _, _) = ownedResourcesSnapshot.computeResourcesSnapshotUpdate(resource, instanceId, newAmount, snapshotTime)
this.copy(
- ownedResources = newResources,
+ ownedResourcesSnapshot = newResources,
stateChangeCounter = this.stateChangeCounter + 1)
}
-
- def resourcesMap = ownedResources.toResourcesMap
- def safeCredits = credits match {
- case c @ CreditSnapshot(_, _) ⇒ c
- case _ ⇒ CreditSnapshot(0.0, 0)
+ def copyForChangeReason(changeReason: UserStateChangeReason) = {
+ this.copy(lastChangeReasonCode = changeReason.code, lastChangeReason = changeReason)
}
+
+ def resourcesMap = ownedResourcesSnapshot.toResourcesMap
+
+// def toShortString = "UserState(%s, %s, %s, %s, %s)".format(
+// userId,
+// _id,
+// parentUserStateId,
+// totalEventsProcessedCounter,
+// calculationReason)
}
object UserState {
def fromJson(json: String): UserState = {
- JsonHelpers.jsonToObject[UserState](json)
+ StdConverters.AllConverters.convertEx[UserState](JsonTextFormat(json))
}
- def fromJValue(jsonAST: JsonAST.JValue): UserState = {
- JsonHelpers.jValueToObject[UserState](jsonAST)
+ object JsonNames {
+ final val _id = "_id"
+ final val userID = "userID"
}
+}
+
+final class BillingMonthInfo private(val year: Int,
+ val month: Int,
+ val startMillis: Long,
+ val stopMillis: Long) extends Ordered[BillingMonthInfo] {
- def fromBytes(bytes: Array[Byte]): UserState = {
- JsonHelpers.jsonBytesToObject[UserState](bytes)
+ def previousMonth: BillingMonthInfo = {
+ BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goPreviousMonth)
}
- def fromXml(xml: String): UserState = {
- fromJValue(Xml.toJson(scala.xml.XML.loadString(xml)))
+ def nextMonth: BillingMonthInfo = {
+ BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goNextMonth)
}
- object JsonNames {
- final val _id = "_id"
- final val userId = "userId"
+
+ def compare(that: BillingMonthInfo) = {
+ val ds = this.startMillis - that.startMillis
+ if(ds < 0) -1 else if(ds == 0) 0 else 1
}
+
+
+ override def equals(any: Any) = any match {
+ case that: BillingMonthInfo ⇒
+ this.year == that.year && this.month == that.month // normally everything else MUST be the same by construction
+ case _ ⇒
+ false
+ }
+
+ override def hashCode() = {
+ 31 * year + month
+ }
+
+ override def toString = "%s-%02d".format(year, month)
+}
+
+object BillingMonthInfo {
+ def fromMillis(millis: Long): BillingMonthInfo = {
+ fromDateCalc(new MutableDateCalc(millis))
+ }
+
+ def fromDateCalc(mdc: MutableDateCalc): BillingMonthInfo = {
+ val year = mdc.getYear
+ val month = mdc.getMonthOfYear
+ val startMillis = mdc.goStartOfThisMonth.getMillis
+ val stopMillis = mdc.goEndOfThisMonth.getMillis // no need to `copy` here, since we are discarding `mdc`
+
+ new BillingMonthInfo(year, month, startMillis, stopMillis)
+ }
+}
+
+sealed trait UserStateChangeReason {
+ /**
+ * Return `true` if the result of the calculation should be stored back to the
+ * [[gr.grnet.aquarium.store.UserStateStore]].
+ *
+ */
+ def shouldStoreUserState: Boolean
+
+ def shouldStoreCalculatedWalletEntries: Boolean
+
+ def forPreviousBillingMonth: UserStateChangeReason
+
+ def calculateCreditsForImplicitlyTerminated: Boolean
+
+ def code: UserStateChangeReasonCodes.ChangeReasonCode
}
-case class BillingMonth(yearOfBillingMonth: Int, billingMonth: Int)
\ No newline at end of file
+object UserStateChangeReasonCodes {
+ type ChangeReasonCode = Int
+
+ final val InitialCalculationCode = 1
+ final val NoSpecificChangeCode = 2
+ final val MonthlyBillingCode = 3
+ final val RealtimeBillingCode = 4
+ final val IMEventArrivalCode = 5
+}
+
+case object InitialUserStateCalculation extends UserStateChangeReason {
+ def shouldStoreUserState = true
+
+ def shouldStoreCalculatedWalletEntries = false
+
+ def forPreviousBillingMonth = this
+
+ def calculateCreditsForImplicitlyTerminated = false
+
+ def code = UserStateChangeReasonCodes.InitialCalculationCode
+}
+/**
+ * A calculation made for no specific reason. Can be for testing, for example.
+ *
+ */
+case object NoSpecificChangeReason extends UserStateChangeReason {
+ def shouldStoreUserState = false
+
+ def shouldStoreCalculatedWalletEntries = false
+
+ def forBillingMonthInfo(bmi: BillingMonthInfo) = this
+
+ def forPreviousBillingMonth = this
+
+ def calculateCreditsForImplicitlyTerminated = false
+
+ def code = UserStateChangeReasonCodes.NoSpecificChangeCode
+}
+
+/**
+ * An authoritative calculation for the billing period.
+ *
+ * This marks a state for caching.
+ *
+ * @param billingMonthInfo
+ */
+case class MonthlyBillingCalculation(billingMonthInfo: BillingMonthInfo) extends UserStateChangeReason {
+ def shouldStoreUserState = true
+
+ def shouldStoreCalculatedWalletEntries = true
+
+ def forPreviousBillingMonth = MonthlyBillingCalculation(billingMonthInfo.previousMonth)
+
+ def calculateCreditsForImplicitlyTerminated = true
+
+ def code = UserStateChangeReasonCodes.MonthlyBillingCode
+}
+
+/**
+ * Used for the realtime billing calculation.
+ *
+ * @param forWhenMillis The time this calculation is for
+ */
+case class RealtimeBillingCalculation(forWhenMillis: Long) extends UserStateChangeReason {
+ def shouldStoreUserState = false
+
+ def shouldStoreCalculatedWalletEntries = false
+
+ def forPreviousBillingMonth = this
+
+ def calculateCreditsForImplicitlyTerminated = false
+
+ def code = UserStateChangeReasonCodes.RealtimeBillingCode
+}
+
+case class IMEventArrival(imEvent: IMEventModel) extends UserStateChangeReason {
+ def shouldStoreUserState = true
+
+ def shouldStoreCalculatedWalletEntries = false
+
+ def forPreviousBillingMonth = this
+
+ def calculateCreditsForImplicitlyTerminated = false
+
+ def code = UserStateChangeReasonCodes.IMEventArrivalCode
+}