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