2 * Copyright 2011-2012 GRNET S.A. All rights reserved.
4 * Redistribution and use in source and binary forms, with or
5 * without modification, are permitted provided that the following
8 * 1. Redistributions of source code must retain the above
9 * copyright notice, this list of conditions and the following
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.
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.
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.
36 package gr.grnet.aquarium.charging
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}
50 * @author Christos KK Loverdos <loverdos@gmail.com>
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
66 protected def rcDebugInfo(rcEvent: ResourceEventModel) = {
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 = {
80 val clog = ContextualLogger.fromOther(
83 "findOrCalculateWorkingUserStateAtEndOfBillingMonth(%s)", billingMonthInfo.toShortDebugString)
86 lazy val clogSome = Some(clog)
88 def computeFullMonthBillingAndSaveState(): WorkingUserState = {
89 val workingUserState = replayFullMonthBilling(
92 defaultResourceTypesMap,
98 val newChargingReason = MonthlyBillChargingReason(chargingReason, billingMonthInfo)
99 workingUserState.chargingReason = newChargingReason
100 val monthlyUserState0 = workingUserState.toUserState(
102 billingMonthInfo.year,
103 billingMonthInfo.month,
107 // We always save the state when it is a full month billing
108 val monthlyUserState1 = userStateRecorder.apply(monthlyUserState0)
110 clog.debug("Stored full %s %s", billingMonthInfo.toDebugString, monthlyUserState1.toJsonString)
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
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)
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(
129 TimeHelpers.nowMillis(),
130 InitialUserStateSetup(Some(chargingReason)) // we record the originating calculation reason
133 logger.debug("Created (from bootstrap) initial user state %s".format(initialUserState0))
135 // We always save the initial state
136 val initialUserState1 = userStateRecorder.apply(initialUserState0)
138 clog.debug("Stored initial state = %s", initialUserState1.toJsonString)
141 return initialUserState1.toWorkingUserState(defaultResourceTypesMap)
144 // Ask DB cache for the latest known user state for this billing period
145 val latestUserStateOpt = userStateStore.findLatestUserStateForFullMonthBilling(
149 latestUserStateOpt match {
151 // Not found, must compute
152 clog.debug("No user state found from cache, will have to (re)compute")
153 val result = computeFullMonthBillingAndSaveState
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(
163 billingMonthStartMillis,
164 billingMonthStopMillis)
166 val counterDiff = actualOOSEventsCounter - latestStateOOSEventsCounter
170 // NOTE: Keep the caller's calculation reason
171 val userStateModel = latestUserState.newWithChargingReason(chargingReason)
173 userStateModel.toWorkingUserState(defaultResourceTypesMap)
175 // We had more, so must recompute
178 "Found %s out of sync events (%s more), will have to (re)compute user state", actualOOSEventsCounter, n)
179 val workingUserState = computeFullMonthBillingAndSaveState
185 val errMsg = "Found %s out of sync events (%s less). DB must be inconsistent".format(actualOOSEventsCounter, n)
187 throw new AquariumInternalError(errMsg)
192 * Processes one resource event and computes relevant charges.
194 * @param resourceEvent
195 * @param workingUserState
196 * @param chargingReason
197 * @param billingMonthInfo
200 def processResourceEvent(
201 resourceEvent: ResourceEventModel,
202 workingUserState: WorkingUserState,
203 chargingReason: ChargingReason,
204 billingMonthInfo: BillingMonthInfo,
205 clogOpt: Option[ContextualLogger]
208 val resourceTypeName = resourceEvent.resource
209 val resourceTypeOpt = workingUserState.findResourceType(resourceTypeName)
210 if(resourceTypeOpt.isEmpty) {
213 val resourceType = resourceTypeOpt.get
214 val resourceAndInstanceInfo = resourceEvent.safeResourceInstanceInfo
216 val chargingBehavior = aquarium.chargingBehaviorOf(resourceType)
218 val (walletEntriesCount, newTotalCredits) = chargingBehavior.chargeResourceEvent(
223 workingUserState.workingAgreementHistory.toAgreementHistory,
224 workingUserState.getChargingDataForResourceEvent(resourceAndInstanceInfo),
225 workingUserState.totalCredits,
226 workingUserState.walletEntries += _,
230 workingUserState.totalCredits = newTotalCredits
233 def processResourceEvents(
234 resourceEvents: Traversable[ResourceEventModel],
235 workingUserState: WorkingUserState,
236 chargingReason: ChargingReason,
237 billingMonthInfo: BillingMonthInfo,
238 clogOpt: Option[ContextualLogger] = None
241 for(currentResourceEvent ← resourceEvents) {
242 processResourceEvent(
243 currentResourceEvent,
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 = {
261 replayMonthChargingUpTo(
263 billingMonthInfo.monthStopMillis,
265 defaultResourceTypesMap,
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.
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
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 = {
295 val isFullMonthBilling = billingEndTimeMillis == billingMonthInfo.monthStopMillis
296 val userID = userStateBootstrap.userID
298 val clog = ContextualLogger.fromOther(
301 "replayMonthChargingUpTo(%s)", TimeHelpers.toYYYYMMDDHHMMSSSSS(billingEndTimeMillis))
304 clog.debug("%s", chargingReason)
306 val clogSome = Some(clog)
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,
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
324 clog.debug("workingUserState=%s", workingUserState)
325 clog.debug("previousBillingMonthUserState(%s) = %s".format(
326 previousBillingMonthInfo.toShortDebugString,
330 var _rcEventsCounter = 0
331 resourceEventStore.foreachResourceEventOccurredInPeriod(
333 billingMonthInfo.monthStartMillis, // from start of month
334 billingEndTimeMillis // to requested time
335 ) { currentResourceEvent ⇒
337 clog.debug("Processing %s".format(currentResourceEvent))
339 processResourceEvent(
340 currentResourceEvent,
347 _rcEventsCounter += 1
350 clog.debug("Found %s resource events for month %s".format(_rcEventsCounter, billingMonthInfo.toShortDebugString))
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
360 if(generatorsOfImplicitEnds.lengthCompare(1) >= 0 || theirImplicitEnds.lengthCompare(1) >= 0) {
362 clog.debug("Process implicitly issued events")
363 clog.debugSeq("generatorsOfImplicitEnds", generatorsOfImplicitEnds, 0)
364 clog.debugSeq("theirImplicitEnds", theirImplicitEnds, 0)
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)
373 processResourceEvents(
375 specialWorkingUserState,
381 workingUserState.walletEntries ++= specialWorkingUserState.walletEntries
382 workingUserState.totalCredits = specialWorkingUserState.totalCredits