2 * Copyright 2011-2012 GRNET S.A. All rights reserved.
4 * Redistribution and use in source and binary forms, with or
5 * without modification, are permitted provided that the following
8 * 1. Redistributions of source code must retain the above
9 * copyright notice, this list of conditions and the following
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.
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.
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.
36 package gr.grnet.aquarium.computation
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}
49 * Methods for converting accounting events to wallet entries.
51 * @author Georgios Gousios <gousiosg@gmail.com>
52 * @author Christos KK Loverdos <loverdos@gmail.com>
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 {}
59 * Breaks a reference timeslot (e.g. billing period) according to policies and agreements.
61 * @param referenceTimeslot
62 * @param policyTimeslots
63 * @param agreementTimeslots
67 def splitTimeslotByPoliciesAndAgreements(referenceTimeslot: Timeslot,
68 policyTimeslots: List[Timeslot],
69 agreementTimeslots: List[Timeslot],
70 clogM: Maybe[ContextualLogger] = NoVal): List[Timeslot] = {
72 // val clog = ContextualLogger.fromOther(clogM, logger, "splitTimeslotByPoliciesAndAgreements()")
75 // Align policy and agreement validity timeslots to the referenceTimeslot
76 val alignedPolicyTimeslots = referenceTimeslot.align(policyTimeslots)
77 val alignedAgreementTimeslots = referenceTimeslot.align(agreementTimeslots)
79 // clog.debug("referenceTimeslot = %s", referenceTimeslot)
80 // clog.debugSeq("alignedPolicyTimeslots", alignedPolicyTimeslots, 0)
81 // clog.debugSeq("alignedAgreementTimeslots", alignedAgreementTimeslots, 0)
83 val result = alignTimeslots(alignedPolicyTimeslots, alignedAgreementTimeslots)
84 // clog.debugSeq("result", result, 1)
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.
95 def resolveEffectiveAlgorithmsAndPriceLists(alignedTimeslot: Timeslot,
96 agreement: DSLAgreement,
97 clogOpt: Option[ContextualLogger] = None):
98 (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
100 val clog = ContextualLogger.fromOther(clogOpt, logger, "resolveEffectiveAlgorithmsAndPriceLists()")
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
109 assert(algorithmByTimeslot.size == pricelistByTimeslot.size)
111 (algorithmByTimeslot, pricelistByTimeslot)
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] = {
121 val clog = ContextualLogger.fromOther(clogOpt, logger, "computeInitialChargeslots()")
124 val policyTimeslots = policiesByTimeslot.keySet
125 val agreementTimeslots = agreementNamesByTimeslot.keySet
127 // clog.debugMap("policiesByTimeslot", policiesByTimeslot, 1)
128 // clog.debugMap("agreementNamesByTimeslot", agreementNamesByTimeslot, 1)
130 def getPolicy(ts: Timeslot): DSLPolicy = {
131 policiesByTimeslot.find(_._1.contains(ts)).get._2
133 def getAgreementName(ts: Timeslot): String = {
134 agreementNamesByTimeslot.find(_._1.contains(ts)).get._2
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")
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
150 // val alignedTimeslotMsg = "alignedTimeslot = %s".format(alignedTimeslot)
151 // clog.begin(alignedTimeslotMsg)
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)
162 val errMsg = "Unknown agreement %s during %s".format(agreementName, alignedTimeslot)
163 clog.error("%s", errMsg)
164 throw new AquariumException(errMsg)
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
174 // Now, the timeslots must be the same
175 val finegrainedTimeslots = algorithmByTimeslot.keySet
177 val chargeslots = for {
178 finegrainedTimeslot <- finegrainedTimeslots
180 // val finegrainedTimeslotMsg = "finegrainedTimeslot = %s".format(finegrainedTimeslot)
181 // clog.begin(finegrainedTimeslotMsg)
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)
194 val chargeslot = (algorithmDefOpt, priceUnitOpt) match {
196 throw new AquariumException(
197 "Unknown algorithm and price unit for resource %s during %s".
198 format(dslResource, finegrainedTimeslot))
200 throw new AquariumException(
201 "Unknown algorithm for resource %s during %s".
202 format(dslResource, finegrainedTimeslot))
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)
211 // clog.end(finegrainedTimeslotMsg)
215 // clog.end(alignedTimeslotMsg)
219 // clog.end("ROUND 2")
222 val result = allChargeslots.flatten
223 // clog.debugSeq("result", allChargeslots, 1)
229 * Compute the charge slots generated by a particular resource event.
232 def computeFullChargeslots(
233 previousResourceEventOpt: Option[ResourceEventModel],
234 currentResourceEvent: ResourceEventModel,
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]) = {
246 val clog = ContextualLogger.fromOther(clogOpt, logger, "computeFullChargeslots()")
249 val occurredDate = currentResourceEvent.occurredDate
250 val occurredMillis = currentResourceEvent.occurredMillis
251 val costPolicy = dslResource.costPolicy
254 val (referenceTimeslot, relevantPolicies, previousValue) = costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
255 // We need a previous event
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)
263 val referenceTimeslot = Timeslot(previousResourceEvent.occurredDate, occurredDate)
264 // clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
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)
271 (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
273 // We do not have a previous event
275 throw new AquariumException(
276 "Unable to charge. No previous event given for %s".
277 format(currentResourceEvent.toDebugString))
280 // We do not need a previous event
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)
288 val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
289 // clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
291 // clog.debug("Calling policyStore.loadValidPolicyEntryAt(%s)", new MutableDateCalc(occurredMillis))
292 val relevantPolicyOpt = policyStore.loadValidPolicyAt(occurredMillis, dsl)
293 // clog.debug(" ==> relevantPolicyM = %s", relevantPolicyM)
295 val relevantPolicies = relevantPolicyOpt match {
296 case Some(relevantPolicy) ⇒
297 Map(referenceTimeslot -> relevantPolicy)
300 throw new AquariumInternalError("No relevant policy found for %s".format(referenceTimeslot))
303 (referenceTimeslot, relevantPolicies, previousValue)
306 val initialChargeslots = computeInitialChargeslots(
310 agreementNamesByTimeslot,
314 val fullChargeslots = initialChargeslots.map {
315 case chargeslot@Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
316 val execAlgorithm = algorithmCompiler.compile(algorithmDefinition)
317 val valueMap = costPolicy.makeValueMap(
321 stopMillis - startMillis,
323 currentResourceEvent.value,
327 // clog.debug("execAlgorithm = %s", execAlgorithm)
328 clog.debugMap("valueMap", valueMap, 1)
331 val credits = execAlgorithm.apply(valueMap)
332 chargeslot.copyWithCredits(credits)
335 val result = referenceTimeslot -> fullChargeslots
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.
345 private[computation] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
346 price: SortedMap[Timeslot, DSLPriceList]):
347 (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
349 val zipped = alg.keySet.zip(price.keySet)
351 zipped.find(p => !p._1.equals(p._2)) match {
352 case None => (alg, price)
354 val algTimeslot = x._1
355 val priTimeslot = x._2
357 assert(algTimeslot.from == priTimeslot.from)
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)
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)
375 * Given two lists of timeslots, produce a list which contains the
376 * set of timeslot slices, as those are defined by
379 * For example, given the timeslots a and b below, split them as shown.
381 * a = |****************|
388 * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
390 private[computation] def alignTimeslots(a: List[Timeslot],
391 b: List[Timeslot]): List[Timeslot] = {
393 def safeTail(foo: List[Timeslot]) = foo match {
395 case x :: Nil => List()
396 case x :: rest => rest
399 if(a.isEmpty) return b
400 if(b.isEmpty) return a
402 assert(a.head.from == b.head.from)
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)
411 a.head :: alignTimeslots(safeTail(a), safeTail(b))