WIP Resource event handling
authorChristos KK Loverdos <loverdos@gmail.com>
Wed, 13 Jun 2012 13:45:42 +0000 (16:45 +0300)
committerChristos KK Loverdos <loverdos@gmail.com>
Wed, 13 Jun 2012 13:45:42 +0000 (16:45 +0300)
- Boundary conditions for first events

src/main/scala/gr/grnet/aquarium/computation/TimeslotComputations.scala
src/main/scala/gr/grnet/aquarium/computation/UserStateComputations.scala
src/main/scala/gr/grnet/aquarium/computation/state/UserStateWorker.scala
src/main/scala/gr/grnet/aquarium/event/model/EventModel.scala
src/main/scala/gr/grnet/aquarium/event/model/resource/ResourceEventModel.scala
src/main/scala/gr/grnet/aquarium/logic/accounting/dsl/DSLCostPolicy.scala

index 5475793..2edf20b 100644 (file)
@@ -229,17 +229,19 @@ trait TimeslotComputations extends Loggable {
    * Compute the charge slots generated by a particular resource event.
    *
    */
-  def computeFullChargeslots(previousResourceEventOpt: Option[ResourceEventModel],
-                             currentResourceEvent: ResourceEventModel,
-                             oldCredits: Double,
-                             oldTotalAmount: Double,
-                             newTotalAmount: Double,
-                             dslResource: DSLResource,
-                             defaultResourceMap: DSLResourcesMap,
-                             agreementNamesByTimeslot: SortedMap[Timeslot, String],
-                             algorithmCompiler: CostPolicyAlgorithmCompiler,
-                             policyStore: PolicyStore,
-                             clogOpt: Option[ContextualLogger] = None): (Timeslot, List[Chargeslot]) = {
+  def computeFullChargeslots(
+      previousResourceEventOpt: Option[ResourceEventModel],
+      currentResourceEvent: ResourceEventModel,
+      oldCredits: Double,
+      oldTotalAmount: Double,
+      newTotalAmount: Double,
+      dslResource: DSLResource,
+      defaultResourceMap: DSLResourcesMap,
+      agreementNamesByTimeslot: SortedMap[Timeslot, String],
+      algorithmCompiler: CostPolicyAlgorithmCompiler,
+      policyStore: PolicyStore,
+      clogOpt: Option[ContextualLogger] = None
+  ): (Timeslot, List[Chargeslot]) = {
 
     val clog = ContextualLogger.fromOther(clogOpt, logger, "computeFullChargeslots()")
     //    clog.begin()
index 56205dc..8dd774b 100644 (file)
@@ -210,26 +210,52 @@ final class UserStateComputations(_aquarium: => Aquarium) extends Loggable {
       // We have a resource (and thus a cost policy)
       case Some(dslResource) ⇒
         val costPolicy = dslResource.costPolicy
-        clog.debug("Cost policy %s for %s", costPolicy, dslResource)
+        clog.debug("%s for %s", costPolicy, dslResource)
         val isBillable = costPolicy.isBillableEventBasedOnValue(theValue)
         if(!isBillable) {
           // The resource event is not billable
-          clog.debug("Ignoring not billable event %s", currentResourceEventDebugInfo)
+          clog.debug("Ignoring not billable %s", currentResourceEventDebugInfo)
         } else {
           // The resource event is billable
           // Find the previous event.
           // This is (potentially) needed to calculate new credit amount and new resource instance amount
-          val previousResourceEventOpt = userStateWorker.findAndRemovePreviousResourceEvent(theResource, theInstanceId)
-          clog.debug("PreviousM %s", previousResourceEventOpt.map(rcDebugInfo(_)))
+          val previousResourceEventOpt0 = userStateWorker.findAndRemovePreviousResourceEvent(theResource, theInstanceId)
+          clog.debug("PreviousM %s", previousResourceEventOpt0.map(rcDebugInfo(_)))
 
-          val havePreviousResourceEvent = previousResourceEventOpt.isDefined
+          val havePreviousResourceEvent = previousResourceEventOpt0.isDefined
           val needPreviousResourceEvent = costPolicy.needsPreviousEventForCreditAndAmountCalculation
-          if(needPreviousResourceEvent && !havePreviousResourceEvent) {
+
+          val (proceed, previousResourceEventOpt1) = if(needPreviousResourceEvent && !havePreviousResourceEvent) {
             // This must be the first resource event of its kind, ever.
             // TODO: We should normally check the DB to verify the claim (?)
-            clog.debug("Ignoring first event of its kind %s", currentResourceEventDebugInfo)
-            userStateWorker.updateIgnored(currentResourceEvent)
+
+            val actualFirstEvent = currentResourceEvent
+
+            if(costPolicy.isBillableFirstEventBasedOnValue(actualFirstEvent.value) &&
+              costPolicy.mustGenerateDummyFirstEvent) {
+
+              clog.debug("First event of its kind %s", currentResourceEventDebugInfo)
+
+              // OK. Let's see what the cost policy decides. If it must generate a dummy first event, we use that.
+              // Otherwise, the current event goes to the ignored list.
+              // The dummy first is considered to exist at the beginning of the billing period
+
+              val dummyFirst = costPolicy.constructDummyFirstEventFor(currentResourceEvent, billingMonthInfo.monthStartMillis)
+
+              clog.debug("Dummy first companion %s", rcDebugInfo(dummyFirst))
+
+              // proceed with charging???
+              (true, Some(dummyFirst))
+            } else {
+              clog.debug("Ignoring first event of its kind %s", currentResourceEventDebugInfo)
+              userStateWorker.updateIgnored(currentResourceEvent)
+              (false, None)
+            }
           } else {
+            (true, previousResourceEventOpt0)
+          }
+
+          if(proceed) {
             val defaultInitialAmount = costPolicy.getResourceInstanceInitialAmount
             val oldAmount = _workingUserState.getResourceInstanceAmount(theResource, theInstanceId, defaultInitialAmount)
             val oldCredits = _workingUserState.totalCredits
@@ -245,7 +271,7 @@ final class UserStateComputations(_aquarium: => Aquarium) extends Loggable {
 
             //              clog.debug("Computing full chargeslots")
             val (referenceTimeslot, fullChargeslots) = timeslotComputations.computeFullChargeslots(
-              previousResourceEventOpt,
+              previousResourceEventOpt1,
               currentResourceEvent,
               oldCredits,
               oldAmount,
@@ -280,7 +306,7 @@ final class UserStateComputations(_aquarium: => Aquarium) extends Loggable {
                 billingMonthInfo.year,
                 billingMonthInfo.month,
                 if(havePreviousResourceEvent)
-                  List(currentResourceEvent, previousResourceEventOpt.get)
+                  List(currentResourceEvent, previousResourceEventOpt1.get)
                 else
                   List(currentResourceEvent),
                 fullChargeslots,
index 6d213f8..6be9f39 100644 (file)
@@ -45,26 +45,29 @@ import gr.grnet.aquarium.computation.state.parts.{IgnoredFirstResourceEventsWork
 /**
  * A helper object holding intermediate state/results during resource event processing.
  *
- * @param previousResourceEvents
- * This is a collection of all the latest resource events.
- * We want these in order to correlate incoming resource events with their previous (in `occurredMillis` time)
- * ones. Will be updated on processing the next resource event.
- *
- * @param implicitlyIssuedStartEvents
- * The implicitly issued resource events at the beginning of the billing period.
- *
- * @param ignoredFirstResourceEvents
- * The resource events that were first (and unused) of their kind.
- *
  * @author Christos KK Loverdos <loverdos@gmail.com>
  */
 case class UserStateWorker(
-                            userID: String,
-                            previousResourceEvents: LatestResourceEventsWorker,
-                            implicitlyIssuedStartEvents: ImplicitlyIssuedResourceEventsWorker,
-                            ignoredFirstResourceEvents: IgnoredFirstResourceEventsWorker,
-                            resourcesMap: DSLResourcesMap
-                            ) {
+    userID: String,
+
+    /**
+     * This is a collection of all the latest resource events.
+     * We want these in order to correlate incoming resource events with their previous (in `occurredMillis` time)
+     * ones. Will be updated on processing the next resource event.
+     */
+    previousResourceEvents: LatestResourceEventsWorker,
+
+    /**
+     * The implicitly issued resource events at the beginning of the billing period.
+     */
+    implicitlyIssuedStartEvents: ImplicitlyIssuedResourceEventsWorker,
+
+    /**
+     * The resource events that were first (and unused) of their kind.
+     */
+    ignoredFirstResourceEvents: IgnoredFirstResourceEventsWorker,
+    resourcesMap: DSLResourcesMap
+) {
 
   /**
    * Finds the previous resource event by checking two possible sources: a) The implicitly terminated resource
index ebd83c8..a5d2abd 100644 (file)
@@ -59,6 +59,8 @@ trait EventModel {
    */
   def idInStore: Option[AnyRef] = None
 
+  def stringIDInStoreOrEmpty = idInStore.map(_.toString).getOrElse("")
+
   def eventVersion: String
 
   /**
index 8a4a3eb..ef71b48 100644 (file)
@@ -148,7 +148,6 @@ trait ResourceEventModel extends ExternalEventModel {
   def isSynthetic = {
     details contains ResourceEventModel.Names.details_aquarium_is_synthetic
   }
-
 }
 
 object ResourceEventModel {
@@ -170,6 +169,12 @@ object ResourceEventModel {
     final val details_aquarium_is_synthetic    = "__aquarium_is_synthetic__"
 
     final val details_aquarium_is_implicit_end = "__aquarium_is_implicit_end__"
+
+    final val details_aquarium_is_dummy_first = "__aquarium_is_dummy_first__"
+
+    final val details_aquarium_reference_event_id = "__aquarium_reference_event_id__"
+
+    final val details_aquarium_reference_event_id_in_store = "__aquarium_reference_event_id_in_store__"
   }
 
   object Names extends NamesT
@@ -180,4 +185,9 @@ object ResourceEventModel {
       updated(Names.details_aquarium_is_implicit_end, "true")
   }
 
+  def setAquariumSyntheticAndDummyFirst(map: Map[String, String]): Map[String, String] = {
+    map.
+      updated(Names.details_aquarium_is_synthetic, "true").
+      updated(Names.details_aquarium_is_dummy_first, "true")
+  }
 }
index 70a0b4d..9091286 100644 (file)
@@ -36,8 +36,8 @@
 package gr.grnet.aquarium.logic.accounting.dsl
 
 import com.ckkloverdos.maybe.{NoVal, Failed, Just, Maybe}
-import gr.grnet.aquarium.AquariumException
 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
+import gr.grnet.aquarium.{AquariumInternalError, AquariumException}
 
 /**
  * A cost policy indicates how charging for a resource will be done
@@ -138,11 +138,6 @@ abstract class DSLCostPolicy(val name: String, val vars: Set[DSLCostPolicyVar])
   def getResourceInstanceUndefinedAmount: Double = -1.0
 
   /**
-   * Get the value that will be used in credit calculation in TimeslotComputations.chargeEvents
-   */
-  def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double]
-
-  /**
    * An event's value by itself should carry enough info to characterize it billable or not.
    *
    * Typically all events are billable by default and indeed this is the default implementation
@@ -160,7 +155,26 @@ abstract class DSLCostPolicy(val name: String, val vars: Set[DSLCostPolicyVar])
    * @return
    */
   def isBillableFirstEventBasedOnValue(eventValue: Double): Boolean
-  
+
+  def mustGenerateDummyFirstEvent: Boolean
+
+  def getDummyFirstEventValue: Double = 0.0
+
+  def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
+    if(!mustGenerateDummyFirstEvent) {
+      throw new AquariumException("constructDummyFirstEventFor() Not compliant with %s".format(this))
+    }
+
+    val newDetails = Map(
+      ResourceEventModel.Names.details_aquarium_is_synthetic   -> "true",
+      ResourceEventModel.Names.details_aquarium_is_dummy_first -> "true",
+      ResourceEventModel.Names.details_aquarium_reference_event_id -> actualFirst.id,
+      ResourceEventModel.Names.details_aquarium_reference_event_id_in_store -> actualFirst.stringIDInStoreOrEmpty
+    )
+
+    actualFirst.withDetailsAndValue(newDetails, getDummyFirstEventValue, newOccurredMillis)
+  }
+
   /**
    * There are resources (cost policies) for which implicit events must be generated at the end of the billing period
    * and also at the beginning of the next one. For these cases, this method must return `true`.
@@ -237,10 +251,15 @@ object DSLCostPolicy {
  *
  */
 case object OnceCostPolicy
-  extends DSLCostPolicy(DSLCostPolicyNames.once, Set(DSLCostPolicyNameVar, DSLCurrentValueVar)) {
+extends DSLCostPolicy(
+    DSLCostPolicyNames.once,
+    Set(DSLCostPolicyNameVar, DSLCurrentValueVar)
+) {
 
   def isBillableFirstEventBasedOnValue(eventValue: Double) = true
 
+  def mustGenerateDummyFirstEvent = false // no need to
+
   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]) = {
     oldAmount
   }
@@ -249,8 +268,6 @@ case object OnceCostPolicy
 
   def getResourceInstanceInitialAmount = 0.0
 
-  def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double) = Just(newEventValue)
-
   def supportsImplicitEvents = false
 
   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = false
@@ -272,8 +289,10 @@ case object OnceCostPolicy
  * is diskspace.
  */
 case object ContinuousCostPolicy
-  extends DSLCostPolicy(DSLCostPolicyNames.continuous,
-                        Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLOldTotalAmountVar, DSLTimeDeltaVar)) {
+extends DSLCostPolicy(
+    DSLCostPolicyNames.continuous,
+    Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLOldTotalAmountVar, DSLTimeDeltaVar)
+) {
 
   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double = {
     // If the total is in the details, get it, or else compute it
@@ -294,14 +313,12 @@ case object ContinuousCostPolicy
     0.0
   }
 
-  def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
-    oldAmountM
-  }
-
   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
-    false
+    true
   }
 
+  def mustGenerateDummyFirstEvent = true
+
   def supportsImplicitEvents = {
     true
   }
@@ -335,8 +352,10 @@ case object ContinuousCostPolicy
  * cloud application and books in a book lending application.
  */
 case object OnOffCostPolicy
-  extends DSLCostPolicy(DSLCostPolicyNames.onoff,
-                        Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLTimeDeltaVar)) {
+extends DSLCostPolicy(
+    DSLCostPolicyNames.onoff,
+    Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLTimeDeltaVar)
+) {
 
   /**
    *
@@ -359,36 +378,6 @@ case object OnOffCostPolicy
   def getResourceInstanceInitialAmount: Double = {
     0.0
   }
-  
-  def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
-    oldAmountM match {
-      case Just(oldAmount) ⇒
-        Maybe(getValueForCreditCalculation(oldAmount, newEventValue))
-      case NoVal ⇒
-        Failed(new AquariumException("NoVal for oldValue instead of Just"))
-      case Failed(e) ⇒
-        Failed(new AquariumException("Failed for oldValue instead of Just", e))
-    }
-  }
-
-  private[this]
-  def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
-    import OnOffCostPolicyValues.{ON, OFF}
-
-    def exception(rs: OnOffPolicyResourceState) =
-      new AquariumException("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(eventValue: Double) = {
     // ON events do not contribute, only OFF ones.
@@ -399,11 +388,12 @@ case object OnOffCostPolicy
     false
   }
 
+  def mustGenerateDummyFirstEvent = false // should be handled by the implicit OFFs
+
   def supportsImplicitEvents = {
     true
   }
 
-
   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
     // If we have ON events with no OFF companions at the end of the billing period,
     // then we must generate implicit OFF events.
@@ -422,7 +412,7 @@ case object OnOffCostPolicy
   }
 
   def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
-    throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
+    throw new AquariumInternalError("constructImplicitStartEventFor() Not compliant with %s".format(this))
   }
 }
 
@@ -443,8 +433,11 @@ object OnOffCostPolicyValues {
  * actions (e.g. the fact that a user has created an account) or resources
  * that should be charged per volume once (e.g. the allocation of a volume)
  */
-case object DiscreteCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.discrete,
-                                                     Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLCurrentValueVar)) {
+case object DiscreteCostPolicy
+extends DSLCostPolicy(
+    DSLCostPolicyNames.discrete,
+    Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLCurrentValueVar)
+) {
 
   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double = {
     oldAmount + newEventValue
@@ -457,15 +450,14 @@ case object DiscreteCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.discrete
   def getResourceInstanceInitialAmount: Double = {
     0.0
   }
-  
-  def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
-    Just(newEventValue)
-  }
 
   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
     false // nope, we definitely need a  previous one.
   }
 
+  // FIXME: Check semantics of this. I just put false until thorough study
+  def mustGenerateDummyFirstEvent = false
+
   def supportsImplicitEvents = {
     false
   }
@@ -475,11 +467,11 @@ case object DiscreteCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.discrete
   }
 
   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, occurredMillis: Long) = {
-    throw new AquariumException("constructImplicitEndEventFor() Not compliant with %s".format(this))
+    throw new AquariumInternalError("constructImplicitEndEventFor() Not compliant with %s".format(this))
   }
 
   def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
-    throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
+    throw new AquariumInternalError("constructImplicitStartEventFor() Not compliant with %s".format(this))
   }
 }