2edf20b0d760b2e4c99ef2366798c37e38b62721
[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(
233       previousResourceEventOpt: Option[ResourceEventModel],
234       currentResourceEvent: ResourceEventModel,
235       oldCredits: Double,
236       oldTotalAmount: Double,
237       newTotalAmount: Double,
238       dslResource: DSLResource,
239       defaultResourceMap: DSLResourcesMap,
240       agreementNamesByTimeslot: SortedMap[Timeslot, String],
241       algorithmCompiler: CostPolicyAlgorithmCompiler,
242       policyStore: PolicyStore,
243       clogOpt: Option[ContextualLogger] = None
244   ): (Timeslot, List[Chargeslot]) = {
245
246     val clog = ContextualLogger.fromOther(clogOpt, logger, "computeFullChargeslots()")
247     //    clog.begin()
248
249     val occurredDate = currentResourceEvent.occurredDate
250     val occurredMillis = currentResourceEvent.occurredMillis
251     val costPolicy = dslResource.costPolicy
252
253     val dsl = new DSL {}
254     val (referenceTimeslot, relevantPolicies, previousValue) = costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
255       // We need a previous event
256       case true ⇒
257         previousResourceEventOpt match {
258           // We have a previous event
259           case Some(previousResourceEvent) ⇒
260             //            clog.debug("Have previous event")
261             //            clog.debug("previousValue = %s", previousResourceEvent.value)
262
263             val referenceTimeslot = Timeslot(previousResourceEvent.occurredDate, occurredDate)
264             //            clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
265
266             // all policies within the interval from previous to current resource event
267             //            clog.debug("Calling policyStore.loadAndSortPoliciesWithin(%s)", referenceTimeslot)
268             val relevantPolicies = policyStore.loadAndSortPoliciesWithin(referenceTimeslot.from.getTime, referenceTimeslot.to.getTime, dsl)
269             //            clog.debugMap("==> relevantPolicies", relevantPolicies, 0)
270
271             (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
272
273           // We do not have a previous event
274           case None ⇒
275             throw new AquariumException(
276               "Unable to charge. No previous event given for %s".
277                 format(currentResourceEvent.toDebugString))
278         }
279
280       // We do not need a previous event
281       case false ⇒
282         // ... so we cannot compute timedelta from a previous event, there is just one chargeslot
283         // referring to (almost) an instant in time
284         //        clog.debug("DO NOT have previous event")
285         val previousValue = costPolicy.getResourceInstanceUndefinedAmount
286         //        clog.debug("previousValue = costPolicy.getResourceInstanceUndefinedAmount = %s", previousValue)
287
288         val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
289         //        clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
290
291         //        clog.debug("Calling policyStore.loadValidPolicyEntryAt(%s)", new MutableDateCalc(occurredMillis))
292         val relevantPolicyOpt = policyStore.loadValidPolicyAt(occurredMillis, dsl)
293         //        clog.debug("  ==> relevantPolicyM = %s", relevantPolicyM)
294
295         val relevantPolicies = relevantPolicyOpt match {
296           case Some(relevantPolicy) ⇒
297             Map(referenceTimeslot -> relevantPolicy)
298
299           case None ⇒
300             throw new AquariumInternalError("No relevant policy found for %s".format(referenceTimeslot))
301         }
302
303         (referenceTimeslot, relevantPolicies, previousValue)
304     }
305
306     val initialChargeslots = computeInitialChargeslots(
307       referenceTimeslot,
308       dslResource,
309       relevantPolicies,
310       agreementNamesByTimeslot,
311       Some(clog)
312     )
313
314     val fullChargeslots = initialChargeslots.map {
315       case chargeslot@Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
316         val execAlgorithm = algorithmCompiler.compile(algorithmDefinition)
317         val valueMap = costPolicy.makeValueMap(
318           oldCredits,
319           oldTotalAmount,
320           newTotalAmount,
321           stopMillis - startMillis,
322           previousValue,
323           currentResourceEvent.value,
324           unitPrice
325         )
326
327         //              clog.debug("execAlgorithm = %s", execAlgorithm)
328         clog.debugMap("valueMap", valueMap, 1)
329
330         // This is it
331         val credits = execAlgorithm.apply(valueMap)
332         chargeslot.copyWithCredits(credits)
333     }
334
335     val result = referenceTimeslot -> fullChargeslots
336
337     result
338   }
339
340   /**
341    * Align charge timeslots between algorithms and pricelists. As algorithm
342    * and pricelists can have different effectivity periods, this method
343    * examines them and splits them as necessary.
344    */
345   private[computation] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
346                                        price: SortedMap[Timeslot, DSLPriceList]):
347   (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
348
349     val zipped = alg.keySet.zip(price.keySet)
350
351     zipped.find(p => !p._1.equals(p._2)) match {
352       case None => (alg, price)
353       case Some(x) =>
354         val algTimeslot = x._1
355         val priTimeslot = x._2
356
357         assert(algTimeslot.from == priTimeslot.from)
358
359         if(algTimeslot.endsAfter(priTimeslot)) {
360           val slices = algTimeslot.slice(priTimeslot.to)
361           val algo = alg.get(algTimeslot).get
362           val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
363           splitChargeChunks(newalg, price)
364         }
365         else {
366           val slices = priTimeslot.slice(priTimeslot.to)
367           val pl = price.get(priTimeslot).get
368           val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
369           splitChargeChunks(alg, newPrice)
370         }
371     }
372   }
373
374   /**
375    * Given two lists of timeslots, produce a list which contains the
376    * set of timeslot slices, as those are defined by
377    * timeslot overlaps.
378    *
379    * For example, given the timeslots a and b below, split them as shown.
380    *
381    * a = |****************|
382    * ^                ^
383    * a.from            a.to
384    * b = |*********|
385    * ^         ^
386    * b.from     b.to
387    *
388    * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
389    */
390   private[computation] def alignTimeslots(a: List[Timeslot],
391                                     b: List[Timeslot]): List[Timeslot] = {
392
393     def safeTail(foo: List[Timeslot]) = foo match {
394       case Nil => List()
395       case x :: Nil => List()
396       case x :: rest => rest
397     }
398
399     if(a.isEmpty) return b
400     if(b.isEmpty) return a
401
402     assert(a.head.from == b.head.from)
403
404     if(a.head.endsAfter(b.head)) {
405       val slice = a.head.slice(b.head.to)
406       slice.head :: alignTimeslots(slice.last :: a.tail, safeTail(b))
407     } else if(b.head.endsAfter(a.head)) {
408       val slice = b.head.slice(a.head.to)
409       slice.head :: alignTimeslots(safeTail(a), slice.last :: b.tail)
410     } else {
411       a.head :: alignTimeslots(safeTail(a), safeTail(b))
412     }
413   }
414 }