root / src / main / scala / gr / grnet / aquarium / logic / accounting / Accounting.scala @ 28b43673
History | View | Annotate | Download (9.3 kB)
1 |
/* |
---|---|
2 |
* Copyright 2011 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.logic.accounting |
37 |
|
38 |
import dsl._ |
39 |
import gr.grnet.aquarium.logic.events.{WalletEntry, ResourceEvent} |
40 |
import collection.immutable.SortedMap |
41 |
import com.ckkloverdos.maybe.{Maybe, Failed, Just} |
42 |
import java.util.Date |
43 |
import gr.grnet.aquarium.util.{CryptoUtils, Loggable} |
44 |
|
45 |
/** |
46 |
* Methods for converting accounting events to wallet entries. |
47 |
* |
48 |
* @author Georgios Gousios <gousiosg@gmail.com> |
49 |
*/ |
50 |
trait Accounting extends DSLUtils with Loggable { |
51 |
|
52 |
/** |
53 |
* Creates a list of wallet entries by applying the agreement provisions on |
54 |
* the resource state. |
55 |
* |
56 |
* @param ev The resource event to create charges for |
57 |
* @param agreement The agreement applicable to the user mentioned in the event |
58 |
* @param resState The current state of the resource |
59 |
* @param lastUpdate The last time the resource state was updated |
60 |
*/ |
61 |
def chargeEvent(ev: ResourceEvent, |
62 |
agreement: DSLAgreement, |
63 |
resState: Float, |
64 |
lastUpdate: Date, |
65 |
related: List[WalletEntry]): |
66 |
Maybe[List[WalletEntry]] = { |
67 |
|
68 |
assert(lastUpdate.getTime <= ev.occurredMillis) |
69 |
|
70 |
if (!ev.validate()) |
71 |
return Failed(new AccountingException("Event not valid")) |
72 |
|
73 |
val resource = Policy.policy.findResource(ev.resource) match { |
74 |
case Some(x) => x |
75 |
case None => return Failed( |
76 |
new AccountingException("No resource [%s]".format(ev.resource))) |
77 |
} |
78 |
|
79 |
val amount = resource.costpolicy match { |
80 |
case ContinuousCostPolicy => resState.asInstanceOf[Float] |
81 |
case DiscreteCostPolicy => ev.value |
82 |
case OnOffCostPolicy => |
83 |
OnOffPolicyResourceState(resState) match { |
84 |
case OnResourceState => |
85 |
OnOffPolicyResourceState(ev.value) match { |
86 |
case OnResourceState => |
87 |
return Failed(new AccountingException(("Resource state " + |
88 |
"transition error(was:%s, now:%s)").format(OnResourceState, |
89 |
OnResourceState))) |
90 |
case OffResourceState => 0 |
91 |
} |
92 |
case OffResourceState => |
93 |
OnOffPolicyResourceState(ev.value) match { |
94 |
case OnResourceState => 1 |
95 |
case OffResourceState => |
96 |
return Failed(new AccountingException(("Resource state " + |
97 |
"transition error(was:%s, now:%s)").format(OffResourceState, |
98 |
OffResourceState))) |
99 |
} |
100 |
} |
101 |
} |
102 |
|
103 |
// We don't do strict checking for all cases for OnOffPolicies as |
104 |
// above, since this point won't be reached in case of error. |
105 |
val isFinal = resource.costpolicy match { |
106 |
case OnOffCostPolicy => |
107 |
OnOffPolicyResourceState(resState) match { |
108 |
case OnResourceState => false |
109 |
case OffResourceState => true |
110 |
} |
111 |
case _ => true |
112 |
} |
113 |
|
114 |
val ts = resource.costpolicy match { |
115 |
case DiscreteCostPolicy => Timeslot(new Date(ev.occurredMillis), |
116 |
new Date(ev.occurredMillis + 1)) |
117 |
case _ => Timeslot(lastUpdate, new Date(ev.occurredMillis)) |
118 |
} |
119 |
|
120 |
val chargeChunks = calcChangeChunks(agreement, amount, resource, ts) |
121 |
|
122 |
val timeReceived = System.currentTimeMillis |
123 |
|
124 |
val rel = related.map{x => x.sourceEventIDs}.flatten ++ List(ev.id) |
125 |
|
126 |
val entries = chargeChunks.map { |
127 |
c => |
128 |
WalletEntry( |
129 |
id = CryptoUtils.sha1(c.id), |
130 |
occurredMillis = ev.occurredMillis, |
131 |
receivedMillis = timeReceived, |
132 |
sourceEventIDs = rel, |
133 |
value = c.cost, |
134 |
reason = c.reason, |
135 |
userId = ev.userId, |
136 |
resource = ev.resource, |
137 |
instanceId = ev.getInstanceId, |
138 |
finalized = isFinal |
139 |
) |
140 |
} |
141 |
Just(entries) |
142 |
} |
143 |
|
144 |
def calcChangeChunks(agr: DSLAgreement, volume: Float, |
145 |
res: DSLResource, t: Timeslot): List[ChargeChunk] = { |
146 |
|
147 |
val alg = resolveEffectiveAlgorithmsForTimeslot(t, agr) |
148 |
val pri = resolveEffectivePricelistsForTimeslot(t, agr) |
149 |
val chunks = splitChargeChunks(alg, pri) |
150 |
val algChunked = chunks._1 |
151 |
val priChunked = chunks._2 |
152 |
|
153 |
assert(algChunked.size == priChunked.size) |
154 |
|
155 |
res.costpolicy match { |
156 |
case DiscreteCostPolicy => calcChargeChunksDiscrete(algChunked, priChunked, volume, res) |
157 |
case _ => calcChargeChunksContinuous(algChunked, priChunked, volume, res) |
158 |
} |
159 |
} |
160 |
|
161 |
private[logic] |
162 |
def calcChargeChunksDiscrete(algChunked: Map[Timeslot, DSLAlgorithm], |
163 |
priChunked: Map[Timeslot, DSLPriceList], |
164 |
volume: Float, res: DSLResource): List[ChargeChunk] = { |
165 |
assert(algChunked.size == 1) |
166 |
assert(priChunked.size == 1) |
167 |
assert(algChunked.keySet.head.compare(priChunked.keySet.head) == 0) |
168 |
|
169 |
List(ChargeChunk(volume, |
170 |
algChunked.valuesIterator.next.algorithms.getOrElse(res, ""), |
171 |
priChunked.valuesIterator.next.prices.getOrElse(res, 0F), |
172 |
algChunked.keySet.head, res)) |
173 |
} |
174 |
|
175 |
private[logic] |
176 |
def calcChargeChunksContinuous(algChunked: Map[Timeslot, DSLAlgorithm], |
177 |
priChunked: Map[Timeslot, DSLPriceList], |
178 |
volume: Float, res: DSLResource): List[ChargeChunk] = { |
179 |
algChunked.keySet.map { |
180 |
x => |
181 |
ChargeChunk(volume, |
182 |
algChunked.get(x).get.algorithms.getOrElse(res, ""), |
183 |
priChunked.get(x).get.prices.getOrElse(res, 0F), x, res) |
184 |
}.toList |
185 |
} |
186 |
|
187 |
/** |
188 |
* Align charge timeslots between algorithms and pricelists. As algorithm |
189 |
* and pricelists can have different effectivity periods, this method |
190 |
* examines them and splits them as necessary. |
191 |
*/ |
192 |
private[logic] def splitChargeChunks(alg: SortedMap[Timeslot, DSLAlgorithm], |
193 |
price: SortedMap[Timeslot, DSLPriceList]) : |
194 |
(Map[Timeslot, DSLAlgorithm], Map[Timeslot, DSLPriceList]) = { |
195 |
|
196 |
val zipped = alg.keySet.zip(price.keySet) |
197 |
|
198 |
zipped.find(p => !p._1.equals(p._2)) match { |
199 |
case None => (alg, price) |
200 |
case Some(x) => |
201 |
val algTimeslot = x._1 |
202 |
val priTimeslot = x._2 |
203 |
|
204 |
assert(algTimeslot.from == priTimeslot.from) |
205 |
|
206 |
if (algTimeslot.endsAfter(priTimeslot)) { |
207 |
val slices = algTimeslot.slice(priTimeslot.to) |
208 |
val algo = alg.get(algTimeslot).get |
209 |
val newalg = alg - algTimeslot ++ Map(slices.apply(0) -> algo) ++ Map(slices.apply(1) -> algo) |
210 |
splitChargeChunks(newalg, price) |
211 |
} |
212 |
else { |
213 |
val slices = priTimeslot.slice(priTimeslot.to) |
214 |
val pl = price.get(priTimeslot).get |
215 |
val newPrice = price - priTimeslot ++ Map(slices.apply(0) -> pl) ++ Map(slices.apply(1) -> pl) |
216 |
splitChargeChunks(alg, newPrice) |
217 |
} |
218 |
} |
219 |
} |
220 |
} |
221 |
|
222 |
case class ChargeChunk(value: Float, algorithm: String, |
223 |
price: Float, when: Timeslot, |
224 |
resource: DSLResource) { |
225 |
assert(value > 0) |
226 |
assert(!algorithm.isEmpty) |
227 |
assert(resource != null) |
228 |
|
229 |
def cost(): Float = |
230 |
//TODO: Apply the algorithm, when we start parsing it |
231 |
resource.costpolicy match { |
232 |
case DiscreteCostPolicy => |
233 |
value * price |
234 |
case _ => |
235 |
value * price * when.hours |
236 |
} |
237 |
|
238 |
def reason(): String = |
239 |
resource.costpolicy match { |
240 |
case DiscreteCostPolicy => |
241 |
"%f %s at %s @ %f/%s".format(value, resource.unit, when.from, price, |
242 |
resource.unit) |
243 |
case ContinuousCostPolicy => |
244 |
"%f %s of %s from %s to %s @ %f/%s".format(value, resource.unit, |
245 |
resource.name, when.from, when.to, price, resource.unit) |
246 |
case OnOffCostPolicy => |
247 |
"%f %s of %s from %s to %s @ %f/%s".format(when.hours, resource.unit, |
248 |
resource.name, when.from, when.to, price, resource.unit) |
249 |
} |
250 |
|
251 |
def id(): String = |
252 |
CryptoUtils.sha1("%f%s%f%s%s%d".format(value, algorithm, price, when.toString, |
253 |
resource.name, System.currentTimeMillis())) |
254 |
} |
255 |
|
256 |
/** An exception raised when something goes wrong with accounting */ |
257 |
class AccountingException(msg: String) extends Exception(msg) |