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.charging
38 import scala.collection.immutable
39 import scala.collection.mutable
41 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
42 import gr.grnet.aquarium.{Aquarium, AquariumInternalError, AquariumException}
43 import gr.grnet.aquarium.policy.{UserAgreementModel, ResourceType}
44 import com.ckkloverdos.key.{TypedKey, TypedKeySkeleton}
45 import gr.grnet.aquarium.util._
46 import gr.grnet.aquarium.util.date.TimeHelpers
47 import gr.grnet.aquarium.charging.wallet.WalletEntry
48 import gr.grnet.aquarium.computation.{TimeslotComputations, BillingMonthInfo}
49 import gr.grnet.aquarium.charging.state.AgreementHistory
50 import gr.grnet.aquarium.logic.accounting.dsl.Timeslot
51 import gr.grnet.aquarium.store.PolicyStore
52 import gr.grnet.aquarium.charging.ChargingBehavior.EnvKeys
55 * A charging behavior indicates how charging for a resource will be done
56 * wrt the various states a resource can be.
58 * @author Christos KK Loverdos <loverdos@gmail.com>
61 abstract class ChargingBehavior(val alias: String, val inputs: Set[ChargingInput]) extends Loggable {
63 final lazy val inputNames = inputs.map(_.name)
68 * @param resourceEvent
70 * @param billingMonthInfo
71 * @param userAgreements
74 * @param walletEntryRecorder
76 * @return The number of wallet entries recorded and the new total credits
78 def chargeResourceEvent(
80 resourceEvent: ResourceEventModel,
81 resourceType: ResourceType,
82 billingMonthInfo: BillingMonthInfo,
83 userAgreements: AgreementHistory,
84 chargingData: mutable.Map[String, Any],
86 walletEntryRecorder: WalletEntry ⇒ Unit,
87 clogOpt: Option[ContextualLogger] = None
90 genericChargeResourceEvent(
103 @inline private[this] def hrs(millis: Double) = {
104 val hours = millis / 1000 / 60 / 60
105 val roundedHours = hours
109 protected def computeCreditsToSubtract(
111 oldAccumulatingAmount: Double,
112 newAccumulatingAmount: Double,
113 timeDeltaMillis: Long,
114 previousValue: Double,
115 currentValue: Double,
117 details: Map[String, String]
120 case ChargingBehaviorAliases.continuous ⇒
121 hrs(timeDeltaMillis) * oldAccumulatingAmount * unitPrice
123 case ChargingBehaviorAliases.discrete ⇒
124 currentValue * unitPrice
126 case ChargingBehaviorAliases.onoff ⇒
127 hrs(timeDeltaMillis) * unitPrice
129 case ChargingBehaviorAliases.once ⇒
133 throw new AquariumInternalError("Cannot compute credit diff for charging behavior %s".format(name))
137 protected def rcDebugInfo(rcEvent: ResourceEventModel) = {
138 rcEvent.toDebugString
143 * @param chargingData
144 * @param previousResourceEventOpt
145 * @param currentResourceEvent
146 * @param billingMonthInfo
147 * @param referenceTimeslot
148 * @param resourceType
149 * @param agreementByTimeslot
150 * @param previousValue
151 * @param totalCredits
153 * @param walletEntryRecorder
154 * @return The number of wallet entries recorded and the new total credits
156 protected def computeChargeslots(
157 chargingData: mutable.Map[String, Any],
158 previousResourceEventOpt: Option[ResourceEventModel],
159 currentResourceEvent: ResourceEventModel,
160 billingMonthInfo: BillingMonthInfo,
161 referenceTimeslot: Timeslot,
162 resourceType: ResourceType,
163 agreementByTimeslot: immutable.SortedMap[Timeslot, UserAgreementModel],
164 previousValue: Double,
165 totalCredits: Double,
166 policyStore: PolicyStore,
167 walletEntryRecorder: WalletEntry ⇒ Unit
170 val currentValue = currentResourceEvent.value
171 val userID = currentResourceEvent.userID
172 val currentDetails = currentResourceEvent.details
174 var _oldAccumulatingAmount = getChargingData(
176 EnvKeys.ResourceInstanceAccumulatingAmount
177 ).getOrElse(getResourceInstanceInitialAmount)
179 var _oldTotalCredits = totalCredits
181 var _newAccumulatingAmount = this.computeNewAccumulatingAmount(_oldAccumulatingAmount, currentValue, currentDetails)
182 setChargingData(chargingData, EnvKeys.ResourceInstanceAccumulatingAmount, _newAccumulatingAmount)
184 val policyByTimeslot = policyStore.loadAndSortPoliciesWithin(
185 referenceTimeslot.from.getTime,
186 referenceTimeslot.to.getTime
189 val initialChargeslots = TimeslotComputations.computeInitialChargeslots(
196 val fullChargeslots = initialChargeslots.map {
197 case chargeslot@Chargeslot(startMillis, stopMillis, unitPrice, _) ⇒
198 val timeDeltaMillis = stopMillis - startMillis
200 val creditsToSubtract = this.computeCreditsToSubtract(
201 _oldTotalCredits, // FIXME ??? Should recalculate ???
202 _oldAccumulatingAmount, // FIXME ??? Should recalculate ???
203 _newAccumulatingAmount, // FIXME ??? Should recalculate ???
211 val newChargeslot = chargeslot.copyWithCreditsToSubtract(creditsToSubtract)
215 if(fullChargeslots.length == 0) {
216 throw new AquariumInternalError("No chargeslots computed for resource event %s".format(currentResourceEvent.id))
219 val sumOfCreditsToSubtract = fullChargeslots.map(_.creditsToSubtract).sum
220 val newTotalCredits = _oldTotalCredits - sumOfCreditsToSubtract
222 val newWalletEntry = WalletEntry(
224 sumOfCreditsToSubtract,
227 TimeHelpers.nowMillis(),
229 billingMonthInfo.year,
230 billingMonthInfo.month,
231 previousResourceEventOpt.map(List(_, currentResourceEvent)).getOrElse(List(currentResourceEvent)),
234 currentResourceEvent.isSynthetic
237 walletEntryRecorder.apply(newWalletEntry)
242 protected def removeChargingData[T: Manifest](
243 chargingData: mutable.Map[String, Any],
247 chargingData.remove(envKey.name).asInstanceOf[Option[T]]
250 protected def getChargingData[T: Manifest](
251 chargingData: mutable.Map[String, Any],
255 chargingData.get(envKey.name).asInstanceOf[Option[T]]
258 protected def setChargingData[T: Manifest](
259 chargingData: mutable.Map[String, Any],
264 chargingData(envKey.name) = value
270 * @param currentResourceEvent
271 * @param resourceType
272 * @param billingMonthInfo
273 * @param userAgreements
274 * @param chargingData
275 * @param totalCredits
276 * @param walletEntryRecorder
278 * @return The number of wallet entries recorded and the new total credits
280 protected def genericChargeResourceEvent(
282 currentResourceEvent: ResourceEventModel,
283 resourceType: ResourceType,
284 billingMonthInfo: BillingMonthInfo,
285 userAgreements: AgreementHistory,
286 chargingData: mutable.Map[String, Any],
287 totalCredits: Double,
288 walletEntryRecorder: WalletEntry ⇒ Unit,
289 clogOpt: Option[ContextualLogger] = None
291 import ChargingBehavior.EnvKeys
293 val clog = ContextualLogger.fromOther(clogOpt, logger, "chargeResourceEvent(%s)", currentResourceEvent.id)
294 val currentResourceEventDebugInfo = rcDebugInfo(currentResourceEvent)
296 val isBillable = this.isBillableEvent(currentResourceEvent)
297 val retval = if(!isBillable) {
298 // The resource event is not billable.
299 clog.debug("Ignoring not billable %s", currentResourceEventDebugInfo)
302 // The resource event is billable.
303 // Find the previous event if needed.
304 // This is (potentially) needed to calculate new credit amount and new resource instance amount
305 if(this.needsPreviousEventForCreditAndAmountCalculation) {
306 val previousResourceEventOpt = removeChargingData(chargingData, EnvKeys.PreviousEvent)
308 if(previousResourceEventOpt.isDefined) {
309 val previousResourceEvent = previousResourceEventOpt.get
310 val previousValue = previousResourceEvent.value
314 previousResourceEventOpt,
315 currentResourceEvent,
317 Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
319 userAgreements.agreementByTimeslot,
322 aquarium.policyStore,
326 // We do not have the needed previous event, so this must be the first resource event of its kind, ever.
327 // Let's see if we can create a dummy previous event.
328 val actualFirstEvent = currentResourceEvent
330 if(this.isBillableFirstEvent(actualFirstEvent) && this.mustGenerateDummyFirstEvent) {
331 clog.debug("First event of its kind %s", currentResourceEventDebugInfo)
333 val dummyFirst = this.constructDummyFirstEventFor(currentResourceEvent, billingMonthInfo.monthStartMillis)
334 clog.debug("Dummy first event %s", rcDebugInfo(dummyFirst))
336 val previousResourceEvent = dummyFirst
337 val previousValue = previousResourceEvent.value
341 Some(previousResourceEvent),
342 currentResourceEvent,
344 Timeslot(previousResourceEvent.occurredMillis, currentResourceEvent.occurredMillis),
346 userAgreements.agreementByTimeslot,
349 aquarium.policyStore,
353 clog.debug("Ignoring first event of its kind %s", currentResourceEventDebugInfo)
354 // userStateWorker.updateIgnored(currentResourceEvent)
359 // No need for previous event. One event does it all.
363 currentResourceEvent,
365 Timeslot(currentResourceEvent.occurredMillis, currentResourceEvent.occurredMillis + 1),
367 userAgreements.agreementByTimeslot,
368 this.getResourceInstanceUndefinedAmount,
370 aquarium.policyStore,
376 // After processing, all events billable or not update the previous state
377 setChargingData(chargingData, EnvKeys.PreviousEvent, currentResourceEvent)
383 * Generate a map where the key is a [[gr.grnet.aquarium.charging.ChargingInput]]
384 * and the value the respective value. This map will be used to do the actual credit charge calculation
385 * by the respective algorithm.
387 * Values are obtained from a corresponding context, which is provided by the parameters. We assume that this context
388 * has been validated before the call to `makeValueMap` is made.
390 * @param totalCredits the value for [[gr.grnet.aquarium.charging.TotalCreditsInput.]]
391 * @param oldTotalAmount the value for [[gr.grnet.aquarium.charging.OldTotalAmountInput]]
392 * @param newTotalAmount the value for [[gr.grnet.aquarium.charging.NewTotalAmountInput]]
393 * @param timeDelta the value for [[gr.grnet.aquarium.charging.TimeDeltaInput]]
394 * @param previousValue the value for [[gr.grnet.aquarium.charging.PreviousValueInput]]
395 * @param currentValue the value for [[gr.grnet.aquarium.charging.CurrentValueInput]]
396 * @param unitPrice the value for [[gr.grnet.aquarium.charging.UnitPriceInput]]
398 * @return a map from [[gr.grnet.aquarium.charging.ChargingInput]]s to respective values.
401 totalCredits: Double,
402 oldTotalAmount: Double,
403 newTotalAmount: Double,
405 previousValue: Double,
406 currentValue: Double,
408 ): Map[ChargingInput, Any] = {
410 ChargingBehavior.makeValueMapFor(
421 def needsPreviousEventForCreditAndAmountCalculation: Boolean = {
422 // If we need any variable that is related to the previous event
423 // then we do need a previous event
424 inputs.exists(_.isDirectlyRelatedToPreviousEvent)
428 * Given the old amount of a resource instance, the value arriving in a new resource event and the new details,
429 * compute the new instance amount.
431 * Note that the `oldAmount` does not make sense for all types of [[gr.grnet.aquarium.charging.ChargingBehavior]],
432 * in which case it is ignored.
434 * @param oldAccumulatingAmount the old accumulating amount
435 * @param newEventValue the value contained in a newly arrived
436 * [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
437 * @param newDetails the `details` of the newly arrived
438 * [[gr.grnet.aquarium.event.model.resource.ResourceEventModel]]
441 def computeNewAccumulatingAmount(
442 oldAccumulatingAmount: Double,
443 newEventValue: Double,
444 newDetails: Map[String, String]
448 * The initial amount.
450 def getResourceInstanceInitialAmount: Double
453 * The amount used when no amount is meant to be relevant.
455 * For example, when there is no need for a previous event but an API requires the amount of the previous event.
457 * Normally, this value will never be used by client code (= charge computation code).
459 def getResourceInstanceUndefinedAmount: Double = Double.NaN
462 * An event carries enough info to characterize it as billable or not.
464 * Typically all events are billable by default and indeed this is the default implementation
467 * The only exception to the rule is ON events for [[gr.grnet.aquarium.charging.OnOffChargingBehavior]].
469 def isBillableEvent(event: ResourceEventModel): Boolean = false
472 * This is called when we have the very first event for a particular resource instance, and we want to know
473 * if it is billable or not.
475 def isBillableFirstEvent(event: ResourceEventModel): Boolean
477 def mustGenerateDummyFirstEvent: Boolean
479 def dummyFirstEventValue: Double = 0.0 // FIXME read from configuration
481 def constructDummyFirstEventFor(actualFirst: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel = {
482 if(!mustGenerateDummyFirstEvent) {
483 throw new AquariumException("constructDummyFirstEventFor() Not compliant with %s".format(this))
486 val newDetails = Map(
487 ResourceEventModel.Names.details_aquarium_is_synthetic -> "true",
488 ResourceEventModel.Names.details_aquarium_is_dummy_first -> "true",
489 ResourceEventModel.Names.details_aquarium_reference_event_id -> actualFirst.id,
490 ResourceEventModel.Names.details_aquarium_reference_event_id_in_store -> actualFirst.stringIDInStoreOrEmpty
493 actualFirst.withDetailsAndValue(newDetails, dummyFirstEventValue, newOccurredMillis)
497 * There are resources (cost policies) for which implicit events must be generated at the end of the billing period
498 * and also at the beginning of the next one. For these cases, this method must return `true`.
500 * The motivating example comes from the [[gr.grnet.aquarium.charging.OnOffChargingBehavior]] for which we
501 * must implicitly assume `OFF` events at the end of the billing period and `ON` events at the beginning of the next
505 def supportsImplicitEvents: Boolean
507 def mustConstructImplicitEndEventFor(resourceEvent: ResourceEventModel): Boolean
509 @throws(classOf[Exception])
510 def constructImplicitEndEventFor(resourceEvent: ResourceEventModel, newOccurredMillis: Long): ResourceEventModel
513 object ChargingBehavior {
514 final case class ChargingBehaviorKey[T: Manifest](override val name: String) extends TypedKeySkeleton[T](name)
517 * Keys used to save information between calls of `chargeResourceEvent`
520 final val PreviousEvent = ChargingBehaviorKey[ResourceEventModel]("previous.event")
522 final val ResourceInstanceAccumulatingAmount = ChargingBehaviorKey[Double]("resource.instance.accumulating.amount")
526 chargingBehavior: ChargingBehavior,
527 totalCredits: Double,
528 oldTotalAmount: Double,
529 newTotalAmount: Double,
531 previousValue: Double,
532 currentValue: Double,
534 ): Map[ChargingInput, Any] = {
536 val inputs = chargingBehavior.inputs
537 var map = Map[ChargingInput, Any]()
539 if(inputs contains ChargingBehaviorNameInput) map += ChargingBehaviorNameInput -> chargingBehavior.alias
540 if(inputs contains TotalCreditsInput ) map += TotalCreditsInput -> totalCredits
541 if(inputs contains OldTotalAmountInput ) map += OldTotalAmountInput -> oldTotalAmount
542 if(inputs contains NewTotalAmountInput ) map += NewTotalAmountInput -> newTotalAmount
543 if(inputs contains TimeDeltaInput ) map += TimeDeltaInput -> timeDelta
544 if(inputs contains PreviousValueInput ) map += PreviousValueInput -> previousValue
545 if(inputs contains CurrentValueInput ) map += CurrentValueInput -> currentValue
546 if(inputs contains UnitPriceInput ) map += UnitPriceInput -> unitPrice