2 * Copyright 2011 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.user
39 import com.ckkloverdos.maybe.{Failed, NoVal, Just, Maybe}
40 import gr.grnet.aquarium.util.date.MutableDateCalc
41 import gr.grnet.aquarium.logic.accounting.dsl.{DSLResourcesMap, DSLPolicy}
42 import gr.grnet.aquarium.logic.events.ResourceEvent
43 import gr.grnet.aquarium.store.{PolicyStore, UserStateStore, ResourceEventStore}
44 import gr.grnet.aquarium.util.{ContextualLogger, Loggable}
45 import gr.grnet.aquarium.logic.accounting.Accounting
46 import gr.grnet.aquarium.logic.accounting.algorithm.SimpleCostPolicyAlgorithmCompiler
50 * @author Christos KK Loverdos <loverdos@gmail.com>
52 class UserStateComputations extends Loggable {
53 def createFirstUserState(userId: String, agreementName: String = "default") = {
61 ImplicitlyIssuedResourceEventsSnapshot(List(), now),
63 LatestResourceEventsSnapshot(List(), now),
65 ActiveStateSnapshot(false, now),
66 CreditSnapshot(0, now),
67 AgreementSnapshot(Agreement(agreementName, now) :: Nil, now),
68 RolesSnapshot(List(), now),
69 OwnedResourcesSnapshot(List(), now)
73 def createFirstUserState(userId: String, agreementName: String, resourcesMap: DSLResourcesMap) = {
81 ImplicitlyIssuedResourceEventsSnapshot(List(), now),
83 LatestResourceEventsSnapshot(List(), now),
85 ActiveStateSnapshot(false, now),
86 CreditSnapshot(0, now),
87 AgreementSnapshot(Agreement(agreementName, now) :: Nil, now),
88 RolesSnapshot(List(), now),
89 OwnedResourcesSnapshot(List(), now)
93 def findUserStateAtEndOfBillingMonth(userId: String,
94 billingMonthInfo: BillingMonthInfo,
95 userStateStore: UserStateStore,
96 resourceEventStore: ResourceEventStore,
97 policyStore: PolicyStore,
98 userCreationMillis: Long,
99 currentUserState: UserState,
100 zeroUserState: UserState,
101 defaultPolicy: DSLPolicy,
102 defaultResourcesMap: DSLResourcesMap,
103 accounting: Accounting,
104 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = {
106 val clog = ContextualLogger.fromOther(
109 "findUserStateAtEndOfBillingMonth(%s)", billingMonthInfo)
112 def doCompute: Maybe[UserState] = {
113 clog.debug("Computing full month billing")
114 doFullMonthlyBilling(
129 val userCreationDateCalc = new MutableDateCalc(userCreationMillis)
130 val billingMonthStartMillis = billingMonthInfo.startMillis
131 val billingMonthStopMillis = billingMonthInfo.stopMillis
133 if(billingMonthStopMillis < userCreationMillis) {
134 // If the user did not exist for this billing month, piece of cake
135 clog.debug("User did not exist before %s. Returning %s", userCreationDateCalc, zeroUserState)
136 clog.endWith(Just(zeroUserState))
138 // Ask DB cache for the latest known user state for this billing period
139 val latestUserStateM = userStateStore.findLatestUserStateForEndOfBillingMonth(
141 billingMonthInfo.year,
142 billingMonthInfo.month)
144 latestUserStateM match {
146 // Not found, must compute
147 clog.debug("No user state found from cache, will have to (re)compute")
148 clog.endWith(doCompute)
150 case failed @ Failed(_, _) ⇒
151 clog.warn("Failure while quering cache for user state: %s", failed)
154 case Just(latestUserState) ⇒
155 // Found a "latest" user state but need to see if it is indeed the true and one latest.
156 // For this reason, we must count the events again.
157 val latestStateOOSEventsCounter = latestUserState.billingPeriodOutOfSyncResourceEventsCounter
158 val actualOOSEventsCounterM = resourceEventStore.countOutOfSyncEventsForBillingPeriod(
160 billingMonthStartMillis,
161 billingMonthStopMillis)
163 actualOOSEventsCounterM match {
165 val errMsg = "No counter computed for out of sync events. Should at least be zero."
167 clog.endWith(Failed(new Exception(errMsg)))
169 case failed @ Failed(_, _) ⇒
170 clog.warn("Failure while querying for out of sync events: %s", failed)
173 case Just(actualOOSEventsCounter) ⇒
174 val counterDiff = actualOOSEventsCounter - latestStateOOSEventsCounter
180 // We had more, so must recompute
183 "Found %s out of sync events (%s more), will have to (re)compute user state", actualOOSEventsCounter, n)
184 clog.endWith(doCompute)
188 val errMsg = "Found %s out of sync events (%s less). DB must be inconsistent".format(actualOOSEventsCounter, n)
190 clog.endWith(Failed(new Exception(errMsg)))
197 def doFullMonthlyBilling(userId: String,
198 billingMonthInfo: BillingMonthInfo,
199 userStateStore: UserStateStore,
200 resourceEventStore: ResourceEventStore,
201 policyStore: PolicyStore,
202 userCreationMillis: Long,
203 currentUserState: UserState,
204 zeroUserState: UserState,
205 defaultPolicy: DSLPolicy,
206 defaultResourcesMap: DSLResourcesMap,
207 accounting: Accounting,
208 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = Maybe {
210 val clog = ContextualLogger.fromOther(
213 "doFullMonthlyBilling(%s)", billingMonthInfo)
216 val previousBillingMonthUserStateM = findUserStateAtEndOfBillingMonth(
218 billingMonthInfo.previousMonth,
231 previousBillingMonthUserStateM match {
233 null // not really... (must throw an exception here probably...)
234 case failed @ Failed(e, _) ⇒
236 case Just(startingUserState) ⇒
237 // This is the real deal
239 // This is a collection of all the latest resource events.
240 // We want these in order to correlate incoming resource events with their previous (in `occurredMillis` time)
242 // Will be updated on processing the next resource event.
243 val previousResourceEvents = startingUserState.latestResourceEventsSnapshot.toMutableWorker
244 clog.debug("previousResourceEvents = %s", previousResourceEvents)
246 val billingMonthStartMillis = billingMonthInfo.startMillis
247 val billingMonthEndMillis = billingMonthInfo.stopMillis
249 // Keep the working (current) user state. This will get updated as we proceed with billing for the month
250 // specified in the parameters.
251 var _workingUserState = startingUserState
253 // Prepare the implicit OFF resource events
254 // (we keep the terminology for historical reasons)
255 val theImplicitOFFs = _workingUserState.implicitlyTerminatedSnapshot.toMutableWorker
256 clog.debug("theImplicitOFFs = %s", theImplicitOFFs)
259 * Finds the previous resource event by checking two possible sources: a) The implicit OFF resource events and
260 * b) the explicit previous resource events. If the event is found, it is removed from the respective source.
262 * If the event is not found, then this must be for a new resource instance.
263 * (and probably then some `zero` resource event must be implied as the previous one)
269 def findAndRemovePreviousResourceEvent(resource: String, instanceId: String): Maybe[ResourceEvent] = {
270 // implicit OFFs are checked first
271 theImplicitOFFs.findAndRemoveResourceEvent(resource, instanceId) match {
272 case just @ Just(_) ⇒
275 // explicit previous are checked second
276 previousResourceEvents.findAndRemoveResourceEvent(resource, instanceId) match {
277 case just @ Just(_) ⇒
287 def rcDebugInfo(rcEvent: ResourceEvent) = {
288 rcEvent.toDebugString(defaultResourcesMap, false)
291 // Find the actual resource events from DB
292 val allResourceEventsForMonth = resourceEventStore.findAllRelevantResourceEventsForBillingPeriod(
294 billingMonthStartMillis,
295 billingMonthEndMillis)
296 var _eventCounter = 0
298 clog.debug("resourceEventStore = %s".format(resourceEventStore))
299 clog.debug("Found %s resource events, starting processing...", allResourceEventsForMonth.size)
302 currentResourceEvent <- allResourceEventsForMonth
304 _eventCounter = _eventCounter + 1
305 val theResource = currentResourceEvent.safeResource
306 val theInstanceId = currentResourceEvent.safeInstanceId
307 val theValue = currentResourceEvent.value
310 clog.debug("Processing %s", currentResourceEvent)
311 clog.debug("Friendlier %s", rcDebugInfo(currentResourceEvent))
314 if(previousResourceEvents.size > 0) {
315 clog.debug("%s previousResourceEvents", previousResourceEvents.size)
317 previousResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
320 if(theImplicitOFFs.size > 0) {
321 clog.debug("%s theImplicitOFFs", theImplicitOFFs.size)
323 theImplicitOFFs.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
327 // Ignore the event if it is not billable (but still record it in the "previous" stuff).
328 // But to make this decision, first we need the resource definition (and its cost policy).
329 val resourceDefM = defaultResourcesMap.findResourceM(theResource)
331 // We have a resource (and thus a cost policy)
332 case Just(resourceDef) ⇒
333 val costPolicy = resourceDef.costPolicy
334 clog.debug("Cost policy: %s", costPolicy)
335 val isBillable = costPolicy.isBillableEventBasedOnValue(theValue)
337 // The resource event is not billable
339 clog.debug("Ignoring not billable event %s", rcDebugInfo(currentResourceEvent))
341 // The resource event is billable
343 // Find the previous event.
344 // This is (potentially) needed to calculate new credit amount and new resource instance amount
345 val previousResourceEventM = findAndRemovePreviousResourceEvent(theResource, theInstanceId)
346 clog.debug("PreviousM %s", previousResourceEventM.map(rcDebugInfo(_)))
347 val defaultInitialAmount = costPolicy.getResourceInstanceInitialAmount
348 val oldAmount = _workingUserState.getResourceInstanceAmount(theResource, theInstanceId, defaultInitialAmount)
349 val oldCredits = _workingUserState.creditsSnapshot.creditAmount
351 // A. Compute new resource instance accumulating amount
352 val newAmount = costPolicy.computeNewAccumulatingAmount(oldAmount, theValue)
354 // B. Compute new wallet entries
355 val alltimeAgreements = _workingUserState.agreementsSnapshot.agreementsByTimeslot
357 accounting.computeChargeChunks(
358 previousResourceEventM,
359 currentResourceEvent,
366 SimpleCostPolicyAlgorithmCompiler
369 // C. Compute new credit amount (based on the wallet entries)
370 // Maybe this can be consolidated inthe previous step (B)
376 // After processing, all event, billable or not update the previous state
377 previousResourceEvents.updateResourceEvent(currentResourceEvent)
379 // We do not have a resource (and no cost policy)
381 // Now, this is a matter of politics: what do we do if no policy was found?
382 clog.error("No cost policy for %s", rcDebugInfo(currentResourceEvent))
384 // Could not retrieve resource (unlikely to happen)
385 case failed @ Failed(e, m) ⇒
386 clog.error("Error obtaining cost policy for %s", rcDebugInfo(currentResourceEvent))
395 clog.endWith(_workingUserState)