WIP: Remodeling UserState store mechanics
[aquarium] / src / main / scala / gr / grnet / aquarium / user / UserState.scala
index 90d5e32..a382e0f 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * 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
 
 
 /**
@@ -52,17 +56,49 @@ import gr.grnet.aquarium.logic.events.WalletEntry
  *
  * 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,
 
     /**
@@ -82,18 +118,18 @@ case class UserState(
      * 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.
@@ -107,53 +143,76 @@ case class UserState(
     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
@@ -161,43 +220,188 @@ case class UserState(
                                      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
+}