/
Propius.pm6
333 lines (297 loc) · 10.1 KB
/
Propius.pm6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#!/usr/bin/env perl6
unit module Propius;
use OO::Monitors;
use TimeUnit;
use Propius::Linked;
#|[Role of time provider.
#
#That provider will use to retrieve current time in seconds.
#You can use custom implementation for testing of time-based
#caches for example.]
role Ticker {
#|Getter of current time in seconds.
method now( --> Int:D) { ... };
}
#|[Default implementation of Ticker.
#
#Uses current system time.]
my class DateTimeTicker does Ticker {
#|Getter of current time as system time in seconds.
method now() {
return DateTime.now.posix;
}
}
#|[Exception witch will be thrown in case provider loader
#return not defined value.]
class X::Propius::LoadingFail {
has $.key;
method message() {
"Specified loader return type object instead of object for key $!key";
}
}
#|Amount of reads we can do without cleanup.
my constant READS_MAX = 20;
#|[Reason of removing some element from a cache.
#
#Expired - in case a value expired;
#Explicit - in case user removed value himself;
#Replaced - in case user overwrite value himself;
#Size - in case when max capacity is reached.]
enum RemoveCause <Expired Explicit Replaced Size>;
#|Name of user action with data in a cache.
my enum ActionType <Access Write>;
#|[Internal representation of value in cache.
#
#Contains key-value pair, times of last actions and links
#to linked chain for each actions type.]
my class ValueStore {
has $.key;
has $.value is rw;
has Propius::Linked::Node %.nodes{ActionType};
has Int %.last-action-at{ActionType};
#|[Constructor.
#
#:$key! - key of stored value;
#:$value! - stored value;
#:@types! - list of ActionType. Times of last actions and linked chains
#will be computed only for that actions.]
multi method new(:$key!, :$value!, :@types!) {
my $blessed = self.new(:$key, :$value);
for @types -> $type {
$blessed.nodes{$type} = Propius::Linked::Node.new: value => $blessed;
}
$blessed;
}
#|[Move chain link to its head.
#
#@types - list of ActionType for witch have to move;
#%chains - Hash of ActionType -> Linked::Chain - chains for each ActionType;
#$now - current time in seconds to save.]
method move-to-head-for(@types, Propius::Linked::Chain %chains, Int $now) {
for %!nodes.keys.grep: * ~~ any(@types) {
%chains{$_}.move-to-head(%!nodes{$_});
%!last-action-at{$_} = $now;
}
}
#|Remove that value from all chains.
method remove-nodes() {
.remove() for %!nodes.values;
}
#|[Return time of last action with the value.
#
#$type - ActionType for retrieving time.]
method last-at(ActionType $type) {
%!last-action-at{$type};
}
}
#|[Cache with loader and eviction by time.
#
#The cache can use object keys. If you want that you have to
#control .WITCH method if keys.]
my monitor EvictionBasedCache {
has &!loader;
has &!removal-listener;
has Any %!expire-after-sec{ActionType};
has Ticker $ticker;
has $!size;
has ValueStore %!store{Any};
has Propius::Linked::Chain %!chains{ActionType};
has $!reads-wo-clean;
submethod BUILD(
:&!loader! where .signature ~~ :(:$key),
:&!removal-listener where .signature ~~ :(:$key, :$value, :$cause) = {},
:%!expire-after-sec = :{(Access) => Inf, (Write) => Inf},
Ticker :$!ticker = DateTimeTicker.new,
:$!size = Inf) {
%!chains{Access} = Propius::Linked::Chain.new;
if %!expire-after-sec{Write} !=== Inf {
%!chains{Write} = Propius::Linked::Chain.new;
}
$!reads-wo-clean = 0;
}
#|[Retrieve value by key.
#
#If there is no value for specified key then loader with be
#used to produce the new value]
method get(Any:D $key) {
my $value = self!retrieve($key);
with $value {
return $value.value;
} else {
self.put(:$key, :&!loader);
return %!store{$key}.value;
}
}
#|[Retrieve value by key only if it exists.
#
#If there is no value for specified key then Any will be returned.]
method get-if-exists(Any:D $key) {
with self!retrieve($key) { .value }
else { Any }
}
#|[Store a value in cache.
#
#It will rewrite any cached value for specified key. In that case
#removal-listener will be called with old value cause Replaced.
#
#In case of cache already reached max capacity value which has not
#been used for a longest time will be removed. In that case
#removal-listener will be called with old value cause Size.]
multi method put(Any:D :$key, Any:D :$value) {
$.clean-up();
my $previous = %!store{$key};
my $move;
with $previous {
self!publish($key, $previous.value, Replaced);
$previous.value = $value;
$move = $previous;
} else {
my $wrap = self!wrap-value($key, $value);
%!store{$key} = $wrap;
$move = $wrap;
}
$move.move-to-head-for(ActionType::.values, %!chains, $!ticker.now);
}
#|[Store a value in cache with specified loader.
#
#It will rewrite any cached value for specified key. In that case
#removal-listener will be called with old value cause Replaced.
#
#In case of cache already reached max capacity value which has not
#been used for a longest time will be removed. In that case
#removal-listener will be called with old value cause Size.]
multi method put(Any:D :$key, :&loader! where .signature ~~ :(:$key)) {
self.put(:$key, value => self!load($key, &loader))
}
#|[Mark value for specified key as invalidate.
#
#The value will be removed and removal-listener will be called with
#old value cause Explicit.]
method invalidate(Any:D $key) {
self!remove($key, Explicit);
}
#|[Mark values for specified keys as invalidate.
#
#The values will be removed and removal-listener will be called for
#each with old values cause Explicit.]
multi method invalidateAll(List:D @keys) {
self.invalidate($_) for @keys;
}
#|[Mark all values in cache as invalidate.
#
#The values will be removed and removal-listener will be called for
#each with old values cause Explicit.]
multi method invalidateAll() {
self.invalidateAll(%!store.keys);
}
#|Return amount of values already stored in the cache.
method elems() {
%!store.elems;
}
#|[Return keys and values stored in cache as Hash.
#
#This is a copy of values. Any modification of returned cache
#will no have an effect on values in the store.]
method hash() {
my %copy{Any};
for %!store.kv -> $key, $value {
%copy{$key} = $value.value;
}
return %copy;
}
#|[Clean evicted values from cache.
#
#This method may be invoked directly by user.
#The method invoked on each write operation and ones for several read operation
#if there was no write operation recently.
#
#It means that evicted values will be removed on just in time of its eviction.
#This is done for the purpose of optimisation - is it not requires special thread
#for checking an eviction. If it is issue for you then you can call it method yourself
#by some scheduled Promise for example.]
method clean-up() {
$!reads-wo-clean = 0;
while $.elems >= $!size {
self!remove(%!chains{Access}.last().value.key, Size);
}
my $now = $!ticker.now;
for %!chains.kv -> $type, $chain {
my $life-time = %!expire-after-sec{$type};
next if $life-time === Inf;
my $wrap = $chain.last.value;
while $wrap.DEFINITE && $wrap.last-at($type) + $life-time <= $now {
self!remove($wrap.key, Expired);
$wrap = $chain.last.value;
}
}
}
#|Retrieve value from cache if it exists.
method !retrieve($key) {
my $value = %!store{$key};
with $value {
++$!reads-wo-clean;
$.clean-up if $!reads-wo-clean >= READS_MAX;
$value.move-to-head-for((Access,), %!chains, $!ticker.now);
return $value;
} else {
return Any;
}
}
#|Wrap key and value into internal representation of value (ValueStore).
method !wrap-value($key, $value) {
ValueStore.new: :$key, :$value, types => %!chains.keys;
}
#|Compute the new value by specified loader.
method !load($key, &loader) {
my $value = self!invoke-with-args((:$key), &loader);
fail X::Propius::LoadingFail.new(:$key) without $value;
$value;
}
#|Call removal-listener about removed value.
method !publish($key, $value, RemoveCause $cause) {
self!invoke-with-args(%(:$key, :$value, :$cause), &!removal-listener)
}
#|Invoke specified sub with specified named arguments.
method !invoke-with-args(%args, &sub) {
my $wanted = &sub.signature.params.map( *.name.substr(1) ).Set;
my %actual = %args.grep( {$wanted{$_.key}} ).hash;
&sub(|%actual);
}
#|Completely remove value from cache and publish an event.
method !remove($key, $cause) {
my $previous = %!store{$key};
with $previous {
%!store{$key}:delete;
$previous.remove-nodes();
self!publish($key, $previous.value, $cause);
}
}
}
#|[Create eviction based cache.
#
#:&loader! - sub with signature like (:$key).
# The sub will be used for producing the new values.
#:&removal-listener - sub with signature like (:$key, :$value, :$cause)
# The sub will be called in case when value removed from the cache.
# $cause is element of enum RemoveCause.
#:$expire-after-write - how long the cache have to store value after its last re/write
#:$expire-after-access - how long the cache have to store value after its last access (read or write)
#:$time-unit - object of TimeUnit, indicate time unit of expire-after-write/access value.
# seconds by default.
#:$ticker - object of Ticker, witch is used for retrieve 'current' time.
# Can be specified for overriding standard behaviour (current system time), for example for testing.
#:$size - max capacity of the cache.]
sub eviction-based-cache (
:&loader! where .signature ~~ :(:$key),
:&removal-listener where .signature ~~ :(:$key, :$value, :$cause) = sub {},
:$expire-after-write = Inf,
:$expire-after-access = Inf,
:$time-unit = seconds,
Ticker :$ticker = DateTimeTicker.new,
:$size = Inf
) is export {
EvictionBasedCache.new: :&loader, :&removal-listener, :$ticker, :$size,
expire-after-sec => :{
(Access) => seconds.from($expire-after-access, $time-unit),
(Write) => seconds.from($expire-after-write, $time-unit)};
}