Stroika Library 3.0d18
 
Loading...
Searching...
No Matches
CallerStalenessCache.h
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#ifndef _Stroika_Foundation_Cache_CallerStalenessCache_h_
5#define _Stroika_Foundation_Cache_CallerStalenessCache_h_ 1
6
7#include "Stroika/Foundation/StroikaPreComp.h"
8
9#include "Stroika/Foundation/Containers/Mapping.h"
11
12/**
13 * \file
14 *
15 * \note Code-Status: <a href="Code-Status.md#Beta">Beta</a>
16 *
17 * TODO:
18 * @todo Add StatsType support (like other caches).
19 *
20 * @todo Add() overload where caller provides the time data was captured (don't assume now)
21 *
22 * @todo Consider adding way to retrieve timestamp for key 'k'. Also consider Iterable<> method (like LRUCache)
23 * so we can dump cache (including timestamps)
24 *
25 */
26
28
29 /**
30 * Shorthand to make code clearer - for caches that support missing key type.
31 */
32 template <typename KEY>
33 static constexpr bool IsKeyedCache = not same_as<KEY, void>;
34
35 /**
36 */
37 struct CallerStalenessCache_Traits_DEFAULT {
38 using TimeStampType = Time::TimePointSeconds; // type must support operator<()
39 using TimeStampDifferenceType = Time::DurationSeconds;
40 static TimeStampType GetCurrentTimestamp ();
41 };
42
43 /**
44 * The idea behind this cache is to track when something is added, and that the lookup function can avoid
45 * a costly call to compute something if its been recently enough added.
46 *
47 * For example, consider a system where memory is stored across a slow bus, and several components need to read data from
48 * across that bus. But the different components have different tolerance for staleness (e.g. PID loop needs fresh temperature sensor
49 * data but GUI can use more stale data).
50 *
51 * This CallerStalenessCache will store when the value is updated, and let the caller either return the
52 * value from cache, or fetch it and update the cache if needed.
53 *
54 * This differs from other forms of caches in that:
55 * o It records the timestamp when a value is last-updated
56 * o It doesn't EXPIRE the data ever (except by explicit Clear or ClearOlderThan call)
57 * o The lookup caller specifies its tolerance for data staleness, and refreshes the data as needed.
58 *
59 * \note Principal difference between CallerStalenessCache and TimedCache lies in where you specify the
60 * max-age for an item: with CallerStalenessCache, its specified on each lookup call (ie with the caller), and with
61 * TimedCache, the expiry is stored with each cached item.
62 *
63 * Because of this, when you use either of these caches with a KEY=void (essentially to cache a single thing)
64 * they become indistinguishable.
65 *
66 * N.B. the KEY=void functionality is NYI for TimedCache, so best to use CallerStalenessCache for that, at least for
67 * now.
68 *
69 * \note KEY may be 'void' - and if so, the KEY parameter to the various Add/Lookup etc functions - is omitted.
70 *
71 * \note Why take 'valid-since' argument to lookup functions, and not just 'backThisTime' - in other words, why force
72 * the use of 'Ago()'. The reason is that in a complex system (say web services) - where the final requester
73 * of the data specifies the allowed staleness, you want the 'ago' computation based on ITS time (not the time
74 * the lookup happens). This is only approximate too, since it doesn't take into account the latency of the return
75 * but its a little closer to accurate.
76 *
77 * \par Example Usage
78 * \code
79 * // no key cache
80 * optional<InternetAddress> LookupExternalInternetAddress_ (optional<Time::DurationSeconds> allowedStaleness = {})
81 * {
82 * static CallerStalenessCache<void, optional<InternetAddress>> sCache_;
83 * return sCache_.Lookup (sCache_.Ago (allowedStaleness.value_or (30)), []() -> optional<InternetAddress> {
84 * ...
85 * return IO::Network::InternetAddress{connection.GET ().GetDataTextInputStream ().ReadAll ().Trim ()};
86 * });
87 * }
88 * optional<InternetAddress> iaddr = LookupExternalInternetAddress_ (); // only invoke connection logic if timed out
89 * \endcode
90 *
91 * \par Example Usage
92 * \code
93 * // keyed cache
94 * optional<int> MapValue_ (int value, optional<Time::DurationSeconds> allowedStaleness = {})
95 * {
96 * static CallerStalenessCache<int, optional<int>> sCache_;
97 * try {
98 * return sCache_.LookupValue (value, sCache_.Ago (allowedStaleness.value_or (30)), [=](int v) -> optional<int> {
99 * return v; // typically more expensive computation
100 * });
101 * }
102 * catch (...) {
103 * // NOTE - to NEGATIVELY CACHE failure, you could call sCache_.Add (value, nullopt);
104 * // return null here, or Execution::ReThrow ()
105 * }
106 * }
107 * EXPECT_EQ (MapValue_ (1), 1); // skips 'more expensive computation' if in cache
108 * EXPECT_EQ (MapValue_ (2), 2); // ''
109 * \endcode
110 *
111 * \par Example Usage
112 * \code
113 * // using KEY=void (so singleton cache)
114 * unsigned int sCalls1_{0};
115 * optional<int> LookupExternalInternetAddress_ (optional<Time::DurationSeconds> allowedStaleness = {})
116 * {
117 * using Cache::CallerStalenessCache;
118 * static CallerStalenessCache<void, optional<int>> sCache_;
119 * return sCache_.LookupValue (sCache_.Ago (allowedStaleness.value_or (30)), [] () -> optional<int> {
120 * ++sCalls1_;
121 * return 1;
122 * });
123 * }
124 * \endcode
125 *
126 * \note Implementation Note - no reason to bother using Debug::AssertExternallySynchronized since uses Mapping,
127 * which does that internally.
128 *
129 * \note \em Thread-Safety <a href="Thread-Safety.md#C++-Standard-Thread-Safety">C++-Standard-Thread-Safety</a>
130 *
131 * @see TimedCache, SynchronizedCallerStalenessCache
132 */
133 template <typename KEY, typename VALUE, typename TIME_TRAITS = CallerStalenessCache_Traits_DEFAULT>
135 public:
136 /**
137 */
138 using TimeStampType = typename TIME_TRAITS::TimeStampType;
139
140 public:
141 /**
142 */
143 using TimeStampDifferenceType = typename TIME_TRAITS::TimeStampDifferenceType;
144
145 public:
146 /**
147 */
148 static TimeStampType GetCurrentTimestamp ();
149
150 public:
151 /**
152 * Return the timestamp backwards the given timestamp.
153 *
154 * \pre backThisTime >= 0
155 *
156 * \par Example Usage
157 * \code
158 * CallerStalenessCache<KEY, VALUE> cc;
159 * if (optional<VALUE> v= cc.Lookup (k, cc.Ago (5)) {
160 * // look key, but throw disregard if older than 5 seconds (from now)
161 * }
162 * \endcode
163 */
164 static TimeStampType Ago (TimeStampDifferenceType backThisTime);
165
166 public:
167 /**
168 */
169 nonvirtual void ClearOlderThan (TimeStampType t);
170
171 public:
172 /**
173 * Clear () -- clear all
174 * Clear (KEY k) - clear just that key
175 */
176 nonvirtual void Clear ();
177 template <typename K = KEY>
178 nonvirtual void Clear (const K& k)
179 requires (IsKeyedCache<K>);
180
181 public:
183
184 public:
185 /**
186 * This not only adds the association of KEY k to VALUE v, but updates the timestamp associated with k.
187 */
188 nonvirtual void Add (Common::ArgByValueType<VALUE> v)
189 requires (not IsKeyedCache<KEY>);
190 template <typename K = KEY>
191 nonvirtual void Add (Common::ArgByValueType<K> k, Common::ArgByValueType<VALUE> v, AddReplaceMode addReplaceMode = AddReplaceMode::eAddReplaces)
192 requires (IsKeyedCache<K>);
193
194 /**
195 * Usually one will use this as (cache fillter overload):
196 * VALUE v = cache.Lookup (key, ts, [this] () -> VALUE {return this->realLookup(key); });
197 *
198 * However, the overload returning an optional is occasionally useful, if you don't want to fill the cache
199 * but just see if a value is present.
200 *
201 * Both the overload with cacheFiller, and defaultValue will update the 'time stored' for the argument key.
202 *
203 * \note Some of these Lookup () methods are not const intentionally - as they DO generally modify the cache;
204 * but others are read-only, and are therefore const.
205 *
206 * \note Lookup (,,DEFAULT_VALUE) - doesn't fill in the cache value - so not quite the same as
207 * Lookup (...,[] () { return DEFAULT_VALUE; });
208 */
209 nonvirtual optional<VALUE> Lookup (TimeStampType staleIfOlderThan) const
210 requires (not IsKeyedCache<KEY>);
211 template <typename K = KEY>
212 nonvirtual optional<VALUE> Lookup (Common::ArgByValueType<K> k, TimeStampType staleIfOlderThan) const
213 requires (IsKeyedCache<K>);
214
215 public:
216 /**
217 * Lookup the value associated with the given key (or key omitted of KEY type is void) and always return it.
218 * In case it was stale (or missing) return the provided defaule value (or cacheFiller computed value).
219 */
220 nonvirtual VALUE LookupValue (TimeStampType staleIfOlderThan, const function<VALUE ()>& cacheFiller)
221 requires (not IsKeyedCache<KEY>);
222 template <typename F, typename K = KEY>
225 template <typename K = KEY>
226 nonvirtual VALUE LookupValue (Common::ArgByValueType<K> k, TimeStampType staleIfOlderThan, const VALUE& defaultValue) const
227 requires (IsKeyedCache<K>);
228
229 public:
230 /**
231 * @aliases Clear/RemoveAll ().
232 */
233 nonvirtual void clear ();
234
235 private:
236 struct myVal_ {
237 VALUE fValue;
238 TimeStampType fDataCapturedAt;
239 myVal_ (VALUE&& v, TimeStampType t)
240 : fValue{forward<VALUE> (v)}
241 , fDataCapturedAt{t}
242 {
243 }
244 };
245
246 private:
247 // @todo see if we can clean this up (https://stackoverflow.com/questions/28432977/generic-way-of-lazily-evaluating-short-circuiting-with-stdconditional-t)
248 // Point of all this mumbo jumbo is - if using void as KEY, trick, we just store an optional single item in caller stalenss cache - dont need whole Map of bogus key to value
249 template <bool B, template <typename...> class TrueTemplate, template <typename...> class FalseTemplate>
250 struct MyLazyConditional_;
251 template <template <typename...> class TrueTemplate, template <typename...> class FalseTemplate>
252 struct MyLazyConditional_<true, TrueTemplate, FalseTemplate> {
253 using type = TrueTemplate<myVal_>;
254 };
255 template <template <typename...> class TrueTemplate, template <typename...> class FalseTemplate>
256 struct MyLazyConditional_<false, TrueTemplate, FalseTemplate> {
257 using type = FalseTemplate<KEY, myVal_>;
258 };
260 DT_ fData_;
261 };
262
263}
264
265/*
266 ********************************************************************************
267 ***************************** Implementation Details ***************************
268 ********************************************************************************
269 */
270#include "CallerStalenessCache.inl"
271
272#endif /*_Stroika_Foundation_Cache_CallerStalenessCache_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
static TimeStampType Ago(TimeStampDifferenceType backThisTime)
nonvirtual optional< VALUE > Lookup(TimeStampType staleIfOlderThan) const
nonvirtual VALUE LookupValue(TimeStampType staleIfOlderThan, const function< VALUE()> &cacheFiller)
nonvirtual void Add(Common::ArgByValueType< VALUE > v)
LRUCache implements a simple least-recently-used caching strategy, with optional hashing (of keys) to...
Definition LRUCache.h:94
static constexpr bool IsKeyedCache
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
AddReplaceMode
Mode flag to say if Adding to a container replaces, or if the first addition wins.