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.charging
38 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
39 import gr.grnet.aquarium.{AquariumInternalError, AquariumException}
42 * A charging behavior indicates how charging for a resource will be done
43 * wrt the various states a resource can be.
45 * @author Christos KK Loverdos <loverdos@gmail.com>
48 abstract class ChargingBehavior(val name: String, val inputs: Set[ChargingInput]) {
50 final lazy val inputNames = inputs.map(_.name)
53 * Generate a map where the key is a [[gr.grnet.aquarium.charging.ChargingInput]]
54 * and the value the respective value. This map will be used to do the actual credit charge calculation
55 * by the respective algorithm.
57 * Values are obtained from a corresponding context, which is provided by the parameters. We assume that this context
58 * has been validated before the call to `makeValueMap` is made.
60 * @param totalCredits the value for [[gr.grnet.aquarium.charging.TotalCreditsInput.]]
61 * @param oldTotalAmount the value for [[gr.grnet.aquarium.charging.OldTotalAmountInput]]
62 * @param newTotalAmount the value for [[gr.grnet.aquarium.charging.NewTotalAmountInput]]
63 * @param timeDelta the value for [[gr.grnet.aquarium.charging.TimeDeltaInput]]
64 * @param previousValue the value for [[gr.grnet.aquarium.charging.PreviousValueInput]]
65 * @param currentValue the value for [[gr.grnet.aquarium.charging.CurrentValueInput]]
66 * @param unitPrice the value for [[gr.grnet.aquarium.charging.UnitPriceInput]]
68 * @return a map from [[gr.grnet.aquarium.charging.ChargingInput]]s to respective values.
72 oldTotalAmount: Double,
73 newTotalAmount: Double,
75 previousValue: Double,
78 ): Map[ChargingInput, Any] = {
80 ChargingBehavior.makeValueMapFor(
91 def isNamed(aName: String): Boolean = aName == name
93 def needsPreviousEventForCreditAndAmountCalculation: Boolean = {
94 // If we need any variable that is related to the previous event
95 // then we do need a previous event
96 inputs.exists(_.isDirectlyRelatedToPreviousEvent)
100 * Given the old amount of a resource instance
101 * (see [[gr.grnet.aquarium.computation.state.parts.ResourceInstanceSnapshot]]), the
102 * value arriving in a new resource event and the new details, compute the new instance amount.
104 * Note that the `oldAmount` does not make sense for all types of [[gr.grnet.aquarium.charging.ChargingBehavior]],
105 * in which case it is ignored.
107 * @param oldAmount the old accumulating amount
108 * @param newEventValue the value contained in a newly arrived
109 * [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
110 * @param details the `details` of the newly arrived
111 * [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
114 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double
117 * The initial amount.
119 def getResourceInstanceInitialAmount: Double
122 * The amount used when no amount is meant to be relevant.
124 * For example, when there is no need for a previous event but an API requires the amount of the previous event.
126 * Normally, this value will never be used by client code (= charge computation code).
128 def getResourceInstanceUndefinedAmount: Double = -1.0
131 * An event carries enough info to characterize it as billable or not.
133 * Typically all events are billable by default and indeed this is the default implementation
136 * The only exception to the rule is ON events for [[gr.grnet.aquarium.charging.OnOffChargingBehavior]].
138 def isBillableEvent(event: ResourceEventModel): Boolean = false
141 * This is called when we have the very first event for a particular resource instance, and we want to know
142 * if it is billable or not.
144 def isBillableFirstEvent(event: ResourceEventModel): Boolean
146 def mustGenerateDummyFirstEvent: Boolean
148 def dummyFirstEventValue: Double = 0.0
150 def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
151 if(!mustGenerateDummyFirstEvent) {
152 throw new AquariumException("constructDummyFirstEventFor() Not compliant with %s".format(this))
155 val newDetails = Map(
156 ResourceEventModel.Names.details_aquarium_is_synthetic -> "true",
157 ResourceEventModel.Names.details_aquarium_is_dummy_first -> "true",
158 ResourceEventModel.Names.details_aquarium_reference_event_id -> actualFirst.id,
159 ResourceEventModel.Names.details_aquarium_reference_event_id_in_store -> actualFirst.stringIDInStoreOrEmpty
162 actualFirst.withDetailsAndValue(newDetails, dummyFirstEventValue, newOccurredMillis)
166 * There are resources (cost policies) for which implicit events must be generated at the end of the billing period
167 * and also at the beginning of the next one. For these cases, this method must return `true`.
169 * The motivating example comes from the [[gr.grnet.aquarium.charging.OnOffChargingBehavior]] for which we
170 * must implicitly assume `OFF` events at the end of the billing period and `ON` events at the beginning of the next
174 def supportsImplicitEvents: Boolean
176 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel): Boolean
178 @throws(classOf[Exception])
179 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel
182 object ChargingBehavior {
183 def apply(name: String): ChargingBehavior = {
186 throw new AquariumException("<null> charging behavior")
188 case name ⇒ name.toLowerCase match {
189 case ChargingBehaviorNames.onoff ⇒ OnOffChargingBehavior
190 case ChargingBehaviorNames.discrete ⇒ DiscreteChargingBehavior
191 case ChargingBehaviorNames.continuous ⇒ ContinuousChargingBehavior
192 case ChargingBehaviorNames.once ⇒ ContinuousChargingBehavior
195 throw new AquariumException("Invalid charging behavior %s".format(name))
201 chargingBehavior: ChargingBehavior,
202 totalCredits: Double,
203 oldTotalAmount: Double,
204 newTotalAmount: Double,
206 previousValue: Double,
207 currentValue: Double,
209 ): Map[ChargingInput, Any] = {
211 val inputs = chargingBehavior.inputs
212 var map = Map[ChargingInput, Any]()
214 if(inputs contains ChargingBehaviorNameInput) map += ChargingBehaviorNameInput -> chargingBehavior.name
215 if(inputs contains TotalCreditsInput ) map += TotalCreditsInput -> totalCredits
216 if(inputs contains OldTotalAmountInput) map += OldTotalAmountInput -> oldTotalAmount
217 if(inputs contains NewTotalAmountInput) map += NewTotalAmountInput -> newTotalAmount
218 if(inputs contains TimeDeltaInput ) map += TimeDeltaInput -> timeDelta
219 if(inputs contains PreviousValueInput ) map += PreviousValueInput -> previousValue
220 if(inputs contains CurrentValueInput ) map += CurrentValueInput -> currentValue
221 if(inputs contains UnitPriceInput ) map += UnitPriceInput -> unitPrice
228 * A charging behavior for which resource events just carry a credit amount that will be added to the total one.
230 * Examples are: a) Give a gift of X credits to the user, b) User bought a book, so charge for the book price.
233 case object OnceChargingBehavior
234 extends ChargingBehavior(
235 ChargingBehaviorNames.once,
236 Set(ChargingBehaviorNameInput, CurrentValueInput)
240 * This is called when we have the very first event for a particular resource instance, and we want to know
241 * if it is billable or not.
243 def isBillableFirstEvent(event: ResourceEventModel) = {
247 def mustGenerateDummyFirstEvent = false // no need to
249 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]) = {
253 def getResourceInstanceInitialAmount = 0.0
255 def supportsImplicitEvents = false
257 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = false
259 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, occurredMillis: Long) = {
260 throw new AquariumException("constructImplicitEndEventFor() Not compliant with %s".format(this))
265 * In practice a resource usage will be charged for the total amount of usage
266 * between resource usage changes.
268 * Example resource that might be adept to a continuous policy
271 case object ContinuousChargingBehavior
272 extends ChargingBehavior(
273 ChargingBehaviorNames.continuous,
274 Set(ChargingBehaviorNameInput, UnitPriceInput, OldTotalAmountInput, TimeDeltaInput)
277 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double = {
278 // If the total is in the details, get it, or else compute it
279 details.get("total") match {
284 oldAmount + newEventValue
288 def getResourceInstanceInitialAmount: Double = {
293 * This is called when we have the very first event for a particular resource instance, and we want to know
294 * if it is billable or not.
296 def isBillableFirstEvent(event: ResourceEventModel) = {
300 def mustGenerateDummyFirstEvent = true
302 def supportsImplicitEvents = {
306 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
310 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long) = {
311 assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
313 val details = resourceEvent.details
314 val newDetails = ResourceEventModel.setAquariumSyntheticAndImplicitEnd(details)
316 resourceEvent.withDetails(newDetails, newOccurredMillis)
321 * An onoff charging behavior expects a resource to be in one of the two allowed
322 * states (`on` and `off`, respectively). It will charge for resource usage
323 * within the timeframes specified by consecutive on and off resource events.
324 * An onoff policy is the same as a continuous policy, except for
325 * the timeframes within the resource is in the `off` state.
327 * Example resources that might be adept to onoff policies are VMs in a
328 * cloud application and books in a book lending application.
330 case object OnOffChargingBehavior
331 extends ChargingBehavior(
332 ChargingBehaviorNames.onoff,
333 Set(ChargingBehaviorNameInput, UnitPriceInput, TimeDeltaInput)
338 * @param oldAmount is ignored
339 * @param newEventValue
342 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double = {
346 def getResourceInstanceInitialAmount: Double = {
351 def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
352 import OnOffChargingBehaviorValues.{ON, OFF}
354 def exception(rs: OnOffPolicyResourceState) =
355 new AquariumException("Resource state transition error (%s -> %s)".format(rs, rs))
357 (oldAmount, newEventValue) match {
359 throw exception(OnResourceState)
365 throw exception(OffResourceState)
369 override def isBillableEvent(event: ResourceEventModel) = {
370 // ON events do not contribute, only OFF ones.
371 OnOffChargingBehaviorValues.isOFFValue(event.value)
375 * This is called when we have the very first event for a particular resource instance, and we want to know
376 * if it is billable or not.
378 def isBillableFirstEvent(event: ResourceEventModel) = {
382 def mustGenerateDummyFirstEvent = false // should be handled by the implicit OFFs
384 def supportsImplicitEvents = {
388 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
389 // If we have ON events with no OFF companions at the end of the billing period,
390 // then we must generate implicit OFF events.
391 OnOffChargingBehaviorValues.isONValue(resourceEvent.value)
394 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long) = {
395 assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
396 assert(OnOffChargingBehaviorValues.isONValue(resourceEvent.value))
398 val details = resourceEvent.details
399 val newDetails = ResourceEventModel.setAquariumSyntheticAndImplicitEnd(details)
400 val newValue = OnOffChargingBehaviorValues.OFF
402 resourceEvent.withDetailsAndValue(newDetails, newValue, newOccurredMillis)
405 def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
406 throw new AquariumInternalError("constructImplicitStartEventFor() Not compliant with %s".format(this))
410 object OnOffChargingBehaviorValues {
414 def isONValue (value: Double) = value == ON
415 def isOFFValue(value: Double) = value == OFF
419 * An discrete charging behavior indicates that a resource should be charged directly
420 * at each resource state change, i.e. the charging is not dependent on
421 * the time the resource.
423 * Example oneoff resources might be individual charges applied to various
424 * actions (e.g. the fact that a user has created an account) or resources
425 * that should be charged per volume once (e.g. the allocation of a volume)
427 case object DiscreteChargingBehavior
428 extends ChargingBehavior(
429 ChargingBehaviorNames.discrete,
430 Set(ChargingBehaviorNameInput, UnitPriceInput, CurrentValueInput)
433 def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double = {
434 oldAmount + newEventValue
437 def getResourceInstanceInitialAmount: Double = {
442 * This is called when we have the very first event for a particular resource instance, and we want to know
443 * if it is billable or not.
445 def isBillableFirstEvent(event: ResourceEventModel) = {
446 false // nope, we definitely need a previous one.
449 // FIXME: Check semantics of this. I just put false until thorough study
450 def mustGenerateDummyFirstEvent = false
452 def supportsImplicitEvents = {
456 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
460 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, occurredMillis: Long) = {
461 throw new AquariumInternalError("constructImplicitEndEventFor() Not compliant with %s".format(this))