1 /*******************************************************************************
2 
3     Expiring (L)east (R)ecently (U)sed Cache.
4 
5     Extends the Cache by providing a settable lifetime and automatically
6     removing  elements where the difference between the current time and the
7     createtime is greater than that lifetime value.
8 
9     Copyright:
10         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
11         All rights reserved.
12 
13     License:
14         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
15         Alternatively, this file may be distributed under the terms of the Tango
16         3-Clause BSD License (see LICENSE_BSD.txt for details).
17 
18 *******************************************************************************/
19 
20 module ocean.util.container.cache.ExpiringLRUCache;
21 
22 
23 import ocean.meta.types.Qualifiers;
24 
25 import ocean.util.container.cache.model.IExpiringCacheInfo;
26 
27 import ocean.util.container.cache.LRUCache;
28 
29 import core.stdc.time: time_t;
30 
31 version (unittest) import ocean.core.Test;
32 
33 /*******************************************************************************
34 
35     Data cache class template with element life time limitation. Stores items of
36     raw data, either of fixed or dynamic size. When the life time of an item,
37     which is the difference between its creation time and the current wall clock
38     time, has expired, it is removed automatically on the next
39     getAndRefreshValue()/exists() access.
40 
41     Params:
42         T = the type of data that will be stored
43 
44 *******************************************************************************/
45 
46 class ExpiringLRUCache(T = void[]) : LRUCache!(T, true), IExpiringCacheInfo
47 {
48     import ocean.core.Verify;
49 
50     /***************************************************************************
51 
52         Life time for all items in seconds; may be changed at any time.
53         This value must be at least 1.
54 
55     ***************************************************************************/
56 
57     public time_t lifetime;
58 
59     /***************************************************************************
60 
61         Counts the number of lookups where an existing element was expired.
62 
63     ***************************************************************************/
64 
65     protected uint n_expired = 0;
66 
67     /***************************************************************************
68 
69         Constructor.
70 
71         Params:
72             max_items = maximum number of items in the cache, set once, cannot
73                         be changed
74             lifetime  = life time for all items in seconds; may be changed at
75                         any time. This value must be at least 1.
76 
77     ***************************************************************************/
78 
79     public this ( size_t max_items, time_t lifetime )
80     {
81         verify (lifetime > 0,
82                 "cache element lifetime is expected to be at least 1");
83 
84         super(max_items);
85 
86         this.lifetime = lifetime;
87     }
88 
89     /***************************************************************************
90 
91         Gets an item from the cache. If the item was found in the cache, its
92         access time is updated. If the item life time has expired, it is
93         automatically removed.
94 
95         Note that, if you change the value referenced by the returned reference,
96         the create time will not be updated.
97 
98         Params:
99             key = key to lookup
100 
101         Returns:
102             a reference to the item value or null if no such item was found or
103             it has expired so it was removed.
104 
105     ***************************************************************************/
106 
107     public override T* getAndRefresh ( hash_t key )
108     {
109         bool existed;
110 
111         return this.getAndRefresh(key, existed);
112     }
113 
114     /// ditto
115     public alias LRUCache!(T, true).getAndRefresh getAndRefresh;
116 
117     /***************************************************************************
118 
119         Gets an item from the cache. If the item was found in the cache, its
120         access time is updated. If the item life time has expired, it is
121         automatically removed.
122 
123         Note that, if you change the value referenced by the returned reference,
124         the create time will not be updated.
125 
126         Params:
127             key     = key to lookup
128             expired = true:  the item was found but expired so it was removed,
129                       false: the item was not found
130 
131         Returns:
132             a reference to the item value or null if no such item was found or
133             it has expired so it was removed.
134 
135         Out:
136             - If expired, the returned reference is null.
137 
138     ***************************************************************************/
139 
140     public T* getAndRefresh ( hash_t key, out bool expired )
141     out (val)
142     {
143         if (expired) assert (val is null);
144     }
145     do
146     {
147         bool existed;
148 
149         T* val = this.getExpiringOrCreate(key, existed);
150 
151         expired = !val && existed;
152 
153         return val;
154     }
155 
156     /***************************************************************************
157 
158         Gets an item from the cache or creates it if not already existing. If
159         the item was found in the cache, its access time is updated, otherwise
160         its create time is set. If the item was found but was expired, the
161         effect is the same as if the item was not found.
162 
163         Note that the create time is set only if an item is created, not if it
164         already existed and you change the value referenced by the returned
165         reference.
166 
167         Params:
168             key     = key to lookup
169             existed = true:  the item already existed,
170                       false: the item was created either because it did not
171                              exist or was expired
172 
173         Returns:
174             a reference to the value of the obtained or created item. If an old
175             item was replaced, this reference refers to the old value.
176 
177         Out:
178             See super class.
179 
180     ***************************************************************************/
181 
182     public override T* getRefreshOrCreate ( hash_t key, out bool existed )
183     {
184         return this.getExpiringOrCreate(key, existed, true);
185     }
186 
187     /***************************************************************************
188 
189         Imports the super class overloaded methods which were hidden by the
190         getOrCreate() override implementation.
191 
192     ***************************************************************************/
193 
194     alias LRUCache!(T, true).getRefreshOrCreate getRefreshOrCreate;
195 
196     /***************************************************************************
197 
198         Checks whether an item exists in the cache and updates its access time.
199         If the life time of the item has expired, it is removed.
200 
201         Params:
202             key     = key to lookup
203             expired = true: an item was found but removed because it was expired
204 
205         Returns:
206             true if item exists in cache and its life time is not yet expired.
207 
208         Out:
209             If expired is false, the return value is false.
210 
211     ***************************************************************************/
212 
213     public bool exists ( hash_t key, out bool expired )
214     out (does_exist)
215     {
216         if (expired) assert (!does_exist);
217     }
218     do
219     {
220         return this.getAndRefresh(key, expired) !is null;
221     }
222 
223     /***************************************************************************
224 
225         Checks whether an item exists in the cache and updates its access time.
226         If the life time of the item has expired, it is removed.
227 
228         Params:
229             key = key to lookup
230 
231         Returns:
232             true if item exists in cache and its life time is not yet expired.
233 
234     ***************************************************************************/
235 
236     override public bool exists ( hash_t key )
237     {
238         bool expired;
239 
240         return this.exists(key, expired);
241     }
242 
243     /***************************************************************************
244 
245         Returns:
246             the number of cache lookups  since instantiation or the last call of
247             resetStats() where the element could be found but was expired.
248 
249     ***************************************************************************/
250 
251     public uint num_expired ( )
252     {
253         return this.n_expired;
254     }
255 
256     /***************************************************************************
257 
258         Resets the statistics counter values.
259 
260     ***************************************************************************/
261 
262     public override void resetStats ( )
263     {
264         super.resetStats();
265         this.n_expired = 0;
266     }
267 
268     /***************************************************************************
269 
270         Gets an item from the cache or optionally creates it if not already
271         existing. If the item was found in the cache, its access time is
272         updated, otherwise its create time is set. If the item was found but was
273         expired, the effect is the same as if the item was not found.
274 
275         Note that the create time is set only if an item is created, not if it
276         already existed and you change the value referenced by the returned
277         reference.
278 
279         Params:
280             key     = key to lookup
281             existed = true:  the item already existed,
282                       false: the item did not exist or was expired
283             create  = true: create the item if it doesn't exist or is expired
284 
285         Returns:
286             a reference to the value of the obtained or created item. If an old
287             item was replaced, this reference refers to the old value.
288 
289         Out:
290             - If create is true, the returned reference is not null.
291             - If create and existed are false, the returned reference is null.
292 
293     ***************************************************************************/
294 
295     private T* getExpiringOrCreate ( hash_t key, out bool existed,
296                                      bool create = false )
297     out (val)
298     {
299         if (create)
300         {
301             assert (val !is null);
302         }
303         else if (!existed)
304         {
305             assert (val is null);
306         }
307     }
308     do
309     {
310         verify (this.lifetime > 0,
311                 "cache element lifetime is expected to be at least 1");
312 
313         Value* item;
314 
315         time_t new_access_time;
316         if (create)
317         {
318             item = this.getRefreshOrCreateRaw(key, new_access_time, existed);
319         }
320         else
321         {
322             item = this.getAndRefreshRaw(key, new_access_time);
323             existed = item !is null;
324         }
325 
326         if (item)
327         {
328             // If we reached that point, then it must be an old item which
329             // existed before. We should check if it has expired
330             if (new_access_time >= item.create_time &&
331                 this.lifetime <= (new_access_time - item.create_time))
332             {
333                 existed = false;
334                 this.remove(key);
335                 item = null;
336 
337                 this.n_expired++;
338                 // TODO: increase these ones
339                 // this.n_misses++;
340             }
341 
342             return &item.value;
343         }
344         else
345         {
346             // The misses was already increased, no need to do it again
347             return null;
348         }
349     }
350 }
351 
352 /******************************************************************************/
353 
354 import core.sys.posix.stdlib: srand48, mrand48, drand48;
355 import core.stdc.time: time;
356 
357 
358 extern (C) int getpid();
359 
360 
361 unittest
362 {
363     srand48(time(null)+getpid());
364 
365     static ulong ulrand ( )
366     {
367         return (cast (ulong) mrand48() << 0x20) | cast (uint) mrand48();
368     }
369 
370     time_t time = 234567;
371 
372     // ---------------------------------------------------------------------
373     // Test of expiring cache
374 
375     {
376         mstring data1 = "hello world".dup,
377                 data2 = "goodbye world".dup,
378                 data3 = "hallo welt".dup;
379 
380         time_t t = 0;
381 
382         scope expiring_cache = new class ExpiringLRUCache!()
383         {
384             this ( ) {super(4, 10);}
385 
386             override time_t now ( ) {return ++t;}
387         };
388 
389         with (expiring_cache)
390         {
391             test(length == 0);
392 
393             *getRefreshOrCreate(1) = data1;
394             test(length == 1);
395             test(exists(1));
396             {
397                 auto data = getAndRefresh(1);
398                 test(data !is null);
399                 test((*data)[] == data1);
400             }
401 
402             test(t <= 5);
403             t = 5;
404 
405             *getRefreshOrCreate(2) = data2;
406             test(length == 2);
407             test(exists(2));
408             {
409                 auto data = getAndRefresh(2);
410                 test(data !is null);
411                 test((*data)[] == data2);
412             }
413 
414             test(t <= 10);
415             t = 10;
416 
417             test(!exists(1));
418 
419             test(t <= 17);
420             t = 17;
421             {
422                 auto data = getAndRefresh(2);
423                 test(data is null);
424             }
425         }
426     }
427 
428 }
429 
430 unittest
431 {
432     // check cache with const elements
433     scope expiring_cache = new ExpiringLRUCache!(cstring)(1, 1);
434     *expiring_cache.getRefreshOrCreate(1) = "abc"[];
435 }