WIP Resource event handling
[aquarium] / src / main / scala / gr / grnet / aquarium / computation / state / UserState.scala
1 /*
2  * Copyright 2011-2012 GRNET S.A. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or
5  * without modification, are permitted provided that the following
6  * conditions are met:
7  *
8  *   1. Redistributions of source code must retain the above
9  *      copyright notice, this list of conditions and the following
10  *      disclaimer.
11  *
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.
16  *
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.
29  *
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.
34  */
35
36 package gr.grnet.aquarium.computation.state
37
38 import gr.grnet.aquarium.converter.{JsonTextFormat, StdConverters}
39 import gr.grnet.aquarium.event.model.NewWalletEntry
40 import gr.grnet.aquarium.util.json.JsonSupport
41 import gr.grnet.aquarium.logic.accounting.dsl.DSLAgreement
42 import gr.grnet.aquarium.computation.reason.{NoSpecificChangeReason, UserStateChangeReason, InitialUserStateSetup}
43 import gr.grnet.aquarium.event.model.resource.ResourceEventModel
44 import gr.grnet.aquarium.computation.BillingMonthInfo
45 import gr.grnet.aquarium.computation.parts.RoleHistory
46 import gr.grnet.aquarium.computation.state.parts.{OwnedResourcesMap, ResourceInstanceSnapshot, OwnedResourcesSnapshot, AgreementHistory, ImplicitlyIssuedResourceEventsSnapshot, LatestResourceEventsSnapshot}
47
48 /**
49  * A comprehensive representation of the User's state.
50  *
51  * Note that it is made of autonomous parts that are actually parts snapshots.
52  *
53  * The different snapshots need not agree on the snapshot time, ie. some state
54  * part may be stale, while other may be fresh.
55  *
56  * The user state is meant to be partially updated according to relevant events landing on Aquarium.
57  *
58  * @define communicatedByIM
59  *         This is communicated to Aquarium from the `IM` system.
60  *
61  *
62  * @param userID
63  * The user ID. $communicatedByIM
64  * @param userCreationMillis
65  * When the user was created.
66  * $communicatedByIM
67  * Set to zero if unknown.
68  * @param stateChangeCounter
69  * @param isFullBillingMonthState
70  * @param theFullBillingMonth
71  * @param implicitlyIssuedSnapshot
72  * @param latestResourceEventsSnapshot
73  * @param billingPeriodOutOfSyncResourceEventsCounter
74  * @param agreementHistory
75  * @param ownedResourcesSnapshot
76  * @param newWalletEntries
77  * The wallet entries computed. Not all user states need to holds wallet entries,
78  * only those that refer to billing periods (end of billing period).
79  * @param lastChangeReason
80  * The [[gr.grnet.aquarium.computation.reason.UserStateChangeReason]] for which the usr state has changed.
81  * @param parentUserStateIDInStore
82  * The `ID` of the parent state. The parent state is the one used as a reference point in order to calculate
83  * this user state.
84  * @param _id
85  * The unique `ID` given by the store.
86  *
87  * @author Christos KK Loverdos <loverdos@gmail.com>
88  */
89 case class UserState(
90     isInitial: Boolean,
91     userID: String,
92
93     userCreationMillis: Long,
94
95     /**
96      * Each time the user state is updated, this must be increased.
97      * The counter is used when accessing user state from the cache (user state store)
98      * in order to get the latest value for a particular billing period.
99      */
100     stateChangeCounter: Long,
101
102     /**
103      * True iff this user state refers to a full billing period, that is a full billing month.
104      */
105     isFullBillingMonthState: Boolean,
106
107     /**
108      * The full billing period for which this user state refers to.
109      * This is set when the user state refers to a full billing period (= month)
110      * and is used to cache the user state for subsequent queries.
111      */
112     theFullBillingMonth: Option[BillingMonthInfo],
113
114     /**
115      * If this is a state for a full billing month, then keep here the implicit OFF
116      * resource events or any other whose cost policy demands an implicit event at the end of the billing period.
117      *
118      * The use case is this: A VM may have been started (ON state) before the end of the billing period
119      * and ended (OFF state) after the beginning of the next billing period. In order to bill this, we must assume
120      * an implicit OFF even right at the end of the billing period and an implicit ON event with the beginning of the
121      * next billing period.
122      */
123     implicitlyIssuedSnapshot: ImplicitlyIssuedResourceEventsSnapshot,
124
125     /**
126      * The latest (previous) resource events per resource instance.
127      */
128     latestResourceEventsSnapshot: LatestResourceEventsSnapshot,
129
130     /**
131      * The out of sync events used to produce this user state for
132      * the billing period recorded by `billingPeriodSnapshot`
133      */
134     billingPeriodOutOfSyncResourceEventsCounter: Long,
135
136     totalCredits: Double,
137
138     roleHistory: RoleHistory,
139
140     agreementHistory: AgreementHistory,
141
142     ownedResourcesSnapshot: OwnedResourcesSnapshot,
143
144     newWalletEntries: List[NewWalletEntry],
145
146     occurredMillis: Long, // When this user state was computed
147
148     // The last known change reason for this userState
149     lastChangeReason: UserStateChangeReason = NoSpecificChangeReason(),
150     // The user state we used to compute this one. Normally the (cached)
151     // state at the beginning of the billing period.
152     parentUserStateIDInStore: Option[String] = None,
153     _id: String = null
154 ) extends JsonSupport {
155
156   def idInStore: Option[String] = _id match {
157     case null ⇒ None
158     case _id ⇒ Some(_id.toString)
159   }
160
161   //  def userCreationDate = new Date(userCreationMillis)
162   //
163   //  def userCreationFormatedDate = new MutableDateCalc(userCreationMillis).toString
164
165   def findDSLAgreementForTime(at: Long): Option[DSLAgreement] = {
166     agreementHistory.findForTime(at)
167   }
168
169   def findResourceInstanceSnapshot(resource: String, instanceId: String): Option[ResourceInstanceSnapshot] = {
170     ownedResourcesSnapshot.findResourceInstanceSnapshot(resource, instanceId)
171   }
172
173   def getResourceInstanceAmount(resource: String, instanceId: String, defaultValue: Double): Double = {
174     ownedResourcesSnapshot.getResourceInstanceAmount(resource, instanceId, defaultValue)
175   }
176
177   def newWithResourcesSnapshotUpdate(resource: String, // resource name
178                                      instanceId: String, // resource instance id
179                                      newAmount: Double,
180                                      snapshotTime: Long): UserState = {
181
182     val (newResources, _, _) =
183       ownedResourcesSnapshot.computeResourcesSnapshotUpdate(resource, instanceId, newAmount, snapshotTime)
184
185     this.copy(
186       isInitial = false,
187       ownedResourcesSnapshot = newResources,
188       stateChangeCounter = this.stateChangeCounter + 1
189     )
190   }
191
192   def newWithChangeReason(changeReason: UserStateChangeReason) = {
193     this.copy(
194       isInitial = false,
195       lastChangeReason = changeReason,
196       stateChangeCounter = this.stateChangeCounter + 1
197     )
198   }
199
200   def newWithRoleHistory(newRoleHistory: RoleHistory, changeReason: UserStateChangeReason) = {
201     // FIXME: Also update agreement
202     this.copy(
203       isInitial = false,
204       stateChangeCounter = this.stateChangeCounter + 1,
205       roleHistory = newRoleHistory,
206       lastChangeReason = changeReason
207     )
208   }
209
210   def resourcesMap: OwnedResourcesMap = {
211     ownedResourcesSnapshot.toResourcesMap
212   }
213
214   def findLatestResourceEvent: Option[ResourceEventModel] = {
215     latestResourceEventsSnapshot.findTheLatest
216   }
217
218   def findLatestResourceEventID: Option[String] = {
219     latestResourceEventsSnapshot.findTheLatestID
220   }
221
222   //  def toShortString = "UserState(%s, %s, %s, %s, %s)".format(
223   //    userId,
224   //    _id,
225   //    parentUserStateId,
226   //    totalEventsProcessedCounter,
227   //    calculationReason)
228 }
229
230 object UserState {
231   def fromJson(json: String): UserState = {
232     StdConverters.AllConverters.convertEx[UserState](JsonTextFormat(json))
233   }
234
235   object JsonNames {
236     final val _id = "_id"
237     final val userID = "userID"
238     final val isFullBillingMonthState = "isFullBillingMonthState"
239     final val occurredMillis = "occurredMillis"
240     final val theFullBillingMonth_year  = "theFullBillingMonth.year"  // FQN
241     final val theFullBillingMonth_month = "theFullBillingMonth.month" // FQN
242
243     object theFullBillingMonth {
244       final val year = "year"
245       final val month = "month"
246     }
247
248   }
249
250   def createInitialUserState(
251       userID: String,
252       userCreationMillis: Long,
253       occurredMillis: Long,
254       totalCredits: Double,
255       initialRole: String,
256       initialAgreement: String,
257       calculationReason: UserStateChangeReason = InitialUserStateSetup(None)
258   ) = {
259
260     UserState(
261       true,
262       userID,
263       userCreationMillis,
264       0L,
265       false,
266       None,
267       ImplicitlyIssuedResourceEventsSnapshot.Empty,
268       LatestResourceEventsSnapshot.Empty,
269       0L,
270       totalCredits,
271       RoleHistory.initial(initialRole, userCreationMillis),
272       AgreementHistory.initial(initialAgreement, userCreationMillis),
273       OwnedResourcesSnapshot.Empty,
274       Nil,
275       occurredMillis,
276       calculationReason
277     )
278   }
279
280   def createInitialUserStateFromBootstrap(
281       usb: UserStateBootstrap,
282       occurredMillis: Long,
283       calculationReason: UserStateChangeReason
284   ): UserState = {
285
286     createInitialUserState(
287       usb.userID,
288       usb.userCreationMillis,
289       occurredMillis,
290       usb.initialCredits,
291       usb.initialRole,
292       usb.initialAgreement,
293       calculationReason
294     )
295   }
296 }