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