Calculating resource instance amounts
authorChristos KK Loverdos <loverdos@gmail.com>
Tue, 31 Jan 2012 12:55:13 +0000 (14:55 +0200)
committerChristos KK Loverdos <loverdos@gmail.com>
Tue, 31 Jan 2012 12:55:13 +0000 (14:55 +0200)
src/main/scala/gr/grnet/aquarium/logic/accounting/Policy.scala
src/main/scala/gr/grnet/aquarium/logic/accounting/dsl/DSLCostPolicy.scala
src/main/scala/gr/grnet/aquarium/user/UserDataSnapshot.scala
src/main/scala/gr/grnet/aquarium/user/UserState.scala
src/main/scala/gr/grnet/aquarium/user/UserStateComputations.scala

index 69eb007..ac5322e 100644 (file)
@@ -70,7 +70,7 @@ object Policy extends DSL with Loggable {
     policies.filter {
       a => a._1.from.before(from) &&
            a._1.to.after(to)
-    }.values.toList
+    }.valuesIterator.toList
   }
   
   def policies(t: Timeslot): List[DSLPolicy] = policies(t.from, t.to)
index 7c9fdab..2907857 100644 (file)
@@ -83,11 +83,22 @@ abstract class DSLCostPolicy(val name: String) extends DSLItem {
    */
   def computeNewResourceInstanceAmount(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double]
 
+  def computeNewResourceInstanceAmount(oldAmount: Double, newEventValue: Double): Double
+
+  def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double
+
+  /**
+   * Th every initial amount.
+   */
+  def getResourceInstanceInitialAmount: Double
+
   /**
    * Get the value that will be used in credit calculation in Accounting.chargeEvents
    */
   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double]
 
+  def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double
+
   /**
    * An event's value by itself should carry enough info to characterize it billable or not.
    *
@@ -137,8 +148,8 @@ case object ContinuousCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.contin
 
   def computeNewResourceInstanceAmount(oldAmountM: Maybe[Double], newEventValue: Double) = {
     oldAmountM match {
-      case Just(oldValue) ⇒
-        Just(oldValue + newEventValue)
+      case Just(oldAmount) ⇒
+        Just(oldAmount + newEventValue)
       case NoVal ⇒
         Failed(new Exception("NoVal for oldValue instead of Just"))
       case Failed(e, m) ⇒
@@ -146,9 +157,26 @@ case object ContinuousCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.contin
     }
   }
 
+  def computeNewResourceInstanceAmount(oldAmount: Double, newEventValue: Double): Double = {
+    oldAmount + newEventValue
+  }
+
+  def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
+    oldAmount
+  }
+
+  def getResourceInstanceInitialAmount: Double = {
+    0.0
+  }
+
   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
     oldAmountM
   }
+
+  def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
+    oldAmount
+  }
+
 }
 
 /**
@@ -171,28 +199,27 @@ case object OnOffCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.onoff) {
   def computeNewResourceInstanceAmount(oldAmountM: Maybe[Double], newEventValue: Double) = {
     Just(newEventValue)
   }
+
+  def computeNewResourceInstanceAmount(oldAmount: Double, newEventValue: Double): Double = {
+    newEventValue
+  }
   
-  def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
+  def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
     import OnOffCostPolicyValues.{ON, OFF}
+    oldAmount match {
+      case ON  ⇒ /* implicit off at the end of the billing period */ OFF
+      case OFF ⇒ OFF
+    }
+  }
 
-    def exception(rs: OnOffPolicyResourceState) =
-      new Exception("Resource state transition error (%s -> %s)".format(rs, rs))
-    def failed(rs: OnOffPolicyResourceState) =
-      Failed(exception(rs))
-    
+  def getResourceInstanceInitialAmount: Double = {
+    0.0
+  }
+  
+  def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
     oldAmountM match {
-      case Just(oldValue) ⇒
-        (oldValue, newEventValue) match {
-          case (ON, ON) ⇒
-            failed(OnResourceState)
-          case (ON, OFF) ⇒
-            Just(OFF)
-          case (OFF, ON) ⇒
-            Just(ON)
-          case (OFF, OFF) ⇒
-            failed(OffResourceState)
-        }
-
+      case Just(oldAmount) ⇒
+        Maybe(getValueForCreditCalculation(oldAmount, newEventValue))
       case NoVal ⇒
         Failed(new Exception("NoVal for oldValue instead of Just"))
       case Failed(e, m) ⇒
@@ -200,6 +227,24 @@ case object OnOffCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.onoff) {
     }
   }
 
