1 /*******************************************************************************
2 
3     Cache with an element expiration facility.
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     The cache can be configured to apply different lifetimes to empty and
10     non-empty values (where a cached record with array length 0 is considered
11     "empty"). See constructor arguments.
12 
13     Copyright:
14         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
15         All rights reserved.
16 
17     License:
18         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
19         Alternatively, this file may be distributed under the terms of the Tango
20         3-Clause BSD License (see LICENSE_BSD.txt for details).
21 
22 *******************************************************************************/
23 
24 module ocean.util.container.cache.ExpiringCache;
25 
26 import ocean.meta.types.Qualifiers;
27 
28 import ocean.util.container.cache.model.IExpiringCacheInfo;
29 
30 import ocean.util.container.cache.Cache;
31 
32 import core.stdc.time: time_t;
33 
34 version (unittest)
35 {
36     import ocean.core.Test;
37 }
38 
39 
40 /*******************************************************************************
41 
42     Data cache class template with element life time limitation. Stores items of
43     raw data, either of fixed or dynamic size. When the life time of an item,
44     which is the difference between its creation time and the current wall clock
45     time, has expired, it is removed automatically on the next getRaw()/exists()
46     access.
47 
48     Params:
49         ValueSize = size of a data item. If 0 is specified (the default), the
50             items stored in the cache are of variable (dynamic) size
51 
52 *******************************************************************************/
53 
54 class ExpiringCache ( size_t ValueSize = 0 ) : Cache!(ValueSize, true),
55                                                IExpiringCacheInfo
56 {
57     import ocean.core.Verify;
58 
59     /***************************************************************************
60 
61         Life time for non-empty items in seconds; may be changed at any time.
62         This value must be at least 1.
63 
64     ***************************************************************************/
65 
66     public time_t lifetime;
67 
68     /***************************************************************************
69 
70         Life time for empty items in seconds; may be changed at any time.
71         This value must be at least 1. Can be same as `lifetime`.
72 
73         ExpiringCache considers a cached record with array length 0 as being
74         "empty".
75 
76     ***************************************************************************/
77 
78     public time_t empty_lifetime;
79 
80     /***************************************************************************
81 
82         Counts the number of lookups where an existing element was expired.
83 
84     ***************************************************************************/
85 
86     protected uint n_expired = 0;
87 
88     /***************************************************************************
89 
90         Constructor.
91 
92         Params:
93             max_items = maximum number of items in the cache, set once, cannot
94                 be changed
95             lifetime = lifetime for non-empty items in seconds; may be changed
96                 at any time. This value must be at least 1.
97             empty_lifetime = lifetime for empty items in seconds. If set to 0
98                 (default), will be same as regular `lifetime`
99 
100     ***************************************************************************/
101 
102     public this ( size_t max_items, time_t lifetime, time_t empty_lifetime = 0 )
103     {
104         verify (lifetime > 0,
105                 "cache element lifetime is expected to be at least 1");
106 
107         super(max_items);
108 
109         this.lifetime = lifetime;
110         this.empty_lifetime = empty_lifetime ? empty_lifetime : lifetime;
111     }
112 
113     /***************************************************************************
114 
115         Gets an item from the cache. If the item was found in the cache, its
116         access time is updated. If the item life time has expired, it is
117         automatically removed.
118 
119         Note that, if you change the value referenced by the returned reference,
120         the create time will not be updated.
121 
122         Params:
123             key = key to lookup
124 
125         Returns:
126             a reference to the item value or null if no such item was found or
127             it has expired so it was removed.
128 
129         Out:
130             See super class.
131 
132     ***************************************************************************/
133 
134     public override ValueRef getRaw ( hash_t key )
135     {
136         bool existed;
137 
138         return this.getExpiringOrCreate(key, existed);
139     }
140 
141     /***************************************************************************
142 
143         Gets an item from the cache. If the item was found in the cache, its
144         access time is updated. If the item life time has expired, it is
145         automatically removed.
146 
147         Note that, if you change the value referenced by the returned reference,
148         the create time will not be updated.
149 
150         Params:
151             key     = key to lookup
152             expired = true:  the item was found but expired so it was removed,
153                       false: the item was not found
154 
155         Returns:
156             a reference to the item value or null if no such item was found or
157             it has expired so it was removed.
158 
159         Out:
160             - If expired, the returned reference is null.
161             - See super class.
162 
163     ***************************************************************************/
164 
165     public ValueRef getRaw ( hash_t key, out bool expired )
166     out (val)
167     {
168         if (expired) assert (val is null);
169     }
170     do
171     {
172         bool existed;
173 
174         ValueRef val = this.getExpiringOrCreate(key, existed);
175 
176         expired = !val && existed;
177 
178         return val;
179     }
180 
181     /***************************************************************************
182 
183         Gets an item from the cache or creates it if not already existing. If
184         the item was found in the cache, its access time is updated, otherwise
185         its create time is set. If the item was found but was expired, the
186         effect is the same as if the item was not found.
187 
188         Note that the create time is set only if an item is created, not if it
189         already existed and you change the value referenced by the returned
190         reference.
191 
192         Params:
193             key     = key to lookup
194             existed = true:  the item already existed,
195                       false: the item was created either because it did not
196                              exist or was expired
197 
198         Returns:
199             a reference to the value of the obtained or created item. If an old
200             item was replaced, this reference refers to the old value.
201 
202         Out:
203             See super class.
204 
205     ***************************************************************************/
206 
207     public override ValueRef getOrCreateRaw ( hash_t key, out bool existed )
208     {
209         return this.getExpiringOrCreate(key, existed, true);
210     }
211 
212     /***************************************************************************
213 
214         Checks whether an item exists in the cache and updates its access time.
215         If the life time of the item has expired, it is removed.
216 
217         Params:
218             key     = key to lookup
219             expired = true: an item was found but removed because it was expired
220 
221         Returns:
222             true if item exists in cache and its life time is not yet expired.
223 
224         Out:
225             If expired is false, the return value is false.
226 
227     ***************************************************************************/
228 
229     public bool exists ( hash_t key, out bool expired )
230     out (does_exist)
231     {
232         if (expired) assert (!does_exist);
233     }
234     do
235     {
236         return this.getRaw(key, expired) !is null;
237     }
238 
239     /***************************************************************************
240 
241         Checks whether an item exists in the cache and updates its access time.
242         If the life time of the item has expired, it is removed.
243 
244         Params:
245             key = key to lookup
246 
247         Returns:
248             true if item exists in cache and its life time is not yet expired.
249 
250     ***************************************************************************/
251 
252     public override bool exists ( hash_t key )
253     {
254         bool expired;
255 
256         return this.exists(key, expired);
257     }
258 
259     /***************************************************************************
260 
261         Returns:
262             the number of cache lookups  since instantiation or the last call of
263             resetStats() where the element could be found but was expired.
264 
265     ***************************************************************************/
266 
267     public uint num_expired ( )
268     {
269         return this.n_expired;
270     }
271 
272     /***************************************************************************
273 
274         Resets the statistics counter values.
275 
276     ***************************************************************************/
277 
278     public override void resetStats ( )
279     {
280         super.resetStats();
281         this.n_expired = 0;
282     }
283 
284     /***************************************************************************
285 
286         Gets an item from the cache or optionally creates it if not already
287         existing. If the item was found in the cache, its access time is
288         updated, otherwise its create time is set. If the item was found but was
289         expired, the effect is the same as if the item was not found.
290 
291         Note that the create time is set only if an item is created, not if it
292         already existed and you change the value referenced by the returned
293         reference.
294 
295         Params:
296             key     = key to lookup
297             existed = true:  the item already existed,
298                       false: the item did not exist or was expired
299             create  = true: create the item if it doesn't exist or is expired
300 
301         Returns:
302             a reference to the value of the obtained or created item. If an old
303             item was replaced, this reference refers to the old value.
304 
305         Out:
306             - If create is true, the returned reference is not null.
307             - If create and existed are false, the returned reference is null.
308 
309     ***************************************************************************/
310 
311     private ValueRef getExpiringOrCreate ( hash_t key, out bool existed,
312                                            bool create = false )
313     out (val)
314     {
315         if (create)
316         {
317             assert (val !is null);
318         }
319         else if (!existed)
320         {
321             assert (val is null);
322         }
323     }
324     do
325     {
326         TimeToIndex.Node** node = key in this;
327 
328         // opIn_r may return null but never a pointer to null.
329 
330         CacheItem* cache_item = null;
331 
332         existed = node !is null;
333 
334         if (existed)
335         {
336             time_t access_time;
337 
338             cache_item = this.access(**node, access_time);
339             // XXX: This was previously an assert() but it was changed to
340             // verify() due to a compiler bug when using -release -inline -O.
341             // In that case the null check is removed by -release and it seems
342             // that due to compiler optimizations and inlining, the compiler
343             // ends up thinking that cache_item is null when used afterwards
344             // (in the following if statement for example). By using verify()
345             // instead of assert() we make sure the compiler can see there is a
346             // null check even when -release is used.
347             verify(cache_item !is null,
348                     "cache item existed, so it shouldn't be null");
349             bool empty = cache_item.value_ref.length == 0;
350 
351             time_t used_lifetime = empty ? this.empty_lifetime : this.lifetime;
352             verify (used_lifetime > 0,
353                 "cache element lifetime is expected to be at least 1");
354 
355             if (access_time >= cache_item.create_time)
356             {
357                 /*
358                  * We silently tolerate the case the element was created after
359                  * its last access because with the system time as external
360                  * data source this is theoretically possible and at least no
361                  * program bug.
362                  */
363 
364                 existed = (access_time - cache_item.create_time) < used_lifetime;
365             }
366 
367             if (!existed)
368             {
369                 if (create)
370                 {
371                     cache_item.create_time = access_time;
372                 }
373                 else
374                 {
375                     this.remove_(key, **node);
376                     cache_item = null;
377                 }
378 
379                 this.n_expired++;
380                 this.n_misses++;
381             }
382         }
383         else if (create)
384         {
385             time_t access_time;
386 
387             cache_item = this.add(key, access_time);
388 
389             cache_item.create_time = access_time;
390         }
391 
392         return cache_item? cache_item.value_ref : null;
393     }
394 }
395 
396 /******************************************************************************/
397 
398 import core.sys.posix.stdlib: srand48, mrand48, drand48;
399 import core.stdc.time: time;
400 
401 extern (C) int getpid();
402 
403 
404 unittest
405 {
406     srand48(time(null)+getpid());
407 
408     static ulong ulrand ( )
409     {
410         return (cast (ulong) mrand48() << 0x20) | cast (uint) mrand48();
411     }
412 
413     time_t time = 234567;
414 
415     // ---------------------------------------------------------------------
416     // Test of expiring cache
417 
418     {
419         mstring data1 = "hello world".dup,
420                 data2 = "goodbye world".dup,
421                 data3 = "hallo welt".dup;
422 
423         time_t t = 0;
424 
425         scope expiring_cache = new class ExpiringCache!()
426         {
427             this ( ) {super(4, 10, 5);}
428 
429             override time_t now ( ) {return ++t;}
430         };
431 
432         with (expiring_cache)
433         {
434             test!("==")(length, 0);
435 
436             *createRaw(1) = data1;
437             test!("==")(length, 1);
438             test(exists(1));
439             {
440                 Value* data = getRaw(1);
441                 test!("!is")(data, null);
442                 test!("==")((*data)[], data1);
443             }
444 
445             test!("<=")(t, 5);
446             t = 5;
447 
448             *createRaw(2) = data2;
449             test!("==")(length, 2);
450             test(exists(2));
451             {
452                 Value* data = getRaw(2);
453                 test!("!is")(data, null);
454                 test!("==")((*data)[], data2);
455             }
456 
457             test!("<=")(t, 10);
458             t = 10;
459 
460             test(!exists(1));
461 
462             test!("<=")(t, 17);
463             t = 17;
464 
465             {
466                 Value* data = getRaw(2);
467                 test!("is")(data, null);
468             }
469         }
470 
471         // Test clear().
472         with (expiring_cache)
473         {
474             t = 5;
475 
476             test!("==")(length, 0);
477 
478             *createRaw(1) = data1;
479             test!("==")(length, 1);
480             test(exists(1));
481 
482             *createRaw(2) = data2;
483             test!("==")(length, 2);
484             test(exists(2));
485 
486             clear();
487             test!("==")(length, 0);
488             test(!exists(1));
489             test(!exists(2));
490         }
491 
492         // Test empty records
493         with (expiring_cache)
494         {
495             t = 0;
496             test!("==")(length, 0);
497 
498             *createRaw(1) = data1;
499             test!("==")(length, 1);
500             test(exists(1));
501 
502             *getRaw(1) = null;
503             test!("==")(length, 1);
504             test(exists(1));
505 
506             t = 6; // less than 10, more than 5
507             {
508                 Value* data = getRaw(1);
509                 test!("is")(data, null);
510             }
511         }
512     }
513 
514 }