133b292e8ba7a70f511efd6e3af872ad60aa8564
[aquarium] / src / test / scala / gr / grnet / aquarium / user / UserStateComputationsTest.scala
1 /*
2  * Copyright 2011-2012 GRNET S.A. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or
5  * without modification, are permitted provided that the following
6  * conditions are met:
7  *
8  *   1. Redistributions of source code must retain the above
9  *      copyright notice, this list of conditions and the following
10  *      disclaimer.
11  *
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.
16  *
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.
29  *
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.
34  */
35
36 package gr.grnet.aquarium.user
37
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}
43 import gr.grnet.aquarium.simulation._
44 import gr.grnet.aquarium.uid.{UIDGenerator, ConcurrentVMLocalUIDGenerator}
45 import com.ckkloverdos.maybe.{Maybe, Just}
46 import org.junit.{Assert, Ignore, Test}
47 import gr.grnet.aquarium.logic.accounting.algorithm.{ExecutableCostPolicyAlgorithm, CostPolicyAlgorithmCompiler}
48 import gr.grnet.aquarium.{AquariumException, Configurator}
49 import gr.grnet.aquarium.computation.{UserState, BillingMonthInfo, UserStateComputations}
50 import gr.grnet.aquarium.computation.reason.MonthlyBillingCalculation
51
52
53 /**
54  *
55  * @author Christos KK Loverdos <loverdos@gmail.com>
56  */
57 class UserStateComputationsTest extends Loggable {
58   final val DoubleDelta = 0.001
59
60   final val BandwidthPriceUnit = 3.3 //
61   final val VMTimePriceUnit    = 1.5 //
62   final val DiskspacePriceUnit = 2.7 //
63
64   final val OnOffPriceUnit = VMTimePriceUnit
65   final val ContinuousPriceUnit = DiskspacePriceUnit
66   final val DiscretePriceUnit = BandwidthPriceUnit
67
68   final val PolicyYAML = """
69 aquariumpolicy:
70   resources:
71     - resource:
72       name: bandwidth
73       unit: MB/Hr
74       complex: false
75       costpolicy: discrete
76     - resource:
77       name: vmtime
78       unit: Hr
79       complex: true
80       costpolicy: onoff
81       descriminatorfield: vmid
82     - resource:
83       name: diskspace
84       unit: MB/hr
85       complex: false
86       costpolicy: continuous
87
88   implicitvars:
89     - price
90     - volume
91
92   algorithms:
93     - algorithm:
94       name: default
95       bandwidth: function bandwidth() {return 1;}
96       vmtime: function vmtime() {return 1;}
97       diskspace: function diskspace() {return 1;}
98       effective:
99         from: 0
100
101   pricelists:
102     - pricelist:
103       name: default
104       bandwidth: %s
105       vmtime: %s
106       diskspace: %s
107       effective:
108         from: 0
109
110   creditplans:
111     - creditplan:
112       name: default
113       credits: 100
114       at: "00 00 1 * *"
115       effective:
116         from: 0
117
118   agreements:
119     - agreement:
120       name: default
121       algorithm: default
122       pricelist: default
123       creditplan: default
124   """.format(
125     BandwidthPriceUnit,
126     VMTimePriceUnit,
127     DiskspacePriceUnit
128   )
129
130   val Computations = new UserStateComputations
131
132   val DefaultPolicy = new DSL{} parse PolicyYAML
133   val DefaultAccounting = new Accounting{}
134   
135   val DefaultAlgorithm = new ExecutableCostPolicyAlgorithm {
136     def creditsForContinuous(timeDelta: Double, oldTotalAmount: Double) =
137       hrs(timeDelta) * oldTotalAmount * ContinuousPriceUnit
138
139     final val creditsForDiskspace = creditsForContinuous(_, _)
140     
141     def creditsForDiscrete(currentValue: Double) =
142       currentValue * DiscretePriceUnit
143
144     final val creditsForBandwidth = creditsForDiscrete(_)
145
146     def creditsForOnOff(timeDelta: Double) =
147       hrs(timeDelta) * OnOffPriceUnit
148
149     final val creditsForVMTime = creditsForOnOff(_)
150
151     @inline private[this]
152     def hrs(millis: Double) = millis / 1000 / 60 / 60
153
154     def apply(vars: Map[DSLCostPolicyVar, Any]): Double = {
155       vars.apply(DSLCostPolicyNameVar) match {
156         case DSLCostPolicyNames.continuous ⇒
157           val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
158           val oldTotalAmount = vars(DSLOldTotalAmountVar).asInstanceOf[Double]
159           val timeDelta = vars(DSLTimeDeltaVar).asInstanceOf[Double]
160
161           Assert.assertEquals(ContinuousPriceUnit, unitPrice, DoubleDelta)
162
163           creditsForContinuous(timeDelta, oldTotalAmount)
164
165         case DSLCostPolicyNames.discrete ⇒
166           val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
167           val currentValue = vars(DSLCurrentValueVar).asInstanceOf[Double]
168
169           Assert.assertEquals(DiscretePriceUnit, unitPrice, DoubleDelta)
170
171           creditsForDiscrete(currentValue)
172
173         case DSLCostPolicyNames.onoff ⇒
174           val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
175           val timeDelta = vars(DSLTimeDeltaVar).asInstanceOf[Double]
176
177           Assert.assertEquals(OnOffPriceUnit, unitPrice, DoubleDelta)
178
179           creditsForOnOff(timeDelta)
180
181         case DSLCostPolicyNames.once ⇒
182           val currentValue = vars(DSLCurrentValueVar).asInstanceOf[Double]
183           currentValue
184
185         case name ⇒
186           throw new AquariumException("Unknown cost policy %s".format(name))
187       }
188     }
189
190     override def toString = "DefaultAlgorithm(%s)".format(
191       Map(
192         DSLCostPolicyNames.continuous -> "hrs(timeDelta) * oldTotalAmount * %s".format(ContinuousPriceUnit),
193         DSLCostPolicyNames.discrete   -> "currentValue * %s".format(DiscretePriceUnit),
194         DSLCostPolicyNames.onoff      -> "hrs(timeDelta) * %s".format(OnOffPriceUnit),
195         DSLCostPolicyNames.once       -> "currentValue"))
196   }
197
198   val DefaultCompiler  = new CostPolicyAlgorithmCompiler {
199     def compile(definition: String): ExecutableCostPolicyAlgorithm = {
200       DefaultAlgorithm
201     }
202   }
203   //val DefaultAlgorithm = justForSure(DefaultCompiler.compile("")).get // hardcoded since we know exactly what this is
204
205   val VMTimeDSLResource = DefaultPolicy.findResource("vmtime").get
206
207   // For this to work, the definitions must match those in the YAML above.
208   // Those StdXXXResourceSim are just for debugging convenience anyway, so they must match by design.
209   val VMTimeResourceSim    = StdVMTimeResourceSim.fromPolicy(DefaultPolicy)
210   val DiskspaceResourceSim = StdDiskspaceResourceSim.fromPolicy(DefaultPolicy)
211   val BandwidthResourceSim = StdBandwidthResourceSim.fromPolicy(DefaultPolicy)
212
213   // There are two client services, synnefo and pithos.
214   val TheUIDGenerator: UIDGenerator[_] = new ConcurrentVMLocalUIDGenerator
215   val Synnefo = ClientSim("synnefo")(TheUIDGenerator)
216   val Pithos  = ClientSim("pithos" )(TheUIDGenerator)
217
218   val mc = Configurator.MasterConfigurator.withStoreProviderClass(classOf[MemStore])
219   Policy.withConfigurator(mc)
220   val StoreProvider = mc.storeProvider
221   val ResourceEventStore = StoreProvider.resourceEventStore
222
223   val StartOfBillingYearDateCalc = new MutableDateCalc(2012,  1, 1)
224   val UserCreationDate           = new MutableDateCalc(2011, 11, 1).toDate
225
226   val BillingMonthInfoJan = {
227     val MutableDateCalcJan = new MutableDateCalc(2012, 1, 1)
228     BillingMonthInfo.fromDateCalc(MutableDateCalcJan)
229   }
230   val BillingMonthInfoFeb = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012,  2, 1))
231   val BillingMonthInfoMar = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012,  3, 1))
232
233   // Store the default policy
234   val policyDateCalc        = StartOfBillingYearDateCalc.copy
235   val policyOccurredMillis  = policyDateCalc.toMillis
236   val policyValidFromMillis = policyDateCalc.copy.goPreviousYear.toMillis
237   val policyValidToMillis   = policyDateCalc.copy.goNextYear.toMillis
238   StoreProvider.policyStore.storePolicyEntry(DefaultPolicy.toPolicyEntry(policyOccurredMillis, policyValidFromMillis, policyValidToMillis))
239
240   val Aquarium = AquariumSim(List(VMTimeResourceSim, DiskspaceResourceSim, BandwidthResourceSim), StoreProvider.resourceEventStore)
241   val DefaultResourcesMap = Aquarium.resourcesMap
242
243   val UserCKKL  = Aquarium.newUser("CKKL", UserCreationDate)
244
245   val InitialUserState = UserState.createInitialUserState(
246     userID = UserCKKL.userId,
247     userCreationMillis = UserCreationDate.getTime,
248     isActive = true,
249     credits = 0.0,
250     roleNames = List("user"),
251     agreementName = DSLAgreement.DefaultAgreementName
252   )
253
254   // By convention
255   // - synnefo is for VMTime and Bandwidth
256   // - pithos is for Diskspace
257   val VMTimeInstanceSim    = VMTimeResourceSim.newInstance   ("VM.1",   UserCKKL, Synnefo)
258   val BandwidthInstanceSim = BandwidthResourceSim.newInstance("3G.1",   UserCKKL, Synnefo)
259   val DiskInstanceSim      = DiskspaceResourceSim.newInstance("DISK.1", UserCKKL, Pithos)
260
261   private[this]
262   def showUserState(clog: ContextualLogger, userState: UserState) {
263     val id = userState._id
264     val parentId = userState.parentUserStateId
265     val credits = userState.creditsSnapshot.creditAmount
266     val newWalletEntries = userState.newWalletEntries.map(_.toDebugString)
267     val changeReason = userState.lastChangeReason
268     val implicitlyIssued = userState.implicitlyIssuedSnapshot.implicitlyIssuedEvents.map(_.toDebugString())
269     val latestResourceEvents = userState.latestResourceEventsSnapshot.resourceEvents.map(_.toDebugString())
270
271     clog.debug("_id = %s", id)
272     clog.debug("parentId = %s", parentId)
273     clog.debug("credits = %s", credits)
274     clog.debug("changeReason = %s", changeReason)
275     clog.debugSeq("implicitlyIssued", implicitlyIssued, 0)
276     clog.debugSeq("latestResourceEvents", latestResourceEvents, 0)
277     clog.debugSeq("newWalletEntries", newWalletEntries, 0)
278   }
279
280   private[this]
281   def showResourceEvents(clog: ContextualLogger): Unit = {
282     clog.debug("")
283     clog.begin("Events by OccurredMillis")
284     clog.withIndent {
285       for(event <- UserCKKL.myResourceEventsByOccurredDate) {
286         clog.debug(event.toDebugString())
287       }
288     }
289     clog.end("Events by OccurredMillis")
290     clog.debug("")
291   }
292
293   private[this]
294   def doFullMonthlyBilling(clog: ContextualLogger, billingMonthInfo: BillingMonthInfo) = {
295     Computations.doFullMonthlyBilling(
296       UserCKKL.userId,
297       billingMonthInfo,
298       StoreProvider,
299       InitialUserState,
300       DefaultResourcesMap,
301       DefaultAccounting,
302       DefaultCompiler,
303       MonthlyBillingCalculation(billingMonthInfo),
304       Some(clog)
305     )
306   }
307   
308   private[this]
309   def justUserState(userStateM: Maybe[UserState]): UserState = {
310     userStateM match {
311       case Just(userState) ⇒ userState
312       case _ ⇒ throw new AquariumException("Unexpected %s".format(userStateM))
313     }
314   }
315   
316   private[this]
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)
324   }
325
326   private[this]
327   def millis2hrs(millis: Long) = millis.toDouble / 1000 / 60 / 60
328
329   private[this]
330   def hrs2millis(hrs: Double) = (hrs * 60 * 60 * 1000).toLong
331
332   /**
333    * Test a sequence of ON, OFF vmtime events.
334    */
335   @Ignore
336   @Test
337   def testFullOnOff: Unit = {
338     val clog = ContextualLogger.fromOther(None, logger, "testFullOnOff()")
339     clog.begin()
340
341     ResourceEventStore.clearResourceEvents()
342     val OnOffDurationHrs = 10
343     val OnOffDurationMillis = hrs2millis(OnOffDurationHrs.toDouble)
344
345     VMTimeInstanceSim.newONOFF(
346       new MutableDateCalc(2012, 01, 10).goPlusHours(13).goPlusMinutes(30).toDate, // 2012-01-10 13:30:00.000
347       OnOffDurationHrs
348     )
349
350     val credits = DefaultAlgorithm.creditsForVMTime(OnOffDurationMillis)
351
352     showResourceEvents(clog)
353
354     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan)
355
356     showUserState(clog, userState)
357
358     expectCredits(clog, credits, userState)
359
360     clog.end()
361   }
362
363   @Ignore
364   @Test
365   def testLonelyON: Unit = {
366     val clog = ContextualLogger.fromOther(None, logger, "testLonelyON()")
367     clog.begin()
368
369     ResourceEventStore.clearResourceEvents()
370     
371     val JanStart = new MutableDateCalc(2012, 01, 01)
372     val JanEnd = JanStart.copy.goEndOfThisMonth
373     val JanStartDate = JanStart.toDate
374     val OnOffImplicitDurationMillis = JanEnd.toMillis - JanStart.toMillis
375     val OnOffImplicitDurationHrs = millis2hrs(OnOffImplicitDurationMillis)
376
377     VMTimeInstanceSim.newON(JanStartDate)
378
379     val credits = DefaultAlgorithm.creditsForVMTime(OnOffImplicitDurationMillis)
380
381     showResourceEvents(clog)
382
383     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan)
384
385     showUserState(clog, userState)
386
387     expectCredits(clog, credits, userState)
388
389     clog.end()
390   }
391
392 //  @Ignore
393   @Test
394   def testOrphanOFF: Unit = {
395     val clog = ContextualLogger.fromOther(None, logger, "testOrphanOFF()")
396     clog.begin()
397
398     ResourceEventStore.clearResourceEvents()
399
400     val JanStart = new MutableDateCalc(2012, 01, 01)
401     val JanEnd = JanStart.copy.goEndOfThisMonth
402     val JanStartDate = JanStart.toDate
403     val OnOffImplicitDurationMillis = JanEnd.toMillis - JanStart.toMillis
404     val OnOffImplicitDurationHrs = millis2hrs(OnOffImplicitDurationMillis)
405
406     VMTimeInstanceSim.newOFF(JanStartDate)
407
408     // This is an orphan event, so no credits will be charged
409     val credits = 0
410
411     showResourceEvents(clog)
412
413     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan)
414
415     showUserState(clog, userState)
416
417     expectCredits(clog, credits, userState)
418
419     clog.end()
420   }
421
422   @Ignore
423   @Test
424   def testOne: Unit = {
425     val clog = ContextualLogger.fromOther(None, logger, "testOne()")
426     clog.begin()
427
428     // Let's create our dates of interest
429     val VMStartDateCalc = StartOfBillingYearDateCalc.copy.goPlusDays(1).goPlusHours(1)
430     val VMStartDate = VMStartDateCalc.toDate
431
432     // Within January, create one VM ON-OFF ...
433     VMTimeInstanceSim.newONOFF(VMStartDate, 9)
434
435     val diskConsumptionDateCalc = StartOfBillingYearDateCalc.copy.goPlusHours(3)
436     val diskConsumptionDate1 = diskConsumptionDateCalc.toDate
437     val diskConsumptionDateCalc2 = diskConsumptionDateCalc.copy.goPlusDays(1).goPlusHours(1)
438     val diskConsumptionDate2 = diskConsumptionDateCalc2.toDate
439
440     // ... and two diskspace changes
441     DiskInstanceSim.consumeMB(diskConsumptionDate1, 99)
442     DiskInstanceSim.consumeMB(diskConsumptionDate2, 23)
443
444     // 100MB 3G bandwidth
445     val bwDateCalc = diskConsumptionDateCalc2.copy.goPlusDays(1)
446     BandwidthInstanceSim.useBandwidth(bwDateCalc.toDate, 100.0)
447
448     // ... and one "future" event
449     DiskInstanceSim.consumeMB(
450       StartOfBillingYearDateCalc.copy.
451         goNextMonth.goPlusDays(6).
452         goPlusHours(7).
453         goPlusMinutes(7).
454         goPlusSeconds(7).
455         goPlusMillis(7).toDate,
456       777)
457
458     showResourceEvents(clog)
459
460     // Policy: from 2012-01-01 to Infinity
461
462     clog.debugMap("DefaultResourcesMap", DefaultResourcesMap.map, 1)
463
464     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan)
465
466     showUserState(clog, userState)
467
468     clog.end()
469   }
470 }