+  def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
+    import OnOffCostPolicyValues.{ON, OFF}
+
+    def exception(rs: OnOffPolicyResourceState) =
+      new Exception("Resource state transition error (%s -> %s)".format(rs, rs))
+
+    (oldAmount, newEventValue) match {
+      case (ON, ON) ⇒
+        throw exception(OnResourceState)
+      case (ON, OFF) ⇒
+        OFF
+      case (OFF, ON) ⇒
+        ON
+      case (OFF, OFF) ⇒
+        throw exception(OffResourceState)
+    }
+  }
+
   override def isBillableEventBasedOnValue(newEventValue: Double) = {
     OnOffCostPolicyValues.isOFF(newEventValue)
   }
@@ -230,12 +275,28 @@ case object DiscreteCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.discrete
   override def resourceEventValueIsDiff = true
 
   def computeNewResourceInstanceAmount(oldAmountM: Maybe[Double], newEventValue: Double) = {
-    Just(newEventValue)
+    oldAmountM.map(_ + newEventValue)
+  }
+
+  def computeNewResourceInstanceAmount(oldAmount: Double, newEventValue: Double): Double = {
+    oldAmount + newEventValue
+  }
+
+  def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double  = {
+    0.0 // ?? def getResourceInstanceInitialAmount
+  }
+
+  def getResourceInstanceInitialAmount: Double = {
+    0.0
   }
   
   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
     Just(newEventValue)
   }
+
+  def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
+    newEventValue
+  }
 }
 
 /**
index 0fadfbf..16dd036 100644 (file)
@@ -90,8 +90,9 @@ case class AgreementSnapshot(data: List[Agreement], snapshotTime: Long)
   ensureNoGaps(data.sortWith((a,b) => if (b.validFrom > a.validFrom) true else false))
 
   def ensureNoGaps(agreements: List[Agreement]): Unit = agreements match {
-    case h :: t => assert(h.validTo - t.head.validFrom == 1); ensureNoGaps(t)
+    case ha :: (t @ (hb :: tail)) => assert(ha.validTo - hb.validFrom == 1); ensureNoGaps(t)
     case h :: Nil => assert(h.validTo == -1)
+    case Nil => ()
   }
 
   /**
@@ -127,11 +128,17 @@ case class AgreementSnapshot(data: List[Agreement], snapshotTime: Long)
  *  - If the resource is simple,  the (name, instanceId) is (DSLResource.name, "1")
  *
  */
