* 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()
// 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
// clog.debug("Computing full chargeslots")
val (referenceTimeslot, fullChargeslots) = timeslotComputations.computeFullChargeslots(
- previousResourceEventOpt,
+ previousResourceEventOpt1,
currentResourceEvent,
oldCredits,
oldAmount,
billingMonthInfo.year,
billingMonthInfo.month,
if(havePreviousResourceEvent)
- List(currentResourceEvent, previousResourceEventOpt.get)
+ List(currentResourceEvent, previousResourceEventOpt1.get)
else
List(currentResourceEvent),
fullChargeslots,
/**
* 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
*/
def idInStore: Option[AnyRef] = None
+ def stringIDInStoreOrEmpty = idInStore.map(_.toString).getOrElse("")
+
def eventVersion: String
/**
def isSynthetic = {
details contains ResourceEventModel.Names.details_aquarium_is_synthetic
}
-
}
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
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")
+ }
}
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
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
* @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`.
*
*/
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
}
def getResourceInstanceInitialAmount = 0.0
- def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double) = Just(newEventValue)
-
def supportsImplicitEvents = false
def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = false
* 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
0.0
}
- def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
- oldAmountM
- }
-
def isBillableFirstEventBasedOnValue(eventValue: Double) = {
- false
+ true
}
+ def mustGenerateDummyFirstEvent = true
+
def supportsImplicitEvents = {
true
}
* 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)
+) {
/**
*
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.
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.
}
def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
- throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
+ throw new AquariumInternalError("constructImplicitStartEventFor() Not compliant with %s".format(this))
}
}
* 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
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
}
}
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))
}
}