dfd29572025ccc4f96f8b806aea4a8bc8b9781b4
[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.util.{Loggable, ContextualLogger}
40 import gr.grnet.aquarium.simulation._
41 import gr.grnet.aquarium.uid.{UIDGenerator, ConcurrentVMLocalUIDGenerator}
42 import org.junit.{Assert, Ignore, Test}
43 import gr.grnet.aquarium.logic.accounting.algorithm.{ExecutableChargingBehaviorAlgorithm, CostPolicyAlgorithmCompiler}
44 import gr.grnet.aquarium.{Aquarium, ResourceLocator, AquariumBuilder, AquariumException}
45 import gr.grnet.aquarium.util.date.MutableDateCalc
46 import gr.grnet.aquarium.computation.BillingMonthInfo
47 import gr.grnet.aquarium.computation.state.UserState
48 import gr.grnet.aquarium.charging._
49 import gr.grnet.aquarium.policy.{PolicyDefinedFullPriceTableRef, StdUserAgreement, EffectiveUnitPrice, EffectivePriceTable, FullPriceTable, ResourceType, StdPolicy, PolicyModel}
50 import gr.grnet.aquarium.Timespan
51 import gr.grnet.aquarium.computation.state.UserStateBootstrap
52 import gr.grnet.aquarium.charging.reason.{NoSpecificChargingReason, MonthlyBillChargingReason}
53 import gr.grnet.aquarium.charging.state.WorkingUserState
54
55
56 /**
57  *
58  * @author Christos KK Loverdos <loverdos@gmail.com>
59  */
60 class UserStateComputationsTest extends Loggable {
61   final val DoubleDelta = 0.001
62
63   final val BandwidthUnitPrice = 3.3 //
64   final val VMTimeUnitPrice    = 1.5 //
65   final val DiskspaceUnitPrice = 2.7 //
66
67   final val OnOffUnitPrice = VMTimeUnitPrice
68   final val ContinuousUnitPrice = DiskspaceUnitPrice
69   final val DiscreteUnitPrice = BandwidthUnitPrice
70
71   final val DefaultPolicy: PolicyModel = StdPolicy(
72     id = "policy-1",
73     parentID = None,
74     validityTimespan = Timespan(0),
75     resourceTypes = Set(
76       ResourceType("bandwidth", "MB/Hr", classOf[DiscreteChargingBehavior].getName),
77       ResourceType("vmtime", "Hr", classOf[OnOffChargingBehavior].getName),
78       ResourceType("diskspace", "MB/Hr", classOf[ContinuousChargingBehavior].getName)
79     ),
80     chargingBehaviors = Set(
81       classOf[DiscreteChargingBehavior].getName,
82       classOf[OnOffChargingBehavior].getName,
83       classOf[ContinuousChargingBehavior].getName,
84       classOf[OnceChargingBehavior].getName
85     ),
86     roleMapping = Map(
87       "default" -> FullPriceTable(Map(
88         "bandwidth" -> EffectivePriceTable(EffectiveUnitPrice(BandwidthUnitPrice, None) :: Nil),
89         "vmtime"    -> EffectivePriceTable(EffectiveUnitPrice(VMTimeUnitPrice, None) :: Nil),
90         "diskspace" -> EffectivePriceTable(EffectiveUnitPrice(DiskspaceUnitPrice, None) :: Nil)
91       ))
92     )
93   )
94
95   val aquarium = new AquariumBuilder(ResourceLocator.AquariumProperties, ResourceLocator.DefaultPolicyModel).
96     update(Aquarium.EnvKeys.storeProvider, new MemStoreProvider).
97     build()
98
99   val ResourceEventStore = aquarium.resourceEventStore
100
101   val ChargingService = aquarium.chargingService
102
103   val DefaultAlgorithm = new ExecutableChargingBehaviorAlgorithm {
104     def creditsForContinuous(timeDelta: Double, oldTotalAmount: Double) =
105       hrs(timeDelta) * oldTotalAmount * ContinuousUnitPrice
106
107     final val creditsForDiskspace = creditsForContinuous(_, _)
108     
109     def creditsForDiscrete(currentValue: Double) =
110       currentValue * DiscreteUnitPrice
111
112     final val creditsForBandwidth = creditsForDiscrete(_)
113
114     def creditsForOnOff(timeDelta: Double) =
115       hrs(timeDelta) * OnOffUnitPrice
116
117     final val creditsForVMTime = creditsForOnOff(_)
118
119     @inline private[this]
120     def hrs(millis: Double) = millis / 1000 / 60 / 60
121
122     def apply(vars: Map[ChargingInput, Any]): Double = {
123       vars.apply(ChargingBehaviorNameInput) match {
124         case ChargingBehaviorAliases.continuous ⇒
125           val unitPrice = vars(UnitPriceInput).asInstanceOf[Double]
126           val oldTotalAmount = vars(OldTotalAmountInput).asInstanceOf[Double]
127           val timeDelta = vars(TimeDeltaInput).asInstanceOf[Double]
128
129           Assert.assertEquals(ContinuousUnitPrice, unitPrice, DoubleDelta)
130
131           creditsForContinuous(timeDelta, oldTotalAmount)
132
133         case ChargingBehaviorAliases.discrete ⇒
134           val unitPrice = vars(UnitPriceInput).asInstanceOf[Double]
135           val currentValue = vars(CurrentValueInput).asInstanceOf[Double]
136
137           Assert.assertEquals(DiscreteUnitPrice, unitPrice, DoubleDelta)
138
139           creditsForDiscrete(currentValue)
140
141         case ChargingBehaviorAliases.onoff ⇒
142           val unitPrice = vars(UnitPriceInput).asInstanceOf[Double]
143           val timeDelta = vars(TimeDeltaInput).asInstanceOf[Double]
144
145           Assert.assertEquals(OnOffUnitPrice, unitPrice, DoubleDelta)
146
147           creditsForOnOff(timeDelta)
148
149         case ChargingBehaviorAliases.once ⇒
150           val currentValue = vars(CurrentValueInput).asInstanceOf[Double]
151           currentValue
152
153         case name ⇒
154           throw new AquariumException("Unknown cost policy %s".format(name))
155       }
156     }
157
158     override def toString = "DefaultAlgorithm(%s)".format(
159       Map(
160         ChargingBehaviorAliases.continuous -> "hrs(timeDelta) * oldTotalAmount * %s".format(ContinuousUnitPrice),
161         ChargingBehaviorAliases.discrete   -> "currentValue * %s".format(DiscreteUnitPrice),
162         ChargingBehaviorAliases.onoff      -> "hrs(timeDelta) * %s".format(OnOffUnitPrice),
163         ChargingBehaviorAliases.once       -> "currentValue"))
164   }
165
166   val DefaultCompiler  = new CostPolicyAlgorithmCompiler {
167     def compile(definition: String): ExecutableChargingBehaviorAlgorithm = {
168       DefaultAlgorithm
169     }
170   }
171   //val DefaultAlgorithm = justForSure(DefaultCompiler.compile("")).get // hardcoded since we know exactly what this is
172
173   val VMTimeDSLResource = DefaultPolicy.resourceTypesMap("vmtime")
174
175   // For this to work, the definitions must match those in the YAML above.
176   // Those StdXXXResourceSim are just for debugging convenience anyway, so they must match by design.
177   val VMTimeResourceSim    = StdVMTimeResourceSim.fromPolicy(DefaultPolicy)
178   val DiskspaceResourceSim = StdDiskspaceResourceSim.fromPolicy(DefaultPolicy)
179   val BandwidthResourceSim = StdBandwidthResourceSim.fromPolicy(DefaultPolicy)
180
181   // There are two client services, synnefo and pithos.
182   val TheUIDGenerator: UIDGenerator[_] = new ConcurrentVMLocalUIDGenerator
183   val Synnefo = ClientSim("synnefo")(TheUIDGenerator)
184   val Pithos  = ClientSim("pithos" )(TheUIDGenerator)
185
186   val StartOfBillingYearDateCalc = new MutableDateCalc(2012,  1, 1)
187   val UserCreationDate           = new MutableDateCalc(2011, 11, 1).toDate
188
189   val BillingMonthInfoJan = {
190     val MutableDateCalcJan = new MutableDateCalc(2012, 1, 1)
191     BillingMonthInfo.fromDateCalc(MutableDateCalcJan)
192   }
193   val BillingMonthInfoFeb = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012,  2, 1))
194   val BillingMonthInfoMar = BillingMonthInfo.fromDateCalc(new MutableDateCalc(2012,  3, 1))
195
196   // Store the default policy
197   val policyDateCalc        = StartOfBillingYearDateCalc.copy
198   val policyOccurredMillis  = policyDateCalc.toMillis
199   val policyValidFromMillis = policyDateCalc.copy.goPreviousYear.toMillis
200   val policyValidToMillis   = policyDateCalc.copy.goNextYear.toMillis
201
202   aquarium.policyStore.insertPolicy(DefaultPolicy)
203
204   val AquariumSim_ = AquariumSim(List(VMTimeResourceSim, DiskspaceResourceSim, BandwidthResourceSim), aquarium.resourceEventStore)
205   val DefaultResourcesMap = DefaultPolicy.resourceTypesMap//AquariumSim_.resourcesMap
206
207   val UserCKKL  = AquariumSim_.newUser("CKKL", UserCreationDate)
208
209 //  val InitialUserState = UserState.createInitialUserState(
210 //    userID = UserCKKL.userID,
211 //    userCreationMillis = UserCreationDate.getTime,
212 //    totalCredits = 0.0,
213 //    initialRole = "default",
214 //    initialAgreement = DSLAgreement.DefaultAgreementName
215 //  )
216
217   val UserStateBootstrapper = UserStateBootstrap(
218     userID = UserCKKL.userID,
219     userCreationMillis = UserCreationDate.getTime(),
220     initialAgreement = StdUserAgreement("", None, 0, Long.MaxValue, "default", PolicyDefinedFullPriceTableRef),
221     initialCredits = 0.0
222   )
223
224   // By convention
225   // - synnefo is for VMTime and Bandwidth
226   // - pithos is for Diskspace
227   val VMTimeInstanceSim    = VMTimeResourceSim.newInstance   ("VM.1",   UserCKKL, Synnefo)
228   val BandwidthInstanceSim = BandwidthResourceSim.newInstance("3G.1",   UserCKKL, Synnefo)
229   val DiskInstanceSim      = DiskspaceResourceSim.newInstance("DISK.1", UserCKKL, Pithos)
230
231   private[this]
232   def showUserState(clog: ContextualLogger, workingUserState: WorkingUserState) {
233 //    val id = workingUserState.id
234 //    val parentId = workingUserState.parentUserStateIDInStore
235 //    val credits = workingUserState.totalCredits
236 //    val newWalletEntries = workingUserState.newWalletEntries.map(_.toDebugString)
237 //    val changeReason = workingUserState.lastChangeReason
238 //    val implicitlyIssued = workingUserState.implicitlyIssuedSnapshot.implicitlyIssuedEvents.map(_.toDebugString)
239 //    val latestResourceEvents = workingUserState.latestResourceEventsSnapshot.resourceEvents.map(_.toDebugString)
240 //
241 //    clog.debug("_id = %s", id)
242 //    clog.debug("parentId = %s", parentId)
243 //    clog.debug("credits = %s", credits)
244 //    clog.debug("changeReason = %s", changeReason)
245 //    clog.debugSeq("implicitlyIssued", implicitlyIssued, 0)
246 //    clog.debugSeq("latestResourceEvents", latestResourceEvents, 0)
247 //    clog.debugSeq("newWalletEntries", newWalletEntries, 0)
248   }
249
250   private[this]
251   def showResourceEvents(clog: ContextualLogger): Unit = {
252     clog.debug("")
253     clog.begin("Events by OccurredMillis")
254     clog.withIndent {
255       for(event <- UserCKKL.myResourceEventsByOccurredDate) {
256         clog.debug(event.toDebugString)
257       }
258     }
259     clog.end("Events by OccurredMillis")
260     clog.debug("")
261   }
262
263   private[this]
264   def doFullMonthlyBilling(
265       clog: ContextualLogger,
266       billingMonthInfo: BillingMonthInfo,
267       billingTimeMillis: Long) = {
268
269     ChargingService.replayMonthChargingUpTo(
270       billingMonthInfo,
271       billingTimeMillis,
272       UserStateBootstrapper,
273       DefaultResourcesMap,
274       MonthlyBillChargingReason(NoSpecificChargingReason(), billingMonthInfo),
275       aquarium.userStateStore.insertUserState,
276       Some(clog)
277     )
278   }
279
280   private[this]
281   def expectCredits(
282       clog: ContextualLogger,
283       creditsConsumed: Double,
284       workingUserState: WorkingUserState,
285       accuracy: Double = 0.001
286   ): Unit = {
287
288     val computed = workingUserState.totalCredits
289     Assert.assertEquals(-creditsConsumed, computed, accuracy)
290
291     clog.info("Consumed %.3f credits [accuracy = %f]", creditsConsumed, accuracy)
292   }
293
294   private[this]
295   def millis2hrs(millis: Long) = millis.toDouble / 1000 / 60 / 60
296
297   private[this]
298   def hrs2millis(hrs: Double) = (hrs * 60 * 60 * 1000).toLong
299
300   /**
301    * Test a sequence of ON, OFF vmtime events.
302    */
303   @Ignore
304   @Test
305   def testFullOnOff: Unit = {
306     val clog = ContextualLogger.fromOther(None, logger, "testFullOnOff()")
307     clog.begin()
308
309     ResourceEventStore.clearResourceEvents()
310     val OnOffDurationHrs = 10
311     val OnOffDurationMillis = hrs2millis(OnOffDurationHrs.toDouble)
312
313     VMTimeInstanceSim.newONOFF(
314       new MutableDateCalc(2012, 01, 10).goPlusHours(13).goPlusMinutes(30).toDate, // 2012-01-10 13:30:00.000
315       OnOffDurationHrs
316     )
317
318     val credits = DefaultAlgorithm.creditsForVMTime(OnOffDurationMillis)
319
320     showResourceEvents(clog)
321
322     val workingUserState = doFullMonthlyBilling(clog, BillingMonthInfoJan, BillingMonthInfoJan.monthStopMillis)
323
324     showUserState(clog, workingUserState)
325
326     expectCredits(clog, credits, workingUserState)
327
328     clog.end()
329   }
330
331   @Ignore
332   @Test
333   def testLonelyON: Unit = {
334     val clog = ContextualLogger.fromOther(None, logger, "testLonelyON()")
335     clog.begin()
336
337     ResourceEventStore.clearResourceEvents()
338     
339     val JanStart = new MutableDateCalc(2012, 01, 01)
340     val JanEnd = JanStart.copy.goEndOfThisMonth
341     val JanStartDate = JanStart.toDate
342     val OnOffImplicitDurationMillis = JanEnd.toMillis - JanStart.toMillis
343     val OnOffImplicitDurationHrs = millis2hrs(OnOffImplicitDurationMillis)
344
345     VMTimeInstanceSim.newON(JanStartDate)
346
347     val credits = DefaultAlgorithm.creditsForVMTime(OnOffImplicitDurationMillis)
348
349     showResourceEvents(clog)
350
351     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan, BillingMonthInfoJan.monthStopMillis)
352
353     showUserState(clog, userState)
354
355     expectCredits(clog, credits, userState)
356
357     clog.end()
358   }
359
360 //  @Ignore
361   @Test
362   def testOrphanOFF: Unit = {
363     val clog = ContextualLogger.fromOther(None, logger, "testOrphanOFF()")
364     clog.begin()
365
366     ResourceEventStore.clearResourceEvents()
367
368     val JanStart = new MutableDateCalc(2012, 01, 01)
369     val JanEnd = JanStart.copy.goEndOfThisMonth
370     val JanStartDate = JanStart.toDate
371     val OnOffImplicitDurationMillis = JanEnd.toMillis - JanStart.toMillis
372     val OnOffImplicitDurationHrs = millis2hrs(OnOffImplicitDurationMillis)
373
374     VMTimeInstanceSim.newOFF(JanStartDate)
375
376     // This is an orphan event, so no credits will be charged
377     val credits = 0
378
379     showResourceEvents(clog)
380
381     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan, BillingMonthInfoJan.monthStopMillis)
382
383     showUserState(clog, userState)
384
385     expectCredits(clog, credits, userState)
386
387     clog.end()
388   }
389
390   @Ignore
391   @Test
392   def testOne: Unit = {
393     val clog = ContextualLogger.fromOther(None, logger, "testOne()")
394     clog.begin()
395
396     // Let's create our dates of interest
397     val VMStartDateCalc = StartOfBillingYearDateCalc.copy.goPlusDays(1).goPlusHours(1)
398     val VMStartDate = VMStartDateCalc.toDate
399
400     // Within January, create one VM ON-OFF ...
401     VMTimeInstanceSim.newONOFF(VMStartDate, 9)
402
403     val diskConsumptionDateCalc = StartOfBillingYearDateCalc.copy.goPlusHours(3)
404     val diskConsumptionDate1 = diskConsumptionDateCalc.toDate
405     val diskConsumptionDateCalc2 = diskConsumptionDateCalc.copy.goPlusDays(1).goPlusHours(1)
406     val diskConsumptionDate2 = diskConsumptionDateCalc2.toDate
407
408     // ... and two diskspace changes
409     DiskInstanceSim.consumeMB(diskConsumptionDate1, 99)
410     DiskInstanceSim.consumeMB(diskConsumptionDate2, 23)
411
412     // 100MB 3G bandwidth
413     val bwDateCalc = diskConsumptionDateCalc2.copy.goPlusDays(1)
414     BandwidthInstanceSim.useBandwidth(bwDateCalc.toDate, 100.0)
415
416     // ... and one "future" event
417     DiskInstanceSim.consumeMB(
418       StartOfBillingYearDateCalc.copy.
419         goNextMonth.goPlusDays(6).
420         goPlusHours(7).
421         goPlusMinutes(7).
422         goPlusSeconds(7).
423         goPlusMillis(7).toDate,
424       777)
425
426     showResourceEvents(clog)
427
428     // Policy: from 2012-01-01 to Infinity
429
430     clog.debugMap("DefaultResourcesMap", DefaultResourcesMap, 1)
431
432     val userState = doFullMonthlyBilling(clog, BillingMonthInfoJan, BillingMonthInfoJan.monthStopMillis)
433
434     showUserState(clog, userState)
435
436     clog.end()
437   }
438 }