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
38 import scala.collection.mutable
40 import com.ckkloverdos.maybe.{Failed, NoVal, Just, Maybe}
41 import gr.grnet.aquarium.logic.accounting.Accounting
42 import gr.grnet.aquarium.util.date.DateCalculator
43 import gr.grnet.aquarium.logic.accounting.dsl.{DSLResourcesMap, DSLCostPolicy, DSLPolicy}
44 import gr.grnet.aquarium.logic.events.ResourceEvent
45 import gr.grnet.aquarium.store.{PolicyStore, UserStateStore, ResourceEventStore}
46 import gr.grnet.aquarium.util.{ContextualLogger, Loggable}
50 * @author Christos KK Loverdos <loverdos@gmail.com>
52 class UserStateComputations extends Loggable {
53 def createFirstUserState(userId: String, agreementName: String = "default") = {
61 ImplicitOFFResourceEventsSnapshot(Map(), now),
63 LatestResourceEventsSnapshot(Map(), now),
65 ActiveStateSnapshot(false, now),
66 CreditSnapshot(0, now),
67 AgreementSnapshot(Agreement(agreementName, now, -1) :: Nil, now),
68 RolesSnapshot(List(), now),
69 OwnedResourcesSnapshot(List(), now)
73 def createFirstUserState(userId: String, agreementName: String, resourcesMap: DSLResourcesMap) = {
81 ImplicitOFFResourceEventsSnapshot(Map(), now),
83 LatestResourceEventsSnapshot(Map(), now),
85 ActiveStateSnapshot(false, now),
86 CreditSnapshot(0, now),
87 AgreementSnapshot(Agreement(agreementName, now, - 1) :: Nil, now),
88 RolesSnapshot(List(), now),
89 OwnedResourcesSnapshot(List(), now)
93 def findUserStateAtEndOfBillingMonth(userId: String,
94 yearOfBillingMonth: Int,
96 userStateStore: UserStateStore,
97 resourceEventStore: ResourceEventStore,
98 policyStore: PolicyStore,
99 userCreationMillis: Long,
100 currentUserState: UserState,
101 zeroUserState: UserState,
102 defaultPolicy: DSLPolicy,
103 defaultResourcesMap: DSLResourcesMap,
104 accounting: Accounting,
105 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = {
107 val clog = ContextualLogger.fromOther(
110 "findUserStateAtEndOfBillingMonth(%s-%02d)", yearOfBillingMonth, billingMonth)
111 // val clog = new ContextualLogger(logger, "findUserStateAtEndOfBillingMonth(%s-%02d)", yearOfBillingMonth, billingMonth)
114 def doCompute: Maybe[UserState] = {
115 clog.debug("Computing full month billing")
116 doFullMonthlyBilling(
132 val billingMonthStartDateCalc = new DateCalculator(yearOfBillingMonth, billingMonth)
133 val userCreationDateCalc = new DateCalculator(userCreationMillis)
134 val billingMonthStartMillis = billingMonthStartDateCalc.toMillis
135 val billingMonthStopMillis = billingMonthStartDateCalc.copy.goEndOfThisMonth.toMillis
137 if(billingMonthStopMillis < userCreationMillis) {
138 // If the user did not exist for this billing month, piece of cake
139 clog.debug("User did not exist before %s. Returning %s", userCreationDateCalc, zeroUserState)
140 clog.endWith(Just(zeroUserState))
142 resourceEventStore.countOutOfSyncEventsForBillingPeriod(userId, billingMonthStartMillis, billingMonthStopMillis) match {
143 case Just(outOfSyncEventCount) ⇒
144 // Have out of sync, so must recompute
145 clog.debug("Found %s out of sync events, will have to (re)compute user state", outOfSyncEventCount)
146 clog.endWith(doCompute)
148 // No out of sync events, ask DB cache
149 userStateStore.findLatestUserStateForEndOfBillingMonth(userId, yearOfBillingMonth, billingMonth) match {
150 case just @ Just(userState) ⇒
152 clog.debug("Found from cache: %s", userState)
156 clog.debug("No user state found from cache, will have to (re)compute")
157 clog.endWith(doCompute)
158 case failed @ Failed(_, _) ⇒
159 clog.warn("Failure while quering cache for user state: %s", failed)
162 case failed @ Failed(_, _) ⇒
163 clog.warn("Failure while querying for out of sync events: %s", failed)
169 def doFullMonthlyBilling(userId: String,
170 yearOfBillingMonth: Int,
172 userStateStore: UserStateStore,
173 resourceEventStore: ResourceEventStore,
174 policyStore: PolicyStore,
175 userCreationMillis: Long,
176 currentUserState: UserState,
177 zeroUserState: UserState,
178 defaultPolicy: DSLPolicy,
179 defaultResourcesMap: DSLResourcesMap,
180 accounting: Accounting,
181 contextualLogger: Maybe[ContextualLogger] = NoVal): Maybe[UserState] = Maybe {
184 val billingMonthStartDateCalc = new DateCalculator(yearOfBillingMonth, billingMonth)
185 val billingMonthEndDateCalc = billingMonthStartDateCalc.copy.goEndOfThisMonth
186 val previousBillingMonthCalc = billingMonthStartDateCalc.copy.goPreviousMonth
187 val previousBillingMonth = previousBillingMonthCalc.getMonthOfYear
188 val yearOfPreviousBillingMonth = previousBillingMonthCalc.getYear
190 val clog = ContextualLogger.fromOther(
193 "doFullMonthlyBilling(%s-%02d)", yearOfBillingMonth, billingMonth)
196 val previousBillingMonthUserStateM = findUserStateAtEndOfBillingMonth(
198 yearOfPreviousBillingMonth,
199 previousBillingMonth,
212 previousBillingMonthUserStateM match {
214 null // not really... (must throw an exception here probably...)
215 case failed @ Failed(e, _) ⇒
217 case Just(startingUserState) ⇒
218 // This is the real deal
220 // This is a collection of all the latest resource events.
221 // We want these in order to correlate incoming resource events with their previous (in `occurredMillis` time)
223 // Will be updated on processing the next resource event.
224 val previousResourceEvents = startingUserState.latestResourceEvents.toMutableWorker
225 clog.debug("previousResourceEvents = %s", previousResourceEvents)
227 val billingMonthStartMillis = billingMonthStartDateCalc.toMillis
228 val billingMonthEndMillis = billingMonthEndDateCalc.toMillis
230 // Keep the working (current) user state. This will get updated as we proceed with billing for the month
231 // specified in the parameters.
232 var _workingUserState = startingUserState
234 // Prepare the implicit OFF resource events
235 val theImplicitOFFs = _workingUserState.implicitOFFs.toMutableWorker
236 clog.debug("theImplicitOFFs = %s", theImplicitOFFs)
239 * Finds the previous resource event by checking two possible sources: a) The implicit OFF resource events and
240 * b) the explicit previous resource events. If the event is found, it is removed from the respective source.
242 * If the event is not found, then this must be for a new resource instance.
243 * (and probably then some `zero` resource event must be implied as the previous one)
249 def findAndRemovePreviousResourceEvent(resource: String, instanceId: String): Maybe[ResourceEvent] = {
250 // implicit OFFs are checked first
251 theImplicitOFFs.findAndRemoveResourceEvent(resource, instanceId) match {
252 case just @ Just(_) ⇒
255 // explicit previous are checked second
256 previousResourceEvents.findAndRemoveResourceEvent(resource, instanceId) match {
257 case just @ Just(_) ⇒
267 def rcDebugInfo(rcEvent: ResourceEvent) = {
268 rcEvent.toDebugString(defaultResourcesMap, false)
271 // Find the actual resource events from DB
272 val allResourceEventsForMonth = resourceEventStore.findAllRelevantResourceEventsForBillingPeriod(
274 billingMonthStartMillis,
275 billingMonthEndMillis)
276 var _eventCounter = 0
278 clog.debug("resourceEventStore = %s".format(resourceEventStore))
279 clog.debug("Found %s resource events, starting processing...", allResourceEventsForMonth.size)
282 currentResourceEvent <- allResourceEventsForMonth
284 _eventCounter = _eventCounter + 1
285 val theResource = currentResourceEvent.resource
286 val theInstanceId = currentResourceEvent.instanceId
287 val theValue = currentResourceEvent.value
290 clog.debug("Processing %s", currentResourceEvent)
291 clog.debug("Friendlier %s", rcDebugInfo(currentResourceEvent))
294 if(previousResourceEvents.size > 0) {
295 clog.debug("%s previousResourceEvents", previousResourceEvents.size)
297 previousResourceEvents.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
300 if(theImplicitOFFs.size > 0) {
301 clog.debug("%s theImplicitOFFs", theImplicitOFFs.size)
303 theImplicitOFFs.foreach(ev ⇒ clog.debug("%s", rcDebugInfo(ev)))
307 // Ignore the event if it is not billable (but still record it in the "previous" stuff).
308 // But to make this decision, we need the cost policy.
309 val costPolicyM = currentResourceEvent.findCostPolicyM(defaultResourcesMap)
311 // We have a cost policy
312 case Just(costPolicy) ⇒
313 clog.debug("Cost policy: %s", costPolicy)
314 val isBillable = costPolicy.isBillableEventBasedOnValue(currentResourceEvent.value)
316 // The resource event is not billable
318 clog.debug("Ignoring not billable event (%s)", currentResourceEvent.beautifyValue(defaultResourcesMap))
320 // The resource event is billable
322 costPolicy.needsPreviousEventForCreditAndAmountCalculation match {
323 // We need the previous event to calculate credit & amount
325 val previousResourceEventM = findAndRemovePreviousResourceEvent(theResource, theInstanceId)
327 previousResourceEventM match {
328 // Found previous event
329 case Just(previousResourceEvent) ⇒
330 clog.debug("Previous %s", rcDebugInfo(previousResourceEvent))
332 // A. Compute new resource instance accumulating amount
333 // But first ask for the current one
334 val (previousAmountM, newAmount) = costPolicy.accumulatesAmount match {
335 // The amount for this resource is accumulating
337 val defaultInstanceAmount = costPolicy.getResourceInstanceInitialAmount
338 val previousAmount = _workingUserState.getResourceInstanceAmount(
341 defaultInstanceAmount)
343 val newAmount = costPolicy.computeNewAccumulatingAmount(previousAmount, theValue)
344 (Just(previousAmount), newAmount)
346 // The amount for this resource is not accumulating
348 val newAmount = costPolicy.computeNewAccumulatingAmount(-1.0, theValue)
352 // C. Compute new wallet entries
353 /*val walletEntriesM = accounting.computeWalletEntriesForAgreement(
355 _workingUserState.credits.creditAmount,
357 previousResourceEventM,
359 currentResourceEvent,
361 defaultPolicy.agreements.head)*/
363 // B. Compute new credit amount
365 // Did not find previous event.
367 // So it must be the first ever, in which case we assume a previous value of zero
369 // Could not find previous event
370 case failed@Failed(_, _) ⇒
374 // We do not need the previous event to calculate credit & amount
376 clog.debug("No need for previous event")
382 // After processing, all event, billable or not update the previous state
383 previousResourceEvents.updateResourceEvent(currentResourceEvent)
385 // We do not have a cost policy
387 // Now, this is a matter of politics: what do we do if no policy was found?
388 clog.error("No cost policy for %s", rcDebugInfo(currentResourceEvent))
390 // Could not retrieve cost policy
391 case failed @ Failed(e, m) ⇒
392 clog.error("Error obtaining cost policy for %s", rcDebugInfo(currentResourceEvent))
401 clog.endWith(_workingUserState)