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
49 * @author Christos KK Loverdos <loverdos@gmail.com>
51 class UserStateComputations extends Loggable {
52 def createFirstUserState(userId: String, agreementName: String = "default") = {
60 ImplicitOFFResourceEventsSnapshot(Map(), now),
62 LatestResourceEventsSnapshot(Map(), now),
64 ActiveStateSnapshot(false, now),
65 CreditSnapshot(0, now),
66 AgreementSnapshot(Agreement(agreementName, now) :: Nil, now),
67 RolesSnapshot(List(), now),
68 OwnedResourcesSnapshot(List(), now)
72 def createFirstUserState(userId: String, agreementName: String, resourcesMap: DSLResourcesMap) = {
80 ImplicitOFFResourceEventsSnapshot(Map(), now),
82 LatestResourceEventsSnapshot(Map(), now),
84 ActiveStateSnapshot(false, now),
85 CreditSnapshot(0, now),
86 AgreementSnapshot(Agreement(agreementName, now) :: Nil, now),
87 RolesSnapshot(List(), now),
88 OwnedResourcesSnapshot(List(), now)
92 def findUserStateAtEndOfBillingMonth(userId: String,
93 billingMonthInfo: BillingMonthInfo,
94 userStateStore: UserStateStore,
95 resourceEventStore: ResourceEventStore,
96 policyStore: PolicyStore,
97 userCreationMillis: Long,
98 currentUserState: UserState,
99 zeroUserState: UserState,
100 defaultPolicy: DSLPolicy,
101 defaultResourcesMap: DSLResourcesMap,
102 accounting: Accounting,
103 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = {
105 val clog = ContextualLogger.fromOther(
108 "findUserStateAtEndOfBillingMonth(%s)", billingMonthInfo)
111 def doCompute: Maybe[UserState] = {
112 clog.debug("Computing full month billing")
113 doFullMonthlyBilling(
128 val userCreationDateCalc = new MutableDateCalc(userCreationMillis)
129 val billingMonthStartMillis = billingMonthInfo.startMillis
130 val billingMonthStopMillis = billingMonthInfo.stopMillis
132 if(billingMonthStopMillis < userCreationMillis) {
133 // If the user did not exist for this billing month, piece of cake
134 clog.debug("User did not exist before %s. Returning %s", userCreationDateCalc, zeroUserState)
135 clog.endWith(Just(zeroUserState))
137 // Ask DB cache for the latest known user state for this billing period
138 val latestUserStateM = userStateStore.findLatestUserStateForEndOfBillingMonth(
140 billingMonthInfo.year,
141 billingMonthInfo.month)
143 latestUserStateM match {
145 // Not found, must compute
146 clog.debug("No user state found from cache, will have to (re)compute")
147 clog.endWith(doCompute)
149 case failed @ Failed(_, _) ⇒
150 clog.warn("Failure while quering cache for user state: %s", failed)
153 case Just(latestUserState) ⇒
154 // Found a "latest" user state but need to see if it is indeed the true and one latest.
155 // For this reason, we must count the events again.
156 val latestStateOOSEventsCounter = latestUserState.billingPeriodOutOfSyncResourceEventsCounter
157 val actualOOSEventsCounterM = resourceEventStore.countOutOfSyncEventsForBillingPeriod(
159 billingMonthStartMillis,
160 billingMonthStopMillis)
162 actualOOSEventsCounterM match {
164 val errMsg = "No counter computed for out of sync events. Should at least be zero."
166 clog.endWith(Failed(new Exception(errMsg)))
168 case failed @ Failed(_, _) ⇒
169 clog.warn("Failure while querying for out of sync events: %s", failed)
172 case Just(actualOOSEventsCounter) ⇒
173 val counterDiff = actualOOSEventsCounter - latestStateOOSEventsCounter
179 // We had more, so must recompute
182 "Found %s out of sync events (%s more), will have to (re)compute user state", actualOOSEventsCounter, n)
183 clog.endWith(doCompute)
187 val errMsg = "Found %s out of sync events (%s less). DB must be inconsistent".format(actualOOSEventsCounter, n)
189 clog.endWith(Failed(new Exception(errMsg)))
196 def doFullMonthlyBilling(userId: String,
197 billingMonthInfo: BillingMonthInfo,
198 userStateStore: UserStateStore,
199 resourceEventStore: ResourceEventStore,
200 policyStore: PolicyStore,
201 userCreationMillis: Long,
202 currentUserState: UserState,
203 zeroUserState: UserState,
204 defaultPolicy: DSLPolicy,
205 defaultResourcesMap: DSLResourcesMap,
206 accounting: Accounting,
207 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = Maybe {
209 val previousBillingMonthData = billingMonthInfo.previousMonth
211 val previousBillingMonth = previousBillingMonthData.month
212 val yearOfPreviousBillingMonth = previousBillingMonthData.year
214 val clog = ContextualLogger.fromOther(
217 "doFullMonthlyBilling(%s)", billingMonthInfo)
220 val previousBillingMonthUserStateM = findUserStateAtEndOfBillingMonth(
235 previousBillingMonthUserStateM match {
237 null // not really... (must throw an exception here probably...)
238 case failed @ Failed(e, _) ⇒
240 case Just(startingUserState) ⇒
241 // This is the real deal
243 // This is a collection of all the latest resource events.
244 // We want these in order to correlate incoming resource events with their previous (in `occurredMillis` time)
246 // Will be updated on processing the next resource event.
247 val previousResourceEvents = startingUserState.latestResourceEventsSnapshot.toMutableWorker
248 clog.debug("previousResourceEvents = %s", previousResourceEvents)
250 val billingMonthStartMillis = billingMonthInfo.startMillis
251 val billingMonthEndMillis = billingMonthInfo.stopMillis
253 // Keep the working (current) user state. This will get updated as we proceed with billing for the month
254 // specified in the parameters.
255 var _workingUserState = startingUserState
257 // Prepare the implicit OFF resource events
258 val theImplicitOFFs = _workingUserState.implicitOFFsSnapshot.toMutableWorker
259 clog.debug("theImplicitOFFs = %s", theImplicitOFFs)
262 * Finds the previous resource event by checking two possible sources: a) The implicit OFF resource events and
263 * b) the explicit previous resource events. If the event is found, it is removed from the respective source.
265 * If the event is not found, then this must be for a new resource instance.
266 * (and probably then some `zero` resource event must be implied as the previous one)
272 def findAndRemovePreviousResourceEvent(resource: String, instanceId: String): Maybe[ResourceEvent] = {
273 // implicit OFFs are checked first
274 theImplicitOFFs.findAndRemoveResourceEvent(resource, instanceId) match {
275 case just @ Just(_) ⇒
278 // explicit previous are checked second
279 previousResourceEvents.findAndRemoveResourceEvent(resource, instanceId) match {
280 case just @ Just(_) ⇒
290 def rcDebugInfo(rcEvent: ResourceEvent) = {
291 rcEvent.toDebugString(defaultResourcesMap, false)
294 // Find the actual resource events from DB
295 val allResourceEventsForMonth = resourceEventStore.findAllRelevantResourceEventsForBillingPeriod(
297 billingMonthStartMillis,
298 billingMonthEndMillis)
299 var _eventCounter = 0
301 clog.debug("resourceEventStore = %s".format(resourceEventStore))
302 clog.debug("Found %s resource events, starting processing...", allResourceEventsForMonth.size)
305 currentResourceEvent <- allResourceEventsForMonth
307 _eventCounter = _eventCounter + 1
308 val theResource = currentResourceEvent.resource
309 val theInstanceId = currentResourceEvent.instanceId
310 val theValue = currentResourceEvent.value
313 clog.debug("Processing %s", currentResourceEvent)
314 clog.debug("Friendlier %s", rcDebugInfo(currentResourceEvent))
317 if(previousResourceEvents.size > 0) {
318 clog.debug("%s previousResourceEvents", previousResourceEvents.size)
320 previousResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
323 if(theImplicitOFFs.size > 0) {
324 clog.debug("%s theImplicitOFFs", theImplicitOFFs.size)
326 theImplicitOFFs.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
330 // Ignore the event if it is not billable (but still record it in the "previous" stuff).
331 // But to make this decision, first we need the resource definiton (and its cost policy).
332 val resourceDefM = defaultResourcesMap.findResourceM(currentResourceEvent.safeResource)
334 // We have a resource (and thus a cost policy)
335 case Just(resourceDef) ⇒
336 val costPolicy = resourceDef.costPolicy
337 clog.debug("Cost policy: %s", costPolicy)
338 val isBillable = costPolicy.isBillableEventBasedOnValue(currentResourceEvent.value)
340 // The resource event is not billable
342 clog.debug("Ignoring not billable event %s", rcDebugInfo(currentResourceEvent))
344 // The resource event is billable
346 // Find the previous event.
347 // This is (potentially) needed to calculate new credit amount and new resource instance amount
348 val previousResourceEventM = findAndRemovePreviousResourceEvent(theResource, theInstanceId)
349 clog.debug("PreviousM %s", previousResourceEventM.map(rcDebugInfo(_)))
350 val defaultInitialAmount = costPolicy.getResourceInstanceInitialAmount
351 val oldAmount = _workingUserState.getResourceInstanceAmount(theResource, theInstanceId, defaultInitialAmount)
352 val oldCredits = _workingUserState.creditsSnapshot.creditAmount
354 // A. Compute new resource instance accumulating amount
355 val newAmount = costPolicy.computeNewAccumulatingAmount(oldAmount, theValue)
357 // B. Compute new wallet entries
358 val alltimeAgreements = _workingUserState.agreementsSnapshot.agreements
359 // val chargeChunksM = accounting.computeChargeChunks(
360 // previousResourceEventM,
361 // currentResourceEvent,
366 // defaultResourcesMap,
367 // alltimeAgreements)
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)