-case class ResourceInstanceSnapshot(
-    name: String,
-    instanceId: String,
-    data: Double,
-    snapshotTime: Long)
+case class ResourceInstanceSnapshot(/**
+                                     * Same as `resource` of [[gr.grnet.aquarium.logic.events.ResourceEvent]]
+                                     */
+                                    resource: String,
+
+                                    /**
+                                     * Same as `instanceId` of [[gr.grnet.aquarium.logic.events.ResourceEvent]]
+                                     */
+                                    instanceId: String,
+                                    data: Double, // FIXME: data is a missleading name here
+                                    snapshotTime: Long)
   extends UserDataSnapshot[Double] {
 
   /**
@@ -144,8 +151,8 @@ case class ResourceInstanceSnapshot(
    */
   def instanceAmount = data
   
-  def isSameResource(name: String, instanceId: String) = {
-    this.name == name &&
+  def isSameResourceInstance(resource: String, instanceId: String) = {
+    this.resource == resource &&
     this.instanceId == instanceId
   }
 }
@@ -155,10 +162,10 @@ case class ResourceInstanceSnapshot(
  * This representation is convenient for computations and updating, while the
  * [[gr.grnet.aquarium.user.OwnedResourcesSnapshot]] representation is convenient for JSON serialization.
  */
-class OwnedResourcesMap(map: Map[(String, String), (Double, Long)]) {
+class OwnedResourcesMap(resourcesMap: Map[(String, String), (Double, Long)]) {
   def toResourcesSnapshot(snapshotTime: Long): OwnedResourcesSnapshot =
     OwnedResourcesSnapshot(
-      map map {
+      resourcesMap map {
         case ((name, instanceId), (value, snapshotTime)) ⇒
           ResourceInstanceSnapshot(name, instanceId, value, snapshotTime
       )} toList,
@@ -170,34 +177,49 @@ case class OwnedResourcesSnapshot(data: List[ResourceInstanceSnapshot], snapshot
   extends UserDataSnapshot[List[ResourceInstanceSnapshot]] with JsonSupport {
 
   def toResourcesMap: OwnedResourcesMap = {
-    val tuples = for(rc <- data) yield ((rc.name, rc.instanceId), (rc.instanceAmount, rc.snapshotTime))
+    val tuples = for(rc <- data) yield ((rc.resource, rc.instanceId), (rc.instanceAmount, rc.snapshotTime))
 
     new OwnedResourcesMap(Map(tuples.toSeq: _*))
   }
 
-  def findResourceSnapshot(name: String, instanceId: String): Option[ResourceInstanceSnapshot] =
-    data.find { x => name == x.name && instanceId == x.instanceId }
+  def resourceInstanceSnapshots = data
 
-  def addOrUpdateResourceSnapshot(name: String,       // resource name
-                                  instanceId: String, // resource instance id
-                                  newRCInstanceAmount: Double,
-                                  snapshotTime: Long): (OwnedResourcesSnapshot, Option[ResourceInstanceSnapshot], ResourceInstanceSnapshot) = {
+  def resourceInstanceSnapshotsExcept(resource: String, instanceId: String) = {
+    // Unfortunately, we have to use a List for data, since JSON serialization is not as flexible
+    // (at least out of the box). Thus, the update is O(L), where L is the length of the data List.
+    resourceInstanceSnapshots.filterNot(_.isSameResourceInstance(resource, instanceId))
+  }
+
+  def findResourceInstanceSnapshot(resource: String, instanceId: String): Option[ResourceInstanceSnapshot] = {
+    data.find(x => resource == x.resource && instanceId == x.instanceId)
+  }
+
+  def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
+    findResourceInstanceSnapshot(resource, instanceId).map(_.instanceAmount).getOrElse(defaultValue)
+  }
+
+  def computeResourcesSnapshotUpdate(resource: String,   // resource name
+                                     instanceId: String, // resource instance id
+                                     newAmount: Double,
+                                     snapshotTime: Long): (OwnedResourcesSnapshot,
+                                                          Option[ResourceInstanceSnapshot],
+                                                          ResourceInstanceSnapshot) = {
 
-    val newRCInstance = ResourceInstanceSnapshot(name, instanceId, newRCInstanceAmount, snapshotTime)
-    val oldRCInstanceOpt = this.findResourceSnapshot(name, instanceId)
+    val newResourceInstance = ResourceInstanceSnapshot(resource, instanceId, newAmount, snapshotTime)
+    val oldResourceInstanceOpt = this.findResourceInstanceSnapshot(resource, instanceId)
 
-    val newData = oldRCInstanceOpt match {
-      case Some(oldRCInstance) ⇒
+    val newResourceInstances = oldResourceInstanceOpt match {
+      case Some(oldResourceInstance) ⇒
         // Resource instance found, so delete the old one and add the new one
-        newRCInstance :: (data.filterNot(_.isSameResource(name, instanceId)))
+        newResourceInstance :: resourceInstanceSnapshotsExcept(resource, instanceId)
       case None ⇒
         // Resource not found, so this is the first time and we just add the new snapshot
-        newRCInstance :: data
+        newResourceInstance :: resourceInstanceSnapshots
     }
 
-    val newOwnedResources = this.copy(data = newData, snapshotTime = snapshotTime)
+    val newOwnedResources = OwnedResourcesSnapshot(newResourceInstances, snapshotTime)
 
-    (newOwnedResources, oldRCInstanceOpt, newRCInstance)
+    (newOwnedResources, oldResourceInstanceOpt, newResourceInstance)
  }
 }
 
index 5292e73..e4c987a 100644 (file)
@@ -120,11 +120,31 @@ case class UserState(
     }
   }
 
+  def findResourceInstanceSnapshot(resource: String, instanceId: String): Maybe[ResourceInstanceSnapshot] = {
+    ownedResources.findResourceInstanceSnapshot(resource, instanceId)
+  }
+
+  def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
+    ownedResources.getResourceInstanceAmount(resource, instanceId, defaultValue)
+  }
+
+  def copyForResourcesSnapshotUpdate(resource: String,   // resource name
+                                     instanceId: String, // resource instance id
+                                     newAmount: Double,
+                                     snapshotTime: Long): UserState = {
+
+    val (newResources, _, _) = ownedResources.computeResourcesSnapshotUpdate(resource, instanceId, newAmount, snapshotTime)
+
+    this.copy(
+      ownedResources = newResources,
+      stateChangeCounter = this.stateChangeCounter + 1)
+  }
+
   def resourcesMap = ownedResources.toResourcesMap
   
   def safeCredits = credits match {
-    case c @ CreditSnapshot(date, millis) ⇒ c
-    case _ ⇒ CreditSnapshot(0, 0)
+    case c @ CreditSnapshot(_, _) ⇒ c
+    case _ ⇒ CreditSnapshot(0.0, 0)
   }
 }
 
index a53d275..285064e 100644 (file)
@@ -100,6 +100,28 @@ class UserStateComputations {
     )
   }
 
+  def createFirstUserState(userId: String, agreementName: String, policy: DSLPolicy) = {
+    val resources = policy.resources
+
+      val now = 0L
+      UserState(
+        userId,
+        now,
+        0L,
+        false,
+        null,
+        0L,
+        ActiveSuspendedSnapshot(false, now),
+        CreditSnapshot(0, now),
+        AgreementSnapshot(Agreement(agreementName, now, now) :: Nil, now),
+        RolesSnapshot(List(), now),
+        PaymentOrdersSnapshot(Nil, now),
+        OwnedGroupsSnapshot(Nil, now),
+        GroupMembershipsSnapshot(Nil, now),
+        OwnedResourcesSnapshot(List(), now)
+      )
+    }
+
   /**
    * Get the user state as computed up to (and not including) the start of the new billing period.
    *
@@ -133,9 +155,9 @@ class UserStateComputations {
    * Find the previous resource event, if needed by the event's cost policy,
    * in order to use it for any credit calculations.
    */
-  def findPreviousRCEventOf(previousRCEventsMap: mutable.Map[ResourceEvent.FullResourceType, ResourceEvent],
-                            rcEvent: ResourceEvent,
-                            costPolicy: DSLCostPolicy): Maybe[ResourceEvent] = {
+  def findPreviousRCEventOf(rcEvent: ResourceEvent,
+                            costPolicy: DSLCostPolicy,
+                            previousRCEventsMap: mutable.Map[ResourceEvent.FullResourceType, ResourceEvent]): Maybe[ResourceEvent] = {
 
     if(costPolicy.needsPreviousEventForCreditCalculation) {
       // Get a previous resource only if this is needed by the policy
@@ -237,16 +259,22 @@ class UserStateComputations {
     // OK. Now that we have a user state to start with (= start of billing period reference point),
     // let us deal with the events themselves.
     val billingStartMillis = billingMonthStartDate.toMillis
-    val billingStopMillis = billingMonthStartDate.endOfThisMonth.toMillis
+    val billingStopMillis  = billingMonthStartDate.endOfThisMonth.toMillis
     val allBillingPeriodRelevantRCEvents = rcEventStore.findAllRelevantResourceEventsForBillingPeriod(userId, billingStartMillis, billingStopMillis)
 
     type FullResourceType = ResourceEvent.FullResourceType
     val previousRCEventsMap = mutable.Map[FullResourceType, ResourceEvent]()
     val impliedRCEventsMap  = mutable.Map[FullResourceType, ResourceEvent]() // those which do not exists but are
     // implied in order to do billing calculations (e.g. the "off" vmtime resource event)
-    var workingUserState = newStartUserState
+
+    // Our temporary state holder.
+    var _workingUserState = newStartUserState
+    val nowMillis = TimeHelpers.nowMillis
 
     for(newRCEvent <- allBillingPeriodRelevantRCEvents) {
+      val resource = newRCEvent.resource
+      val instanceId = newRCEvent.instanceId
+
       // We need to do these kinds of calculations:
       // 1. Credit state calculations
       // 2. Resource state calculations
@@ -264,36 +292,42 @@ class UserStateComputations {
       //
       // BUT ALL THE ABOVE SHOULD NOT BE CONSIDERED HERE; RATHER THEY ARE POLYMORPHIC BEHAVIOURS
 
-      // We need:
-      // A. The previous event
-
+      // What we need to do is:
+      // A. Update user state with new resource instance amount
+      // B. Update user state with new credit
+      // C. Update ??? state with wallet entries
 
       // The DSLCostPolicy for the resource does not change, so it is safe to use the default DSLPolicy to obtain it.
       val costPolicyM = newRCEvent.findCostPolicy(defaultPolicy)
       costPolicyM match {
         case Just(costPolicy) ⇒
-          val previousRCEventM = findPreviousRCEventOf(previousRCEventsMap, newRCEvent, costPolicy)
-          val previousRCEventValueM = previousRCEventM.map(_.value)
-//          val previousRCInstanceAmount = workingUserState.ownedResources.
-
-          // 1. Update resource state
-          val newRCInstanceAmountM = costPolicy.computeNewResourceInstanceAmount(previousRCEventValueM, newRCEvent.value)
-          newRCInstanceAmountM match {
-            case Just(newRCInstanceAmount) ⇒
-              workingUserState.ownedResources.addOrUpdateResourceSnapshot(
-                newRCEvent.resource,
-                newRCEvent.instanceId,
-                newRCInstanceAmount,
-                TimeHelpers.nowMillis)
-            case NoVal ⇒
-              () // ERROR
-            case failed @ Failed(_, _) ⇒
-              () // ERROR
-          }
-
-          // 2. Update credit state
-
-          // 3. Calc wallet entries
+          ///////////////////////////////////////
+          // A. Update user state with new resource instance amount
+          // TODO: Check if we are at beginning of billing period, so as to use
+          //       costPolicy.computeResourceInstanceAmountForNewBillingPeriod
+          val DefaultResourceInstanceAmount = costPolicy.getResourceInstanceInitialAmount
+
+          val previousAmount = currentUserState.getResourceInstanceAmount(resource, instanceId, DefaultResourceInstanceAmount)
+          val newAmount = costPolicy.computeNewResourceInstanceAmount(previousAmount, newRCEvent.value)
+
+          _workingUserState = _workingUserState.copyForResourcesSnapshotUpdate(resource, instanceId, newAmount, nowMillis)
+          // A. Update user state with new resource instance amount
+          ///////////////////////////////////////
+
+
+          ///////////////////////////////////////
+          // B. Update user state with new credit
+          val previousRCEventM = findPreviousRCEventOf(newRCEvent, costPolicy, previousRCEventsMap)
+          _workingUserState.findResourceInstanceSnapshot(resource, instanceId)
+          // B. Update user state with new credit
+          ///////////////////////////////////////
+
+
+          ///////////////////////////////////////
+          // C. Update ??? state with wallet entries
+
+          // C. Update ??? state with wallet entries
+          ///////////////////////////////////////
 
         case NoVal ⇒
           () // ERROR