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