7a0b78a1bccddc3b9acdf103383e20c037b97c68
[aquarium] / src / main / scala / gr / grnet / aquarium / logic / accounting / dsl / DSLCostPolicy.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.logic.accounting.dsl
37
38 import com.ckkloverdos.maybe.{NoVal, Failed, Just, Maybe}
39 import gr.grnet.aquarium.AquariumException
40 import gr.grnet.aquarium.event.resource.ResourceEventModel
41
42 /**
43  * A cost policy indicates how charging for a resource will be done
44  * wrt the various states a resource can be.
45  *
46  * @author Georgios Gousios <gousiosg@gmail.com>
47  * @author Christos KK Loverdos <loverdos@gmail.com>
48  */
49
50 abstract class DSLCostPolicy(val name: String, val vars: Set[DSLCostPolicyVar]) extends DSLItem {
51
52   def varNames = vars.map(_.name)
53
54   /**
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.
58    *
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.
61    *
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]]
69    *
70    * @return a map from [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicyVar]]s to respective values.
71    */
72   def makeValueMap(totalCredits: Double,
73                    oldTotalAmount: Double,
74                    newTotalAmount: Double,
75                    timeDelta: Double,
76                    previousValue: Double,
77                    currentValue: Double,
78                    unitPrice: Double): Map[DSLCostPolicyVar, Any] = {
79
80     DSLCostPolicy.makeValueMapFor(
81       this,
82       totalCredits,
83       oldTotalAmount,
84       newTotalAmount,
85       timeDelta,
86       previousValue,
87       currentValue,
88       unitPrice)
89   }
90
91   def isOnOff: Boolean = isNamed(DSLCostPolicyNames.onoff)
92
93   def isContinuous: Boolean = isNamed(DSLCostPolicyNames.continuous)
94
95   def isDiscrete: Boolean = isNamed(DSLCostPolicyNames.discrete)
96   
97   def isOnce: Boolean = isNamed(DSLCostPolicyNames.once)
98
99   def isNamed(aName: String): Boolean = aName == name
100
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)
105   }
106
107   /**
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.
110    *
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.
113    *
114    * @param oldAmount the old accumulating amount
115    * @param newEventValue the value contained in a newly arrived [[gr.grnet.aquarium.event.resource.ResourceEventModel]]
116    * @return
117    */
118   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double
119
120   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double
121
122   /**
123    * The initial amount.
124    */
125   def getResourceInstanceInitialAmount: Double
126
127   /**
128    * The amount used when no amount is meant to be relevant.
129    *
130    * For example, when there is no need for a previous event but an API requires the amount of the previous event.
131    *
132    * Normally, this value will never be used by client code (= charge computation code).
133    */
134   def getResourceInstanceUndefinedAmount: Double = -1.0
135
136   /**
137    * Get the value that will be used in credit calculation in Accounting.chargeEvents
138    */
139   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double]
140
141   /**
142    * An event's value by itself should carry enough info to characterize it billable or not.
143    *
144    * Typically all events are billable by default and indeed this is the default implementation
145    * provided here.
146    *
147    * The only exception to the rule is ON events for [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]].
148    */
149   def isBillableEventBasedOnValue(eventValue: Double): Boolean = true
150
151   /**
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.
154    *
155    * @param eventValue
156    * @return
157    */
158   def isBillableFirstEventBasedOnValue(eventValue: Double): Boolean
159   
160   /**
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`.
163    *
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
166    * one.
167    *
168    */
169   def supportsImplicitEvents: Boolean
170
171   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel): Boolean
172
173   @throws(classOf[Exception])
174   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel
175
176   @throws(classOf[Exception])
177   def constructImplicitStartEventFor(resourceEvent: ResourceEventModel): ResourceEventModel
178 }
179
180 object DSLCostPolicyNames {
181   final val onoff      = "onoff"
182   final val discrete   = "discrete"
183   final val continuous = "continuous"
184   final val once       = "once"
185 }
186
187 object DSLCostPolicy {
188   def apply(name: String): DSLCostPolicy  = {
189     name match {
190       case null ⇒
191         throw new DSLParseException("<null> cost policy")
192
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
198
199         case _ ⇒
200           throw new DSLParseException("Invalid cost policy %s".format(name))
201       }
202     }
203   }
204
205   def makeValueMapFor(costPolicy: DSLCostPolicy,
206                       totalCredits: Double,
207                       oldTotalAmount: Double,
208                       newTotalAmount: Double,
209                       timeDelta: Double,
210                       previousValue: Double,
211                       currentValue: Double,
212                       unitPrice: Double): Map[DSLCostPolicyVar, Any] = {
213     val vars = costPolicy.vars
214     var map = Map[DSLCostPolicyVar, Any]()
215
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
224
225     map
226   }
227 }
228
229 /**
230  * A cost policy for which resource events just carry a credit amount that will be added to the total one.
231  *
232  * Examples are: a) Give a gift of X credits to the user, b) User bought a book, so charge for the book price.
233  *
234  */
235 case object OnceCostPolicy
236   extends DSLCostPolicy(DSLCostPolicyNames.once, Set(DSLCostPolicyNameVar, DSLCurrentValueVar)) {
237
238   def isBillableFirstEventBasedOnValue(eventValue: Double) = true
239
240   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double) = oldAmount
241
242   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double) = getResourceInstanceInitialAmount
243
244   def getResourceInstanceInitialAmount = 0.0
245
246   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double) = Just(newEventValue)
247
248   def supportsImplicitEvents = false
249
250   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = false
251
252   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, occurredMillis: Long) = {
253     throw new AquariumException("constructImplicitEndEventFor() Not compliant with %s".format(this))
254   }
255
256   def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
257     throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
258   }
259 }
260
261 /**
262  * In practice a resource usage will be charged for the total amount of usage
263  * between resource usage changes.
264  *
265  * Example resource that might be adept to a continuous policy
266  * is diskspace.
267  */
268 case object ContinuousCostPolicy
269   extends DSLCostPolicy(DSLCostPolicyNames.continuous,
270                         Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLOldTotalAmountVar, DSLTimeDeltaVar)) {
271
272   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
273     oldAmount + newEventValue
274   }
275
276   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
277     oldAmount
278   }
279
280   def getResourceInstanceInitialAmount: Double = {
281     0.0
282   }
283
284   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
285     oldAmountM
286   }
287
288   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
289     false
290   }
291
292   def supportsImplicitEvents = {
293     true
294   }
295
296   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
297     true
298   }
299
300   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long) = {
301     assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
302
303     val details = resourceEvent.details
304     val newDetails = ResourceEventModel.setAquariumSyntheticAndImplicitEnd(details)
305
306     resourceEvent.withDetails(newDetails, newOccurredMillis)
307   }
308
309   def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
310     throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
311   }
312 }
313
314 /**
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.
320  *
321  * Example resources that might be adept to onoff policies are VMs in a
322  * cloud application and books in a book lending application.
323  */
324 case object OnOffCostPolicy
325   extends DSLCostPolicy(DSLCostPolicyNames.onoff,
326                         Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLTimeDeltaVar)) {
327
328   /**
329    *
330    * @param oldAmount is ignored
331    * @param newEventValue
332    * @return
333    */
334   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
335     newEventValue
336   }
337   
338   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
339     import OnOffCostPolicyValues.{ON, OFF}
340     oldAmount match {
341       case ON  ⇒ /* implicit off at the end of the billing period */ OFF
342       case OFF ⇒ OFF
343     }
344   }
345
346   def getResourceInstanceInitialAmount: Double = {
347     0.0
348   }
349   
350   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
351     oldAmountM match {
352       case Just(oldAmount) ⇒
353         Maybe(getValueForCreditCalculation(oldAmount, newEventValue))
354       case NoVal ⇒
355         Failed(new AquariumException("NoVal for oldValue instead of Just"))
356       case Failed(e) ⇒
357         Failed(new AquariumException("Failed for oldValue instead of Just", e))
358     }
359   }
360
361   private[this]
362   def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
363     import OnOffCostPolicyValues.{ON, OFF}
364
365     def exception(rs: OnOffPolicyResourceState) =
366       new AquariumException("Resource state transition error (%s -> %s)".format(rs, rs))
367
368     (oldAmount, newEventValue) match {
369       case (ON, ON) ⇒
370         throw exception(OnResourceState)
371       case (ON, OFF) ⇒
372         OFF
373       case (OFF, ON) ⇒
374         ON
375       case (OFF, OFF) ⇒
376         throw exception(OffResourceState)
377     }
378   }
379
380   override def isBillableEventBasedOnValue(eventValue: Double) = {
381     // ON events do not contribute, only OFF ones.
382     OnOffCostPolicyValues.isOFFValue(eventValue)
383   }
384
385   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
386     false
387   }
388
389   def supportsImplicitEvents = {
390     true
391   }
392
393
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)
398   }
399
400   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long) = {
401     assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
402     assert(OnOffCostPolicyValues.isONValue(resourceEvent.value))
403
404     val details = resourceEvent.details
405     val newDetails = ResourceEventModel.setAquariumSyntheticAndImplicitEnd(details)
406     val newValue   = OnOffCostPolicyValues.OFF
407
408     resourceEvent.withDetailsAndValue(newDetails, newValue, newOccurredMillis)
409   }
410
411   def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
412     throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
413   }
414 }
415
416 object OnOffCostPolicyValues {
417   final val ON  = 1.0
418   final val OFF = 0.0
419
420   def isONValue (value: Double) = value == ON
421   def isOFFValue(value: Double) = value == OFF
422 }
423
424 /**
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.
428  *
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)
432  */
433 case object DiscreteCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.discrete,
434                                                      Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLCurrentValueVar)) {
435
436   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
437     oldAmount + newEventValue
438   }
439
440   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double  = {
441     0.0 // ?? def getResourceInstanceInitialAmount
442   }
443
444   def getResourceInstanceInitialAmount: Double = {
445     0.0
446   }
447   
448   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
449     Just(newEventValue)
450   }
451
452   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
453     false // nope, we definitely need a  previous one.
454   }
455
456   def supportsImplicitEvents = {
457     false
458   }
459
460   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel) = {
461     false
462   }
463
464   def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, occurredMillis: Long) = {
465     throw new AquariumException("constructImplicitEndEventFor() Not compliant with %s".format(this))
466   }
467
468   def constructImplicitStartEventFor(resourceEvent: ResourceEventModel) = {
469     throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
470   }
471 }
472
473 /**
474  * Encapsulates the possible states that a resource with an
475  * [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]]
476  * can be.
477  */
478 abstract class OnOffPolicyResourceState(val state: String) {
479   def isOn: Boolean = !isOff
480   def isOff: Boolean = !isOn
481 }
482
483 object OnOffPolicyResourceState {
484   def apply(name: Any): OnOffPolicyResourceState = {
485     name match {
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))
493     }
494   }
495 }
496
497 object OnOffPolicyResourceStateNames {
498   final val on  = "on"
499   final val off = "off"
500 }
501
502 object OnResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.on) {
503   override def isOn = true
504 }
505 object OffResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.off) {
506   override def isOff = true
507 }