Rename package and make-dist with maven offline mode
[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.event.ResourceEvent
40 import gr.grnet.aquarium.AquariumException
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.ResourceEvent]]
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: ResourceEvent): Boolean
172
173   @throws(classOf[Exception])
174   def constructImplicitEndEventFor(resourceEvent: ResourceEvent, newOccurredMillis: Long): ResourceEvent
175
176   @throws(classOf[Exception])
177   def constructImplicitStartEventFor(resourceEvent: ResourceEvent): ResourceEvent
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: ResourceEvent) = false
251
252   def constructImplicitEndEventFor(resourceEvent: ResourceEvent, occurredMillis: Long) = {
253     throw new AquariumException("constructImplicitEndEventFor() Not compliant with %s".format(this))
254   }
255
256   def constructImplicitStartEventFor(resourceEvent: ResourceEvent) = {
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: ResourceEvent) = {
297     true
298   }
299
300   def constructImplicitEndEventFor(resourceEvent: ResourceEvent, newOccurredMillis: Long) = {
301     assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
302
303     val details = resourceEvent.details
304     val newDetails = ResourceEvent.setAquariumSyntheticAndImplicitEnd(details)
305     val newValue   = resourceEvent.value
306
307     resourceEvent.copy(
308       occurredMillis = newOccurredMillis,
309       details = newDetails,
310       value = newValue
311     )
312   }
313
314   def constructImplicitStartEventFor(resourceEvent: ResourceEvent) = {
315     throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
316   }
317 }
318
319 /**
320  * An onoff cost policy expects a resource to be in one of the two allowed
321  * states (`on` and `off`, respectively). It will charge for resource usage
322  * within the timeframes specified by consecutive on and off resource events.
323  * An onoff policy is the same as a continuous policy, except for
324  * the timeframes within the resource is in the `off` state.
325  *
326  * Example resources that might be adept to onoff policies are VMs in a
327  * cloud application and books in a book lending application.
328  */
329 case object OnOffCostPolicy
330   extends DSLCostPolicy(DSLCostPolicyNames.onoff,
331                         Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLTimeDeltaVar)) {
332
333   /**
334    *
335    * @param oldAmount is ignored
336    * @param newEventValue
337    * @return
338    */
339   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
340     newEventValue
341   }
342   
343   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
344     import OnOffCostPolicyValues.{ON, OFF}
345     oldAmount match {
346       case ON  ⇒ /* implicit off at the end of the billing period */ OFF
347       case OFF ⇒ OFF
348     }
349   }
350
351   def getResourceInstanceInitialAmount: Double = {
352     0.0
353   }
354   
355   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
356     oldAmountM match {
357       case Just(oldAmount) ⇒
358         Maybe(getValueForCreditCalculation(oldAmount, newEventValue))
359       case NoVal ⇒
360         Failed(new AquariumException("NoVal for oldValue instead of Just"))
361       case Failed(e) ⇒
362         Failed(new AquariumException("Failed for oldValue instead of Just", e))
363     }
364   }
365
366   private[this]
367   def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
368     import OnOffCostPolicyValues.{ON, OFF}
369
370     def exception(rs: OnOffPolicyResourceState) =
371       new AquariumException("Resource state transition error (%s -> %s)".format(rs, rs))
372
373     (oldAmount, newEventValue) match {
374       case (ON, ON) ⇒
375         throw exception(OnResourceState)
376       case (ON, OFF) ⇒
377         OFF
378       case (OFF, ON) ⇒
379         ON
380       case (OFF, OFF) ⇒
381         throw exception(OffResourceState)
382     }
383   }
384
385   override def isBillableEventBasedOnValue(eventValue: Double) = {
386     // ON events do not contribute, only OFF ones.
387     OnOffCostPolicyValues.isOFFValue(eventValue)
388   }
389
390   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
391     false
392   }
393
394   def supportsImplicitEvents = {
395     true
396   }
397
398
399   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEvent) = {
400     // If we have ON events with no OFF companions at the end of the billing period,
401     // then we must generate implicit OFF events.
402     OnOffCostPolicyValues.isONValue(resourceEvent.value)
403   }
404
405   def constructImplicitEndEventFor(resourceEvent: ResourceEvent, newOccurredMillis: Long) = {
406     assert(supportsImplicitEvents && mustConstructImplicitEndEventFor(resourceEvent))
407     assert(OnOffCostPolicyValues.isONValue(resourceEvent.value))
408
409     val details = resourceEvent.details
410     val newDetails = ResourceEvent.setAquariumSyntheticAndImplicitEnd(details)
411     val newValue   = OnOffCostPolicyValues.OFF
412
413     resourceEvent.copy(
414       occurredMillis = newOccurredMillis,
415       details = newDetails,
416       value = newValue
417     )
418   }
419
420   def constructImplicitStartEventFor(resourceEvent: ResourceEvent) = {
421     throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
422   }
423 }
424
425 object OnOffCostPolicyValues {
426   final val ON  = 1.0
427   final val OFF = 0.0
428
429   def isONValue (value: Double) = value == ON
430   def isOFFValue(value: Double) = value == OFF
431 }
432
433 /**
434  * An discrete cost policy indicates that a resource should be charged directly
435  * at each resource state change, i.e. the charging is not dependent on
436  * the time the resource.
437  *
438  * Example oneoff resources might be individual charges applied to various
439  * actions (e.g. the fact that a user has created an account) or resources
440  * that should be charged per volume once (e.g. the allocation of a volume)
441  */
442 case object DiscreteCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.discrete,
443                                                      Set(DSLCostPolicyNameVar, DSLUnitPriceVar, DSLCurrentValueVar)) {
444
445   def computeNewAccumulatingAmount(oldAmount: Double, newEventValue: Double): Double = {
446     oldAmount + newEventValue
447   }
448
449   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double  = {
450     0.0 // ?? def getResourceInstanceInitialAmount
451   }
452
453   def getResourceInstanceInitialAmount: Double = {
454     0.0
455   }
456   
457   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
458     Just(newEventValue)
459   }
460
461   def isBillableFirstEventBasedOnValue(eventValue: Double) = {
462     false // nope, we definitely need a  previous one.
463   }
464
465   def supportsImplicitEvents = {
466     false
467   }
468
469   def mustConstructImplicitEndEventFor(resourceEvent: ResourceEvent) = {
470     false
471   }
472
473   def constructImplicitEndEventFor(resourceEvent: ResourceEvent, occurredMillis: Long) = {
474     throw new AquariumException("constructImplicitEndEventFor() Not compliant with %s".format(this))
475   }
476
477   def constructImplicitStartEventFor(resourceEvent: ResourceEvent) = {
478     throw new AquariumException("constructImplicitStartEventFor() Not compliant with %s".format(this))
479   }
480 }
481
482 /**
483  * Encapsulates the possible states that a resource with an
484  * [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]]
485  * can be.
486  */
487 abstract class OnOffPolicyResourceState(val state: String) {
488   def isOn: Boolean = !isOff
489   def isOff: Boolean = !isOn
490 }
491
492 object OnOffPolicyResourceState {
493   def apply(name: Any): OnOffPolicyResourceState = {
494     name match {
495       case x: String if (x.equalsIgnoreCase(OnOffPolicyResourceStateNames.on))  => OnResourceState
496       case y: String if (y.equalsIgnoreCase(OnOffPolicyResourceStateNames.off)) => OffResourceState
497       case a: Double if (a == 0) => OffResourceState
498       case b: Double if (b == 1) => OnResourceState
499       case i: Int if (i == 0) => OffResourceState
500       case j: Int if (j == 1) => OnResourceState
501       case _ => throw new DSLParseException("Invalid OnOffPolicyResourceState %s".format(name))
502     }
503   }
504 }
505
506 object OnOffPolicyResourceStateNames {
507   final val on  = "on"
508   final val off = "off"
509 }
510
511 object OnResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.on) {
512   override def isOn = true
513 }
514 object OffResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.off) {
515   override def isOff = true
516 }