098783ef45fb8b6b7875f1a8e23b8d3a56f31057
[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.MemStoreProvider
39 import gr.grnet.aquarium.logic.accounting.dsl._
40 import gr.grnet.aquarium.logic.accounting.Policy
41 import gr.grnet.aquarium.util.{Loggable, ContextualLogger}
42 import gr.grnet.aquarium.simulation._
43 import gr.grnet.aquarium.uid.{UIDGenerator, ConcurrentVMLocalUIDGenerator}
44 import org.junit.{Assert, Ignore, Test}
45 import gr.grnet.aquarium.logic.accounting.algorithm.{ExecutableCostPolicyAlgorithm, CostPolicyAlgorithmCompiler}
46 import gr.grnet.aquarium.{Aquarium, ResourceLocator, AquariumBuilder, AquariumException}
47 import gr.grnet.aquarium.computation.reason.{NoSpecificChangeReason, MonthlyBillingCalculation}
48 import gr.grnet.aquarium.util.date.MutableDateCalc
49 import gr.grnet.aquarium.computation.BillingMonthInfo
50 import gr.grnet.aquarium.computation.state.{UserStateBootstrap, UserState}
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 aquarium = new AquariumBuilder(ResourceLocator.AquariumProperties).
131     update(Aquarium.EnvKeys.storeProvider, new MemStoreProvider).
132     build()
133
134   Policy.withConfigurator(aquarium) // FIXME
135
136   val ResourceEventStore = aquarium.resourceEventStore
137
138   val Computations = aquarium.userStateComputations
139
140   val DSL = new DSL {}
141   val DefaultPolicy = DSL parse PolicyYAML
142
143   val DefaultAlgorithm = new ExecutableCostPolicyAlgorithm {
144     def creditsForContinuous(timeDelta: Double, oldTotalAmount: Double) =
145       hrs(timeDelta) * oldTotalAmount * ContinuousPriceUnit
146
147     final val creditsForDiskspace = creditsForContinuous(_, _)
148     
149     def creditsForDiscrete(currentValue: Double) =
150       currentValue * DiscretePriceUnit
151
152     final val creditsForBandwidth = creditsForDiscrete(_)
153
154     def creditsForOnOff(timeDelta: Double) =
155       hrs(timeDelta) * OnOffPriceUnit
156
157     final val creditsForVMTime = creditsForOnOff(_)
158
159     @inline private[this]
160     def hrs(millis: Double) = millis / 1000 / 60 / 60
161
162     def apply(vars: Map[DSLCostPolicyVar, Any]): Double = {
163       vars.apply(DSLCostPolicyNameVar) match {
164         case DSLCostPolicyNames.continuous ⇒
165           val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
166           val oldTotalAmount = vars(DSLOldTotalAmountVar).asInstanceOf[Double]
167           val timeDelta = vars(DSLTimeDeltaVar).asInstanceOf[Double]
168
169           Assert.assertEquals(ContinuousPriceUnit, unitPrice, DoubleDelta)
170
171           creditsForContinuous(timeDelta, oldTotalAmount)
172
173         case DSLCostPolicyNames.discrete ⇒
174           val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
175           val currentValue = vars(DSLCurrentValueVar).asInstanceOf[Double]
176
177           Assert.assertEquals(DiscretePriceUnit, unitPrice, DoubleDelta)
178
179           creditsForDiscrete(currentValue)
180
181         case DSLCostPolicyNames.onoff ⇒
182           val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
183           val timeDelta = vars(DSLTimeDeltaVar).asInstanceOf[Double]
184
185           Assert.assertEquals(OnOffPriceUnit, unitPrice, DoubleDelta)
186
187           creditsForOnOff(timeDelta)
188
189         case DSLCostPolicyNames.once ⇒
190           val currentValue = vars(DSLCurrentValueVar).asInstanceOf[Double]
191           currentValue
192
193         case name ⇒
194           throw new AquariumException("Unknown cost policy %s".format(name))
195       }
196     }
197
198     override def toString = "DefaultAlgorithm(%s)".format(
199       Map(
200         DSLCostPolicyNames.continuous -> "hrs(timeDelta) * oldTotalAmount * %s".format(ContinuousPriceUnit),
201         DSLCostPolicyNames.discrete   -> "currentValue * %s".format(DiscretePriceUnit),
202         DSLCostPolicyNames.onoff      -> "hrs(timeDelta) * %s".format(OnOffPriceUnit),
203         DSLCostPolicyNames.once       -> "currentValue"))
204   }
205
206   val DefaultCompiler  = new CostPolicyAlgorithmCompiler {
207     def compile(definition: String): ExecutableCostPolicyAlgorithm = {
208       DefaultAlgorithm
209     }
210   }
211   //val DefaultAlgorithm = justForSure(DefaultCompiler.compile("")).get // hardcoded since we know exactly what this is
212
213   val VMTimeDSLResource = DefaultPolicy.findResource("vmtime").get
214
215   // For this to work, the definitions must match those in the YAML above.
216   // Those StdXXXResourceSim are just for debugging convenience anyway, so they must match by design.
217   val VMTimeResourceSim    = StdVMTimeResourceSim.fromPolicy(DefaultPolicy)
218   val DiskspaceResourceSim = StdDiskspaceResourceSim.fromPolicy(DefaultPolicy)
219   val BandwidthResourceSim = StdBandwidthResourceSim.fromPolicy(DefaultPolicy)
220
221   // There are two client services, synnefo and pithos.
222   val TheUIDGenerator: UIDGenerator[_] = new ConcurrentVMLocalUIDGenerator
223   val Synnefo = ClientSim("synnefo")(TheUIDGenerator)
224   val Pithos  = ClientSim("pithos" )(TheUIDGenerator)
225
226   val StartOfBillingYearDateCalc = new MutableDateCalc(2012,  1, 1)
227   val UserCreationDate           = new MutableDateCalc(2011, 11, 1).toDate
228
229   val BillingMonthInfoJan = {
230     val MutableDateCalcJan = new MutableDateCalc(2012, 1, 1)
231     BillingMonthInfo.fromDateCalc(MutableDateCalcJan)
232   }
233   val BillingMonthInfoFeb = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012,  2, 1))
234   val BillingMonthInfoMar = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012,  3, 1))
235
236   // Store the default policy
237   val policyDateCalc        = StartOfBillingYearDateCalc.copy
238   val policyOccurredMillis  = policyDateCalc.toMillis
239   val policyValidFromMillis = policyDateCalc.copy.goPreviousYear.toMillis
240   val policyValidToMillis   = policyDateCalc.copy.goNextYear.toMillis
241   aquarium.policyStore.storePolicyEntry(DefaultPolicy.toPolicyEntry(policyOccurredMillis, policyValidFromMillis,
242     policyValidToMillis))
243
244   val AquariumSim_ = AquariumSim(List(VMTimeResourceSim, DiskspaceResourceSim, BandwidthResourceSim), aquarium.resourceEventStore)
245   val DefaultResourcesMap = AquariumSim_.resourcesMap
246
247   val UserCKKL  = AquariumSim_.newUser("CKKL", UserCreationDate)
248
249 //  val InitialUserState = UserState.createInitialUserState(
250 //    userID = UserCKKL.userID,
251 //    userCreationMillis = UserCreationDate.getTime,
252 //    totalCredits = 0.0,
253 //    initialRole = "default",
254 //    initialAgreement = DSLAgreement.DefaultAgreementName
255 //  )
256
257   val UserStateBootstrapper = UserStateBootstrap(
258     userID = UserCKKL.userID,
259     userCreationMillis = UserCreationDate.getTime(),
260     initialRole = "default",
261     initialAgreement = DSLAgreement.DefaultAgreementName,
262     initialCredits = 0.0
263   )
264
265   // By convention
266   // - synnefo is for VMTime and Bandwidth
267   // - pithos is for Diskspace
268   val VMTimeInstanceSim    = VMTimeResourceSim.newInstance   ("VM.1",   UserCKKL, Synnefo)
269   val BandwidthInstanceSim = BandwidthResourceSim.newInstance("3G.1",   UserCKKL, Synnefo)
270   val DiskInstanceSim      = DiskspaceResourceSim.newInstance("DISK.1", UserCKKL, Pithos)
271
272   private[this]
273   def showUserState(clog: ContextualLogger, userState: UserState) {
274     val id = userState._id
275     val parentId = userState.parentUserStateIDInStore
276     val credits = userState.totalCredits
277     val newWalletEntries = userState.newWalletEntries.map(_.toDebugString)
278     val changeReason = userState.lastChangeReason
279     val implicitlyIssued = userState.implicitlyIssuedSnapshot.implicitlyIssuedEvents.map(_.toDebugString)
280     val latestResourceEvents = userState.latestResourceEventsSnapshot.resourceEvents.map(_.toDebugString)
281
282     clog.debug("_id = %s", id)
283     clog.debug("parentId = %s", parentId)
284     clog.debug("credits = %s", credits)
285     clog.debug("changeReason = %s", changeReason)
286     clog.debugSeq("implicitlyIssued", implicitlyIssued, 0)
287     clog.debugSeq("latestResourceEvents", latestResourceEvents, 0)
288     clog.debugSeq("newWalletEntries", newWalletEntries, 0)
289   }
290
291   private[this]
292   def showResourceEvents(clog: ContextualLogger): Unit = {
293     clog.debug("")
294     clog.begin("Events by OccurredMillis")
295     clog.withIndent {
296       for(event <- UserCKKL.myResourceEventsByOccurredDate) {
297         clog.debug(event.toDebugString)
298       }
299     }
300     clog.end("Events by OccurredMillis")
301     clog.debug("")
302   }
303
304   private[this]
305   def doFullMonthlyBilling(
306       clog: ContextualLogger,
307       billingMonthInfo: BillingMonthInfo,
308       billingTimeMillis: Long) = {
309
310     Computations.doMonthBillingUpTo(
311       billingMonthInfo,
312       billingTimeMillis,
313       UserStateBootstrapper,
314       DefaultResourcesMap,
315       MonthlyBillingCalculation(NoSpecificChangeReason(), billingMonthInfo),
316       aquarium.userStateStore.insertUserState,
317       Some(clog)
318     )
319   }
320
321   private[this]
322   def expectCredits(
323       clog: ContextualLogger,
324       creditsConsumed: Double,
325       userState: UserState,
326       accuracy: Double = 0.001
327   ): Unit = {
328
329     val computed = userState.totalCredits
330     Assert.assertEquals(-creditsConsumed, computed, accuracy)
331
332     clog.info("Consumed %.3f credits [accuracy = %f]", creditsConsumed, accuracy)
333   }
334
335   private[this]
336   def millis2hrs(millis: Long) = millis.toDouble / 1000 / 60 / 60
337
338   private[this]
339   def hrs2millis(hrs: Double) = (hrs * 60 * 60 * 1000).toLong
340
341   /**
342    * Test a sequence of ON, OFF vmtime events.
343    */
344   @Ignore
345   @Test
346   def testFullOnOff: Unit = {
347     val clog = ContextualLogger.fromOther(None, logger, "testFullOnOff()")
348     clog.begin()
349
350     ResourceEventStore.clearResourceEvents()
351     val OnOffDurationHrs = 10
352     val OnOffDurationMillis = hrs2millis(OnOffDurationHrs.toDouble)
353
354     VMTimeInstanceSim.newONOFF(
355       new MutableDateCalc(2012, 01, 10).goPlusHours(13).goPlusMinutes(30).toDate, // 2012-01-10 13:30:00.000
356       OnOffDurationHrs
357     )
358
359     val credits = DefaultAlgorithm.creditsForVMTime(OnOffDurationMillis)
360
361     showResourceEvents(clog)
362
363     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan, BillingMonthInfoJan.monthStopMillis)
364
365     showUserState(clog, userState)
366
367     expectCredits(clog, credits, userState)
368
369     clog.end()
370   }
371
372   @Ignore
373   @Test
374   def testLonelyON: Unit = {
375     val clog = ContextualLogger.fromOther(None, logger, "testLonelyON()")
376     clog.begin()
377
378     ResourceEventStore.clearResourceEvents()
379     
380     val JanStart = new MutableDateCalc(2012, 01, 01)
381     val JanEnd = JanStart.copy.goEndOfThisMonth
382     val JanStartDate = JanStart.toDate
383     val OnOffImplicitDurationMillis = JanEnd.toMillis - JanStart.toMillis
384     val OnOffImplicitDurationHrs = millis2hrs(OnOffImplicitDurationMillis)
385
386     VMTimeInstanceSim.newON(JanStartDate)
387
388     val credits = DefaultAlgorithm.creditsForVMTime(OnOffImplicitDurationMillis)
389
390     showResourceEvents(clog)
391
392     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan, BillingMonthInfoJan.monthStopMillis)
393
394     showUserState(clog, userState)
395
396     expectCredits(clog, credits, userState)
397
398     clog.end()
399   }
400
401 //  @Ignore
402   @Test
403   def testOrphanOFF: Unit = {
404     val clog = ContextualLogger.fromOther(None, logger, "testOrphanOFF()")
405     clog.begin()
406
407     ResourceEventStore.clearResourceEvents()
408
409     val JanStart = new MutableDateCalc(2012, 01, 01)
410     val JanEnd = JanStart.copy.goEndOfThisMonth
411     val JanStartDate = JanStart.toDate
412     val OnOffImplicitDurationMillis = JanEnd.toMillis - JanStart.toMillis
413     val OnOffImplicitDurationHrs = millis2hrs(OnOffImplicitDurationMillis)
414
415     VMTimeInstanceSim.newOFF(JanStartDate)
416
417     // This is an orphan event, so no credits will be charged
418     val credits = 0
419
420     showResourceEvents(clog)
421
422     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan, BillingMonthInfoJan.monthStopMillis)
423
424     showUserState(clog, userState)
425
426     expectCredits(clog, credits, userState)
427
428     clog.end()
429   }
430
431   @Ignore
432   @Test
433   def testOne: Unit = {
434     val clog = ContextualLogger.fromOther(None, logger, "testOne()")
435     clog.begin()
436
437     // Let's create our dates of interest
438     val VMStartDateCalc = StartOfBillingYearDateCalc.copy.goPlusDays(1).goPlusHours(1)
439     val VMStartDate = VMStartDateCalc.toDate
440
441     // Within January, create one VM ON-OFF ...
442     VMTimeInstanceSim.newONOFF(VMStartDate, 9)
443
444     val diskConsumptionDateCalc = StartOfBillingYearDateCalc.copy.goPlusHours(3)
445     val diskConsumptionDate1 = diskConsumptionDateCalc.toDate
446     val diskConsumptionDateCalc2 = diskConsumptionDateCalc.copy.goPlusDays(1).goPlusHours(1)
447     val diskConsumptionDate2 = diskConsumptionDateCalc2.toDate
448
449     // ... and two diskspace changes
450     DiskInstanceSim.consumeMB(diskConsumptionDate1, 99)
451     DiskInstanceSim.consumeMB(diskConsumptionDate2, 23)
452
453     // 100MB 3G bandwidth
454     val bwDateCalc = diskConsumptionDateCalc2.copy.goPlusDays(1)
455     BandwidthInstanceSim.useBandwidth(bwDateCalc.toDate, 100.0)
456
457     // ... and one "future" event
458     DiskInstanceSim.consumeMB(
459       StartOfBillingYearDateCalc.copy.
460         goNextMonth.goPlusDays(6).
461         goPlusHours(7).
462         goPlusMinutes(7).
463         goPlusSeconds(7).
464         goPlusMillis(7).toDate,
465       777)
466
467     showResourceEvents(clog)
468
469     // Policy: from 2012-01-01 to Infinity
470
471     clog.debugMap("DefaultResourcesMap", DefaultResourcesMap.map, 1)
472
473     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan, BillingMonthInfoJan.monthStopMillis)
474
475     showUserState(clog, userState)
476
477     clog.end()
478   }
479 }