2 * Copyright 2011-2012 GRNET S.A. All rights reserved.
4 * Redistribution and use in source and binary forms, with or
5 * without modification, are permitted provided that the following
8 * 1. Redistributions of source code must retain the above
9 * copyright notice, this list of conditions and the following
12 * 2. Redistributions in binary form must reproduce the above
13 * copyright notice, this list of conditions and the following
14 * disclaimer in the documentation and/or other materials
15 * provided with the distribution.
17 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 * POSSIBILITY OF SUCH DAMAGE.
30 * The views and conclusions contained in the software and
31 * documentation are those of the authors and should not be
32 * interpreted as representing official policies, either expressed
33 * or implied, of GRNET S.A.
36 package gr.grnet.aquarium.logic.accounting.dsl
38 import com.ckkloverdos.maybe.{NoVal, Failed, Just, Maybe}
39 import gr.grnet.aquarium.AquariumException
40 import gr.grnet.aquarium.event.resource.ResourceEventModel
43 * A cost policy indicates how charging for a resource will be done
44 * wrt the various states a resource can be.
46 * @author Georgios Gousios <gousiosg@gmail.com>
47 * @author Christos KK Loverdos <loverdos@gmail.com>
50 abstract class DSLCostPolicy(val name: String, val vars: Set[DSLCostPolicyVar]) extends DSLItem {
52 def varNames = vars.map(_.name)
55 * Generate a map where the key is a [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicyVar]]
56 * and the value the respective value. This map will be used to do the actual credit charge calculation
57 * by the respective algorithm.
59 * Values are obtained from a corresponding context, which is provided by the parameters. We assume that this context
60 * has been validated before the call to `makeValueMap` is made.
62 * @param totalCredits the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLTotalCreditsVar]]
63 * @param oldTotalAmount the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLOldTotalAmountVar]]
64 * @param newTotalAmount the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLNewTotalAmountVar]]
65 * @param timeDelta the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLTimeDeltaVar]]
66 * @param previousValue the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLPreviousValueVar]]
67 * @param currentValue the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLCurrentValueVar]]
68 * @param unitPrice the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLUnitPriceVar]]
70 * @return a map from [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicyVar]]s to respective values.
72 def makeValueMap(totalCredits: Double,
73 oldTotalAmount: Double,
74 newTotalAmount: Double,
76 previousValue: Double,
78 unitPrice: Double): Map[DSLCostPolicyVar, Any] = {
80 DSLCostPolicy.makeValueMapFor(
91 def isOnOff: Boolean = isNamed(DSLCostPolicyNames.onoff)
93 def isContinuous: Boolean = isNamed(DSLCostPolicyNames.continuous)
95 def isDiscrete: Boolean = isNamed(DSLCostPolicyNames.discrete)
97 def isOnce: Boolean = isNamed(DSLCostPolicyNames.once)
99 def isNamed(aName: String): Boolean = aName == name
101 def needsPreviousEventForCreditAndAmountCalculation: Boolean = {
102 // If we need any variable that is related to the previous event
103 // then we do need a previous event
104 vars.exists(_.isDirectlyRelatedToPreviousEvent)
108 * Given the old amount of a resource instance (see [[gr.grnet.aquarium.user.ResourceInstanceSnapshot]]) and the
109 * value arriving in a new resource event, compute the new instance amount.
111 * Note that the `oldAmount` does not make sense for all types of [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicy]],
112 * in which case it is ignored.
114 * @param oldAmount the old accumulating amount
115 * @param newEventValue the value contained in a newly arrived [[gr.grnet.aquarium.event.resource.ResourceEventModel]]
118 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double
120 def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double
123 * The initial amount.
125 def getResourceInstanceInitialAmount: Double
128 * The amount used when no amount is meant to be relevant.
130 * For example, when there is no need for a previous event but an API requires the amount of the previous event.
132 * Normally, this value will never be used by client code (= charge computation code).
134 def getResourceInstanceUndefinedAmount: Double = -1.0
137 * Get the value that will be used in credit calculation in Accounting.chargeEvents
139 def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double]
142 * An event's value by itself should carry enough info to characterize it billable or not.
144 * Typically all events are billable by default and indeed this is the default implementation
147 * The only exception to the rule is ON events for [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]].
149 def isBillableEventBasedOnValue(eventValue: Double): Boolean = true
152 * This is called when we have the very first event for a particular resource instance, and we want to know
153 * if it is billable or not.
158 def isBillableFirstEventBasedOnValue(eventValue: Double): Boolean
161 * There are resources (cost policies) for which implicit events must be generated at the end of the billing period
162 * and also at the beginning of the next one. For these cases, this method must return `true`.
164 * The motivating example comes from the [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]] for which we
165 * must implicitly assume `OFF` events at the end of the billing period and `ON` events at the beginning of the next
169 def supportsImplicitEvents: Boolean
171 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel): Boolean
173 @throws(classOf[Exception])
174 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel
176 @throws(classOf[Exception])
177 def constructImplicitStartEventFor(resourceEvent: ResourceEventModel): ResourceEventModel
180 object DSLCostPolicyNames {
181 final val onoff = "onoff"
182 final val discrete = "discrete"
183 final val continuous = "continuous"
184 final val once = "once"
187 object DSLCostPolicy {
188 def apply(name: String): DSLCostPolicy = {
191 throw new DSLParseException("<null> cost policy")
193 case name ⇒ name.toLowerCase match {
194 case DSLCostPolicyNames.onoff ⇒ OnOffCostPolicy
195 case DSLCostPolicyNames.discrete ⇒ DiscreteCostPolicy
196 case DSLCostPolicyNames.continuous ⇒ ContinuousCostPolicy
197 case DSLCostPolicyNames.once ⇒ ContinuousCostPolicy
200 throw new DSLParseException("Invalid cost policy %s".format(name))
205 def makeValueMapFor(costPolicy: DSLCostPolicy,
206 totalCredits: Double,
207 oldTotalAmount: Double,
208 newTotalAmount: Double,
210 previousValue: Double,
211 currentValue: Double,
212 unitPrice: Double): Map[DSLCostPolicyVar, Any] = {
213 val vars = costPolicy.vars
214 var map = Map[DSLCostPolicyVar, Any]()
216 if(vars contains DSLCostPolicyNameVar) map += DSLCostPolicyNameVar -> costPolicy.name
217 if(vars contains DSLTotalCreditsVar ) map += DSLTotalCreditsVar -> totalCredits
218 if(vars contains DSLOldTotalAmountVar) map += DSLOldTotalAmountVar -> oldTotalAmount
219 if(vars contains DSLNewTotalAmountVar) map += DSLNewTotalAmountVar -> newTotalAmount
220 if(vars contains DSLTimeDeltaVar ) map += DSLTimeDeltaVar -> timeDelta
221 if(vars contains DSLPreviousValueVar ) map += DSLPreviousValueVar -> previousValue
222 if(vars contains DSLCurrentValueVar ) map += DSLCurrentValueVar -> currentValue
223 if(vars contains DSLUnitPriceVar ) map += DSLUnitPriceVar -> unitPrice
230 * A cost policy for which resource events just carry a credit amount that will be added to the total one.
232 * Examples are: a) Give a gift of X credits to the user, b) User bought a book, so charge for the book price.
235 case object OnceCostPolicy
236 extends DSLCostPolicy(DSLCostPolicyNames.once, Set(DSLCostPolicyNameVar, DSLCurrentValueVar)) {
238 def isBillableFirstEventBasedOnValue(eventValue: Double) = true
240 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double) = oldAmount
242 def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double) = getResourceInstanceInitialAmount
244 def getResourceInstanceInitialAmount = 0.0
246 def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double) = Just(newEventValue)
248 def supportsImplicitEvents = false
250 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = false
252 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, occurredMillis: Long) = {
253 throw new AquariumException("constructImplicitEndEventFor() Not compliant with %s".format(this))
256 def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
257 throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
262 * In practice a resource usage will be charged for the total amount of usage
263 * between resource usage changes.
265 * Example resource that might be adept to a continuous policy
268 case object ContinuousCostPolicy
269 extends DSLCostPolicy(DSLCostPolicyNames.continuous,
270 Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLOldTotalAmountVar, DSLTimeDeltaVar)) {
272 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
273 oldAmount + newEventValue
276 def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
280 def getResourceInstanceInitialAmount: Double = {
284 def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
288 def isBillableFirstEventBasedOnValue(eventValue: Double) = {
292 def supportsImplicitEvents = {
296 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
300 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long) = {
301 assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
303 val details = resourceEvent.details
304 val newDetails = ResourceEventModel.setAquariumSyntheticAndImplicitEnd(details)
306 resourceEvent.withDetails(newDetails, newOccurredMillis)
309 def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
310 throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
315 * An onoff cost policy expects a resource to be in one of the two allowed
316 * states (`on` and `off`, respectively). It will charge for resource usage
317 * within the timeframes specified by consecutive on and off resource events.
318 * An onoff policy is the same as a continuous policy, except for
319 * the timeframes within the resource is in the `off` state.
321 * Example resources that might be adept to onoff policies are VMs in a
322 * cloud application and books in a book lending application.
324 case object OnOffCostPolicy
325 extends DSLCostPolicy(DSLCostPolicyNames.onoff,
326 Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLTimeDeltaVar)) {
330 * @param oldAmount is ignored
331 * @param newEventValue
334 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
338 def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
339 import OnOffCostPolicyValues.{ON, OFF}
341 case ON ⇒ /* implicit off at the end of the billing period */ OFF
346 def getResourceInstanceInitialAmount: Double = {
350 def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
352 case Just(oldAmount) ⇒
353 Maybe(getValueForCreditCalculation(oldAmount, newEventValue))
355 Failed(new AquariumException("NoVal for oldValue instead of Just"))
357 Failed(new AquariumException("Failed for oldValue instead of Just", e))
362 def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
363 import OnOffCostPolicyValues.{ON, OFF}
365 def exception(rs: OnOffPolicyResourceState) =
366 new AquariumException("Resource state transition error (%s -> %s)".format(rs, rs))
368 (oldAmount, newEventValue) match {
370 throw exception(OnResourceState)
376 throw exception(OffResourceState)
380 override def isBillableEventBasedOnValue(eventValue: Double) = {
381 // ON events do not contribute, only OFF ones.
382 OnOffCostPolicyValues.isOFFValue(eventValue)
385 def isBillableFirstEventBasedOnValue(eventValue: Double) = {
389 def supportsImplicitEvents = {
394 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
395 // If we have ON events with no OFF companions at the end of the billing period,
396 // then we must generate implicit OFF events.
397 OnOffCostPolicyValues.isONValue(resourceEvent.value)
400 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long) = {
401 assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
402 assert(OnOffCostPolicyValues.isONValue(resourceEvent.value))
404 val details = resourceEvent.details
405 val newDetails = ResourceEventModel.setAquariumSyntheticAndImplicitEnd(details)
406 val newValue = OnOffCostPolicyValues.OFF
408 resourceEvent.withDetailsAndValue(newDetails, newValue, newOccurredMillis)
411 def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
412 throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
416 object OnOffCostPolicyValues {
420 def isONValue (value: Double) = value == ON
421 def isOFFValue(value: Double) = value == OFF
425 * An discrete cost policy indicates that a resource should be charged directly
426 * at each resource state change, i.e. the charging is not dependent on
427 * the time the resource.
429 * Example oneoff resources might be individual charges applied to various
430 * actions (e.g. the fact that a user has created an account) or resources
431 * that should be charged per volume once (e.g. the allocation of a volume)
433 case object DiscreteCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.discrete,
434 Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLCurrentValueVar)) {
436 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
437 oldAmount + newEventValue
440 def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
441 0.0 // ?? def getResourceInstanceInitialAmount
444 def getResourceInstanceInitialAmount: Double = {
448 def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
452 def isBillableFirstEventBasedOnValue(eventValue: Double) = {
453 false // nope, we definitely need a previous one.
456 def supportsImplicitEvents = {
460 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
464 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, occurredMillis: Long) = {
465 throw new AquariumException("constructImplicitEndEventFor() Not compliant with %s".format(this))
468 def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
469 throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
474 * Encapsulates the possible states that a resource with an
475 * [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]]
478 abstract class OnOffPolicyResourceState(val state: String) {
479 def isOn: Boolean = !isOff
480 def isOff: Boolean = !isOn
483 object OnOffPolicyResourceState {
484 def apply(name: Any): OnOffPolicyResourceState = {
486 case x: String if (x.equalsIgnoreCase(OnOffPolicyResourceStateNames.on)) => OnResourceState
487 case y: String if (y.equalsIgnoreCase(OnOffPolicyResourceStateNames.off)) => OffResourceState
488 case a: Double if (a == 0) => OffResourceState
489 case b: Double if (b == 1) => OnResourceState
490 case i: Int if (i == 0) => OffResourceState
491 case j: Int if (j == 1) => OnResourceState
492 case _ => throw new DSLParseException("Invalid OnOffPolicyResourceState %s".format(name))
497 object OnOffPolicyResourceStateNames {
499 final val off = "off"
502 object OnResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.on) {
503 override def isOn = true
505 object OffResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.off) {
506 override def isOff = true