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 // We always save the initial state
129 val initialUserState1 = userStateRecorder.apply(initialUserState0)
131 clog.debug("Stored initial state = %s", initialUserState1.toJsonString)
134 return initialUserState1.toWorkingUserState(defaultResourceTypesMap)
137 // Ask DB cache for the latest known user state for this billing period
138 val latestUserStateOpt = userStateStore.findLatestUserStateForFullMonthBilling(
142 latestUserStateOpt match {
144 // Not found, must compute
145 clog.debug("No user state found from cache, will have to (re)compute")
146 val result = computeFullMonthBillingAndSaveState
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(
156 billingMonthStartMillis,
157 billingMonthStopMillis)
159 val counterDiff = actualOOSEventsCounter - latestStateOOSEventsCounter
163 // NOTE: Keep the caller's calculation reason
164 val userStateModel = latestUserState.newWithChargingReason(chargingReason)
166 userStateModel.toWorkingUserState(defaultResourceTypesMap)
168 // We had more, so must recompute
171 "Found %s out of sync events (%s more), will have to (re)compute user state", actualOOSEventsCounter, n)
172 val workingUserState = computeFullMonthBillingAndSaveState
178 val errMsg = "Found %s out of sync events (%s less). DB must be inconsistent".format(actualOOSEventsCounter, n)
180 throw new AquariumInternalError(errMsg)
185 * Processes one resource event and computes relevant charges.
187 * @param resourceEvent
188 * @param workingUserState
189 * @param chargingReason
190 * @param billingMonthInfo
193 def processResourceEvent(
194 resourceEvent: ResourceEventModel,
195 workingUserState: WorkingUserState,
196 chargingReason: ChargingReason,
197 billingMonthInfo: BillingMonthInfo,
198 clogOpt: Option[ContextualLogger]
201 val resourceTypeName = resourceEvent.resource
202 val resourceTypeOpt = workingUserState.findResourceType(resourceTypeName)
203 if(resourceTypeOpt.isEmpty) {
206 val resourceType = resourceTypeOpt.get
207 val resourceAndInstanceInfo = resourceEvent.safeResourceInstanceInfo
209 val chargingBehavior = aquarium.chargingBehaviorOf(resourceType)
211 val (walletEntriesCount, newTotalCredits) = chargingBehavior.chargeResourceEvent(
216 workingUserState.workingAgreementHistory.toAgreementHistory,
217 workingUserState.getChargingDataForResourceEvent(resourceAndInstanceInfo),
218 workingUserState.totalCredits,
219 workingUserState.walletEntries += _,
223 workingUserState.totalCredits = newTotalCredits
226 def processResourceEvents(
227 resourceEvents: Traversable[ResourceEventModel],
228 workingUserState: WorkingUserState,
229 chargingReason: ChargingReason,
230 billingMonthInfo: BillingMonthInfo,
231 clogOpt: Option[ContextualLogger] = None
234 for(currentResourceEvent ← resourceEvents) {
235 processResourceEvent(
236 currentResourceEvent,
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 = {
254 replayMonthChargingUpTo(
256 billingMonthInfo.monthStopMillis,
258 defaultResourceTypesMap,
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.
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
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 = {
288 val isFullMonthBilling = billingEndTimeMillis == billingMonthInfo.monthStopMillis
289 val userID = userStateBootstrap.userID
291 val clog = ContextualLogger.fromOther(
294 "replayMonthChargingUpTo(%s)", TimeHelpers.toYYYYMMDDHHMMSSSSS(billingEndTimeMillis))
297 clog.debug("%s", chargingReason)
299 val clogSome = Some(clog)
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,
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
317 clog.debug("previousBillingMonthUserState(%s) = %s".format(
318 previousBillingMonthInfo.toShortDebugString,
319 workingUserState.toJsonString)
322 var _rcEventsCounter = 0
323 resourceEventStore.foreachResourceEventOccurredInPeriod(
325 billingMonthInfo.monthStartMillis, // from start of month
326 billingEndTimeMillis // to requested time
327 ) { currentResourceEvent ⇒
329 clog.debug("Processing %s".format(currentResourceEvent))
331 processResourceEvent(
332 currentResourceEvent,
339 _rcEventsCounter += 1
342 clog.debug("Found %s resource events for month %s".format(_rcEventsCounter, billingMonthInfo.toShortDebugString))
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
352 if(generatorsOfImplicitEnds.lengthCompare(1) >= 0 || theirImplicitEnds.lengthCompare(1) >= 0) {
354 clog.debug("Process implicitly issued events")
355 clog.debugSeq("generatorsOfImplicitEnds", generatorsOfImplicitEnds, 0)
356 clog.debugSeq("theirImplicitEnds", theirImplicitEnds, 0)
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)
365 processResourceEvents(
367 specialWorkingUserState,
373 workingUserState.walletEntries ++= specialWorkingUserState.walletEntries
374 workingUserState.totalCredits = specialWorkingUserState.totalCredits