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 gr.grnet.aquarium.store.ResourceEventStore
41 import com.ckkloverdos.maybe.{Failed, NoVal, Just, Maybe}
42 import gr.grnet.aquarium.logic.accounting.Accounting
43 import gr.grnet.aquarium.logic.events.ResourceEvent
44 import gr.grnet.aquarium.logic.accounting.dsl.{DSLCostPolicy, DSLPolicy, DSLAgreement}
45 import gr.grnet.aquarium.util.date.{TimeHelpers, DateCalculator}
47 sealed abstract class CalculationType(_name: String) {
52 * Normal calculations that are part of the bill generation procedure
54 case object PeriodicCalculation extends CalculationType("periodic")
57 * Adhoc calculations, e.g. when computing the state in realtime.
59 case object AdhocCalculation extends CalculationType("adhoc")
61 trait UserPolicyFinder {
62 def findUserPolicyAt(userId: String, whenMillis: Long): DSLPolicy
65 trait FullStateFinder {
66 def findFullState(userId: String, whenMillis: Long): Any
69 trait UserStateCache {
70 def findUserStateAtEndOfPeriod(userId: String, year: Int, month: Int): Maybe[UserState]
73 * Find the most up-to-date user state for the particular billing period.
75 def findLatestUserStateForBillingMonth(userId: String, yearOfBillingMonth: Int, billingMonth: Int): Maybe[UserState]
80 * @author Christos KK Loverdos <loverdos@gmail.com>
82 class UserStateComputations {
83 def createFirstUserState(userId: String, agreementName: String) = {
92 ActiveSuspendedSnapshot(false, now),
93 CreditSnapshot(0, now),
94 AgreementSnapshot(Agreement(agreementName, now, now) :: Nil, now),
95 RolesSnapshot(List(), now),
96 PaymentOrdersSnapshot(Nil, now),
97 OwnedGroupsSnapshot(Nil, now),
98 GroupMembershipsSnapshot(Nil, now),
99 OwnedResourcesSnapshot(List(), now)
104 * Get the user state as computed up to (and not including) the start of the new billing period.
106 * Always compute, taking into account any "out of sync" resource events
108 def computeUserStateAtStartOfBillingPeriod(billingYear: Int,
110 knownUserState: UserState,
111 accounting: Accounting): UserState = {
113 val billingDate = new DateCalculator(billingYear, billingMonth, 1)
114 val billingDateMillis = billingDate.toMillis
116 // if(billingDateMillis < knownUserState.startDateMillis) {
117 // val userId = knownUserState.userId
118 // val agreementName = knownUserState.agreement match {
119 // case null ⇒ "default"
120 // case agreement ⇒ agreement.data
122 // createFirstUserState(userId, agreementName)
124 // We really need to compute the user state here
126 // get all events that
133 * Find the previous resource event, if needed by the event's cost policy,
134 * in order to use it for any credit calculations.
136 def findPreviousRCEventOf(previousRCEventsMap: mutable.Map[ResourceEvent.FullResourceType, ResourceEvent],
137 rcEvent: ResourceEvent,
138 costPolicy: DSLCostPolicy): Maybe[ResourceEvent] = {
140 if(costPolicy.needsPreviousEventForCreditCalculation) {
141 // Get a previous resource only if this is needed by the policy
142 previousRCEventsMap.get(rcEvent.fullResourceInfo) match {
143 case Some(previousRCEvent) ⇒
144 Just(previousRCEvent)
146 queryForPreviousRCEvent(rcEvent)
149 // No need for previous event. Will return NoVal
157 def queryForPreviousRCEvent(rcEvent: ResourceEvent): Maybe[ResourceEvent] = {
161 def updatePreviousRCEventWith(previousRCEventsMap: mutable.Map[ResourceEvent.FullResourceType, ResourceEvent],
162 newRCEvent: ResourceEvent): Unit = {
163 previousRCEventsMap(newRCEvent.fullResourceInfo) = newRCEvent
167 * Do a full month billing.
169 * Takes into account "out of sync events".
172 def computeFullMonthlyBilling(yearOfBillingMonth: Int,
175 policyFinder: UserPolicyFinder,
176 fullStateFinder: FullStateFinder,
177 userStateCache: UserStateCache,
178 timeUnitInMillis: Long,
179 rcEventStore: ResourceEventStore,
180 currentUserState: UserState,
181 otherStuff: Traversable[Any],
182 defaultPolicy: DSLPolicy, // Policy.policy
183 accounting: Accounting): Maybe[UserState] = Maybe {
185 val billingMonthStartDate = new DateCalculator(yearOfBillingMonth, billingMonth, 1)
186 val prevBillingMonthStartDate = billingMonthStartDate.previousMonth
187 val yearOfPrevBillingMonth = prevBillingMonthStartDate.year
188 val prevBillingMonth = prevBillingMonthStartDate.monthOfYear
190 // Check if this value is already cached and valid, otherwise compute the value
191 // TODO : cache it in case of new computation
192 val cachedStartUserStateM = userStateCache.findLatestUserStateForBillingMonth(
194 yearOfPrevBillingMonth,
197 val (previousStartUserState, newStartUserState) = cachedStartUserStateM match {
198 case Just(cachedStartUserState) ⇒
199 // So, we do have a cached user state but must check if this is still valid
201 // Check how many resource events were used to produce this user state
202 val cachedHowmanyRCEvents = cachedStartUserState.resourceEventsCounter
204 // Ask resource event store to see if we had any "out of sync" events for the particular (== previous)
206 val prevHowmanyOutOfSyncRCEvents = rcEventStore.countOutOfSyncEventsForBillingMonth(
208 yearOfPrevBillingMonth,
211 val recomputedStartUserState = if(prevHowmanyOutOfSyncRCEvents == 0) {
212 // This is good, there were no "out of sync" resource events, so we can use the cached value
215 // Oops, there are "out of sync" resource event. Must compute (potentially recursively)
216 computeUserStateAtStartOfBillingPeriod(
217 yearOfPrevBillingMonth,
219 cachedStartUserState,
223 (cachedStartUserState, recomputedStartUserState)
225 // We do not even have a cached value, so computate one!
226 val recomputedStartUserState = computeUserStateAtStartOfBillingPeriod(
227 yearOfPrevBillingMonth,
232 (recomputedStartUserState, recomputedStartUserState)
234 throw new Exception(m, e)
237 // OK. Now that we have a user state to start with (= start of billing period reference point),
238 // let us deal with the events themselves.
239 val billingStartMillis = billingMonthStartDate.toMillis
240 val billingStopMillis = billingMonthStartDate.endOfThisMonth.toMillis
241 val allBillingPeriodRelevantRCEvents = rcEventStore.findAllRelevantResourceEventsForBillingPeriod(userId, billingStartMillis, billingStopMillis)
243 type FullResourceType = ResourceEvent.FullResourceType
244 val previousRCEventsMap = mutable.Map[FullResourceType, ResourceEvent]()
245 val impliedRCEventsMap = mutable.Map[FullResourceType, ResourceEvent]() // those which do not exists but are
246 // implied in order to do billing calculations (e.g. the "off" vmtime resource event)
247 var workingUserState = newStartUserState
249 for(newRCEvent <- allBillingPeriodRelevantRCEvents) {
250 // We need to do these kinds of calculations:
251 // 1. Credit state calculations
252 // 2. Resource state calculations
254 // How credits are computed:
255 // - "onoff" events (think "vmtime"):
256 // - need to be considered in on/off pairs
257 // - just use the time difference of this event to the previous one for the credit computation
258 // - "discrete" events (think "bandwidth"):
259 // - just use their value, which is a difference already for the credit computation
260 // - "continuous" events (think "bandwidth"):
261 // - need the previous absolute value
262 // - need the time difference of this event to the previous one
263 // - use both the above (previous absolute value, time difference) for the credit computation
265 // BUT ALL THE ABOVE SHOULD NOT BE CONSIDERED HERE; RATHER THEY ARE POLYMORPHIC BEHAVIOURS
268 // A. The previous event
271 // The DSLCostPolicy for the resource does not change, so it is safe to use the default DSLPolicy to obtain it.
272 val costPolicyM = newRCEvent.findCostPolicy(defaultPolicy)
274 case Just(costPolicy) ⇒
275 val previousRCEventM = findPreviousRCEventOf(previousRCEventsMap, newRCEvent, costPolicy)
276 val previousRCEventValueM = previousRCEventM.map(_.value)
277 // val previousRCInstanceAmount = workingUserState.ownedResources.
279 // 1. Update resource state
280 val newRCInstanceAmountM = costPolicy.computeNewResourceInstanceAmount(previousRCEventValueM, newRCEvent.value)
281 newRCInstanceAmountM match {
282 case Just(newRCInstanceAmount) ⇒
283 workingUserState.ownedResources.addOrUpdateResourceSnapshot(
285 newRCEvent.instanceId,
287 TimeHelpers.nowMillis)
290 case failed @ Failed(_, _) ⇒
294 // 2. Update credit state
296 // 3. Calc wallet entries
300 case failed @ Failed(_, _) ⇒
305 updatePreviousRCEventWith(previousRCEventsMap, newRCEvent)
306 } // for(newResourceEvent <- allBillingPeriodRelevantRCEvents)
314 * Runs the billing algorithm on the specified period.
315 * By default, a billing period is monthly.
316 * The start of the billing period is midnight of the first day of the month we compute the bill for.
319 def doPartialMonthlyBilling(startBillingYear: Int,
320 startBillingMonth: Int,
321 stopBillingMillis: Long,
323 policyFinder: UserPolicyFinder,
324 fullStateFinder: FullStateFinder,
325 userStateFinder: UserStateCache,
326 timeUnitInMillis: Long,
327 rcEventStore: ResourceEventStore,
328 currentUserState: UserState,
329 otherStuff: Traversable[Any],
330 accounting: Accounting): Maybe[UserState] = Maybe {
333 null.asInstanceOf[UserState]
337 object DefaultUserStateComputations extends UserStateComputations