Finer-grained handling of implictly issued events
[aquarium] / src / main / scala / gr / grnet / aquarium / logic / accounting / dsl / DSLCostPolicy.scala
1 /*
2  * Copyright 2011 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.logic.events.ResourceEvent
40
41 /**
42  * A cost policy indicates how charging for a resource will be done
43  * wrt the various states a resource can be.
44  *
45  * @author Georgios Gousios <gousiosg@gmail.com>
46  * @author Christos KK Loverdos <loverdos@gmail.com>
47  */
48
49 abstract class DSLCostPolicy(val name: String, val vars: Set[DSLCostPolicyVar]) extends DSLItem {
50
51   def varNames = vars.map(_.name)
52
53   /**
54    * Generate a map where the key is a [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicyVar]]
55    * and the value the respective value. This map will be used to do the actual credit charge calculation
56    * by the respective algorithm.
57    *
58    * Values are obtained from a corresponding context, which is provided by the parameters. We assume that this context
59    * has been validated before the call to `makeValueMap` is made.
60    *
61    * @param totalCredits   the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLTotalCreditsVar]]
62    * @param oldTotalAmount the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLOldTotalAmountVar]]
63    * @param newTotalAmount the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLNewTotalAmountVar]]
64    * @param timeDelta      the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLTimeDeltaVar]]
65    * @param previousValue  the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLPreviousValueVar]]
66    * @param currentValue   the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLCurrentValueVar]]
67    * @param unitPrice      the value for [[gr.grnet.aquarium.logic.accounting.dsl.DSLUnitPriceVar]]
68    *
69    * @return a map from [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicyVar]]s to respective values.
70    */
71   def makeValueMap(costPolicyName: String,
72                    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       costPolicyName,
83       totalCredits,
84       oldTotalAmount,
85       newTotalAmount,
86       timeDelta,
87       previousValue,
88       currentValue,
89       unitPrice)
90   }
91
92   def isOnOff: Boolean = isNamed(DSLCostPolicyNames.onoff)
93
94   def isContinuous: Boolean = isNamed(DSLCostPolicyNames.continuous)
95
96   def isDiscrete: Boolean = isNamed(DSLCostPolicyNames.discrete)
97   
98   def isOnce: Boolean = isNamed(DSLCostPolicyNames.once)
99
100   def isNamed(aName: String): Boolean = aName == name
101
102   def needsPreviousEventForCreditAndAmountCalculation: Boolean = {
103     // If we need any variable that is related to the previous event
104     // then we do need a previous event
105     vars.exists(_.isDirectlyRelatedToPreviousEvent)
106   }
107
108   /**
109    * Given the old amount of a resource instance (see [[gr.grnet.aquarium.user.ResourceInstanceSnapshot]]) and the
110    * value arriving in a new resource event, compute the new instance amount.
111    *
112    * Note that the `oldAmount` does not make sense for all types of [[gr.grnet.aquarium.logic.accounting.dsl.DSLCostPolicy]],
113    * in which case it is ignored.
114    *
115    * @param oldAmount the old accumulating amount
116    * @param newEventValue the value contained in a newly arrived [[gr.grnet.aquarium.logic.events.ResourceEvent]]
117    * @return
118    */
119   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double
120
121   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double
122
123   /**
124    * The initial amount.
125    */
126   def getResourceInstanceInitialAmount: Double
127
128   /**
129    * The amount used when no amount is meant to be relevant.
130    *
131    * For example, when there is no need for a previous event but an API requires the amount of the previous event.
132    *
133    * Normally, this value will never be used by client code (= charge computation code).
134    */
135   def getResourceInstanceUndefinedAmount: Double = -1.0
136
137   /**
138    * Get the value that will be used in credit calculation in Accounting.chargeEvents
139    */
140   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double]
141
142   /**
143    * An event's value by itself should carry enough info to characterize it billable or not.
144    *
145    * Typically all events are billable by default and indeed this is the default implementation
146    * provided here.
147    *
148    * The only exception to the rule is ON events for [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]].
149    */
150   def isBillableEventBasedOnValue(eventValue: Double): Boolean = true
151
152   /**
153    * This is called when we have the very first event for a particular resource instance, and we want to know
154    * if it is billable or not.
155    *
156    * @param eventValue
157    * @return
158    */
159   def isBillableFirstEventBasedOnValue(eventValue: Double): Boolean
160   
161   /**
162    * There are resources (cost policies) for which implicit events must be generated at the end of the billing period
163    * and also at the beginning of the next one. For these cases, this method must return `true`.
164    *
165    * The motivating example comes from the [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]] for which we
166    * must implicitly assume `OFF` events at the end of the billing period and `ON` events at the beginning of the next
167    * one.
168    *
169    */
170   def supportsImplicitEvents: Boolean
171
172   def mustConstructImplicitEndEventFor(eventValue: Double): Boolean
173
174   @throws(classOf[Exception])
175   def constructImplicitEndEventFor(resourceEvent: ResourceEvent): ResourceEvent
176
177   @throws(classOf[Exception])
178   def constructImplicitStartEventFor(resourceEvent: ResourceEvent): ResourceEvent
179 }
180
181 object DSLCostPolicyNames {
182   final val onoff      = "onoff"
183   final val discrete   = "discrete"
184   final val continuous = "continuous"
185   final val once       = "once"
186 }
187
188 object DSLCostPolicy {
189   def apply(name: String): DSLCostPolicy  = {
190     name match {
191       case null ⇒
192         throw new DSLParseException("<null> cost policy")
193
194       case name ⇒ name.toLowerCase match {
195         case DSLCostPolicyNames.onoff      ⇒ OnOffCostPolicy
196         case DSLCostPolicyNames.discrete   ⇒ DiscreteCostPolicy
197         case DSLCostPolicyNames.continuous ⇒ ContinuousCostPolicy
198         case DSLCostPolicyNames.once       ⇒ ContinuousCostPolicy
199
200         case _ ⇒
201           throw new DSLParseException("Invalid cost policy %s".format(name))
202       }
203     }
204   }
205
206   def makeValueMapFor(costPolicy: DSLCostPolicy,
207                       costPolicyName: String,
208                       totalCredits: Double,
209                       oldTotalAmount: Double,
210                       newTotalAmount: Double,
211                       timeDelta: Double,
212                       previousValue: Double,
213                       currentValue: Double,
214                       unitPrice: Double): Map[DSLCostPolicyVar, Any] = {
215     val vars = costPolicy.vars
216     var map = Map[DSLCostPolicyVar, Any]()
217
218     if(vars contains DSLCostPolicyNameVar) map += DSLCostPolicyNameVar -> costPolicyName
219     if(vars contains DSLTotalCreditsVar  ) map += DSLTotalCreditsVar   -> totalCredits
220     if(vars contains DSLOldTotalAmountVar) map += DSLOldTotalAmountVar -> oldTotalAmount
221     if(vars contains DSLNewTotalAmountVar) map += DSLNewTotalAmountVar -> newTotalAmount
222     if(vars contains DSLTimeDeltaVar     ) map += DSLTimeDeltaVar      -> timeDelta
223     if(vars contains DSLPreviousValueVar ) map += DSLPreviousValueVar  -> previousValue
224     if(vars contains DSLCurrentValueVar  ) map += DSLCurrentValueVar   -> currentValue
225     if(vars contains DSLUnitPriceVar     ) map += DSLUnitPriceVar      -> unitPrice
226
227     map
228   }
229 }
230
231 /**
232  * A cost policy for which resource events just carry a credit amount that will be added to the total one.
233  *
234  * Examples are: a) Give a gift of X credits to the user, b) User bought a book, so charge for the book price.
235  *
236  */
237 case object OnceCostPolicy
238   extends DSLCostPolicy(DSLCostPolicyNames.once, Set(DSLCostPolicyNameVar, DSLCurrentValueVar)) {
239
240   def supportsImplicitEvents = false
241
242   def mustConstructImplicitEndEventFor(eventValue: Double) = false
243
244   def isBillableFirstEventBasedOnValue(eventValue: Double) = true
245
246   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double) = oldAmount
247
248   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double) = getResourceInstanceInitialAmount
249
250   def getResourceInstanceInitialAmount = 0.0
251
252   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double) = Just(newEventValue)
253
254   def constructImplicitEndEventFor(resourceEvent: ResourceEvent) = {
255     throw new Exception("constructImplicitEndEventFor() Not compliant with %s".format(this))
256   }
257
258   def constructImplicitStartEventFor(resourceEvent: ResourceEvent) = {
259     throw new Exception("constructImplicitStartEventFor() Not compliant with %s".format(this))
260   }
261 }
262
263 /**
264  * In practice a resource usage will be charged for the total amount of usage
265  * between resource usage changes.
266  *
267  * Example resource that might be adept to a continuous policy
268  * is diskspace.
269  */
270 case object ContinuousCostPolicy
271   extends DSLCostPolicy(DSLCostPolicyNames.continuous,
272                         Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLOldTotalAmountVar, DSLTimeDeltaVar)) {
273
274   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
275     oldAmount + newEventValue
276   }
277
278   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
279     oldAmount
280   }
281
282   def getResourceInstanceInitialAmount: Double = {
283     0.0
284   }
285
286   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
287     oldAmountM
288   }
289
290   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
291     false
292   }
293
294   def supportsImplicitEvents = {
295     false
296   }
297
298   def mustConstructImplicitEndEventFor(eventValue: Double) = false
299
300   def constructImplicitEndEventFor(resourceEvent: ResourceEvent) = {
301     throw new Exception("constructImplicitEndEventFor() Not compliant with %s".format(this))
302   }
303
304   def constructImplicitStartEventFor(resourceEvent: ResourceEvent) = {
305     throw new Exception("constructImplicitStartEventFor() Not compliant with %s".format(this))
306   }
307 }
308
309 /**
310  * An onoff cost policy expects a resource to be in one of the two allowed
311  * states (`on` and `off`, respectively). It will charge for resource usage
312  * within the timeframes specified by consecutive on and off resource events.
313  * An onoff policy is the same as a continuous policy, except for
314  * the timeframes within the resource is in the `off` state.
315  *
316  * Example resources that might be adept to onoff policies are VMs in a
317  * cloud application and books in a book lending application.
318  */
319 case object OnOffCostPolicy
320   extends DSLCostPolicy(DSLCostPolicyNames.onoff,
321                         Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLTimeDeltaVar)) {
322
323   /**
324    *
325    * @param oldAmount is ignored
326    * @param newEventValue
327    * @return
328    */
329   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
330     newEventValue
331   }
332   
333   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
334     import OnOffCostPolicyValues.{ON, OFF}
335     oldAmount match {
336       case ON  ⇒ /* implicit off at the end of the billing period */ OFF
337       case OFF ⇒ OFF
338     }
339   }
340
341   def getResourceInstanceInitialAmount: Double = {
342     0.0
343   }
344   
345   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
346     oldAmountM match {
347       case Just(oldAmount) ⇒
348         Maybe(getValueForCreditCalculation(oldAmount, newEventValue))
349       case NoVal ⇒
350         Failed(new Exception("NoVal for oldValue instead of Just"))
351       case Failed(e, m) ⇒
352         Failed(new Exception("Failed for oldValue instead of Just", e), m)
353     }
354   }
355
356   private[this]
357   def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
358     import OnOffCostPolicyValues.{ON, OFF}
359
360     def exception(rs: OnOffPolicyResourceState) =
361       new Exception("Resource state transition error (%s -> %s)".format(rs, rs))
362
363     (oldAmount, newEventValue) match {
364       case (ON, ON) ⇒
365         throw exception(OnResourceState)
366       case (ON, OFF) ⇒
367         OFF
368       case (OFF, ON) ⇒
369         ON
370       case (OFF, OFF) ⇒
371         throw exception(OffResourceState)
372     }
373   }
374
375   override def isBillableEventBasedOnValue(eventValue: Double) = {
376     // ON events do not contribute, only OFF ones.
377     OnOffCostPolicyValues.isOFF(eventValue)
378   }
379
380   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
381     false
382   }
383
384   def supportsImplicitEvents = {
385     true
386   }
387
388
389   def mustConstructImplicitEndEventFor(eventValue: Double) = {
390     // If we have ON events with no OFF companions at the end of the billing period,
391     // then we must generate implicit OFF events.
392     OnOffCostPolicyValues.isON(eventValue)
393   }
394
395   def constructImplicitEndEventFor(resourceEvent: ResourceEvent) = {
396     throw new Exception("constructImplicitEndEventFor() Not compliant with %s".format(this))
397   }
398
399   def constructImplicitStartEventFor(resourceEvent: ResourceEvent) = {
400     throw new Exception("constructImplicitStartEventFor() Not compliant with %s".format(this))
401   }
402 }
403
404 object OnOffCostPolicyValues {
405   final val ON : Double = 1.0
406   final val OFF: Double = 0.0
407
408   def isON (value: Double) = value == ON
409   def isOFF(value: Double) = value == OFF
410 }
411
412 /**
413  * An discrete cost policy indicates that a resource should be charged directly
414  * at each resource state change, i.e. the charging is not dependent on
415  * the time the resource.
416  *
417  * Example oneoff resources might be individual charges applied to various
418  * actions (e.g. the fact that a user has created an account) or resources
419  * that should be charged per volume once (e.g. the allocation of a volume)
420  */
421 case object DiscreteCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.discrete,
422                                                      Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLCurrentValueVar)) {
423
424   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
425     oldAmount + newEventValue
426   }
427
428   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double  = {
429     0.0 // ?? def getResourceInstanceInitialAmount
430   }
431
432   def getResourceInstanceInitialAmount: Double = {
433     0.0
434   }
435   
436   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
437     Just(newEventValue)
438   }
439
440   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
441     false // nope, we definitely need a  previous one.
442   }
443
444   def supportsImplicitEvents = {
445     false
446   }
447
448   def mustConstructImplicitEndEventFor(eventValue: Double) = false
449
450   def constructImplicitEndEventFor(resourceEvent: ResourceEvent) = {
451     throw new Exception("constructImplicitEndEventFor() Not compliant with %s".format(this))
452   }
453
454   def constructImplicitStartEventFor(resourceEvent: ResourceEvent) = {
455     throw new Exception("constructImplicitStartEventFor() Not compliant with %s".format(this))
456   }
457 }
458
459 /**
460  * Encapsulates the possible states that a resource with an
461  * [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]]
462  * can be.
463  */
464 abstract class OnOffPolicyResourceState(val state: String) {
465   def isOn: Boolean = !isOff
466   def isOff: Boolean = !isOn
467 }
468
469 object OnOffPolicyResourceState {
470   def apply(name: Any): OnOffPolicyResourceState = {
471     name match {
472       case x: String if (x.equalsIgnoreCase(OnOffPolicyResourceStateNames.on))  => OnResourceState
473       case y: String if (y.equalsIgnoreCase(OnOffPolicyResourceStateNames.off)) => OffResourceState
474       case a: Double if (a == 0) => OffResourceState
475       case b: Double if (b == 1) => OnResourceState
476       case i: Int if (i == 0) => OffResourceState
477       case j: Int if (j == 1) => OnResourceState
478       case _ => throw new DSLParseException("Invalid OnOffPolicyResourceState %s".format(name))
479     }
480   }
481 }
482
483 object OnOffPolicyResourceStateNames {
484   final val on  = "on"
485   final val off = "off"
486 }
487
488 object OnResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.on) {
489   override def isOn = true
490 }
491 object OffResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.off) {
492   override def isOff = true
493 }