547579399e7a4926322a8f327262fb949269e633
[aquarium] / src / main / scala / gr / grnet / aquarium / computation / TimeslotComputations.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.computation
37
38 import collection.immutable.SortedMap
39 import com.ckkloverdos.maybe.{NoVal, Maybe, Just}
40 import gr.grnet.aquarium.util.{ContextualLogger, Loggable}
41 import gr.grnet.aquarium.store.PolicyStore
42 import gr.grnet.aquarium.util.date.MutableDateCalc
43 import gr.grnet.aquarium.{AquariumInternalError, AquariumException}
44 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
45 import gr.grnet.aquarium.logic.accounting.algorithm.CostPolicyAlgorithmCompiler
46 import gr.grnet.aquarium.logic.accounting.dsl.{DSL, DSLResourcesMap, DSLPolicy, DSLResource, DSLPriceList, DSLAlgorithm, DSLAgreement, Timeslot, DSLUtils}
47
48 /**
49  * Methods for converting accounting events to wallet entries.
50  *
51  * @author Georgios Gousios <gousiosg@gmail.com>
52  * @author Christos KK Loverdos <loverdos@gmail.com>
53  */
54 trait TimeslotComputations extends Loggable {
55   // TODO: favour composition over inheritance until we decide what to do with DSLUtils (and TimeslotComputations).
56   protected val dslUtils = new DSLUtils {}
57
58   /**
59    * Breaks a reference timeslot (e.g. billing period) according to policies and agreements.
60    *
61    * @param referenceTimeslot
62    * @param policyTimeslots
63    * @param agreementTimeslots
64    * @return
65    */
66   protected
67   def splitTimeslotByPoliciesAndAgreements(referenceTimeslot: Timeslot,
68                                            policyTimeslots: List[Timeslot],
69                                            agreementTimeslots: List[Timeslot],
70                                            clogM: Maybe[ContextualLogger] = NoVal): List[Timeslot] = {
71
72     //    val clog = ContextualLogger.fromOther(clogM, logger, "splitTimeslotByPoliciesAndAgreements()")
73     //    clog.begin()
74
75     // Align policy and agreement validity timeslots to the referenceTimeslot
76     val alignedPolicyTimeslots = referenceTimeslot.align(policyTimeslots)
77     val alignedAgreementTimeslots = referenceTimeslot.align(agreementTimeslots)
78
79     //    clog.debug("referenceTimeslot = %s", referenceTimeslot)
80     //    clog.debugSeq("alignedPolicyTimeslots", alignedPolicyTimeslots, 0)
81     //    clog.debugSeq("alignedAgreementTimeslots", alignedAgreementTimeslots, 0)
82
83     val result = alignTimeslots(alignedPolicyTimeslots, alignedAgreementTimeslots)
84     //    clog.debugSeq("result", result, 1)
85     //    clog.end()
86     result
87   }
88
89   /**
90    * Given a reference timeslot, we have to break it up to a series of timeslots where a particular
91    * algorithm and price unit is in effect.
92    *
93    */
94   protected
95   def resolveEffectiveAlgorithmsAndPriceLists(alignedTimeslot: Timeslot,
96                                               agreement: DSLAgreement,
97                                               clogOpt: Option[ContextualLogger] = None):
98   (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
99
100     val clog = ContextualLogger.fromOther(clogOpt, logger, "resolveEffectiveAlgorithmsAndPriceLists()")
101
102     // Note that most of the code is taken from calcChangeChunks()
103     val alg = dslUtils.resolveEffectiveAlgorithmsForTimeslot(alignedTimeslot, agreement)
104     val pri = dslUtils.resolveEffectivePricelistsForTimeslot(alignedTimeslot, agreement)
105     val chargeChunks = splitChargeChunks(alg, pri)
106     val algorithmByTimeslot = chargeChunks._1
107     val pricelistByTimeslot = chargeChunks._2
108
109     assert(algorithmByTimeslot.size == pricelistByTimeslot.size)
110
111     (algorithmByTimeslot, pricelistByTimeslot)
112   }
113
114   protected
115   def computeInitialChargeslots(referenceTimeslot: Timeslot,
116                                 dslResource: DSLResource,
117                                 policiesByTimeslot: Map[Timeslot, DSLPolicy],
118                                 agreementNamesByTimeslot: Map[Timeslot, String],
119                                 clogOpt: Option[ContextualLogger] = None): List[Chargeslot] = {
120
121     val clog = ContextualLogger.fromOther(clogOpt, logger, "computeInitialChargeslots()")
122     //    clog.begin()
123
124     val policyTimeslots = policiesByTimeslot.keySet
125     val agreementTimeslots = agreementNamesByTimeslot.keySet
126
127     //    clog.debugMap("policiesByTimeslot", policiesByTimeslot, 1)
128     //    clog.debugMap("agreementNamesByTimeslot", agreementNamesByTimeslot, 1)
129
130     def getPolicy(ts: Timeslot): DSLPolicy = {
131       policiesByTimeslot.find(_._1.contains(ts)).get._2
132     }
133     def getAgreementName(ts: Timeslot): String = {
134       agreementNamesByTimeslot.find(_._1.contains(ts)).get._2
135     }
136
137     // 1. Round ONE: split time according to overlapping policies and agreements.
138     //    clog.begin("ROUND 1")
139     val alignedTimeslots = splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList, Just(clog))
140     //    clog.debugSeq("alignedTimeslots", alignedTimeslots, 1)
141     //    clog.end("ROUND 1")
142
143     // 2. Round TWO: Use the aligned timeslots of Round ONE to produce even more
144     //    fine-grained timeslots according to applicable algorithms.
145     //    Then pack the info into charge slots.
146     //    clog.begin("ROUND 2")
147     val allChargeslots = for {
148       alignedTimeslot <- alignedTimeslots
149     } yield {
150       //      val alignedTimeslotMsg = "alignedTimeslot = %s".format(alignedTimeslot)
151       //      clog.begin(alignedTimeslotMsg)
152
153       val dslPolicy = getPolicy(alignedTimeslot)
154       //      clog.debug("dslPolicy = %s", dslPolicy)
155       val agreementName = getAgreementName(alignedTimeslot)
156       //      clog.debug("agreementName = %s", agreementName)
157       val agreementOpt = dslPolicy.findAgreement(agreementName)
158       //      clog.debug("agreementOpt = %s", agreementOpt)
159
160       agreementOpt match {
161         case None ⇒
162           val errMsg = "Unknown agreement %s during %s".format(agreementName, alignedTimeslot)
163           clog.error("%s", errMsg)
164           throw new AquariumException(errMsg)
165
166         case Some(agreement) ⇒
167           // TODO: Factor this out, just like we did with:
168           // TODO:  val alignedTimeslots = splitTimeslotByPoliciesAndAgreements
169           // Note that most of the code is already taken from calcChangeChunks()
170           val r = resolveEffectiveAlgorithmsAndPriceLists(alignedTimeslot, agreement, Some(clog))
171           val algorithmByTimeslot: Map[Timeslot, DSLAlgorithm] = r._1
172           val pricelistByTimeslot: Map[Timeslot, DSLPriceList] = r._2
173
174           // Now, the timeslots must be the same
175           val finegrainedTimeslots = algorithmByTimeslot.keySet
176
177           val chargeslots = for {
178             finegrainedTimeslot <- finegrainedTimeslots
179           } yield {
180             //            val finegrainedTimeslotMsg = "finegrainedTimeslot = %s".format(finegrainedTimeslot)
181             //            clog.begin(finegrainedTimeslotMsg)
182
183             val dslAlgorithm = algorithmByTimeslot(finegrainedTimeslot) // TODO: is this correct?
184             //            clog.debug("dslAlgorithm = %s", dslAlgorithm)
185             //            clog.debugMap("dslAlgorithm.algorithms", dslAlgorithm.algorithms, 1)
186             val dslPricelist = pricelistByTimeslot(finegrainedTimeslot) // TODO: is this correct?
187             //            clog.debug("dslPricelist = %s", dslPricelist)
188             //            clog.debug("dslResource = %s", dslResource)
189             val algorithmDefOpt = dslAlgorithm.algorithms.get(dslResource)
190             //            clog.debug("algorithmDefOpt = %s", algorithmDefOpt)
191             val priceUnitOpt = dslPricelist.prices.get(dslResource)
192             //            clog.debug("priceUnitOpt = %s", priceUnitOpt)
193
194             val chargeslot = (algorithmDefOpt, priceUnitOpt) match {
195               case (None, None) ⇒
196                 throw new AquariumException(
197                   "Unknown algorithm and price unit for resource %s during %s".
198                     format(dslResource, finegrainedTimeslot))
199               case (None, _) ⇒
200                 throw new AquariumException(
201                   "Unknown algorithm for resource %s during %s".
202                     format(dslResource, finegrainedTimeslot))
203               case (_, None) ⇒
204                 throw new AquariumException(
205                   "Unknown price unit for resource %s during %s".
206                     format(dslResource, finegrainedTimeslot))
207               case (Some(algorithmDefinition), Some(priceUnit)) ⇒
208                 Chargeslot(finegrainedTimeslot.from.getTime, finegrainedTimeslot.to.getTime, algorithmDefinition, priceUnit)
209             }
210
211             //            clog.end(finegrainedTimeslotMsg)
212             chargeslot
213           }
214
215           //          clog.end(alignedTimeslotMsg)
216           chargeslots.toList
217       }
218     }
219     //    clog.end("ROUND 2")
220
221
222     val result = allChargeslots.flatten
223     //    clog.debugSeq("result", allChargeslots, 1)
224     //    clog.end()
225     result
226   }
227
228   /**
229    * Compute the charge slots generated by a particular resource event.
230    *
231    */
232   def computeFullChargeslots(previousResourceEventOpt: Option[ResourceEventModel],
233                              currentResourceEvent: ResourceEventModel,
234                              oldCredits: Double,
235                              oldTotalAmount: Double,
236                              newTotalAmount: Double,
237                              dslResource: DSLResource,
238                              defaultResourceMap: DSLResourcesMap,
239                              agreementNamesByTimeslot: SortedMap[Timeslot, String],
240                              algorithmCompiler: CostPolicyAlgorithmCompiler,
241                              policyStore: PolicyStore,
242                              clogOpt: Option[ContextualLogger] = None): (Timeslot, List[Chargeslot]) = {
243
244     val clog = ContextualLogger.fromOther(clogOpt, logger, "computeFullChargeslots()")
245     //    clog.begin()
246
247     val occurredDate = currentResourceEvent.occurredDate
248     val occurredMillis = currentResourceEvent.occurredMillis
249     val costPolicy = dslResource.costPolicy
250
251     val dsl = new DSL {}
252     val (referenceTimeslot, relevantPolicies, previousValue) = costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
253       // We need a previous event
254       case true ⇒
255         previousResourceEventOpt match {
256           // We have a previous event
257           case Some(previousResourceEvent) ⇒
258             //            clog.debug("Have previous event")
259             //            clog.debug("previousValue = %s", previousResourceEvent.value)
260
261             val referenceTimeslot = Timeslot(previousResourceEvent.occurredDate, occurredDate)
262             //            clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
263
264             // all policies within the interval from previous to current resource event
265             //            clog.debug("Calling policyStore.loadAndSortPoliciesWithin(%s)", referenceTimeslot)
266             val relevantPolicies = policyStore.loadAndSortPoliciesWithin(referenceTimeslot.from.getTime, referenceTimeslot.to.getTime, dsl)
267             //            clog.debugMap("==> relevantPolicies", relevantPolicies, 0)
268
269             (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
270
271           // We do not have a previous event
272           case None ⇒
273             throw new AquariumException(
274               "Unable to charge. No previous event given for %s".
275                 format(currentResourceEvent.toDebugString))
276         }
277
278       // We do not need a previous event
279       case false ⇒
280         // ... so we cannot compute timedelta from a previous event, there is just one chargeslot
281         // referring to (almost) an instant in time
282         //        clog.debug("DO NOT have previous event")
283         val previousValue = costPolicy.getResourceInstanceUndefinedAmount
284         //        clog.debug("previousValue = costPolicy.getResourceInstanceUndefinedAmount = %s", previousValue)
285
286         val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
287         //        clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
288
289         //        clog.debug("Calling policyStore.loadValidPolicyEntryAt(%s)", new MutableDateCalc(occurredMillis))
290         val relevantPolicyOpt = policyStore.loadValidPolicyAt(occurredMillis, dsl)
291         //        clog.debug("  ==> relevantPolicyM = %s", relevantPolicyM)
292
293         val relevantPolicies = relevantPolicyOpt match {
294           case Some(relevantPolicy) ⇒
295             Map(referenceTimeslot -> relevantPolicy)
296
297           case None ⇒
298             throw new AquariumInternalError("No relevant policy found for %s".format(referenceTimeslot))
299         }
300
301         (referenceTimeslot, relevantPolicies, previousValue)
302     }
303
304     val initialChargeslots = computeInitialChargeslots(
305       referenceTimeslot,
306       dslResource,
307       relevantPolicies,
308       agreementNamesByTimeslot,
309       Some(clog)
310     )
311
312     val fullChargeslots = initialChargeslots.map {
313       case chargeslot@Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
314         val execAlgorithm = algorithmCompiler.compile(algorithmDefinition)
315         val valueMap = costPolicy.makeValueMap(
316           oldCredits,
317           oldTotalAmount,
318           newTotalAmount,
319           stopMillis - startMillis,
320           previousValue,
321           currentResourceEvent.value,
322           unitPrice
323         )
324
325         //              clog.debug("execAlgorithm = %s", execAlgorithm)
326         clog.debugMap("valueMap", valueMap, 1)
327
328         // This is it
329         val credits = execAlgorithm.apply(valueMap)
330         chargeslot.copyWithCredits(credits)
331     }
332
333     val result = referenceTimeslot -> fullChargeslots
334
335     result
336   }
337
338   /**
339    * Align charge timeslots between algorithms and pricelists. As algorithm
340    * and pricelists can have different effectivity periods, this method
341    * examines them and splits them as necessary.
342    */
343   private[computation] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
344                                        price: SortedMap[Timeslot, DSLPriceList]):
345   (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
346
347     val zipped = alg.keySet.zip(price.keySet)
348
349     zipped.find(p => !p._1.equals(p._2)) match {
350       case None => (alg, price)
351       case Some(x) =>
352         val algTimeslot = x._1
353         val priTimeslot = x._2
354
355         assert(algTimeslot.from == priTimeslot.from)
356
357         if(algTimeslot.endsAfter(priTimeslot)) {
358           val slices = algTimeslot.slice(priTimeslot.to)
359           val algo = alg.get(algTimeslot).get
360           val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
361           splitChargeChunks(newalg, price)
362         }
363         else {
364           val slices = priTimeslot.slice(priTimeslot.to)
365           val pl = price.get(priTimeslot).get
366           val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
367           splitChargeChunks(alg, newPrice)
368         }
369     }
370   }
371
372   /**
373    * Given two lists of timeslots, produce a list which contains the
374    * set of timeslot slices, as those are defined by
375    * timeslot overlaps.
376    *
377    * For example, given the timeslots a and b below, split them as shown.
378    *
379    * a = |****************|
380    * ^                ^
381    * a.from            a.to
382    * b = |*********|
383    * ^         ^
384    * b.from     b.to
385    *
386    * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
387    */
388   private[computation] def alignTimeslots(a: List[Timeslot],
389                                     b: List[Timeslot]): List[Timeslot] = {
390
391     def safeTail(foo: List[Timeslot]) = foo match {
392       case Nil => List()
393       case x :: Nil => List()
394       case x :: rest => rest
395     }
396
397     if(a.isEmpty) return b
398     if(b.isEmpty) return a
399
400     assert(a.head.from == b.head.from)
401
402     if(a.head.endsAfter(b.head)) {
403       val slice = a.head.slice(b.head.to)
404       slice.head :: alignTimeslots(slice.last :: a.tail, safeTail(b))
405     } else if(b.head.endsAfter(a.head)) {
406       val slice = b.head.slice(a.head.to)
407       slice.head :: alignTimeslots(safeTail(a), slice.last :: b.tail)
408     } else {
409       a.head :: alignTimeslots(safeTail(a), safeTail(b))
410     }
411   }
412 }