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.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}
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(Some(billingMonthInfo), None)
102 // We always save the state when it is a full month billing
103 val monthlyUserState1 = userStateRecorder.apply(monthlyUserState0)
105 clog.debug("Stored full %s %s", billingMonthInfo.toDebugString, monthlyUserState1.toJsonString)
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
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)
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(
124 TimeHelpers.nowMillis(),
125 InitialUserStateSetup(Some(chargingReason)) // we record the originating calculation reason
128 logger.debug("Created (from bootstrap) initial user state %s".format(initialUserState0))
130 // We always save the initial state
131 val initialUserState1 = userStateRecorder.apply(initialUserState0)
133 clog.debug("Stored initial state = %s", initialUserState1.toJsonString)
136 return initialUserState1.toWorkingUserState(defaultResourceTypesMap)
139 // Ask DB cache for the latest known user state for this billing period
140 val latestUserStateOpt = userStateStore.findLatestUserStateForFullMonthBilling(
144 latestUserStateOpt match {
146 // Not found, must compute
147 clog.debug("No user state found from cache, will have to (re)compute")
148 val result = computeFullMonthBillingAndSaveState
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(
158 billingMonthStartMillis,
159 billingMonthStopMillis)
161 val counterDiff = actualOOSEventsCounter - latestStateOOSEventsCounter
165 // NOTE: Keep the caller's calculation reason
166 val userStateModel = latestUserState.newWithChargingReason(chargingReason)
168 userStateModel.toWorkingUserState(defaultResourceTypesMap)
170 // We had more, so must recompute
173 "Found %s out of sync events (%s more), will have to (re)compute user state", actualOOSEventsCounter, n)
174 val workingUserState = computeFullMonthBillingAndSaveState
180 val errMsg = "Found %s out of sync events (%s less). DB must be inconsistent".format(actualOOSEventsCounter, n)
182 throw new AquariumInternalError(errMsg)
187 * Processes one resource event and computes relevant charges.
189 * @param resourceEvent
190 * @param workingUserState
191 * @param chargingReason
192 * @param billingMonthInfo
195 def processResourceEvent(
196 resourceEvent: ResourceEventModel,
197 workingUserState: WorkingUserState,
198 chargingReason: ChargingReason,
199 billingMonthInfo: BillingMonthInfo,
200 clogOpt: Option[ContextualLogger]
203 val resourceTypeName = resourceEvent.resource
204 val resourceTypeOpt = workingUserState.findResourceType(resourceTypeName)
205 if(resourceTypeOpt.isEmpty) {
208 val resourceType = resourceTypeOpt.get
209 val resourceAndInstanceInfo = resourceEvent.safeResourceInstanceInfo
211 val chargingBehavior = aquarium.chargingBehaviorOf(resourceType)
213 val (walletEntriesCount, newTotalCredits) = chargingBehavior.chargeResourceEvent(
218 workingUserState.workingAgreementHistory.toAgreementHistory,
219 workingUserState.getChargingDataForResourceEvent(resourceAndInstanceInfo),
220 workingUserState.totalCredits,
221 workingUserState.walletEntries += _,
225 workingUserState.totalCredits = newTotalCredits
228 def processResourceEvents(
229 resourceEvents: Traversable[ResourceEventModel],
230 workingUserState: WorkingUserState,
231 chargingReason: ChargingReason,
232 billingMonthInfo: BillingMonthInfo,
233 clogOpt: Option[ContextualLogger] = None
236 for(currentResourceEvent ← resourceEvents) {
237 processResourceEvent(
238 currentResourceEvent,
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 = {
256 replayMonthChargingUpTo(
258 billingMonthInfo.monthStopMillis,
260 defaultResourceTypesMap,
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.
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
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 = {
290 val isFullMonthBilling = billingEndTimeMillis == billingMonthInfo.monthStopMillis
291 val userID = userStateBootstrap.userID
293 val clog = ContextualLogger.fromOther(
296 "replayMonthChargingUpTo(%s)", TimeHelpers.toYYYYMMDDHHMMSSSSS(billingEndTimeMillis))
299 clog.debug("%s", chargingReason)
301 val clogSome = Some(clog)
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,
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
319 clog.debug("previousBillingMonthUserState(%s) = %s".format(
320 previousBillingMonthInfo.toShortDebugString,
321 workingUserState.toJsonString)
324 var _rcEventsCounter = 0
325 resourceEventStore.foreachResourceEventOccurredInPeriod(
327 billingMonthInfo.monthStartMillis, // from start of month
328 billingEndTimeMillis // to requested time
329 ) { currentResourceEvent ⇒
331 clog.debug("Processing %s".format(currentResourceEvent))
333 processResourceEvent(
334 currentResourceEvent,
341 _rcEventsCounter += 1
344 clog.debug("Found %s resource events for month %s".format(_rcEventsCounter, billingMonthInfo.toShortDebugString))
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
354 if(generatorsOfImplicitEnds.lengthCompare(1) >= 0 || theirImplicitEnds.lengthCompare(1) >= 0) {
356 clog.debug("Process implicitly issued events")
357 clog.debugSeq("generatorsOfImplicitEnds", generatorsOfImplicitEnds, 0)
358 clog.debugSeq("theirImplicitEnds", theirImplicitEnds, 0)
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)
367 processResourceEvents(
369 specialWorkingUserState,
375 workingUserState.walletEntries ++= specialWorkingUserState.walletEntries
376 workingUserState.totalCredits = specialWorkingUserState.totalCredits