Delete unused IMEvent (old model)
[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, 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}
49
50
51 /**
52  *
53  * @author Christos KK Loverdos <loverdos@gmail.com>
54  */
55 class UserStateComputationsTest extends Loggable {
56   final val DoubleDelta = 0.001
57
58   final val BandwidthPriceUnit = 3.3 //
59   final val VMTimePriceUnit    = 1.5 //
60   final val DiskspacePriceUnit = 2.7 //
61
62   final val OnOffPriceUnit = VMTimePriceUnit
63   final val ContinuousPriceUnit = DiskspacePriceUnit
64   final val DiscretePriceUnit = BandwidthPriceUnit
65
66   final val PolicyYAML = """
67 aquariumpolicy:
68   resources:
69     - resource:
70       name: bandwidth
71       unit: MB/Hr
72       complex: false
73       costpolicy: discrete
74     - resource:
75       name: vmtime
76       unit: Hr
77       complex: true
78       costpolicy: onoff
79       descriminatorfield: vmid
80     - resource:
81       name: diskspace
82       unit: MB/hr
83       complex: false
84       costpolicy: continuous
85
86   implicitvars:
87     - price
88     - volume
89
90   algorithms:
91     - algorithm:
92       name: default
93       bandwidth: function bandwidth() {return 1;}
94       vmtime: function vmtime() {return 1;}
95       diskspace: function diskspace() {return 1;}
96       effective:
97         from: 0
98
99   pricelists:
100     - pricelist:
101       name: default
102       bandwidth: %s
103       vmtime: %s
104       diskspace: %s
105       effective:
106         from: 0
107
108   creditplans:
109     - creditplan:
110       name: default
111       credits: 100
112       at: "00 00 1 * *"
113       effective:
114         from: 0
115
116   agreements:
117     - agreement:
118       name: default
119       algorithm: default
120       pricelist: default
121       creditplan: default
122   """.format(
123     BandwidthPriceUnit,
124     VMTimePriceUnit,
125     DiskspacePriceUnit
126   )
127
128   val Computations = new UserStateComputations
129
130   val DefaultPolicy = new DSL{} parse PolicyYAML
131   val DefaultAccounting = new Accounting{}
132   
133   val DefaultAlgorithm = new ExecutableCostPolicyAlgorithm {
134     def creditsForContinuous(timeDelta: Double, oldTotalAmount: Double) =
135       hrs(timeDelta) * oldTotalAmount * ContinuousPriceUnit
136
137     final val creditsForDiskspace = creditsForContinuous(_, _)
138     
139     def creditsForDiscrete(currentValue: Double) =
140       currentValue * DiscretePriceUnit
141
142     final val creditsForBandwidth = creditsForDiscrete(_)
143
144     def creditsForOnOff(timeDelta: Double) =
145       hrs(timeDelta) * OnOffPriceUnit
146
147     final val creditsForVMTime = creditsForOnOff(_)
148
149     @inline private[this]
150     def hrs(millis: Double) = millis / 1000 / 60 / 60
151
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]
158
159           Assert.assertEquals(ContinuousPriceUnit, unitPrice, DoubleDelta)
160
161           creditsForContinuous(timeDelta, oldTotalAmount)
162
163         case DSLCostPolicyNames.discrete ⇒
164           val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
165           val currentValue = vars(DSLCurrentValueVar).asInstanceOf[Double]
166
167           Assert.assertEquals(DiscretePriceUnit, unitPrice, DoubleDelta)
168
169           creditsForDiscrete(currentValue)
170
171         case DSLCostPolicyNames.onoff ⇒
172           val unitPrice = vars(DSLUnitPriceVar).asInstanceOf[Double]
173           val timeDelta = vars(DSLTimeDeltaVar).asInstanceOf[Double]
174
175           Assert.assertEquals(OnOffPriceUnit, unitPrice, DoubleDelta)
176
177           creditsForOnOff(timeDelta)
178
179         case DSLCostPolicyNames.once ⇒
180           val currentValue = vars(DSLCurrentValueVar).asInstanceOf[Double]
181           currentValue
182
183         case name ⇒
184           throw new AquariumException("Unknown cost policy %s".format(name))
185       }
186     }
187
188     override def toString = "DefaultAlgorithm(%s)".format(
189       Map(
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"))
194   }
195
196   val DefaultCompiler  = new CostPolicyAlgorithmCompiler {
197     def compile(definition: String): Maybe[ExecutableCostPolicyAlgorithm] = {
198       Just(DefaultAlgorithm)
199     }
200   }
201   //val DefaultAlgorithm = justForSure(DefaultCompiler.compile("")).get // hardcoded since we know exactly what this is
202
203   val VMTimeDSLResource = DefaultPolicy.findResource("vmtime").get
204
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)
210
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)
215
216   val mc = Configurator.MasterConfigurator.withStoreProviderClass(classOf[MemStore])
217   Policy.withConfigurator(mc)
218   val StoreProvider = mc.storeProvider
219   val ResourceEventStore = StoreProvider.resourceEventStore
220
221   val StartOfBillingYearDateCalc = new MutableDateCalc(2012,  1, 1)
222   val UserCreationDate           = new MutableDateCalc(2011, 11, 1).toDate
223
224   val BillingMonthInfoJan = {
225     val MutableDateCalcJan = new MutableDateCalc(2012, 1, 1)
226     BillingMonthInfo.fromDateCalc(MutableDateCalcJan)
227   }
228   val BillingMonthInfoFeb = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012,  2, 1))
229   val BillingMonthInfoMar = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012,  3, 1))
230
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))
237
238   val Aquarium = AquariumSim(List(VMTimeResourceSim, DiskspaceResourceSim, BandwidthResourceSim), StoreProvider.resourceEventStore)
239   val DefaultResourcesMap = Aquarium.resourcesMap
240
241   val UserCKKL  = Aquarium.newUser("CKKL", UserCreationDate)
242
243   val InitialUserState = Computations.createInitialUserState(
244     userId = UserCKKL.userId,
245     userCreationMillis = UserCreationDate.getTime,
246     isActive = true,
247     credits = 0.0,
248     roleNames = List("user"),
249     agreementName = DSLAgreement.DefaultAgreementName
250   )
251
252   // By convention
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)
258
259   private[this]
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())
269
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)
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       Just(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(NoVal, 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 userStateM = doFullMonthlyBilling(clog, BillingMonthInfoJan)
355     val userState = justUserState(userStateM)
356     
357     showUserState(clog, userState)
358
359     expectCredits(clog, credits, userState)
360
361     clog.end()
362   }
363
364   @Ignore
365   @Test
366   def testLonelyON: Unit = {
367     val clog = ContextualLogger.fromOther(NoVal, logger, "testLonelyON()")
368     clog.begin()
369
370     ResourceEventStore.clearResourceEvents()
371     
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)
377
378     VMTimeInstanceSim.newON(JanStartDate)
379
380     val credits = DefaultAlgorithm.creditsForVMTime(OnOffImplicitDurationMillis)
381
382     showResourceEvents(clog)
383
384     val userStateM = doFullMonthlyBilling(clog, BillingMonthInfoJan)
385     val userState = justUserState(userStateM)
386
387     showUserState(clog, userState)
388
389     expectCredits(clog, credits, userState)
390
391     clog.end()
392   }
393
394 //  @Ignore
395   @Test
396   def testOrphanOFF: Unit = {
397     val clog = ContextualLogger.fromOther(NoVal, logger, "testOrphanOFF()")
398     clog.begin()
399
400     ResourceEventStore.clearResourceEvents()
401
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)
407
408     VMTimeInstanceSim.newOFF(JanStartDate)
409
410     // This is an orphan event, so no credits will be charged
411     val credits = 0
412
413     showResourceEvents(clog)
414
415     val userStateM = doFullMonthlyBilling(clog, BillingMonthInfoJan)
416     val userState = justUserState(userStateM)
417
418     showUserState(clog, userState)
419
420     expectCredits(clog, credits, userState)
421
422     clog.end()
423   }
424
425   @Ignore
426   @Test
427   def testOne: Unit = {
428     val clog = ContextualLogger.fromOther(NoVal, logger, "testOne()")
429     clog.begin()
430
431     // Let's create our dates of interest
432     val VMStartDateCalc = StartOfBillingYearDateCalc.copy.goPlusDays(1).goPlusHours(1)
433     val VMStartDate = VMStartDateCalc.toDate
434
435     // Within January, create one VM ON-OFF ...
436     VMTimeInstanceSim.newONOFF(VMStartDate, 9)
437
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
442
443     // ... and two diskspace changes
444     DiskInstanceSim.consumeMB(diskConsumptionDate1, 99)
445     DiskInstanceSim.consumeMB(diskConsumptionDate2, 23)
446
447     // 100MB 3G bandwidth
448     val bwDateCalc = diskConsumptionDateCalc2.copy.goPlusDays(1)
449     BandwidthInstanceSim.useBandwidth(bwDateCalc.toDate, 100.0)
450
451     // ... and one "future" event
452     DiskInstanceSim.consumeMB(
453       StartOfBillingYearDateCalc.copy.
454         goNextMonth.goPlusDays(6).
455         goPlusHours(7).
456         goPlusMinutes(7).
457         goPlusSeconds(7).
458         goPlusMillis(7).toDate,
459       777)
460
461     showResourceEvents(clog)
462
463     // Policy: from 2012-01-01 to Infinity
464
465     clog.debugMap("DefaultResourcesMap", DefaultResourcesMap.map, 1)
466
467     val userStateM = doFullMonthlyBilling(clog, BillingMonthInfoJan)
468     val userState = justUserState(userStateM)
469     showUserState(clog, userState)
470
471     clog.end()
472   }
473 }