Add a UserStateModelSkeleton
[aquarium] / src / main / scala / gr / grnet / aquarium / charging / ChargingService.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.charging
37
38 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
39 import gr.grnet.aquarium.computation.BillingMonthInfo
40 import gr.grnet.aquarium.charging.state.UserStateBootstrap
41 import gr.grnet.aquarium.policy.ResourceType
42 import gr.grnet.aquarium.util.{Lifecycle, Loggable, ContextualLogger}
43 import gr.grnet.aquarium.util.date.{MutableDateCalc, TimeHelpers}
44 import gr.grnet.aquarium.{AquariumInternalError, AquariumAwareSkeleton}
45 import gr.grnet.aquarium.charging.state.{WorkingUserState, UserStateModel, StdUserState}
46 import gr.grnet.aquarium.charging.reason.{MonthlyBillChargingReason, InitialUserStateSetup, ChargingReason}
47
48 /**
49  *
50  * @author Christos KK Loverdos <loverdos@gmail.com>
51  */
52
53 final class ChargingService extends AquariumAwareSkeleton with Lifecycle with Loggable {
54   lazy val policyStore = aquarium.policyStore
55   lazy val userStateStore = aquarium.userStateStore
56   lazy val resourceEventStore = aquarium.resourceEventStore
57
58   //+ Lifecycle
59   def start() = ()
60
61   def stop() = ()
62   //- Lifecycle
63
64
65   //+ Utility methods
66   protected def rcDebugInfo(rcEvent: ResourceEventModel) = {
67     rcEvent.toDebugString
68   }
69   //- Utility methods
70
71   def findOrCalculateWorkingUserStateAtEndOfBillingMonth(
72       billingMonthInfo: BillingMonthInfo,
73       userStateBootstrap: UserStateBootstrap,
74       defaultResourceTypesMap: Map[String, ResourceType],
75       chargingReason: ChargingReason,
76       userStateRecorder: UserStateModel ⇒ UserStateModel,
77       clogOpt: Option[ContextualLogger]
78   ): WorkingUserState = {
79
80     val clog = ContextualLogger.fromOther(
81       clogOpt,
82       logger,
83       "findOrCalculateWorkingUserStateAtEndOfBillingMonth(%s)", billingMonthInfo.toShortDebugString)
84     clog.begin()
85
86     lazy val clogSome = Some(clog)
87
88     def computeFullMonthBillingAndSaveState(): WorkingUserState = {
89       val workingUserState = replayFullMonthBilling(
90         userStateBootstrap,
91         billingMonthInfo,
92         defaultResourceTypesMap,
93         chargingReason,
94         userStateRecorder,
95         clogSome
96       )
97
98       val newChargingReason = MonthlyBillChargingReason(chargingReason, billingMonthInfo)
99       workingUserState.chargingReason = newChargingReason
100       val monthlyUserState0 = workingUserState.toUserState(
101         true,
102         billingMonthInfo.year,
103         billingMonthInfo.month,
104         None
105       )
106
107       // We always save the state when it is a full month billing
108       val monthlyUserState1 = userStateRecorder.apply(monthlyUserState0)
109
110       clog.debug("Stored full %s %s", billingMonthInfo.toDebugString, monthlyUserState1.toJsonString)
111
112       workingUserState
113     }
114
115     val userID = userStateBootstrap.userID
116     val userCreationMillis = userStateBootstrap.userCreationMillis
117     val userCreationDateCalc = new MutableDateCalc(userCreationMillis)
118     val billingMonthStartMillis = billingMonthInfo.monthStartMillis
119     val billingMonthStopMillis = billingMonthInfo.monthStopMillis
120
121     if(billingMonthStopMillis < userCreationMillis) {
122       // If the user did not exist for this billing month, piece of cake
123       clog.debug("User did not exist before %s", userCreationDateCalc)
124
125       // TODO: The initial user state might have already been created.
126       //       First ask if it exists and compute only if not
127       val initialUserState0 = StdUserState.createInitialUserStateFromBootstrap(
128         userStateBootstrap,
129         TimeHelpers.nowMillis(),
130         InitialUserStateSetup(Some(chargingReason)) // we record the originating calculation reason
131       )
132
133       logger.debug("Created (from bootstrap) initial user state %s".format(initialUserState0))
134
135       // We always save the initial state
136       val initialUserState1 = userStateRecorder.apply(initialUserState0)
137
138       clog.debug("Stored initial state = %s", initialUserState1.toJsonString)
139       clog.end()
140
141       return initialUserState1.toWorkingUserState(defaultResourceTypesMap)
142     }
143
144     // Ask DB cache for the latest known user state for this billing period
145     val latestUserStateOpt = userStateStore.findLatestUserStateForFullMonthBilling(
146       userID,
147       billingMonthInfo)
148
149     latestUserStateOpt match {
150       case None ⇒
151         // Not found, must compute
152         clog.debug("No user state found from cache, will have to (re)compute")
153         val result = computeFullMonthBillingAndSaveState
154         clog.end()
155         result
156
157       case Some(latestUserState) ⇒
158         // Found a "latest" user state but need to see if it is indeed the true and one latest.
159         // For this reason, we must count the events again.
160         val latestStateOOSEventsCounter = latestUserState.billingPeriodOutOfSyncResourceEventsCounter
161         val actualOOSEventsCounter = resourceEventStore.countOutOfSyncResourceEventsForBillingPeriod(
162           userID,
163           billingMonthStartMillis,
164           billingMonthStopMillis)
165
166         val counterDiff = actualOOSEventsCounter - latestStateOOSEventsCounter
167         counterDiff match {
168           // ZERO, we are OK!
169           case 0 ⇒
170             // NOTE: Keep the caller's calculation reason
171             val userStateModel = latestUserState.newWithChargingReason(chargingReason)
172             clog.end()
173             userStateModel.toWorkingUserState(defaultResourceTypesMap)
174
175           // We had more, so must recompute
176           case n if n > 0 ⇒
177             clog.debug(
178               "Found %s out of sync events (%s more), will have to (re)compute user state", actualOOSEventsCounter, n)
179             val workingUserState = computeFullMonthBillingAndSaveState
180             clog.end()
181             workingUserState
182
183           // We had less????
184           case n if n < 0 ⇒
185             val errMsg = "Found %s out of sync events (%s less). DB must be inconsistent".format(actualOOSEventsCounter, n)
186             clog.warn(errMsg)
187             throw new AquariumInternalError(errMsg)
188         }
189     }
190   }
191   /**
192    * Processes one resource event and computes relevant charges.
193    *
194    * @param resourceEvent
195    * @param workingUserState
196    * @param chargingReason
197    * @param billingMonthInfo
198    * @param clogOpt
199    */
200   def processResourceEvent(
201       resourceEvent: ResourceEventModel,
202       workingUserState: WorkingUserState,
203       chargingReason: ChargingReason,
204       billingMonthInfo: BillingMonthInfo,
205       clogOpt: Option[ContextualLogger]
206   ): Unit = {
207
208     val resourceTypeName = resourceEvent.resource
209     val resourceTypeOpt = workingUserState.findResourceType(resourceTypeName)
210     if(resourceTypeOpt.isEmpty) {
211       return
212     }
213     val resourceType = resourceTypeOpt.get
214     val resourceAndInstanceInfo = resourceEvent.safeResourceInstanceInfo
215
216     val chargingBehavior = aquarium.chargingBehaviorOf(resourceType)
217
218     val (walletEntriesCount, newTotalCredits) = chargingBehavior.chargeResourceEvent(
219       aquarium,
220       resourceEvent,
221       resourceType,
222       billingMonthInfo,
223       workingUserState.workingAgreementHistory.toAgreementHistory,
224       workingUserState.getChargingDataForResourceEvent(resourceAndInstanceInfo),
225       workingUserState.totalCredits,
226       workingUserState.walletEntries += _,
227       clogOpt
228     )
229
230     workingUserState.totalCredits = newTotalCredits
231   }
232
233   def processResourceEvents(
234       resourceEvents: Traversable[ResourceEventModel],
235       workingUserState: WorkingUserState,
236       chargingReason: ChargingReason,
237       billingMonthInfo: BillingMonthInfo,
238       clogOpt: Option[ContextualLogger] = None
239   ): Unit = {
240
241     for(currentResourceEvent ← resourceEvents) {
242       processResourceEvent(
243         currentResourceEvent,
244         workingUserState,
245         chargingReason,
246         billingMonthInfo,
247         clogOpt
248       )
249     }
250   }
251
252   def replayFullMonthBilling(
253       userStateBootstrap: UserStateBootstrap,
254       billingMonthInfo: BillingMonthInfo,
255       defaultResourceTypesMap: Map[String, ResourceType],
256       chargingReason: ChargingReason,
257       userStateRecorder: UserStateModel ⇒ UserStateModel,
258       clogOpt: Option[ContextualLogger]
259   ): WorkingUserState = {
260
261     replayMonthChargingUpTo(
262       billingMonthInfo,
263       billingMonthInfo.monthStopMillis,
264       userStateBootstrap,
265       defaultResourceTypesMap,
266       chargingReason,
267       userStateRecorder,
268       clogOpt
269     )
270   }
271
272   /**
273    * Replays the charging procedure over the set of resource events that happened within the given month and up to
274    * the specified point in time.
275    *
276    * @param billingMonthInfo Which month to bill.
277    * @param billingEndTimeMillis Bill from start of month up to (and including) this time.
278    * @param userStateBootstrap
279    * @param resourceTypesMap
280    * @param chargingReason
281    * @param userStateRecorder
282    * @param clogOpt
283    * @return
284    */
285   def replayMonthChargingUpTo(
286       billingMonthInfo: BillingMonthInfo,
287       billingEndTimeMillis: Long,
288       userStateBootstrap: UserStateBootstrap,
289       resourceTypesMap: Map[String, ResourceType],
290       chargingReason: ChargingReason,
291       userStateRecorder: UserStateModel ⇒ UserStateModel,
292       clogOpt: Option[ContextualLogger]
293   ): WorkingUserState = {
294
295     val isFullMonthBilling = billingEndTimeMillis == billingMonthInfo.monthStopMillis
296     val userID = userStateBootstrap.userID
297
298     val clog = ContextualLogger.fromOther(
299       clogOpt,
300       logger,
301       "replayMonthChargingUpTo(%s)", TimeHelpers.toYYYYMMDDHHMMSSSSS(billingEndTimeMillis))
302     clog.begin()
303
304     clog.debug("%s", chargingReason)
305
306     val clogSome = Some(clog)
307
308     // In order to replay the full month, we start with the state at the beginning of the month.
309     val previousBillingMonthInfo = billingMonthInfo.previousMonth
310     val workingUserState = findOrCalculateWorkingUserStateAtEndOfBillingMonth(
311       previousBillingMonthInfo,
312       userStateBootstrap,
313       resourceTypesMap,
314       chargingReason,
315       userStateRecorder,
316       clogSome
317     )
318
319     // FIXME the below comments
320     // Keep the working (current) user state. This will get updated as we proceed with billing for the month
321     // specified in the parameters.
322     // NOTE: The calculation reason is not the one we get from the previous user state but the one our caller specifies
323
324     clog.debug("workingUserState=%s", workingUserState)
325     clog.debug("previousBillingMonthUserState(%s) = %s".format(
326       previousBillingMonthInfo.toShortDebugString,
327       workingUserState)
328     )
329
330     var _rcEventsCounter = 0
331     resourceEventStore.foreachResourceEventOccurredInPeriod(
332       userID,
333       billingMonthInfo.monthStartMillis, // from start of month
334       billingEndTimeMillis               // to requested time
335     ) { currentResourceEvent ⇒
336
337       clog.debug("Processing %s".format(currentResourceEvent))
338
339       processResourceEvent(
340         currentResourceEvent,
341         workingUserState,
342         chargingReason,
343         billingMonthInfo,
344         clogSome
345       )
346
347       _rcEventsCounter += 1
348     }
349
350     clog.debug("Found %s resource events for month %s".format(_rcEventsCounter, billingMonthInfo.toShortDebugString))
351
352     if(isFullMonthBilling) {
353       // For the remaining events which must contribute an implicit OFF, we collect those OFFs
354       // ... in order to generate an implicit ON later (during the next billing cycle).
355       val (generatorsOfImplicitEnds, theirImplicitEnds) = workingUserState.findAndRemoveGeneratorsOfImplicitEndEvents(
356         aquarium.chargingBehaviorOf(_),
357         billingMonthInfo.monthStopMillis
358       )
359
360       if(generatorsOfImplicitEnds.lengthCompare(1) >= 0 || theirImplicitEnds.lengthCompare(1) >= 0) {
361         clog.debug("")
362         clog.debug("Process implicitly issued events")
363         clog.debugSeq("generatorsOfImplicitEnds", generatorsOfImplicitEnds, 0)
364         clog.debugSeq("theirImplicitEnds", theirImplicitEnds, 0)
365       }
366
367       // Now, the previous and implicitly started must be our base for the following computation, so we create an
368       // appropriate worker
369       val specialWorkingUserState = workingUserState.newForImplicitEndsAsPreviousEvents(
370         WorkingUserState.makePreviousResourceEventMap(generatorsOfImplicitEnds)
371       )
372
373       processResourceEvents(
374         theirImplicitEnds,
375         specialWorkingUserState,
376         chargingReason,
377         billingMonthInfo,
378         clogSome
379       )
380
381       workingUserState.walletEntries ++= specialWorkingUserState.walletEntries
382       workingUserState.totalCredits    = specialWorkingUserState.totalCredits
383     }
384
385     clog.end()
386     workingUserState
387   }
388 }