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 gr.grnet.aquarium.util.json.{JsonHelpers, JsonSupport}
39 import net.liftweb.json.{JsonAST, Xml}
40 import gr.grnet.aquarium.logic.accounting.dsl.DSLAgreement
41 import com.ckkloverdos.maybe.{Failed, Maybe}
42 import gr.grnet.aquarium.logic.events.WalletEntry
43 import gr.grnet.aquarium.util.date.MutableDateCalc
47 * A comprehensive representation of the User's state.
49 * Note that it is made of autonomous parts that are actually data snapshots.
51 * The different snapshots need not agree on the snapshot time, ie. some state
52 * part may be stale, while other may be fresh.
54 * The user state is meant to be partially updated according to relevant events landing on Aquarium.
56 * @author Christos KK Loverdos <loverdos@gmail.com>
63 * When the user was created in the system (not Aquarium). We use this as a basis for billing periods. Set to
67 userCreationMillis: Long,
70 * Each time the user state is updated, this must be increased.
71 * The counter is used when accessing user state from the cache (user state store)
72 * in order to get the latest value for a particular billing period.
74 stateChangeCounter: Long,
77 * True iff this user state refers to a full billing period, that is a full billing month.
79 isFullBillingMonthState: Boolean,
82 * The full billing period for which this user state refers to.
83 * This is set when the user state refers to a full billing period (= month)
84 * and is used to cache the user state for subsequent queries.
86 theFullBillingMonth: BillingMonthInfo,
89 * If this is a state for a full billing month, then keep here the implicit OFF
90 * resource events or any other whose cost policy demands an implicit event at the end of the billing period.
92 * The use case is this: A VM may have been started (ON state) before the end of the billing period
93 * and ended (OFF state) after the beginning of the next billing period. In order to bill this, we must assume
94 * an implicit OFF even right at the end of the billing period and an implicit ON event with the beginning of the
95 * next billing period.
97 implicitlyTerminatedSnapshot: ImplicitlyIssuedResourceEventsSnapshot,
100 * So far computed wallet entries for the current billing month.
102 billingMonthWalletEntries: List[WalletEntry],
105 * Wallet entries that were computed for out of sync events.
106 * (for the current billing month ??)
108 outOfSyncWalletEntries: List[WalletEntry],
111 * The latest resource events per resource instance
113 latestResourceEventsSnapshot: LatestResourceEventsSnapshot,
116 * Counts the total number of resource events used to produce this user state for
117 * the billing period recorded by `billingPeriodSnapshot`
119 billingPeriodResourceEventsCounter: Long,
122 * The out of sync events used to produce this user state for
123 * the billing period recorded by `billingPeriodSnapshot`
125 billingPeriodOutOfSyncResourceEventsCounter: Long,
127 activeStateSnapshot: ActiveStateSnapshot,
128 creditsSnapshot: CreditSnapshot,
129 agreementsSnapshot: AgreementSnapshot,
130 rolesSnapshot: RolesSnapshot,
131 ownedResourcesSnapshot: OwnedResourcesSnapshot,
132 calculationReason: UserStateCalculationReason = NoSpecificCalculationReason,
133 totalEventsProcessedCounter: Long = 0L,
134 // The user state we used to compute this one. Normally the (cached)
135 // state at the beginning of the billing period.
136 parentUserStateId: Option[String] = None,
138 ) extends JsonSupport {
140 private[this] def _allSnapshots: List[Long] = {
142 activeStateSnapshot.snapshotTime,
143 creditsSnapshot.snapshotTime, agreementsSnapshot.snapshotTime, rolesSnapshot.snapshotTime,
144 ownedResourcesSnapshot.snapshotTime,
145 implicitlyTerminatedSnapshot.snapshotTime,
146 latestResourceEventsSnapshot.snapshotTime
150 def oldestSnapshotTime: Long = _allSnapshots min
152 def newestSnapshotTime: Long = _allSnapshots max
154 def idOpt: Option[String] = _id match {
160 // def userCreationDate = new Date(userCreationMillis)
162 // def userCreationFormatedDate = new MutableDateCalc(userCreationMillis).toString
164 def maybeDSLAgreement(at: Long): Maybe[DSLAgreement] = {
165 agreementsSnapshot match {
166 case snapshot @ AgreementSnapshot(data, _) ⇒
167 snapshot.getAgreement(at)
169 Failed(new Exception("No agreement snapshot found for user %s".format(userId)))
173 def findResourceInstanceSnapshot(resource: String, instanceId: String): Maybe[ResourceInstanceSnapshot] = {
174 ownedResourcesSnapshot.findResourceInstanceSnapshot(resource, instanceId)
177 def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
178 ownedResourcesSnapshot.getResourceInstanceAmount(resource, instanceId, defaultValue)
181 def copyForResourcesSnapshotUpdate(resource: String, // resource name
182 instanceId: String, // resource instance id
184 snapshotTime: Long): UserState = {
186 val (newResources, _, _) = ownedResourcesSnapshot.computeResourcesSnapshotUpdate(resource, instanceId, newAmount, snapshotTime)
189 ownedResourcesSnapshot = newResources,
190 stateChangeCounter = this.stateChangeCounter + 1)
193 def resourcesMap = ownedResourcesSnapshot.toResourcesMap
195 def safeCredits = creditsSnapshot match {
196 case c @ CreditSnapshot(_, _) ⇒ c
197 case _ ⇒ CreditSnapshot(0.0, 0)
203 def fromJson(json: String): UserState = {
204 JsonHelpers.jsonToObject[UserState](json)
207 def fromJValue(jsonAST: JsonAST.JValue): UserState = {
208 JsonHelpers.jValueToObject[UserState](jsonAST)
211 def fromBytes(bytes: Array[Byte]): UserState = {
212 JsonHelpers.jsonBytesToObject[UserState](bytes)
215 def fromXml(xml: String): UserState = {
216 fromJValue(Xml.toJson(scala.xml.XML.loadString(xml)))
220 final val _id = "_id"
221 final val userId = "userId"
225 final class BillingMonthInfo private(val year: Int,
227 val startMillis: Long,
228 val stopMillis: Long) extends Ordered[BillingMonthInfo] {
230 def previousMonth: BillingMonthInfo = {
231 BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goPreviousMonth)
234 def nextMonth: BillingMonthInfo = {
235 BillingMonthInfo.fromDateCalc(new MutableDateCalc(year, month).goNextMonth)
239 def compare(that: BillingMonthInfo) = {
240 val ds = this.startMillis - that.startMillis
241 if(ds < 0) -1 else if(ds == 0) 0 else 1
245 override def equals(any: Any) = any match {
246 case that: BillingMonthInfo ⇒
247 this.year == that.year && this.month == that.month // normally everything else MUST be the same by construction
252 override def hashCode() = {
256 override def toString = "%s-%02d".format(year, month)
259 object BillingMonthInfo {
260 def fromMillis(millis: Long): BillingMonthInfo = {
261 fromDateCalc(new MutableDateCalc(millis))
264 def fromDateCalc(mdc: MutableDateCalc): BillingMonthInfo = {
265 val year = mdc.getYear
266 val month = mdc.getMonthOfYear
267 val startMillis = mdc.goStartOfThisMonth.getMillis
268 val stopMillis = mdc.goEndOfThisMonth.getMillis // no need to `copy` here, since we are discarding `mdc`
270 new BillingMonthInfo(year, month, startMillis, stopMillis)
274 sealed trait UserStateCalculationReason {
276 * Return `true` if the result of the calculation should be stored back to the
277 * [[gr.grnet.aquarium.store.UserStateStore]].
280 def shouldStoreUserState: Boolean
282 def forPreviousBillingMonth: UserStateCalculationReason
286 * A calculation made for no specific reason. Can be for testing, for example.
289 case object NoSpecificCalculationReason extends UserStateCalculationReason {
290 def shouldStoreUserState = false
292 def forBillingMonthInfo(bmi: BillingMonthInfo) = this
294 def forPreviousBillingMonth = this
298 * An authoritative calculation for the billing period.
300 * This marks a state for caching.
302 * @param billingMonthInfo
304 case class MonthlyBillingCalculation(billingMonthInfo: BillingMonthInfo) extends UserStateCalculationReason {
305 def shouldStoreUserState = true
307 def forPreviousBillingMonth = MonthlyBillingCalculation(billingMonthInfo.previousMonth)
313 * @param forWhenMillis The time this calculation is for
315 case class GenericBillingCalculation(forWhenMillis: Long) extends UserStateCalculationReason {
316 def shouldStoreUserState = false
318 def forPreviousBillingMonth = this