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