29078579a92fdf1e7a0598718b78e7a17fd1fe20
[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
40 /**
41  * A cost policy indicates how charging for a resource will be done
42  * wrt the various states a resource can be.
43  *
44  * @author Georgios Gousios <gousiosg@gmail.com>
45  * @author Christos KK Loverdos <loverdos@gmail.com>
46  */
47
48 abstract class DSLCostPolicy(val name: String) extends DSLItem {
49
50   def isOnOff: Boolean = isNamed(DSLCostPolicyNames.onoff)
51
52   def isContinuous: Boolean = isNamed(DSLCostPolicyNames.continuous)
53
54   def isDiscrete: Boolean = isNamed(DSLCostPolicyNames.discrete)
55   
56   def isNamed(aName: String): Boolean = aName == name
57   
58   def needsPreviousEventForCreditCalculation: Boolean
59
60   /**
61    * True if the resource event has an absolute value.
62    */
63   def resourceEventValueIsAbs: Boolean = !resourceEventValueIsDiff
64   def resourceEventValueIsDiff: Boolean = !resourceEventValueIsAbs
65
66   /**
67    * True if an absolute value is needed for the credit calculation (for example: diskspace).
68    * An absolute value is obtained using the beginning of the billing period as a reference point
69    * and is modified every time a new resource event arrives.
70    *
71    * At the beginning of the billing period, a resource's absolute value can either:
72    * a) Set to zero
73    * b) Set to a non-zero, constant value
74    * c) Set to a varying value, based on some function
75    * d) Not change at all. For this case, we need some initial value to start with.
76    */
77   def needsAbsValueForCreditCalculation: Boolean = !needsDiffValueForCreditCalculation
78   def needsDiffValueForCreditCalculation: Boolean = !needsAbsValueForCreditCalculation
79
80   /**
81    * Given the old amount of a resource instance (see [[gr.grnet.aquarium.user.ResourceInstanceSnapshot]]) and the
82    * value arriving in a new resource event, compute the new instance amount.
83    */
84   def computeNewResourceInstanceAmount(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double]
85
86   def computeNewResourceInstanceAmount(oldAmount: Double, newEventValue: Double): Double
87
88   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double
89
90   /**
91    * Th every initial amount.
92    */
93   def getResourceInstanceInitialAmount: Double
94
95   /**
96    * Get the value that will be used in credit calculation in Accounting.chargeEvents
97    */
98   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double]
99
100   def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double
101
102   /**
103    * An event's value by itself should carry enough info to characterize it billable or not.
104    *
105    * Typically all events are billable by default and indeed this is the default implementation
106    * provided here.
107    */
108   def isBillableEventBasedOnValue(newEventValue: Double): Boolean = true
109 }
110
111 object DSLCostPolicyNames {
112   final val onoff      = "onoff"
113   final val discrete   = "discrete"
114   final val continuous = "continuous"
115 }
116
117 object DSLCostPolicy {
118   def apply(name: String): DSLCostPolicy  = {
119     name match {
120       case null ⇒
121         throw new DSLParseException("<null> cost policy")
122
123       case name ⇒ name.toLowerCase match {
124         case DSLCostPolicyNames.onoff      ⇒ OnOffCostPolicy
125         case DSLCostPolicyNames.discrete   ⇒ DiscreteCostPolicy
126         case DSLCostPolicyNames.continuous ⇒ ContinuousCostPolicy
127
128         case _ ⇒
129           throw new DSLParseException("Invalid cost policy %s".format(name))
130       }
131     }
132   }
133 }
134
135 /**
136  * In practice a resource usage will be charged for the total amount of usage
137  * between resource usage changes.
138  *
139  * Example resource that might be adept to a continuous policy
140  * is diskspace.
141  */
142 case object ContinuousCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.continuous) {
143   def needsPreviousEventForCreditCalculation: Boolean = true
144
145   override def needsAbsValueForCreditCalculation = true
146
147   override def resourceEventValueIsDiff = true
148
149   def computeNewResourceInstanceAmount(oldAmountM: Maybe[Double], newEventValue: Double) = {
150     oldAmountM match {
151       case Just(oldAmount) ⇒
152         Just(oldAmount + newEventValue)
153       case NoVal ⇒
154         Failed(new Exception("NoVal for oldValue instead of Just"))
155       case Failed(e, m) ⇒
156         Failed(new Exception("Failed for oldValue instead of Just", e), m)
157     }
158   }
159
160   def computeNewResourceInstanceAmount(oldAmount: Double, newEventValue: Double): Double = {
161     oldAmount + newEventValue
162   }
163
164   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
165     oldAmount
166   }
167
168   def getResourceInstanceInitialAmount: Double = {
169     0.0
170   }
171
172   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
173     oldAmountM
174   }
175
176   def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
177     oldAmount
178   }
179
180 }
181
182 /**
183  * An onoff cost policy expects a resource to be in one of the two allowed
184  * states (`on` and `off`, respectively). It will charge for resource usage
185  * within the timeframes specified by consecutive on and off resource events.
186  * An onoff policy is the same as a continuous policy, except for
187  * the timeframes within the resource is in the `off` state.
188  *
189  * Example resources that might be adept to onoff policies are VMs in a
190  * cloud application and books in a book lending application.
191  */
192 case object OnOffCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.onoff) {
193   def needsPreviousEventForCreditCalculation: Boolean = true
194
195   override def needsAbsValueForCreditCalculation = true
196
197   override def resourceEventValueIsAbs = true
198
199   def computeNewResourceInstanceAmount(oldAmountM: Maybe[Double], newEventValue: Double) = {
200     Just(newEventValue)
201   }
202
203   def computeNewResourceInstanceAmount(oldAmount: Double, newEventValue: Double): Double = {
204     newEventValue
205   }
206   
207   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double = {
208     import OnOffCostPolicyValues.{ON, OFF}
209     oldAmount match {
210       case ON  ⇒ /* implicit off at the end of the billing period */ OFF
211       case OFF ⇒ OFF
212     }
213   }
214
215   def getResourceInstanceInitialAmount: Double = {
216     0.0
217   }
218   
219   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
220     oldAmountM match {
221       case Just(oldAmount) ⇒
222         Maybe(getValueForCreditCalculation(oldAmount, newEventValue))
223       case NoVal ⇒
224         Failed(new Exception("NoVal for oldValue instead of Just"))
225       case Failed(e, m) ⇒
226         Failed(new Exception("Failed for oldValue instead of Just", e), m)
227     }
228   }
229
230   def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
231     import OnOffCostPolicyValues.{ON, OFF}
232
233     def exception(rs: OnOffPolicyResourceState) =
234       new Exception("Resource state transition error (%s -> %s)".format(rs, rs))
235
236     (oldAmount, newEventValue) match {
237       case (ON, ON) ⇒
238         throw exception(OnResourceState)
239       case (ON, OFF) ⇒
240         OFF
241       case (OFF, ON) ⇒
242         ON
243       case (OFF, OFF) ⇒
244         throw exception(OffResourceState)
245     }
246   }
247
248   override def isBillableEventBasedOnValue(newEventValue: Double) = {
249     OnOffCostPolicyValues.isOFF(newEventValue)
250   }
251 }
252
253 object OnOffCostPolicyValues {
254   final val ON : Double = 1.0
255   final val OFF: Double = 0.0
256
257   def isON (value: Double) = value == ON
258   def isOFF(value: Double) = value == OFF
259 }
260
261 /**
262  * An discrete cost policy indicates that a resource should be charged directly
263  * at each resource state change, i.e. the charging is not dependent on
264  * the time the resource.
265  *
266  * Example oneoff resources might be individual charges applied to various
267  * actions (e.g. the fact that a user has created an account) or resources
268  * that should be charged per volume once (e.g. the allocation of a volume)
269  */
270 case object DiscreteCostPolicy extends DSLCostPolicy(DSLCostPolicyNames.discrete) {
271   def needsPreviousEventForCreditCalculation: Boolean = false
272
273   override def needsDiffValueForCreditCalculation = true
274
275   override def resourceEventValueIsDiff = true
276
277   def computeNewResourceInstanceAmount(oldAmountM: Maybe[Double], newEventValue: Double) = {
278     oldAmountM.map(_ + newEventValue)
279   }
280
281   def computeNewResourceInstanceAmount(oldAmount: Double, newEventValue: Double): Double = {
282     oldAmount + newEventValue
283   }
284
285   def computeResourceInstanceAmountForNewBillingPeriod(oldAmount: Double): Double  = {
286     0.0 // ?? def getResourceInstanceInitialAmount
287   }
288
289   def getResourceInstanceInitialAmount: Double = {
290     0.0
291   }
292   
293   def getValueForCreditCalculation(oldAmountM: Maybe[Double], newEventValue: Double): Maybe[Double] = {
294     Just(newEventValue)
295   }
296
297   def getValueForCreditCalculation(oldAmount: Double, newEventValue: Double): Double = {
298     newEventValue
299   }
300 }
301
302 /**
303  * Encapsulates the possible states that a resource with an
304  * [[gr.grnet.aquarium.logic.accounting.dsl.OnOffCostPolicy]]
305  * can be.
306  */
307 abstract class OnOffPolicyResourceState(val state: String) {
308   def isOn: Boolean = !isOff
309   def isOff: Boolean = !isOn
310 }
311
312 object OnOffPolicyResourceState {
313   def apply(name: Any): OnOffPolicyResourceState = {
314     name match {
315       case x: String if (x.equalsIgnoreCase(OnOffPolicyResourceStateNames.on))  => OnResourceState
316       case y: String if (y.equalsIgnoreCase(OnOffPolicyResourceStateNames.off)) => OffResourceState
317       case a: Double if (a == 0) => OffResourceState
318       case b: Double if (b == 1) => OnResourceState
319       case i: Int if (i == 0) => OffResourceState
320       case j: Int if (j == 1) => OnResourceState
321       case _ => throw new DSLParseException("Invalid OnOffPolicyResourceState %s".format(name))
322     }
323   }
324 }
325
326 object OnOffPolicyResourceStateNames {
327   final val on  = "on"
328   final val off = "off"
329 }
330
331 object OnResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.on) {
332   override def isOn = true
333 }
334 object OffResourceState extends OnOffPolicyResourceState(OnOffPolicyResourceStateNames.off) {
335   override def isOff = true
336 }