2 * Copyright 2011 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.logic.accounting
38 import algorithm.CostPolicyAlgorithmCompiler
40 import gr.grnet.aquarium.logic.events.{WalletEntry, ResourceEvent}
41 import collection.immutable.SortedMap
43 import com.ckkloverdos.maybe.{NoVal, Maybe, Failed, Just}
44 import gr.grnet.aquarium.util.date.MutableDateCalc
45 import gr.grnet.aquarium.util.{ContextualLogger, CryptoUtils, Loggable}
46 import gr.grnet.aquarium.store.PolicyStore
49 * A timeslot together with the algorithm and unit price that apply for this particular timeslot.
53 * @param algorithmDefinition
55 * @param computedCredits The computed credits
57 case class Chargeslot(startMillis: Long,
59 algorithmDefinition: String,
61 computedCredits: Option[Double] = None)
64 * Methods for converting accounting events to wallet entries.
66 * @author Georgios Gousios <gousiosg@gmail.com>
67 * @author Christos KK Loverdos <loverdos@gmail.com>
69 trait Accounting extends DSLUtils with Loggable {
71 * Breaks a reference timeslot (e.g. billing period) according to policies and agreements.
73 * @param referenceTimeslot
74 * @param policyTimeslots
75 * @param agreementTimeslots
79 def splitTimeslotByPoliciesAndAgreements(referenceTimeslot: Timeslot,
80 policyTimeslots: List[Timeslot],
81 agreementTimeslots: List[Timeslot],
82 clogM: Maybe[ContextualLogger] = NoVal): List[Timeslot] = {
84 val clog = ContextualLogger.fromOther(clogM, logger, "splitTimeslotByPoliciesAndAgreements()")
87 // Align policy and agreement validity timeslots to the referenceTimeslot
88 val alignedPolicyTimeslots = referenceTimeslot.align(policyTimeslots)
89 val alignedAgreementTimeslots = referenceTimeslot.align(agreementTimeslots)
91 ContextualLogger.debugList(clog, "alignedPolicyTimeslots", alignedPolicyTimeslots)
92 ContextualLogger.debugList(clog, "alignedAgreementTimeslots", alignedAgreementTimeslots)
94 val result = alignTimeslots(alignedPolicyTimeslots, alignedAgreementTimeslots, Just(clog))
100 * Given a reference timeslot, we have to break it up to a series of timeslots where a particular
101 * algorithm and price unit is in effect.
103 * @param referenceTimeslot
104 * @param policiesByTimeslot
105 * @param agreementNamesByTimeslot
109 def computeInitialChargeslots(referenceTimeslot: Timeslot,
110 dslResource: DSLResource,
111 policiesByTimeslot: Map[Timeslot, DSLPolicy],
112 agreementNamesByTimeslot: Map[Timeslot, String],
113 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[List[Chargeslot]] = Maybe {
115 val clog = ContextualLogger.fromOther(contextualLogger, logger, "computeInitialChargeslots()")
118 val policyTimeslots = policiesByTimeslot.keySet
119 val agreementTimeslots = agreementNamesByTimeslot.keySet
121 clog.debug("policiesByTimeslot:")
123 policyTimeslots.foreach(pt ⇒ clog.debug("%s: %s", pt, policiesByTimeslot(pt)))
125 clog.debug("agreementNamesByTimeslot:")
127 agreementTimeslots.foreach(at ⇒ clog.debug("%s: %s", at, agreementNamesByTimeslot(at)))
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 val alignedTimeslots = splitTimeslotByPoliciesAndAgreements(referenceTimeslot, policyTimeslots.toList, agreementTimeslots.toList, Just(clog))
139 clog.debug("ROUND 1: alignedTimeslots:")
141 alignedTimeslots.foreach(ts ⇒ clog.debug("%s", ts))
144 // 2. Round TWO: Use the aligned timeslots of Round ONE to produce even more
145 // fine-grained timeslots according to applicable algorithms.
146 // Then pack the info into charge slots.
147 clog.debug("ROUND 2")
149 val allChargeslots = for {
150 alignedTimeslot <- alignedTimeslots
152 val dslPolicy = getPolicy(alignedTimeslot)
153 val agreementName = getAgreementName(alignedTimeslot)
154 val agreementOpt = dslPolicy.findAgreement(agreementName)
158 val errMsg = "Unknown agreement %s during %s".format(agreementName, alignedTimeslot)
159 clog.error("%s", errMsg)
160 throw new Exception(errMsg)
162 case Some(agreement) ⇒
163 // TODO: Factor this out, just like we did with:
164 // TODO: val alignedTimeslots = splitTimeslotByPoliciesAndAgreements
165 // TODO: Note that most of the code is already taken from calcChangeChunks()
166 val alg = resolveEffectiveAlgorithmsForTimeslot(alignedTimeslot, agreement)
167 val pri = resolveEffectivePricelistsForTimeslot(alignedTimeslot, agreement)
168 val chargeChunks = splitChargeChunks(alg, pri)
169 val algorithmByTimeslot = chargeChunks._1
170 val pricelistByTimeslot = chargeChunks._2
172 // Now, the timeslots must be the same
173 val finegrainedTimeslots = algorithmByTimeslot.keySet
175 val chargeslots = for {
176 finegrainedTimeslot <- finegrainedTimeslots
178 val dslAlgorithm = algorithmByTimeslot(finegrainedTimeslot) // TODO: is this correct?
179 val dslPricelist = pricelistByTimeslot(finegrainedTimeslot) // TODO: is this correct?
180 val algorithmDefOpt = dslAlgorithm.algorithms.get(dslResource)
181 val priceUnitOpt = dslPricelist.prices.get(dslResource)
183 clog.debug("%s:", finegrainedTimeslot)
185 clog.debug("dslAlgorithm = %s", dslAlgorithm)
186 clog.debug("dslPricelist = %s", dslPricelist)
187 clog.debug("algorithmDefOpt = %s", algorithmDefOpt)
188 clog.debug("priceUnitOpt = %s", priceUnitOpt)
191 (algorithmDefOpt, priceUnitOpt) match {
194 "Unknown algorithm and price unit for resource %s during %s".
195 format(dslResource.name, finegrainedTimeslot))
198 "Unknown algorithm for resource %s during %s".
199 format(dslResource.name, finegrainedTimeslot))
202 "Unknown price unit for resource %s during %s".
203 format(dslResource.name, finegrainedTimeslot))
204 case (Some(algorithmDefinition), Some(priceUnit)) ⇒
205 Chargeslot(finegrainedTimeslot.from.getTime, finegrainedTimeslot.to.getTime, algorithmDefinition, priceUnit)
212 clog.unindent() // ROUND 2
219 allChargeslots.flatten
223 * Compute the charge slots generated by a particular resource event.
226 def computeFullChargeslots(previousResourceEventM: Maybe[ResourceEvent],
227 currentResourceEvent: ResourceEvent,
229 oldTotalAmount: Double,
230 newTotalAmount: Double,
231 dslResource: DSLResource,
232 defaultResourceMap: DSLResourcesMap,
233 agreementNamesByTimeslot: Map[Timeslot, String],
234 algorithmCompiler: CostPolicyAlgorithmCompiler,
235 policyStore: PolicyStore,
236 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[List[Chargeslot]] = Maybe {
238 val clog = ContextualLogger.fromOther(contextualLogger, logger, "computeFullChargeslots()")
241 val occurredDate = currentResourceEvent.occurredDate
242 val occurredMillis = currentResourceEvent.occurredMillis
243 val costPolicy = dslResource.costPolicy
246 val (referenceTimeslot, relevantPolicies, previousValue) = costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
247 // We need a previous event
249 previousResourceEventM match {
250 // We have a previous event
251 case Just(previousResourceEvent) ⇒
252 clog.debug("Have previous event")
253 val referenceTimeslot = Timeslot(previousResourceEvent.occurredDate, occurredDate)
255 // all policies within the interval from previous to current resource event
256 clog.debug("Calling policyStore.loadAndSortPoliciesWithin(%s)", referenceTimeslot)
257 val relevantPolicies = policyStore.loadAndSortPoliciesWithin(referenceTimeslot.from.getTime, referenceTimeslot.to.getTime, dsl)
259 (referenceTimeslot, relevantPolicies, previousResourceEvent.value)
261 // We do not have a previous event
264 "Unable to charge. No previous event given for %s".
265 format(currentResourceEvent.toDebugString(defaultResourceMap)))
267 // We could not obtain a previous event
268 case failed @ Failed(e, m) ⇒
270 "Unable to charge. Could not obtain previous event for %s".
271 format(currentResourceEvent.toDebugString(defaultResourceMap)), e)
274 // We do not need a previous event
276 // ... so we cannot compute timedelta from a previous event, there is just one chargeslot
277 // referring to (almost) an instant in time
278 clog.debug("DO NOT have previous event")
279 val referenceTimeslot = Timeslot(new MutableDateCalc(occurredDate).goPreviousMilli.toDate, occurredDate)
280 clog.debug("Calling policyStore.loadValidPolicyEntryAt(%s)", new MutableDateCalc(occurredMillis))
281 val relevantPolicyM = policyStore.loadValidPolicyAt(occurredMillis, dsl)
282 val relevantPolicies = relevantPolicyM match {
283 case Just(relevantPolicy) ⇒
284 Map(referenceTimeslot -> relevantPolicy)
286 throw new Exception("No relevant policy found for %s".format(referenceTimeslot))
287 case failed @ Failed(e, _) ⇒
288 throw new Exception("No relevant policy found for %s".format(referenceTimeslot), e)
292 (referenceTimeslot, relevantPolicies, costPolicy.getResourceInstanceUndefinedAmount)
294 clog.debug("previousValue = %s".format(previousValue))
295 clog.debug("referenceTimeslot = %s".format(referenceTimeslot))
296 clog.debug("relevantPolicies:")
298 val timeslots = relevantPolicies.keysIterator
299 for(ts <- timeslots) {
300 clog.debug("%s: %s", ts, relevantPolicies(ts))
304 val initialChargeslotsM = computeInitialChargeslots(
308 agreementNamesByTimeslot,
312 val fullChargeslotsM = initialChargeslotsM.map { chargeslots ⇒
314 case chargeslot @ Chargeslot(startMillis, stopMillis, algorithmDefinition, unitPrice, _) ⇒
315 val execAlgorithmM = algorithmCompiler.compile(algorithmDefinition)
316 execAlgorithmM match {
318 throw new Exception("Could not compile algorithm %s".format(algorithmDefinition))
320 case failed @ Failed(e, m) ⇒
321 throw new Exception(m, e)
323 case Just(execAlgorithm) ⇒
324 val valueMap = costPolicy.makeValueMap(
329 stopMillis - startMillis,
331 currentResourceEvent.value,
336 val creditsM = execAlgorithm.apply(valueMap)
341 "Could not compute credits for resource %s during %s".
342 format(dslResource.name, Timeslot(new Date(startMillis), new Date(stopMillis))))
344 case failed @ Failed(e, m) ⇒
345 throw new Exception(m, e)
348 chargeslot.copy(computedCredits = Some(credits))
354 val result = fullChargeslotsM match {
355 case Just(fullChargeslots) ⇒
359 case failed @ Failed(e, m) ⇒
360 throw new Exception(m, e)
369 * Create a list of wallet entries by charging for a resource event.
371 * @param currentResourceEvent The resource event to create charges for
372 * @param agreements The user's agreement names, indexed by their
373 * applicability timeslot
374 * @param previousAmount The current state of the resource
375 * @param previousOccurred The last time the resource state was updated
377 def chargeEvent(currentResourceEvent: ResourceEvent,
378 agreements: SortedMap[Timeslot, String],
379 previousAmount: Double,
380 previousOccurred: Date,
381 related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
383 assert(previousOccurred.getTime <= currentResourceEvent.occurredMillis)
384 val occuredDate = new Date(currentResourceEvent.occurredMillis)
386 /* The following makes sure that agreements exist between the start
387 * and end days of the processed event. As agreement updates are
388 * guaranteed not to leave gaps, this means that the event can be
389 * processed correctly, as at least one agreement will be valid
390 * throughout the event's life.
393 agreements.keysIterator.exists {
394 p => p.includes(occuredDate)
395 } && agreements.keysIterator.exists {
396 p => p.includes(previousOccurred)
400 val t = Timeslot(previousOccurred, occuredDate)
402 // Align policy and agreement validity timeslots to the event's boundaries
403 val policyTimeslots = t.align(
404 Policy.policies(previousOccurred, occuredDate).keysIterator.toList)
405 val agreementTimeslots = t.align(agreements.keysIterator.toList)
408 * Get a set of timeslot slices covering the different durations of
409 * agreements and policies.
411 val aligned = alignTimeslots(policyTimeslots, agreementTimeslots)
413 val walletEntries = aligned.map {
415 // Retrieve agreement from the policy valid at time of event
416 val agreementName = agreements.find(y => y._1.contains(x)) match {
418 case None => return Failed(new AccountingException(("Cannot find" +
419 " user agreement for period %s").format(x)))
422 // Do the wallet entry calculation
423 val entries = chargeEvent(
424 currentResourceEvent,
425 Policy.policy(x.from).findAgreement(agreementName._2).getOrElse(
426 return Failed(new AccountingException("Cannot get agreement for %s".format()))
434 case Failed(f, e) => return Failed(f,e)
444 * Creates a list of wallet entries by applying the agreement provisions on
445 * the resource state.
447 * @param event The resource event to create charges for
448 * @param agr The agreement implementation to use
449 * @param previousAmount The current state of the resource
450 * @param previousOccurred The timestamp of the previous event
451 * @param related Related wallet entries (TODO: should remove)
452 * @param chargeFor The duration for which the charge should be done.
453 * Should fall between the previous and current
454 * resource event boundaries
455 * @return A list of wallet entries, one for each
457 def chargeEvent(event: ResourceEvent,
459 previousAmount: Double,
460 previousOccurred: Date,
461 related: List[WalletEntry],
462 chargeFor: Option[Timeslot]): Maybe[List[WalletEntry]] = {
464 // If chargeFor is not null, make sure it falls within
465 // event time boundaries
466 chargeFor.map{x => assert(true,
467 Timeslot(previousOccurred, new Date(event.occurredMillis)))}
469 if (!event.validate())
470 return Failed(new AccountingException("Event not valid"))
472 val policy = Policy.policy
473 val dslResource = policy.findResource(event.resource) match {
475 case None => return Failed(
476 new AccountingException("No resource [%s]".format(event.resource)))
479 /* This is a safeguard against the special case where the last
480 * resource state update, as marked by the lastUpdate parameter
481 * is equal to the time of the event occurrence. This means that
482 * this is the first time the resource state has been recorded.
483 * Charging in this case only makes sense for discrete resources.
485 if (previousOccurred.getTime == event.occurredMillis) {
486 dslResource.costPolicy match {
487 case DiscreteCostPolicy => //Ok
488 case _ => return Some(List())
492 val creditCalculationValueM = dslResource.costPolicy.getValueForCreditCalculation(Just(previousAmount), event.value)
493 val amount = creditCalculationValueM match {
494 case failed @ Failed(_, _) ⇒
502 // We don't do strict checking for all cases for OnOffPolicies as
503 // above, since this point won't be reached in case of error.
504 val isFinal = dslResource.costPolicy match {
505 case OnOffCostPolicy =>
506 OnOffPolicyResourceState(previousAmount) match {
507 case OnResourceState => false
508 case OffResourceState => true
514 * Get the timeslot for which this event will be charged. In case we
515 * have a discrete resource, we do not really care for the time duration
516 * of an event. To process all events in a uniform way, we create an
517 * artificial timeslot lasting the minimum amount of time. In all other
518 * cases, we first check whether a desired charge period passed as
521 val timeslot = dslResource.costPolicy match {
522 case DiscreteCostPolicy => Timeslot(new Date(event.occurredMillis - 1),
523 new Date(event.occurredMillis))
524 case _ => chargeFor match {
526 case None => Timeslot(previousOccurred, new Date(event.occurredMillis))
531 * The following splits the chargable timeslot into smaller timeslots to
532 * comply with different applicability periods for algorithms and
533 * pricelists defined by the provided agreement.
535 val chargeChunks = calcChangeChunks(agr, amount, dslResource, timeslot)
537 val timeReceived = System.currentTimeMillis
539 val rel = event.id :: related.map{x => x.sourceEventIDs}.flatten
541 val entries = chargeChunks.map { c=>
543 id = CryptoUtils.sha1(c.id),
544 occurredMillis = event.occurredMillis,
545 receivedMillis = timeReceived,
546 sourceEventIDs = rel,
549 userId = event.userId,
550 resource = event.resource,
551 instanceId = event.instanceId,
561 def calcChangeChunks(agr: DSLAgreement, volume: Double,
562 res: DSLResource, t: Timeslot): List[ChargeChunk] = {
564 val alg = resolveEffectiveAlgorithmsForTimeslot(t, agr)
565 val pri = resolveEffectivePricelistsForTimeslot(t, agr)
566 val chunks = splitChargeChunks(alg, pri)
567 val algChunked = chunks._1
568 val priChunked = chunks._2
570 assert(algChunked.size == priChunked.size)
572 res.costPolicy match {
573 case DiscreteCostPolicy => calcChargeChunksDiscrete(algChunked, priChunked, volume, res)
574 case _ => calcChargeChunksContinuous(algChunked, priChunked, volume, res)
579 * Get a list of charge chunks for discrete resources.
582 def calcChargeChunksDiscrete(algChunked: Map[Timeslot, DSLAlgorithm],
583 priChunked: Map[Timeslot, DSLPriceList],
584 volume: Double, res: DSLResource): List[ChargeChunk] = {
585 // In case of descrete resources, we only a expect a
586 assert(algChunked.size == 1)
587 assert(priChunked.size == 1)
588 assert(algChunked.keySet.head.compare(priChunked.keySet.head) == 0)
590 List(ChargeChunk(volume,
591 algChunked.valuesIterator.next.algorithms.getOrElse(res, ""),
592 priChunked.valuesIterator.next.prices.getOrElse(res, 0),
593 algChunked.keySet.head, res))
597 * Get a list of charge chunks for continuous resources.
600 def calcChargeChunksContinuous(algChunked: Map[Timeslot, DSLAlgorithm],
601 priChunked: Map[Timeslot, DSLPriceList],
602 volume: Double, res: DSLResource): List[ChargeChunk] = {
603 algChunked.keysIterator.map {
606 algChunked.get(x).get.algorithms.getOrElse(res, ""),
607 priChunked.get(x).get.prices.getOrElse(res, 0), x, res)
612 * Align charge timeslots between algorithms and pricelists. As algorithm
613 * and pricelists can have different effectivity periods, this method
614 * examines them and splits them as necessary.
616 private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
617 price: SortedMap[Timeslot, DSLPriceList]) :
618 (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
620 val zipped = alg.keySet.zip(price.keySet)
622 zipped.find(p => !p._1.equals(p._2)) match {
623 case None => (alg, price)
625 val algTimeslot = x._1
626 val priTimeslot = x._2
628 assert(algTimeslot.from == priTimeslot.from)
630 if (algTimeslot.endsAfter(priTimeslot)) {
631 val slices = algTimeslot.slice(priTimeslot.to)
632 val algo = alg.get(algTimeslot).get
633 val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo)
634 splitChargeChunks(newalg, price)
637 val slices = priTimeslot.slice(priTimeslot.to)
638 val pl = price.get(priTimeslot).get
639 val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl)
640 splitChargeChunks(alg, newPrice)
646 * Given two lists of timeslots, produce a list which contains the
647 * set of timeslot slices, as those are defined by
650 * For example, given the timeslots a and b below, split them as shown.
652 * a = |****************|
659 * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
661 private[logic] def alignTimeslots(a: List[Timeslot],
663 clogM: Maybe[ContextualLogger] = NoVal): List[Timeslot] = {
664 val clog = ContextualLogger.fromOther(clogM, logger, "alignTimeslots()")
667 ContextualLogger.debugList(clog, "a", a)
668 ContextualLogger.debugList(clog, "b", b)
670 if (a.isEmpty) return b.tail
671 if (b.isEmpty) return a.tail
672 assert (a.head.from == b.head.from)
674 val clogJ = Just(clog)
675 val result = if (a.head.endsAfter(b.head)) {
676 clog.debug("Branch: a.head.endsAfter(b.head)")
677 a.head.slice(b.head.to) ::: alignTimeslots(a.tail, b.tail, clogJ)
678 } else if (b.head.endsAfter(a.head)) {
679 clog.debug("Branch: b.head.endsAfter(a.head)")
680 b.head.slice(a.head.to) ::: alignTimeslots(a.tail, b.tail, clogJ)
682 clog.debug("Branch: !a.head.endsAfter(b.head) && !b.head.endsAfter(a.head)")
683 a.head :: alignTimeslots(a.tail, b.tail, clogJ)
692 * Encapsulates a computation for a specific timeslot of
695 case class ChargeChunk(value: Double, algorithm: String,
696 price: Double, when: Timeslot,
697 resource: DSLResource) {
699 assert(!algorithm.isEmpty)
700 assert(resource != null)
703 //TODO: Apply the algorithm, when we start parsing it
704 resource.costPolicy match {
705 case DiscreteCostPolicy =>
708 value * price * when.hours
711 def reason(): String =
712 resource.costPolicy match {
713 case DiscreteCostPolicy =>
714 "%f %s at %s @ %f/%s".format(value, resource.unit, when.from, price,
716 case ContinuousCostPolicy =>
717 "%f %s of %s from %s to %s @ %f/%s".format(value, resource.unit,
718 resource.name, when.from, when.to, price, resource.unit)
719 case OnOffCostPolicy =>
720 "%f %s of %s from %s to %s @ %f/%s".format(when.hours, resource.unit,
721 resource.name, when.from, when.to, price, resource.unit)
725 CryptoUtils.sha1("%f%s%f%s%s%d".format(value, algorithm, price, when.toString,
726 resource.name, System.currentTimeMillis()))
729 /** An exception raised when something goes wrong with accounting */
730 class AccountingException(msg: String) extends Exception(msg)