Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
TimedCache.h
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#ifndef _Stroika_Foundation_Cache_TimedCache_h_
5#define _Stroika_Foundation_Cache_TimedCache_h_ 1
6
7#include "Stroika/Foundation/StroikaPreComp.h"
8
9#include <map>
10#include <mutex>
11#include <optional>
12
14#include "Stroika/Foundation/Common/Common.h"
21
22/**
23 * \file
24 *
25 * \note Code-Status: <a href="Code-Status.md#Beta">Beta</a>
26 *
27 * TODO:
28 *
29 * @todo This class is logically a map. But you may want to have individual values with timed cache!
30 * Basically - KEY=RESULT? And then the arg to add/lookup don't take key? Maybe key is void?
31 *
32 * That maybe best. Template specialization where KEY=void?
33 *
34 * THEN - maybe reverse order of template params? VALUE/KEY - so then we can have KEY=void as default
35 * arg?
36 *
37 * NOTE - I DID this already for CallerStalenessCache, so pretty easy, but for the case where KEY=void, you can
38 * really use either so LOW PRIORITY.
39 *
40 * @todo Improve Regression Tests And Docs (quite weak)
41 *
42 * @todo Use Concepts or other such constraint on T/ELEMENT declarations (and docs)
43 *
44 * @todo Perhaps use Stroika Mapping<> instead of std::map<> - and in that way - we can use aribtrary externally
45 * specified map impl - so can use HASHING or BTREE, based on passed in arg. So we don't ahve problem with
46 * creating the default, specify default type to create in the TRAITS object (so for example, if using Hash,
47 * we don't force having operator< for BTREE map).
48 *
49 * Implementation Note:
50 *
51 * This module uses stl:map<> instead of a Stroika Mapping since we are comfortable with
52 * the current implementation using btree's, and to avoid any dependencies between
53 * Caching and Containers. We may want to re-think that, and just use Mapping here.
54 */
55
57
58 namespace TimedCacheSupport {
59 /**
60 * The DefaultTraits<> is a simple default traits implementation for building an TimedCache<>.
61 *
62 * \note This class was incompatibly changed in Stroika 3.0d1. It used to have a TRACK_READ_ACCESS parameter.
63 * Since Stroika 3.0d1, instead, if you wish to set that true, call Lookup (..., eTreatFoundThroughLookupAsRefreshed) instead
64 * of Lookup ()
65 */
66 template <typename KEY, typename VALUE, typename STRICT_INORDER_COMPARER = less<KEY>>
68 using KeyType = KEY;
69 using ResultType = VALUE;
70
72
73 /**
74 */
76 };
77
78 /**
79 * Flag to facilitate automatic cleanup of internal data structures as data tracked becomes uneeded.
80 */
81 enum class PurgeSpoiledDataFlagType {
82 eAutomaticallyPurgeSpoiledData,
83 eDontAutomaticallyPurgeSpoiledData
84 };
85
86 /**
87 *
88 */
89 enum class LookupMarksDataAsRefreshed {
90 eTreatFoundThroughLookupAsRefreshed,
91 eDontTreatFoundThroughLookupAsRefreshed
92 };
93 }
94
95 /**
96 * \brief Keep track of a bunch of objects, each with an associated 'freshness' which meet a TimedCache-associated minimal reshness limit.
97 *
98 * We define 'fresheness' somewhat arbitrarily, but by default, this means since the item was added. However, the TimedCache
99 * also provides other apis to update the 'freshness' of a stored object, depending on application needs.
100 *
101 * Keeps track of all items - indexed by Key - but throws away items which are any more
102 * stale than given by the staleness limit.
103 *
104 * \note Comparison with LRUCache
105 * The main difference beweeen an LRUCache and TimedCache has to do with when an element is evicted from the Cache.
106 * With a TimedCache, its evicted only when its overly aged. With an LRUCache, its more random, and depends a
107 * bit on luck (when using hashing) and how recently an item was last accessed.
108 *
109 * \note Principal difference between CallerStalenessCache and TimedCache lies in where you specify the
110 * max-age for an item: with CallerStalenessCache, its specified on each lookup call (ie with the caller), and with
111 * TimedCache, the expiry is stored with each cached item.
112 *
113 * Because of this, when you use either of these caches with a KEY=void (essentially to cache a single thing)
114 * they become indistinguishable.
115 *
116 * N.B. the KEY=void functionality is NYI for TimedCache, so best to use CallerStalenessCache for that, at least for
117 * now.
118 *
119 * \par Example Usage
120 * Use TimedCache to avoid needlessly redundant lookups
121 *
122 * \code
123 * optional<String> ReverseDNSLookup_ (const InternetAddress& inetAddr)
124 * {
125 * const Time::Duration kCacheTTL_{5min};
126 * static Cache::TimedCache<InternetAddress, optional<String>> sCache_{kCacheTTL_};
127 * return sCache_.LookupValue (inetAddr, [] (const InternetAddress& inetAddr) {
128 * return DNS::kThe.ReverseLookup (inetAddr);
129 * });
130 * }
131 * \endcode
132 *
133 * \par Example Usage
134 * Assume 'LookupDiskStats_' returns DiskSpaceUsageType, but its expensive, and the results change only slowly...
135 *
136 * \code
137 * struct DiskSpaceUsageType {
138 * int size;
139 * };
140 * // do the actual lookup part which maybe slow
141 * auto LookupDiskStats_ ([[maybe_unused]] const String& filename) -> DiskSpaceUsageType { return DiskSpaceUsageType{33}; };
142 *
143 * Cache::TimedCache<String, DiskSpaceUsageType> sDiskUsageCache_{5.0_duration};
144 *
145 * // explicitly caller maintaining the cache
146 * optional<DiskSpaceUsageType> LookupDiskStats_Try1 (String diskName)
147 * {
148 * optional<DiskSpaceUsageType> o = sDiskUsageCache_.Lookup (diskName); // maybe use eTreatFoundThroughLookupAsRefreshed depending on your application
149 * if (not o.has_value ()) {
150 * o = LookupDiskStats_ (diskName);
151 * if (o) {
152 * sDiskUsageCache_.Add (diskName, *o);
153 * }
154 * }
155 * return o;
156 * }
157 *
158 * // more automatic maintainance of that update pattern
159 * DiskSpaceUsageType LookupDiskStats_Try2 (String diskName)
160 * {
161 * // maybe use eTreatFoundThroughLookupAsRefreshed depending on your application
162 * return sDiskUsageCache_.LookupValue (diskName,
163 * [](String diskName) -> DiskSpaceUsageType {
164 * return LookupDiskStats_ (diskName);
165 * });
166 * }
167 *
168 * // or still simpler
169 * DiskSpaceUsageType LookupDiskStats_Try3 (String diskName)
170 * {
171 * // maybe use eTreatFoundThroughLookupAsRefreshed depending on your application
172 * return sDiskUsageCache_.LookupValue (diskName, LookupDiskStats_);
173 * }
174 * void DoIt ()
175 * {
176 * // example usage
177 * EXPECT_TRUE (Memory::NullCoalesce (LookupDiskStats_Try1 ("xx")).size == 33);
178 * EXPECT_TRUE (LookupDiskStats_Try2 ("xx").size == 33);
179 * EXPECT_TRUE (LookupDiskStats_Try3 ("xx").size == 33);
180 * }
181 * \endcode
182 *
183 * \note Only calls to @Add, @Lookup (...,eTreatFoundThroughLookupAsRefreshed), and @LookupValue (on a cache miss when updating) update the
184 * lastRefreshed time.
185 *
186 * For most use cases (when caching something) - the default behavior of only updating
187 * the last-access time on Add makes sense. But for the case where this class is used
188 * to OWN an object (see shared_ptr example below) - then treating a read asccess as a refresh can be helpful.
189 *
190 * Before Stroika 3.0d1, there was a template parameter which allowed treating Lookup () this way. But that
191 * has significant downsides (making lookup non-const which has threading implications). Instead now, we have
192 * non-const methods you can use todo this instead of Lookup, and then there is no need for special
193 * template paremeters, or non-cost Lookup.
194 *
195 * \par Example Usage
196 * To use TimedCache<> to 'own' a set of objects (say a set caches where we are the only
197 * possible updater) - you can make the 'VALUE' type a shared_ptr<X>, and use Lookup (...,eTreatFoundThroughLookupAsRefreshed) instead
198 * of Lookup ().
199 *
200 * In this example, there is a set of files on disk in a folder, which is complex to analyze
201 * but once analyzed, lots of calls come in at once to read (and maybe update) the set of files
202 * and once nobody has asked for a while, we throw that cache away, and rebuild it as needed.
203 *
204 * This example ALSO shows how to wrap a cache object in 'Synchronized' for thread safety.
205 *
206 * \code
207 * using ScanFolderKey_ = String;
208 * static constexpr Time::DurationSeconds kAgeForScanPersistenceCache_{5 * 60.0s};
209 * struct FolderDetails_ {
210 * int size; // ...info to cache about a folder
211 * };
212 * Synchronized<Cache::TimedCache<
213 * ScanFolderKey_,
214 * shared_ptr<FolderDetails_>,
215 * shared_ptr<FolderDetails_>>>
216 * sCachedScanFoldersDetails_{kAgeForScanPersistenceCache_};
217 *
218 * shared_ptr<FolderDetails_> AccessFolder_ (const ScanFolderKey_& folder)
219 * {
220 * auto lockedCache = sCachedScanFoldersDetails_.rwget ();
221 * if (optional<shared_ptr<FolderDetails_>> o = lockedCache->Lookup (folder, eTreatFoundThroughLookupAsRefreshed)) {
222 * return *o;
223 * }
224 * else {
225 * shared_ptr<FolderDetails_> fd = make_shared<FolderDetails_> (); // and fill in default values looking at disk
226 * lockedCache->Add (folder, fd);
227 * return fd;
228 * }
229 * }
230 *
231 * void DoIt ()
232 * {
233 * auto f1 = AccessFolder_ ("folder1"_k);
234 * auto f2 = AccessFolder_ ("folder2"_k);
235 * auto f1again = AccessFolder_ ("folder1"); // if you trace through the debug code you'll see this is a cache hit
236 * }
237 * \endcode
238 *
239 * \note This cache will keep using more and more memory until the cached items become
240 * out of date. For a cache that limits the max number of entries, use the @see LRUCache.
241 *
242 * \note This cache assumes one timeout for all items. To have timeouts vary by item,
243 * @see CallerStalenessCache.
244 *
245 * \note \em Thread-Safety <a href="Thread-Safety.md#C++-Standard-Thread-Safety">C++-Standard-Thread-Safety</a>
246 *
247 * \note we REQUIRE (without a way to enforce) - that the STATS object be internally synchronized, so that we can
248 * maintain statistics, without requiring the lookup method be non-const; this is only for tuning/debugging, anyhow...
249 *
250 * @see SynchronizedTimedCache<> - for internally synchonized implementation
251 *
252 * @see SyncrhonizedCallerStalenessCache
253 * @see CallerStalenessCache
254 * @see LRUCache
255 */
256 template <typename KEY, typename VALUE, typename TRAITS = TimedCacheSupport::DefaultTraits<KEY, VALUE>>
258 public:
259 using TraitsType = TRAITS;
260
261 public:
263 "TraitsType::InOrderComparerType - comparer not valid IInOrderComparer- see "
264 "ComparisonRelationDeclaration<Common::ComparisonRelationType::eStrictInOrder, function<bool(T, T)>");
265
266 public:
267 using LookupMarksDataAsRefreshed = TimedCacheSupport::LookupMarksDataAsRefreshed;
268
269 public:
270 using PurgeSpoiledDataFlagType = TimedCacheSupport::PurgeSpoiledDataFlagType;
271
272 public:
273 /**
274 */
276 TimedCache (TimedCache&&) = default;
277 TimedCache (const TimedCache&) = default;
278
279 public:
280 nonvirtual TimedCache& operator= (TimedCache&&) = default;
281 nonvirtual TimedCache& operator= (const TimedCache&) = default;
282
283 public:
284 /**
285 * When items are added to the timed cache, there is a universal (for the entire cache) minimum allowed freshness (how old item
286 * allowed to be before thrown away).
287 */
288 nonvirtual Time::Duration GetMinimumAllowedFreshness () const;
289
290 public:
291 /**
292 * @see GetMinimumAllowedFreshness ()
293 */
295
296 public:
297 /**
298 */
299 struct CacheElement {
300 KEY fKey;
301 VALUE fValue;
302 Time::TimePointSeconds fLastRefreshedAt;
303 };
304
305 public:
306 /**
307 * \note This returns the non-expired elements of the current cache object.
308 */
310
311 public:
312 /**
313 * \brief Returns the value associated with argument 'key', or nullopt, if its missing (missing same as expired). Can be used to retrieve lastRefreshedAt
314 *
315 * If lastRefreshedAt is provided, it is ignored, except if Lookup returns true, the value pointed to will contain the last time
316 * the data was refreshed.
317 *
318 * Occasionally, a caller might want to have the ACT of doing a lookup mark the item as fresh, in which case call
319 * Lookup (..., eTreatFoundThroughLookupAsRefreshed) instead.
320 *
321 * Occasionally, a caller might want to ASSURE it gets data, and just use the cached value if fresh enuf, and specify
322 * a lookup lambda to fetch the actual data if its not fresh, in which case call LookupValue ().
323 *
324 * \note Before Stroika 3.0d1, this used to support TraitsType::kTrackReadAccess, and if it was true did the same
325 * as the newer Lookup (..., eTreatFoundThroughLookupAsRefreshed)
326 */
328 nonvirtual optional<VALUE> Lookup (typename Common::ArgByValueType<KEY> key, LookupMarksDataAsRefreshed successfulLookupRefreshesAcceesFlag);
329
330 public:
331 /**
332 * Usually one will use this as (cacheFiller overload):
333 * \code
334 * VALUE v = cache.Lookup (key, ts, [this] () -> VALUE {return this->realLookup(key); });
335 * \endcode
336 *
337 * However, the method (Lookup) returing an optional is occasionally useful, if you don't want to fill the cache
338 * but just see if a value is present.
339 *
340 * The overload with cacheFiller, will update the 'time stored' for the argument key if a new value is fetched.
341 *
342 * \note This function may update the TimedCache (which is why it is non-const).
343 */
344 nonvirtual VALUE LookupValue (typename Common::ArgByValueType<KEY> key, const function<VALUE (typename Common::ArgByValueType<KEY>)>& cacheFiller,
345 LookupMarksDataAsRefreshed successfulLookupRefreshesAcceesFlag = LookupMarksDataAsRefreshed::eDontTreatFoundThroughLookupAsRefreshed,
346 PurgeSpoiledDataFlagType purgeSpoiledData = PurgeSpoiledDataFlagType::eAutomaticallyPurgeSpoiledData);
347
348 public:
349 /**
350 * Updates/adds the given value associated with key, and updates the last-access date to now (or argument freshAsOf).
351 *
352 * The parameter PurgeSpoiledData defaults to eAutomaticallyPurgeSpoiledData; this allows the accumulated data
353 * to automatically be purged as it becomes irrelevant (@see PurgeSpoiledData). But for performance sake,
354 * callers may call Add (..., eDontAutomaticallyPurgeSpoiledData)
355 */
356 nonvirtual void Add (typename Common::ArgByValueType<KEY> key, typename Common::ArgByValueType<VALUE> result,
357 PurgeSpoiledDataFlagType purgeSpoiledData = PurgeSpoiledDataFlagType::eAutomaticallyPurgeSpoiledData);
359
360 public:
361 /**
362 */
363 nonvirtual void Remove (typename Common::ArgByValueType<KEY> key);
364
365 public:
366 /**
367 * Remove everything from the cache
368 */
369 nonvirtual void clear ();
370
371 public:
372 /**
373 * May be called occasionally to free resources used by cached items that are out of date.
374 * Not necessary to call - but can save memory.
375 *
376 * Can be triggered automatically (so not explicitly) by passing eAutomaticallyPurgeSpoiledData to Add ()
377 */
378 nonvirtual void PurgeSpoiledData ();
379
380 public:
381 [[deprecated ("Since Stroika v3.0d1, use PurgeSpoiledData or count on Add's purgeSpoiledData parameter)")]] nonvirtual void DoBookkeeping ()
382 {
384 }
385 [[deprecated ("Since Stroika 3.0d1 use GetMinimumAllowedFreshness")]] Time::Duration GetTimeout () const
386 {
388 }
389 [[deprecated ("Since Stroika 3.0d1 use GetMinimumAllowedFreshness")]] void SetTimeout (Time::Duration timeout)
390 {
392 }
393
394 private:
395 [[no_unique_address]] Debug::AssertExternallySynchronizedMutex fAssertExternallySyncrhonized_;
396
397 private:
398 Time::DurationSeconds fMinimumAllowedFreshness_;
399 Time::TimePointSeconds fNextAutoClearAt_;
400
401 private:
402 nonvirtual void ClearIfNeeded_ ();
403 nonvirtual void ClearOld_ ();
404
405 private:
406 struct MyResult_ {
407 VALUE fResult;
408 Time::TimePointSeconds fLastRefreshedAt{Time::GetTickCount ()};
409 };
410
411 private:
413 MyMapType_ fMap_;
414
415 private:
416 [[no_unique_address]] mutable typename TRAITS::StatsType fStats_;
417 };
418
419}
420
421/*
422 ********************************************************************************
423 ***************************** Implementation Details ***************************
424 ********************************************************************************
425 */
426#include "TimedCache.inl"
427
428#endif /*_Stroika_Foundation_Cache_TimedCache_h_*/
time_point< RealtimeClock, DurationSeconds > TimePointSeconds
TimePointSeconds is a simpler approach to chrono::time_point, which doesn't require using templates e...
Definition Realtime.h:82
chrono::duration< double > DurationSeconds
chrono::duration<double> - a time span (length of time) measured in seconds, but high precision.
Definition Realtime.h:57
LRUCache implements a simple least-recently-used caching strategy, with optional hashing (of keys) to...
Definition LRUCache.h:94
Keep track of a bunch of objects, each with an associated 'freshness' which meet a TimedCache-associa...
Definition TimedCache.h:257
nonvirtual void SetMinimumAllowedFreshness(Time::Duration minimumAllowedFreshness)
nonvirtual Traversal::Iterable< CacheElement > Elements() const
nonvirtual VALUE LookupValue(typename Common::ArgByValueType< KEY > key, const function< VALUE(typename Common::ArgByValueType< KEY >)> &cacheFiller, LookupMarksDataAsRefreshed successfulLookupRefreshesAcceesFlag=LookupMarksDataAsRefreshed::eDontTreatFoundThroughLookupAsRefreshed, PurgeSpoiledDataFlagType purgeSpoiledData=PurgeSpoiledDataFlagType::eAutomaticallyPurgeSpoiledData)
nonvirtual optional< VALUE > Lookup(typename Common::ArgByValueType< KEY > key, Time::TimePointSeconds *lastRefreshedAt=nullptr) const
Returns the value associated with argument 'key', or nullopt, if its missing (missing same as expired...
nonvirtual Time::Duration GetMinimumAllowedFreshness() const
nonvirtual void Add(typename Common::ArgByValueType< KEY > key, typename Common::ArgByValueType< VALUE > result, PurgeSpoiledDataFlagType purgeSpoiledData=PurgeSpoiledDataFlagType::eAutomaticallyPurgeSpoiledData)
NOT a real mutex - just a debugging infrastructure support tool so in debug builds can be assured thr...
Duration is a chrono::duration<double> (=.
Definition Duration.h:96
Iterable<T> is a base class for containers which easily produce an Iterator<T> to traverse them.
Definition Iterable.h:237
conditional_t<(sizeof(CHECK_T)<=2 *sizeof(void *)) and is_trivially_copyable_v< CHECK_T >, CHECK_T, const CHECK_T & > ArgByValueType
This is an alias for 'T' - but how we want to pass it on stack as formal parameter.
Definition TypeHints.h:32