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.user
38 import gr.grnet.aquarium.store.memory.MemStore
39 import gr.grnet.aquarium.util.date.MutableDateCalc
40 import gr.grnet.aquarium.logic.accounting.dsl._
41 import gr.grnet.aquarium.logic.accounting.{Policy, Accounting}
42 import gr.grnet.aquarium.util.{Loggable, ContextualLogger, justForSure}
43 import gr.grnet.aquarium.simulation._
44 import gr.grnet.aquarium.uid.{UIDGenerator, ConcurrentVMLocalUIDGenerator}
45 import com.ckkloverdos.maybe.{Maybe, Just, NoVal}
46 import org.junit.{Assert, Ignore, Test}
47 import gr.grnet.aquarium.logic.accounting.algorithm.{ExecutableCostPolicyAlgorithm, CostPolicyAlgorithmCompiler, SimpleCostPolicyAlgorithmCompiler}
48 import gr.grnet.aquarium.{AquariumException, Configurator}
53 * @author Christos KK Loverdos <loverdos@gmail.com>
55 class UserStateComputationsTest extends Loggable {
56 final val DoubleDelta = 0.001
58 final val BandwidthPriceUnit = 3.3 //
59 final val VMTimePriceUnit = 1.5 //
60 final val DiskspacePriceUnit = 2.7 //
62 final val OnOffPriceUnit = VMTimePriceUnit
63 final val ContinuousPriceUnit = DiskspacePriceUnit
64 final val DiscretePriceUnit = BandwidthPriceUnit
66 final val PolicyYAML = """
79 descriminatorfield: vmid
84 costpolicy: continuous
93 bandwidth: function bandwidth() {return 1;}
94 vmtime: function vmtime() {return 1;}
95 diskspace: function diskspace() {return 1;}
128 val Computations = new UserStateComputations
130 val DefaultPolicy = new DSL{} parse PolicyYAML
131 val DefaultAccounting = new Accounting{}
133 val DefaultAlgorithm = new ExecutableCostPolicyAlgorithm {
134 def creditsForContinuous(timeDelta: Double, oldTotalAmount: Double) =
135 hrs(timeDelta) * oldTotalAmount * ContinuousPriceUnit
137 final val creditsForDiskspace = creditsForContinuous(_, _)
139 def creditsForDiscrete(currentValue: Double) =
140 currentValue * DiscretePriceUnit
142 final val creditsForBandwidth = creditsForDiscrete(_)
144 def creditsForOnOff(timeDelta: Double) =
145 hrs(timeDelta) * OnOffPriceUnit
147 final val creditsForVMTime = creditsForOnOff(_)
149 @inline private[this]
150 def hrs(millis: Double) = millis / 1000 / 60 / 60
152 def apply(vars: Map[DSLCostPolicyVar, Any]): Maybe[Double] = Maybe {
153 vars.apply(DSLCostPolicyNameVar) match {
154 case DSLCostPolicyNames.continuous ⇒
155 val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
156 val oldTotalAmount = vars(DSLOldTotalAmountVar).asInstanceOf[Double]
157 val timeDelta = vars(DSLTimeDeltaVar).asInstanceOf[Double]
159 Assert.assertEquals(ContinuousPriceUnit, unitPrice, DoubleDelta)
161 creditsForContinuous(timeDelta, oldTotalAmount)
163 case DSLCostPolicyNames.discrete ⇒
164 val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
165 val currentValue = vars(DSLCurrentValueVar).asInstanceOf[Double]
167 Assert.assertEquals(DiscretePriceUnit, unitPrice, DoubleDelta)
169 creditsForDiscrete(currentValue)
171 case DSLCostPolicyNames.onoff ⇒
172 val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
173 val timeDelta = vars(DSLTimeDeltaVar).asInstanceOf[Double]
175 Assert.assertEquals(OnOffPriceUnit, unitPrice, DoubleDelta)
177 creditsForOnOff(timeDelta)
179 case DSLCostPolicyNames.once ⇒
180 val currentValue = vars(DSLCurrentValueVar).asInstanceOf[Double]
184 throw new AquariumException("Unknown cost policy %s".format(name))
188 override def toString = "DefaultAlgorithm(%s)".format(
190 DSLCostPolicyNames.continuous -> "hrs(timeDelta) * oldTotalAmount * %s".format(ContinuousPriceUnit),
191 DSLCostPolicyNames.discrete -> "currentValue * %s".format(DiscretePriceUnit),
192 DSLCostPolicyNames.onoff -> "hrs(timeDelta) * %s".format(OnOffPriceUnit),
193 DSLCostPolicyNames.once -> "currentValue"))
196 val DefaultCompiler = new CostPolicyAlgorithmCompiler {
197 def compile(definition: String): Maybe[ExecutableCostPolicyAlgorithm] = {
198 Just(DefaultAlgorithm)
201 //val DefaultAlgorithm = justForSure(DefaultCompiler.compile("")).get // hardcoded since we know exactly what this is
203 val VMTimeDSLResource = DefaultPolicy.findResource("vmtime").get
205 // For this to work, the definitions must match those in the YAML above.
206 // Those StdXXXResourceSim are just for debugging convenience anyway, so they must match by design.
207 val VMTimeResourceSim = StdVMTimeResourceSim.fromPolicy(DefaultPolicy)
208 val DiskspaceResourceSim = StdDiskspaceResourceSim.fromPolicy(DefaultPolicy)
209 val BandwidthResourceSim = StdBandwidthResourceSim.fromPolicy(DefaultPolicy)
211 // There are two client services, synnefo and pithos.
212 val TheUIDGenerator: UIDGenerator[_] = new ConcurrentVMLocalUIDGenerator
213 val Synnefo = ClientSim("synnefo")(TheUIDGenerator)
214 val Pithos = ClientSim("pithos" )(TheUIDGenerator)
216 val mc = Configurator.MasterConfigurator.withStoreProviderClass(classOf[MemStore])
217 Policy.withConfigurator(mc)
218 val StoreProvider = mc.storeProvider
219 val ResourceEventStore = StoreProvider.resourceEventStore
221 val StartOfBillingYearDateCalc = new MutableDateCalc(2012, 1, 1)
222 val UserCreationDate = new MutableDateCalc(2011, 11, 1).toDate
224 val BillingMonthInfoJan = {
225 val MutableDateCalcJan = new MutableDateCalc(2012, 1, 1)
226 BillingMonthInfo.fromDateCalc(MutableDateCalcJan)
228 val BillingMonthInfoFeb = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012, 2, 1))
229 val BillingMonthInfoMar = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012, 3, 1))
231 // Store the default policy
232 val policyDateCalc = StartOfBillingYearDateCalc.copy
233 val policyOccurredMillis = policyDateCalc.toMillis
234 val policyValidFromMillis = policyDateCalc.copy.goPreviousYear.toMillis
235 val policyValidToMillis = policyDateCalc.copy.goNextYear.toMillis
236 StoreProvider.policyStore.storePolicyEntry(DefaultPolicy.toPolicyEntry(policyOccurredMillis, policyValidFromMillis, policyValidToMillis))
238 val Aquarium = AquariumSim(List(VMTimeResourceSim, DiskspaceResourceSim, BandwidthResourceSim), StoreProvider.resourceEventStore)
239 val DefaultResourcesMap = Aquarium.resourcesMap
241 val UserCKKL = Aquarium.newUser("CKKL", UserCreationDate)
243 val InitialUserState = Computations.createInitialUserState(
244 userId = UserCKKL.userId,
245 userCreationMillis = UserCreationDate.getTime,
248 roleNames = List("user"),
249 agreementName = DSLAgreement.DefaultAgreementName
253 // - synnefo is for VMTime and Bandwidth
254 // - pithos is for Diskspace
255 val VMTimeInstanceSim = VMTimeResourceSim.newInstance ("VM.1", UserCKKL, Synnefo)
256 val BandwidthInstanceSim = BandwidthResourceSim.newInstance("3G.1", UserCKKL, Synnefo)
257 val DiskInstanceSim = DiskspaceResourceSim.newInstance("DISK.1", UserCKKL, Pithos)
260 def showUserState(clog: ContextualLogger, userState: UserState) {
261 val id = userState.id
262 val parentId = userState.parentUserStateId
263 val credits = userState.creditsSnapshot.creditAmount
264 val newWalletEntries = userState.newWalletEntries.map(_.toDebugString)
265 val changeReasonCode = userState.lastChangeReasonCode
266 val changeReason = userState.lastChangeReason
267 val implicitlyIssued = userState.implicitlyIssuedSnapshot.implicitlyIssuedEvents.map(_.toDebugString())
268 val latestResourceEvents = userState.latestResourceEventsSnapshot.resourceEvents.map(_.toDebugString())
270 clog.debug("_id = %s", id)
271 clog.debug("parentId = %s", parentId)
272 clog.debug("credits = %s", credits)
273 clog.debug("changeReasonCode = %s", changeReasonCode)
274 clog.debug("changeReason = %s", changeReason)
275 clog.debugSeq("implicitlyIssued", implicitlyIssued, 0)
276 clog.debugSeq("latestResourceEvents", latestResourceEvents, 0)
277 clog.debugSeq("newWalletEntries", newWalletEntries, 0)
281 def showResourceEvents(clog: ContextualLogger): Unit = {
283 clog.begin("Events by OccurredMillis")
285 for(event <- UserCKKL.myResourceEventsByOccurredDate) {
286 clog.debug(event.toDebugString())
289 clog.end("Events by OccurredMillis")
294 def doFullMonthlyBilling(clog: ContextualLogger, billingMonthInfo: BillingMonthInfo) = {
295 Computations.doFullMonthlyBilling(
303 MonthlyBillingCalculation(billingMonthInfo),
309 def justUserState(userStateM: Maybe[UserState]): UserState = {
311 case Just(userState) ⇒ userState
312 case _ ⇒ throw new AquariumException("Unexpected %s".format(userStateM))
317 def expectCredits(clog: ContextualLogger,
318 creditsConsumed: Double,
319 userState: UserState,
320 accuracy: Double = 0.001): Unit = {
321 val computed = userState.creditsSnapshot.creditAmount
322 Assert.assertEquals(-creditsConsumed, computed, accuracy)
323 clog.info("Consumed %.3f credits [accuracy = %f]", creditsConsumed, accuracy)
327 def millis2hrs(millis: Long) = millis.toDouble / 1000 / 60 / 60
330 def hrs2millis(hrs: Double) = (hrs * 60 * 60 * 1000).toLong
333 * Test a sequence of ON, OFF vmtime events.
337 def testFullOnOff: Unit = {
338 val clog = ContextualLogger.fromOther(NoVal, logger, "testFullOnOff()")
341 ResourceEventStore.clearResourceEvents()
342 val OnOffDurationHrs = 10
343 val OnOffDurationMillis = hrs2millis(OnOffDurationHrs.toDouble)
345 VMTimeInstanceSim.newONOFF(
346 new MutableDateCalc(2012, 01, 10).goPlusHours(13).goPlusMinutes(30).toDate, // 2012-01-10 13:30:00.000
350 val credits = DefaultAlgorithm.creditsForVMTime(OnOffDurationMillis)
352 showResourceEvents(clog)
354 val userStateM = doFullMonthlyBilling(clog, BillingMonthInfoJan)
355 val userState = justUserState(userStateM)
357 showUserState(clog, userState)
359 expectCredits(clog, credits, userState)
366 def testLonelyON: Unit = {
367 val clog = ContextualLogger.fromOther(NoVal, logger, "testLonelyON()")
370 ResourceEventStore.clearResourceEvents()
372 val JanStart = new MutableDateCalc(2012, 01, 01)
373 val JanEnd = JanStart.copy.goEndOfThisMonth
374 val JanStartDate = JanStart.toDate
375 val OnOffImplicitDurationMillis = JanEnd.toMillis - JanStart.toMillis
376 val OnOffImplicitDurationHrs = millis2hrs(OnOffImplicitDurationMillis)
378 VMTimeInstanceSim.newON(JanStartDate)
380 val credits = DefaultAlgorithm.creditsForVMTime(OnOffImplicitDurationMillis)
382 showResourceEvents(clog)
384 val userStateM = doFullMonthlyBilling(clog, BillingMonthInfoJan)
385 val userState = justUserState(userStateM)
387 showUserState(clog, userState)
389 expectCredits(clog, credits, userState)
396 def testOrphanOFF: Unit = {
397 val clog = ContextualLogger.fromOther(NoVal, logger, "testOrphanOFF()")
400 ResourceEventStore.clearResourceEvents()
402 val JanStart = new MutableDateCalc(2012, 01, 01)
403 val JanEnd = JanStart.copy.goEndOfThisMonth
404 val JanStartDate = JanStart.toDate
405 val OnOffImplicitDurationMillis = JanEnd.toMillis - JanStart.toMillis
406 val OnOffImplicitDurationHrs = millis2hrs(OnOffImplicitDurationMillis)
408 VMTimeInstanceSim.newOFF(JanStartDate)
410 // This is an orphan event, so no credits will be charged
413 showResourceEvents(clog)
415 val userStateM = doFullMonthlyBilling(clog, BillingMonthInfoJan)
416 val userState = justUserState(userStateM)
418 showUserState(clog, userState)
420 expectCredits(clog, credits, userState)
427 def testOne: Unit = {
428 val clog = ContextualLogger.fromOther(NoVal, logger, "testOne()")
431 // Let's create our dates of interest
432 val VMStartDateCalc = StartOfBillingYearDateCalc.copy.goPlusDays(1).goPlusHours(1)
433 val VMStartDate = VMStartDateCalc.toDate
435 // Within January, create one VM ON-OFF ...
436 VMTimeInstanceSim.newONOFF(VMStartDate, 9)
438 val diskConsumptionDateCalc = StartOfBillingYearDateCalc.copy.goPlusHours(3)
439 val diskConsumptionDate1 = diskConsumptionDateCalc.toDate
440 val diskConsumptionDateCalc2 = diskConsumptionDateCalc.copy.goPlusDays(1).goPlusHours(1)
441 val diskConsumptionDate2 = diskConsumptionDateCalc2.toDate
443 // ... and two diskspace changes
444 DiskInstanceSim.consumeMB(diskConsumptionDate1, 99)
445 DiskInstanceSim.consumeMB(diskConsumptionDate2, 23)
447 // 100MB 3G bandwidth
448 val bwDateCalc = diskConsumptionDateCalc2.copy.goPlusDays(1)
449 BandwidthInstanceSim.useBandwidth(bwDateCalc.toDate, 100.0)
451 // ... and one "future" event
452 DiskInstanceSim.consumeMB(
453 StartOfBillingYearDateCalc.copy.
454 goNextMonth.goPlusDays(6).
458 goPlusMillis(7).toDate,
461 showResourceEvents(clog)
463 // Policy: from 2012-01-01 to Infinity
465 clog.debugMap("DefaultResourcesMap", DefaultResourcesMap.map, 1)
467 val userStateM = doFullMonthlyBilling(clog, BillingMonthInfoJan)
468 val userState = justUserState(userStateM)
469 showUserState(clog, userState)