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