d5cabe1cddc72c77c997c34e656d274b88d3a939
[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}
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
44 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
45 import gr.grnet.aquarium.logic.accounting.algorithm.SimpleExecutableChargingBehaviorAlgorithm
46 import gr.grnet.aquarium.logic.accounting.dsl.Timeslot
47 import gr.grnet.aquarium.policy._
48 import collection.immutable
49 import com.ckkloverdos.maybe.Just
50 import gr.grnet.aquarium.policy.ResourceType
51 import gr.grnet.aquarium.policy.EffectiveUnitPrice
52
53 /**
54  * Methods for converting accounting events to wallet entries.
55  *
56  * @author Georgios Gousios <gousiosg@gmail.com>
57  * @author Christos KK Loverdos <loverdos@gmail.com>
58  */
59 trait TimeslotComputations extends Loggable {
60   // TODO: favour composition over inheritance until we decide what to do with DSLUtils (and TimeslotComputations).
61   //protected val dslUtils = new DSLUtils {}
62
63   /**
64    * Breaks a reference timeslot (e.g. billing period) according to policies and agreements.
65    *
66    * @param referenceTimeslot
67    * @param policyTimeslots
68    * @param agreementTimeslots
69    * @return
70    */
71   protected
72   def splitTimeslotByPoliciesAndAgreements(
73       referenceTimeslot: Timeslot,
74       policyTimeslots: List[Timeslot],
75       agreementTimeslots: List[Timeslot],
76       clogM: Maybe[ContextualLogger] = NoVal
77   ): List[Timeslot] = {
78
79     // Align policy and agreement validity timeslots to the referenceTimeslot
80     val alignedPolicyTimeslots = referenceTimeslot.align(policyTimeslots)
81     val alignedAgreementTimeslots = referenceTimeslot.align(agreementTimeslots)
82
83     val result = alignTimeslots(alignedPolicyTimeslots, alignedAgreementTimeslots)
84
85     result
86   }
87
88   /**
89    * Given a reference timeslot, we have to break it up to a series of timeslots where a particular
90    * algorithm and price unit is in effect.
91    *
92    */
93   protected
94   def resolveEffectiveUnitPrices(
95       alignedTimeslot: Timeslot,
96       policy: PolicyModel,
97       agreement: UserAgreementModel,
98       resourceType: ResourceType,
99       clogOpt: Option[ContextualLogger] = None
100   ): SortedMap[Timeslot, Double] = {
101
102     val clog = ContextualLogger.fromOther(clogOpt, logger, "resolveEffectiveUnitPrices()")
103
104     // Note that most of the code is taken from calcChangeChunks()
105     val ret = resolveEffectiveUnitPricesForTimeslot(alignedTimeslot, policy, agreement, resourceType)
106     ret map {case (t,p) => (t,p.unitPrice)}
107   }
108
109   protected
110   def computeInitialChargeslots(
111       referenceTimeslot: Timeslot,
112       resourceType: ResourceType,
113       policyByTimeslot: SortedMap[Timeslot, PolicyModel],
114       agreementByTimeslot: SortedMap[Timeslot, UserAgreementModel],
115       clogOpt: Option[ContextualLogger] = None
116   ): List[Chargeslot] = {
117
118     val clog = ContextualLogger.fromOther(clogOpt, logger, "computeInitialChargeslots()")
119
120     val policyTimeslots = policyByTimeslot.keySet
121     val agreementTimeslots = agreementByTimeslot.keySet
122
123     def getPolicyWithin(ts: Timeslot): PolicyModel = {
124       policyByTimeslot.find(_._1.contains(ts)).get._2
125     }
126     def getAgreementWithin(ts: Timeslot): UserAgreementModel = {
127       agreementByTimeslot.find(_._1.contains(ts)).get._2
128     }
129
130     // 1. Round ONE: split time according to overlapping policies and agreements.
131     val alignedTimeslots = splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList, Just(clog))
132
133     // 2. Round TWO: Use the aligned timeslots of Round ONE to produce even more
134     //    fine-grained timeslots according to applicable algorithms.
135     //    Then pack the info into charge slots.
136     //    clog.begin("ROUND 2")
137     val allChargeslots = for {
138       alignedTimeslot <- alignedTimeslots
139     } yield {
140       val policy = getPolicyWithin(alignedTimeslot)
141       //      clog.debug("dslPolicy = %s", dslPolicy)
142       val userAgreement = getAgreementWithin(alignedTimeslot)
143
144       // TODO: Factor this out, just like we did with:
145       // TODO:  val alignedTimeslots = splitTimeslotByPoliciesAndAgreements
146       // Note that most of the code is already taken from calcChangeChunks()
147       val unitPriceByTimeslot = resolveEffectiveUnitPrices(alignedTimeslot, policy, userAgreement, resourceType, Some(clog))
148
149       // Now, the timeslots must be the same
150       val finegrainedTimeslots = unitPriceByTimeslot.keySet
151
152       val chargeslots = for (finegrainedTimeslot ← finegrainedTimeslots) yield {
153         Chargeslot(
154           finegrainedTimeslot.from.getTime,
155           finegrainedTimeslot.to.getTime,
156           unitPriceByTimeslot(finegrainedTimeslot)
157         )
158       }
159
160       chargeslots.toList
161     }
162
163     val result = allChargeslots.flatten
164
165     result
166   }
167
168   /**
169    * Compute the charge slots generated by a particular resource event.
170    *
171    */
172   def computeFullChargeslots(
173       previousResourceEventOpt: Option[ResourceEventModel],
174       currentResourceEvent: ResourceEventModel,
175       oldCredits: Double,
176       oldTotalAmount: Double,
177       newTotalAmount: Double,
178       resourceType: ResourceType,
179       agreementByTimeslot: SortedMap[Timeslot, UserAgreementModel],
180       policyStore: PolicyStore,
181       clogOpt: Option[ContextualLogger] = None
182   ): (Timeslot, List[Chargeslot]) = {
183
184     val clog = ContextualLogger.fromOther(clogOpt, logger, "computeFullChargeslots()")
185     //    clog.begin()
186
187     val occurredDate = currentResourceEvent.occurredDate
188     val occurredMillis = currentResourceEvent.occurredMillis
189     val chargingBehavior = resourceType.chargingBehavior
190
191     val (referenceTimeslot, policyByTimeslot, previousValue) = chargingBehavior.needsPreviousEventForCreditAndAmountCalculation match {
192       // We need a previous event
193       case true ⇒
194         previousResourceEventOpt match {
195           // We have a previous event
196           case Some(previousResourceEvent) ⇒
197             val referenceTimeslot = Timeslot(previousResourceEvent.occurredDate, occurredDate)
198             // all policies within the interval from previous to current resource event
199             //            clog.debug("Calling policyStore.loadAndSortPoliciesWithin(%s)", referenceTimeslot)
200             // TODO: store policies in mem?
201             val policyByTimeslot = policyStore.loadAndSortPoliciesWithin(referenceTimeslot.from.getTime, referenceTimeslot.to.getTime)
202
203             (referenceTimeslot, policyByTimeslot, previousResourceEvent.value)
204
205           // We do not have a previous event
206           case None ⇒
207             throw new AquariumInternalError(
208               "Unable to charge. No previous event given for %s".format(currentResourceEvent.toDebugString))
209         }
210
211       // We do not need a previous event
212       case false ⇒
213         // ... so we cannot compute timedelta from a previous event, there is just one chargeslot
214         // referring to (almost) an instant in time
215         val previousValue = chargingBehavior.getResourceInstanceUndefinedAmount
216
217         // TODO: Check semantics of this
218         val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
219
220         // TODO: store policies in mem?
221         val relevantPolicyOpt: Option[PolicyModel] = policyStore.loadValidPolicyAt(occurredMillis)
222
223         val policyByTimeslot = relevantPolicyOpt match {
224           case Some(relevantPolicy) ⇒
225             SortedMap(referenceTimeslot -> relevantPolicy)
226
227           case None ⇒
228             throw new AquariumInternalError("No relevant policy found for %s".format(referenceTimeslot))
229         }
230
231         (referenceTimeslot, policyByTimeslot, previousValue)
232     }
233
234     val initialChargeslots = computeInitialChargeslots(
235       referenceTimeslot,
236       resourceType,
237       policyByTimeslot,
238       agreementByTimeslot,
239       Some(clog)
240     )
241
242     val fullChargeslots = initialChargeslots.map {
243       case chargeslot@Chargeslot(startMillis, stopMillis, unitPrice, _) ⇒
244         val valueMap = chargingBehavior.makeValueMap(
245           oldCredits,
246           oldTotalAmount,
247           newTotalAmount,
248           stopMillis - startMillis,
249           previousValue,
250           currentResourceEvent.value,
251           unitPrice
252         )
253
254         //              clog.debug("execAlgorithm = %s", execAlgorithm)
255         clog.debugMap("valueMap", valueMap, 1)
256
257         // This is it
258         val credits = SimpleExecutableChargingBehaviorAlgorithm.apply(valueMap)
259         chargeslot.copyWithCredits(credits)
260     }
261
262     val result = referenceTimeslot -> fullChargeslots
263
264     result
265   }
266
267   /**
268    * Given two lists of timeslots, produce a list which contains the
269    * set of timeslot slices, as those are defined by
270    * timeslot overlaps.
271    *
272    * For example, given the timeslots a and b below, split them as shown.
273    *
274    * a = |****************|
275    * ^                ^
276    * a.from            a.to
277    * b = |*********|
278    * ^         ^
279    * b.from     b.to
280    *
281    * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
282    */
283   private[computation] def alignTimeslots(a: List[Timeslot],
284                                     b: List[Timeslot]): List[Timeslot] = {
285
286     def safeTail(foo: List[Timeslot]) = foo match {
287       case Nil => List()
288       case x :: Nil => List()
289       case x :: rest => rest
290     }
291
292     if(a.isEmpty) return b
293     if(b.isEmpty) return a
294
295     assert(a.head.from == b.head.from)
296
297     if(a.head.endsAfter(b.head)) {
298       val slice = a.head.slice(b.head.to)
299       slice.head :: alignTimeslots(slice.last :: a.tail, safeTail(b))
300     } else if(b.head.endsAfter(a.head)) {
301       val slice = b.head.slice(a.head.to)
302       slice.head :: alignTimeslots(safeTail(a), slice.last :: b.tail)
303     } else {
304       a.head :: alignTimeslots(safeTail(a), safeTail(b))
305     }
306   }
307
308     type PriceMap =  immutable.SortedMap[Timeslot, EffectiveUnitPrice]
309     private type PriceList = List[EffectiveUnitPrice]
310     private def emptyMap = immutable.SortedMap[Timeslot,EffectiveUnitPrice]()
311
312     /**
313      * Resolves the effective price list for each chunk of the
314      * provided timeslot and returns it as a Map
315      */
316     private def resolveEffectiveUnitPricesForTimeslot(
317                                                alignedTimeslot: Timeslot,
318                                                policy: PolicyModel,
319                                                agreement: UserAgreementModel,
320                                                resourceType: ResourceType
321                                                ): PriceMap = {
322
323       val role = agreement.role
324       val fullPriceTable = agreement.fullPriceTableRef match {
325         case PolicyDefinedFullPriceTableRef ⇒
326           policy.roleMapping.get(role) match {
327             case Some(fullPriceTable) ⇒
328               fullPriceTable
329
330             case None ⇒
331               throw new AquariumInternalError("Unknown role %s".format(role))
332           }
333
334         case AdHocFullPriceTableRef(fullPriceTable) ⇒
335           fullPriceTable
336       }
337
338       val effectivePriceTable = fullPriceTable.perResource.get(resourceType.name) match {
339         case None ⇒
340           throw new AquariumInternalError("Unknown resource type %s".format(role))
341
342         case Some(effectivePriceTable) ⇒
343           effectivePriceTable
344       }
345
346       resolveEffective(alignedTimeslot, effectivePriceTable.priceOverrides)
347     }
348
349     private def printPriceList(p: PriceList) : Unit = {
350       Console.err.println("BEGIN PRICE LIST")
351       for { p1 <- p } Console.err.println(p1)
352       Console.err.println("END PRICE LIST")
353     }
354
355     private def printPriceMap(m: PriceMap) = {
356       Console.err.println("BEGIN PRICE MAP")
357       for { (t,p) <- m.toList } Console.err.println("Timeslot " + t + "\t\t" + p)
358       Console.err.println("END PRICE MAP")
359     }
360
361     private def resolveEffective(alignedTimeslot: Timeslot,p:PriceList): PriceMap = {
362       Console.err.println("\n\nInput timeslot: " + alignedTimeslot + "\n\n")
363       printPriceList(p)
364       val ret =  resolveEffective3(alignedTimeslot,p) //HERE
365       printPriceMap(ret)
366       ret
367     }
368
369
370     private def resolveEffective3(alignedTimeslot: Timeslot, effectiveUnitPrices: PriceList): PriceMap =
371       effectiveUnitPrices match {
372         case Nil =>
373           emptyMap
374         case hd::tl =>
375           val (satisfied,notSatisfied) = hd splitTimeslot alignedTimeslot
376           val satisfiedMap = satisfied.foldLeft (emptyMap)  {(map,t) =>
377           //Console.err.println("Adding timeslot" + t +
378           // " for policy " + policy.name)
379             map + ((t,hd))
380           }
381           val notSatisfiedMap = notSatisfied.foldLeft (emptyMap) {(map,t) =>
382             val otherMap = resolveEffective3(t,tl)
383             //Console.err.println("Residual timeslot: " + t)
384             val ret = map ++ otherMap
385             ret
386           }
387           val ret = satisfiedMap ++ notSatisfiedMap
388           ret
389       }
390 }