eb1c979ab22cc5998b96aeda435411bd43151803
[aquarium] / src / main / scala / gr / grnet / aquarium / charging / ChargingBehavior.scala
1 /*
2  * Copyright 2011-2012 GRNET S.A. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or
5  * without modification, are permitted provided that the following
6  * conditions are met:
7  *
8  *   1. Redistributions of source code must retain the above
9  *      copyright notice, this list of conditions and the following
10  *      disclaimer.
11  *
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.
16  *
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.
29  *
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.
34  */
35
36 package gr.grnet.aquarium.charging
37
38 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
39 import gr.grnet.aquarium.{AquariumInternalError, AquariumException}
40
41 /**
42  * A charging behavior indicates how charging for a resource will be done
43  * wrt the various states a resource can be.
44  *
45  * @author Christos KK Loverdos <loverdos@gmail.com>
46  */
47
48 abstract class ChargingBehavior(val name: String, val inputs: Set[ChargingInput]) {
49
50   final lazy val inputNames = inputs.map(_.name)
51
52   /**
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.
56    *
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.
59    *
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]]
67    *
68    * @return a map from [[gr.grnet.aquarium.charging.ChargingInput]]s to respective values.
69    */
70   def makeValueMap(
71       totalCredits: Double,
72       oldTotalAmount: Double,
73       newTotalAmount: Double,
74       timeDelta: Double,
75       previousValue: Double,
76       currentValue: Double,
77       unitPrice: Double
78   ): Map[ChargingInput, Any] = {
79
80     ChargingBehavior.makeValueMapFor(
81       this,
82       totalCredits,
83       oldTotalAmount,
84       newTotalAmount,
85       timeDelta,
86       previousValue,
87       currentValue,
88       unitPrice)
89   }
90
91   def isNamed(aName: String): Boolean = aName == name
92
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)
97   }
98
99   /**
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.
103    *
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.
106    *
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]]
112    * @return
113    */
114   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double
115
116   /**
117    * The initial amount.
118    */
119   def getResourceInstanceInitialAmount: Double
120
121   /**
122    * The amount used when no amount is meant to be relevant.
123    *
124    * For example, when there is no need for a previous event but an API requires the amount of the previous event.
125    *
126    * Normally, this value will never be used by client code (= charge computation code).
127    */
128   def getResourceInstanceUndefinedAmount: Double = -1.0
129
130   /**
131    * An event carries enough info to characterize it as billable or not.
132    *
133    * Typically all events are billable by default and indeed this is the default implementation
134    * provided here.
135    *
136    * The only exception to the rule is ON events for [[gr.grnet.aquarium.charging.OnOffChargingBehavior]].
137    */
138   def isBillableEvent(event: ResourceEventModel): Boolean = false
139
140   /**
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.
143    */
144   def isBillableFirstEvent(event: ResourceEventModel): Boolean
145
146   def mustGenerateDummyFirstEvent: Boolean
147
148   def dummyFirstEventValue: Double = 0.0
149
150   def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
151     if(!mustGenerateDummyFirstEvent) {
152       throw new AquariumException("constructDummyFirstEventFor() Not compliant with %s".format(this))
153     }
154
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
160     )
161
162     actualFirst.withDetailsAndValue(newDetails, dummyFirstEventValue, newOccurredMillis)
163   }
164
165   /**
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`.
168    *
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
171    * one.
172    *
173    */
174   def supportsImplicitEvents: Boolean
175
176   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel): Boolean
177
178   @throws(classOf[Exception])
179   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel
180 }
181
182 object ChargingBehavior {
183   def apply(name: String): ChargingBehavior  = {
184     name match {
185       case null ⇒
186         throw new AquariumException("<null> charging behavior")
187
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
193
194         case _ ⇒
195           throw new AquariumException("Invalid charging behavior %s".format(name))
196       }
197     }
198   }
199
200   def makeValueMapFor(
201       chargingBehavior: ChargingBehavior,
202       totalCredits: Double,
203       oldTotalAmount: Double,
204       newTotalAmount: Double,
205       timeDelta: Double,
206       previousValue: Double,
207       currentValue: Double,
208       unitPrice: Double
209   ): Map[ChargingInput, Any] = {
210     
211     val inputs = chargingBehavior.inputs
212     var map = Map[ChargingInput, Any]()
213
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
222
223     map
224   }
225 }
226
227 /**
228  * A charging behavior for which resource events just carry a credit amount that will be added to the total one.
229  *
230  * Examples are: a) Give a gift of X credits to the user, b) User bought a book, so charge for the book price.
231  *
232  */
233 case object OnceChargingBehavior
234 extends ChargingBehavior(
235     ChargingBehaviorNames.once,
236     Set(ChargingBehaviorNameInput, CurrentValueInput)
237 ) {
238
239   /**
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.
242    */
243   def isBillableFirstEvent(event: ResourceEventModel) = {
244     true
245   }
246
247   def mustGenerateDummyFirstEvent = false // no need to
248
249   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]) = {
250     oldAmount
251   }
252
253   def getResourceInstanceInitialAmount = 0.0
254
255   def supportsImplicitEvents = false
256
257   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = false
258
259   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, occurredMillis: Long) = {
260     throw new AquariumException("constructImplicitEndEventFor() Not compliant with %s".format(this))
261   }
262 }
263
264 /**
265  * In practice a resource usage will be charged for the total amount of usage
266  * between resource usage changes.
267  *
268  * Example resource that might be adept to a continuous policy
269  * is diskspace.
270  */
271 case object ContinuousChargingBehavior
272 extends ChargingBehavior(
273     ChargingBehaviorNames.continuous,
274     Set(ChargingBehaviorNameInput, UnitPriceInput, OldTotalAmountInput, TimeDeltaInput)
275 ) {
276
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 {
280       case Some(total) ⇒
281         total.toDouble
282
283       case _ ⇒
284         oldAmount + newEventValue
285     }
286   }
287
288   def getResourceInstanceInitialAmount: Double = {
289     0.0
290   }
291
292   /**
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.
295    */
296   def isBillableFirstEvent(event: ResourceEventModel) = {
297     true
298   }
299
300   def mustGenerateDummyFirstEvent = true
301
302   def supportsImplicitEvents = {
303     true
304   }
305
306   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
307     true
308   }
309
310   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long) = {
311     assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
312
313     val details = resourceEvent.details
314     val newDetails = ResourceEventModel.setAquariumSyntheticAndImplicitEnd(details)
315
316     resourceEvent.withDetails(newDetails, newOccurredMillis)
317   }
318 }
319
320 /**
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.
326  *
327  * Example resources that might be adept to onoff policies are VMs in a
328  * cloud application and books in a book lending application.
329  */
330 case object OnOffChargingBehavior
331 extends ChargingBehavior(
332     ChargingBehaviorNames.onoff,
333     Set(ChargingBehaviorNameInput, UnitPriceInput, TimeDeltaInput)
334 ) {
335
336   /**
337    *
338    * @param oldAmount is ignored
339    * @param newEventValue
340    * @return
341    */
342   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double = {
343     newEventValue
344   }
345
346   def getResourceInstanceInitialAmount: Double = {
347     0.0
348   }
349
350   private[this]
351   def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
352     import OnOffChargingBehaviorValues.{ON, OFF}
353
354     def exception(rs: OnOffPolicyResourceState) =
355       new AquariumException("Resource state transition error (%s -> %s)".format(rs, rs))
356
357     (oldAmount, newEventValue) match {
358       case (ON, ON) ⇒
359         throw exception(OnResourceState)
360       case (ON, OFF) ⇒
361         OFF
362       case (OFF, ON) ⇒
363         ON
364       case (OFF, OFF) ⇒
365         throw exception(OffResourceState)
366     }
367   }
368
369   override def isBillableEvent(event: ResourceEventModel) = {
370     // ON events do not contribute, only OFF ones.
371     OnOffChargingBehaviorValues.isOFFValue(event.value)
372   }
373
374   /**
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.
377    */
378   def isBillableFirstEvent(event: ResourceEventModel) = {
379     false
380   }
381
382   def mustGenerateDummyFirstEvent = false // should be handled by the implicit OFFs
383
384   def supportsImplicitEvents = {
385     true
386   }
387
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)
392   }
393
394   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long) = {
395     assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
396     assert(OnOffChargingBehaviorValues.isONValue(resourceEvent.value))
397
398     val details = resourceEvent.details
399     val newDetails = ResourceEventModel.setAquariumSyntheticAndImplicitEnd(details)
400     val newValue   = OnOffChargingBehaviorValues.OFF
401
402     resourceEvent.withDetailsAndValue(newDetails, newValue, newOccurredMillis)
403   }
404
405   def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
406     throw new AquariumInternalError("constructImplicitStartEventFor() Not compliant with %s".format(this))
407   }
408 }
409
410 object OnOffChargingBehaviorValues {
411   final val ON  = 1.0
412   final val OFF = 0.0
413
414   def isONValue (value: Double) = value == ON
415   def isOFFValue(value: Double) = value == OFF
416 }
417
418 /**
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.
422  *
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)
426  */
427 case object DiscreteChargingBehavior
428 extends ChargingBehavior(
429     ChargingBehaviorNames.discrete,
430     Set(ChargingBehaviorNameInput, UnitPriceInput, CurrentValueInput)
431 ) {
432
433   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double, details: Map[String, String]): Double = {
434     oldAmount + newEventValue
435   }
436
437   def getResourceInstanceInitialAmount: Double = {
438     0.0
439   }
440
441   /**
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.
444    */
445   def isBillableFirstEvent(event: ResourceEventModel) = {
446     false // nope, we definitely need a  previous one.
447   }
448
449   // FIXME: Check semantics of this. I just put false until thorough study
450   def mustGenerateDummyFirstEvent = false
451
452   def supportsImplicitEvents = {
453     false
454   }
455
456   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
457     false
458   }
459
460   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, occurredMillis: Long) = {
461     throw new AquariumInternalError("constructImplicitEndEventFor() Not compliant with %s".format(this))
462   }
463 }