Applicability timeslots for policies and timeslots
authorGeorgios Gousios <gousiosg@gmail.com>
Fri, 17 Feb 2012 14:17:49 +0000 (16:17 +0200)
committerGeorgios Gousios <gousiosg@gmail.com>
Fri, 17 Feb 2012 14:17:49 +0000 (16:17 +0200)
Charge events by spliting charge time into
slices defined by the applicability timeslots for
policies and agreements

src/main/scala/gr/grnet/aquarium/logic/accounting/Accounting.scala
src/test/scala/gr/grnet/aquarium/logic/test/AccountingTest.scala

index b097f4e..4edbcbc 100644 (file)
@@ -127,22 +127,97 @@ trait Accounting extends DSLUtils with Loggable {
       }
     }.flatten1
   }
+
   /**
-   * Creates a list of wallet entries by applying the agreement provisions on
-   * the resource state.
+   * Creates a list of wallet entries by examining the on the resource state.
    *
    * @param currentResourceEvent The resource event to create charges for
-   * @param agreement The agreement applicable to the user mentioned in the event
+   * @param agreements The user's agreement names, indexed by their
+   *                   applicability timeslot
    * @param previousAmount The current state of the resource
    * @param previousOccurred The last time the resource state was updated
    */
   def chargeEvent(currentResourceEvent: ResourceEvent,
-                  agreements: List[String],
+                  agreements: SortedMap[Timeslot, String],
                   previousAmount: Double,
                   previousOccurred: Date,
                   related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
 
     assert(previousOccurred.getTime <= currentResourceEvent.occurredMillis)
+    val occuredDate = new Date(currentResourceEvent.occurredMillis)
+
+    /* The following makes sure that agreements exist between the start
+     * and end days of the processed event. As agreement updates are
+     * guaranteed not to leave gaps, this means that the event can be
+     * processed correctly, as at least one agreement will be valid
+     * throughout the event's life.
+     */
+    assert(
+      agreements.keys.exists {
+        p => p.includes(occuredDate)
+      } && agreements.keys.exists {
+        p => p.includes(previousOccurred)
+      }
+    )
+
+    val t = Timeslot(previousOccurred, occuredDate)
+
+    // Align policy and agreement validity timeslots to the event's boundaries
+    val policyTimeslots = t.align(
+      Policy.policies(previousOccurred, occuredDate).keys.toList)
+    val agreementTimeslots = t.align(agreements.keys.toList)
+
+    /*
+     * Get a set of timeslot slices covering the different durations of
+     * agreements and policies.
+     */
+    val aligned = alignTimeslots(policyTimeslots, agreementTimeslots)
+
+    val walletEntries = aligned.map {
+      x =>
+        // Retrieve agreement from policy valid at time of event
+        val agreementName = agreements.find(y => y._1.contains(x)) match {
+          case Some(x) => x
+          case None => return Failed(new AccountingException(("Cannot find" +
+            " user agreement for period %s").format(x)))
+        }
+
+        // Do the wallet entry calculation
+        val entries = chargeEvent(
+          currentResourceEvent,
+          Policy.policy(x.from).findAgreement(agreementName._2).getOrElse(
+            return Failed(new AccountingException("Cannot get agreement for "))
+          ),
+          previousAmount,
+          previousOccurred,
+          related
+        ) match {
+          case Just(x) => x
+          case Failed(f, e) => return Failed(f,e)
+          case NoVal => List()
+        }
+        entries
+    }.flatten
+
+    Just(walletEntries)
+  }
+
+  /**
+   * Creates a list of wallet entries by applying the agreement provisions on
+   * the resource state.
+   *
+   * @param currentResourceEvent The resource event to create charges for
+   * @param agr The agreement implementation to use
+   * @param previousAmount The current state of the resource
+   * @param previousOccurred
+   * @param related
+   * @return
+   */
+  def chargeEvent(currentResourceEvent: ResourceEvent,
+                  agr: DSLAgreement,
+                  previousAmount: Double,
+                  previousOccurred: Date,
+                  related: List[WalletEntry]): Maybe[List[WalletEntry]] = {
 
     if (!currentResourceEvent.validate())
       return Failed(new AccountingException("Event not valid"))
@@ -193,7 +268,7 @@ trait Accounting extends DSLUtils with Loggable {
       case _ => Timeslot(previousOccurred, new Date(currentResourceEvent.occurredMillis))
     }
 
-    val chargeChunks = calcChangeChunks(agreement, amount, dslResource, timeslot)
+    val chargeChunks = calcChangeChunks(agr, amount, dslResource, timeslot)
 
     val timeReceived = System.currentTimeMillis
 
@@ -266,7 +341,7 @@ trait Accounting extends DSLUtils with Loggable {
    * examines them and splits them as necessary.
    */
   private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm],
-                        price: SortedMap[Timeslot, DSLPriceList]) :
+                                       price: SortedMap[Timeslot, DSLPriceList]) :
     (Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = {
 
     val zipped = alg.keySet.zip(price.keySet)
@@ -293,8 +368,43 @@ trait Accounting extends DSLUtils with Loggable {
         }
     }
   }
+
+  /**
+   * Given two lists of timeslots, produce a list which contains the
+   * set of timeslot slices, as those are defined by
+   * timeslot overlaps.
+   *
+   * For example, given the timeslots a and b below, split them as shown.
+   *
+   * a = |****************|
+   *     ^                ^
+   *   a.from            a.to
+   * b = |*********|
+   *     ^         ^
+   *   b.from     b.to
+   *
+   * result: List(Timeslot(a.from, b.to), Timeslot(b.to, a.to))
+   */
+  private[logic] def alignTimeslots(a: List[Timeslot],
+                                    b: List[Timeslot]): List[Timeslot] = {
+    if (a.isEmpty) return b.tail
+    if (b.isEmpty) return a.tail
+    assert (a.head.from == b.head.from)
+
+    if (a.head.endsAfter(b.head)) {
+      a.head.slice(b.head.to) ::: alignTimeslots(a.tail, b.tail)
+    } else if (b.head.endsAfter(a.head)) {
+      b.head.slice(a.head.to) ::: alignTimeslots(a.tail, b.tail)
+    } else {
+      a.head :: alignTimeslots(a.tail, b.tail)
+    }
+  }
 }
 
+/**
+ * Encapsulates a computation for a specific timeslot of
+ * resource usage.
+ */
 case class ChargeChunk(value: Double, algorithm: String,
                        price: Double, when: Timeslot,
                        resource: DSLResource) {
index 444e47c..1a3acea 100644 (file)
@@ -52,6 +52,19 @@ import com.ckkloverdos.maybe.{NoVal, Failed, Just}
 class AccountingTest extends DSLTestBase with Accounting with TestMethods {
 
   @Test
+  def testAlignTimeslots() {
+    before
+    val from = new Date(1322555880000L) //Tue, 29 Nov 2011 10:38:00 EET
+    val to = new Date(1322689082000L) //Wed, 30 Nov 2011 23:38:02 EET
+    val agr = dsl.findAgreement("scaledbandwidth").get
+    val a = resolveEffectiveAlgorithmsForTimeslot(Timeslot(from, to), agr).keys.toList
+    val b = resolveEffectivePricelistsForTimeslot(Timeslot(from, to), agr).keys.toList
+
+    val result = alignTimeslots(a, b)
+    assertEquals(12, result.size)
+  }
+
+  @Test
   def testSplitChargeChunks() = {
     before 
     val from = new Date(1322555880000L) //Tue, 29 Nov 2011 10:38:00 EET