userver: userver/cache/expirable_lru_cache.hpp Source File
Loading...
Searching...
No Matches
expirable_lru_cache.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/cache/expirable_lru_cache.hpp
4/// @brief @copybrief cache::ExpirableLruCache
5
6#include <atomic>
7#include <chrono>
8#include <optional>
9
10#include <userver/cache/lru_cache_config.hpp>
11#include <userver/cache/lru_cache_statistics.hpp>
12#include <userver/cache/nway_lru_cache.hpp>
13#include <userver/concurrent/mutex_set.hpp>
14#include <userver/dump/common.hpp>
15#include <userver/dump/dumper.hpp>
16#include <userver/engine/async.hpp>
17#include <userver/utils/datetime.hpp>
18#include <userver/utils/impl/cached_time.hpp>
19#include <userver/utils/impl/wait_token_storage.hpp>
20#include <userver/utils/statistics/writer.hpp>
21
22USERVER_NAMESPACE_BEGIN
23
24namespace cache {
25
26namespace impl {
27
28template <typename Value>
29struct ExpirableValue final {
30 Value value;
31 std::chrono::steady_clock::time_point update_time;
32};
33
34template <typename Value>
35void Write(dump::Writer& writer, const impl::ExpirableValue<Value>& value) {
36 const auto [now, steady_now] = utils::impl::GetGlobalTime();
37 writer.Write(value.value);
38 writer.Write(value.update_time - steady_now + now);
39}
40
41template <typename Value>
42impl::ExpirableValue<Value> Read(dump::Reader& reader, dump::To<impl::ExpirableValue<Value>>) {
43 const auto [now, steady_now] = utils::impl::GetGlobalTime();
44 // Evaluation order of arguments is guaranteed in brace-initialization.
45 return impl::ExpirableValue<
46 Value>{reader.Read<Value>(), reader.Read<std::chrono::system_clock::time_point>() - now + steady_now};
47}
48
49} // namespace impl
50
51/// @ingroup userver_containers
52/// @brief Class for expirable LRU cache.
53///
54/// Use @ref cache::LruMap for non-expirable LRU cache.
55///
56/// @snippet core/src/cache/expirable_lru_cache_test.cpp Sample ExpirableLruCache
57template <typename Key, typename Value, typename Hash = std::hash<Key>, typename Equal = std::equal_to<Key>>
58class ExpirableLruCache final {
59public:
60 /// @brief Type of function used to compute or refresh a value for a key
61 using UpdateValueFunc = std::function<Value(const Key&)>;
62
63 /// @brief Cache read mode for @ref Get.
64 enum class ReadMode {
65 kSkipCache, ///< Do not cache value got from update function
66 kUseCache, ///< Cache value got from update function
67 };
68
69 /// @brief Constructs the cache.
70 ///
71 /// @param ways Number of ways (shards). See @ref cache::NWayLRU.
72 /// @param way_size Maximum size per way. See @ref cache::NWayLRU.
73 /// @param hash Hash functor for keys.
74 /// @param equal Equality functor for keys.
75 ExpirableLruCache(size_t ways, size_t way_size, const Hash& hash = Hash(), const Equal& equal = Equal());
76
77 /// @brief Destroys the cache and waits for all background update tasks to
78 /// complete
80
81 /// @brief Sets maximum size per way.
82 ///
83 /// @param way_size Maximum size per way. See @ref cache::NWayLRU.
84 void SetWaySize(size_t way_size);
85
86 /// @brief Returns the maximum lifetime of a cached value before it is
87 /// considered expired.
88 ///
89 /// @return Current max lifetime, or zero if expiration is disabled.
90 std::chrono::milliseconds GetMaxLifetime() const noexcept;
91
92 /// @brief Sets the maximum lifetime of a cached value before it is
93 /// considered expired.
94 ///
95 /// @param max_lifetime Max lifetime; zero disables expiration.
96 void SetMaxLifetime(std::chrono::milliseconds max_lifetime);
97
98 /// @brief Sets background update mode.
99 ///
100 /// If \p background_update is kDisabled, expiring values are not updated
101 /// in background; if kEnabled, they are updated asynchronously when
102 /// lifetime is near end.
103 ///
104 /// @param background_update Either kDisabled or kEnabled.
105 void SetBackgroundUpdate(BackgroundUpdateMode background_update);
106
107 /// @brief Returns value by key, or computes and optionally caches it.
108 ///
109 /// Equivalent to @ref GetOptional(\p key, \p update_func); if that is
110 /// std::nullopt, returns update_func(\p key) and stores it in cache when
111 /// \p read_mode is ReadMode::kUseCache.
112 ///
113 /// @param key Cache key.
114 /// @param update_func Function to compute value when key is missing or
115 /// expired.
116 /// @param read_mode If kUseCache, caches the result of \p update_func.
117 /// @return Cached or freshly computed value.
118 Value Get(const Key& key, const UpdateValueFunc& update_func, ReadMode read_mode = ReadMode::kUseCache);
119
120 /// @brief Returns value by key if present and not expired; may trigger
121 /// background update.
122 ///
123 /// If background update is kEnabled and the entry is near expiry, schedules
124 /// an async update via \p update_func. Does not block on that update.
125 ///
126 /// @param key Cache key.
127 /// @param update_func Function used for background refresh when entry is
128 /// near expiry.
129 /// @return Value if key is in cache and not expired, otherwise std::nullopt.
130 std::optional<Value> GetOptional(const Key& key, const UpdateValueFunc& update_func);
131
132 /// @brief @ref GetOptional without expiry checks and without value updates.
133 ///
134 /// @param key Cache key.
135 /// @return Value if key is in cache, otherwise std::nullopt.
136 /// @note Used during fallback in FallbackELruCache.
137 std::optional<Value> GetOptionalUnexpirable(const Key& key);
138
139 /// @brief @ref GetOptional without expiry check; may trigger background update.
140 ///
141 /// @param key Cache key.
142 /// @param update_func Function used for background refresh when entry is
143 /// near expiry.
144 /// @return Value if key is in cache, otherwise std::nullopt.
145 /// @note Used during fallback in FallbackELruCache.
146 std::optional<Value> GetOptionalUnexpirableWithUpdate(const Key& key, const UpdateValueFunc& update_func);
147
148 /// @brief @ref GetOptional without triggering value updates (no background
149 /// refresh).
150 ///
151 /// @param key Cache key.
152 /// @return Value if key is in cache and not expired, otherwise std::nullopt.
153 std::optional<Value> GetOptionalNoUpdate(const Key& key);
154
155 /// @brief @ref GetOptionalNoUpdate, but returns the value together with its
156 /// last update time.
157 ///
158 /// @param key Cache key.
159 /// @return Value and update time if key is in cache and not expired,
160 /// otherwise std::nullopt.
161 std::optional<impl::ExpirableValue<Value>> GetOptionalNoUpdateWithLastUpdateTime(const Key& key);
162
163 /// @brief Inserts or updates the value for the given key with current
164 /// timestamp.
165 ///
166 /// @param key Cache key.
167 /// @param value Value to store.
168 void Put(const Key& key, const Value& value);
169
170 /// @brief Inserts or updates the value for the given key with current
171 /// timestamp (\p value is moved).
172 ///
173 /// @param key Cache key.
174 /// @param value Value to store (moved).
175 void Put(const Key& key, Value&& value);
176
177 /// @brief Returns cache statistics (hits, misses, background updates, etc.).
178 ///
179 /// @return Reference to impl::ExpirableLruCacheStatistics.
180 const impl::ExpirableLruCacheStatistics& GetStatistics() const;
181
182 /// @brief Returns approximate number of entries in the cache.
183 ///
184 /// @return Approximate size (sum of all ways).
185 size_t GetSizeApproximate() const;
186
187 /// @brief Clears the cache (removes all entries).
189
190 /// @brief Erases the entry for the given key.
191 ///
192 /// @param key Cache key to erase.
193 void InvalidateByKey(const Key& key);
194
195 /// @brief Erases the key from cache if \p pred returns true for the
196 /// current value.
197 ///
198 /// @param key Cache key.
199 /// @param pred Predicate invoked with current value; entry is erased only
200 /// if it returns true and value is not expired.
201 template <typename Predicate>
202 void InvalidateByKeyIf(const Key& key, Predicate pred);
203
204 /// @brief Schedules an async task to update the value by update_func(\p key).
205 ///
206 /// @param key Cache key to update.
207 /// @param update_func Function to compute the new value.
208 void UpdateInBackground(const Key& key, UpdateValueFunc update_func);
209
210 /// @brief Serializes cache state to the dump writer.
211 ///
212 /// Used for @ref dump::Dumper integration.
213 ///
214 /// @param writer Dump writer to write to.
215 void Write(dump::Writer& writer) const;
216
217 /// @brief Restores cache state from the dump reader.
218 ///
219 /// Used for @ref dump::Dumper integration.
220 ///
221 /// @param reader Dump reader to read from.
222 void Read(dump::Reader& reader);
223
224 /// @brief Sets the dumper that will be notified of cache updates.
225 ///
226 /// @param dumper Shared pointer to @ref dump::Dumper.
227 /// @note This method is not thread-safe.
228 void SetDumper(std::shared_ptr<dump::Dumper> dumper);
229
230private:
231 bool IsExpired(std::chrono::steady_clock::time_point update_time, std::chrono::steady_clock::time_point now) const;
232
233 bool ShouldUpdate(std::chrono::steady_clock::time_point update_time, std::chrono::steady_clock::time_point now)
234 const;
235
236 cache::NWayLRU<Key, impl::ExpirableValue<Value>, Hash, Equal> lru_;
237 std::atomic<std::chrono::milliseconds> max_lifetime_{std::chrono::milliseconds(0)};
238 std::atomic<BackgroundUpdateMode> background_update_mode_{BackgroundUpdateMode::kDisabled};
239 impl::ExpirableLruCacheStatistics stats_;
240 concurrent::MutexSet<Key, Hash, Equal> mutex_set_;
241 utils::impl::WaitTokenStorage wait_token_storage_;
242};
243
244template <typename Key, typename Value, typename Hash, typename Equal>
245ExpirableLruCache<
246 Key,
247 Value,
248 Hash,
249 Equal>::ExpirableLruCache(size_t ways, size_t way_size, const Hash& hash, const Equal& equal)
250 : lru_(ways, way_size, hash, equal),
251 mutex_set_{ways, way_size, hash, equal}
252{}
253
254template <typename Key, typename Value, typename Hash, typename Equal>
255ExpirableLruCache<Key, Value, Hash, Equal>::~ExpirableLruCache() {
256 wait_token_storage_.WaitForAllTokens();
257}
258
259template <typename Key, typename Value, typename Hash, typename Equal>
260void ExpirableLruCache<Key, Value, Hash, Equal>::SetWaySize(size_t way_size) {
261 lru_.UpdateWaySize(way_size);
262}
263
264template <typename Key, typename Value, typename Hash, typename Equal>
265std::chrono::milliseconds ExpirableLruCache<Key, Value, Hash, Equal>::GetMaxLifetime() const noexcept {
266 return max_lifetime_.load();
267}
268
269template <typename Key, typename Value, typename Hash, typename Equal>
270void ExpirableLruCache<Key, Value, Hash, Equal>::SetMaxLifetime(std::chrono::milliseconds max_lifetime) {
271 max_lifetime_ = max_lifetime;
272}
273
274template <typename Key, typename Value, typename Hash, typename Equal>
275void ExpirableLruCache<Key, Value, Hash, Equal>::SetBackgroundUpdate(BackgroundUpdateMode background_update) {
276 background_update_mode_ = background_update;
277}
278
279template <typename Key, typename Value, typename Hash, typename Equal>
280Value ExpirableLruCache<
281 Key,
282 Value,
283 Hash,
284 Equal>::Get(const Key& key, const UpdateValueFunc& update_func, ReadMode read_mode) {
285 auto now = utils::datetime::SteadyNow();
286 auto opt_old_value = GetOptional(key, update_func);
287 if (opt_old_value) {
288 return std::move(*opt_old_value);
289 }
290
291 auto mutex = mutex_set_.GetMutexForKey(key);
292 const std::lock_guard lock(mutex);
293 // Test one more time - concurrent ExpirableLruCache::Get()
294 // might have put the value
295 auto old_value = lru_.Get(key);
296 if (old_value && !IsExpired(old_value->update_time, now)) {
297 return std::move(old_value->value);
298 }
299
300 auto value = update_func(key);
301 if (read_mode == ReadMode::kUseCache) {
302 lru_.Put(key, {value, now});
303 }
304 return value;
305}
306
307template <typename Key, typename Value, typename Hash, typename Equal>
308std::optional<Value> ExpirableLruCache<
309 Key,
310 Value,
311 Hash,
312 Equal>::GetOptional(const Key& key, const UpdateValueFunc& update_func) {
313 auto now = utils::datetime::SteadyNow();
314 auto old_value = lru_.Get(key);
315
316 if (old_value) {
317 if (!IsExpired(old_value->update_time, now)) {
318 impl::CacheHit(stats_);
319
320 if (ShouldUpdate(old_value->update_time, now)) {
321 UpdateInBackground(key, update_func);
322 }
323
324 return std::move(old_value->value);
325 } else {
326 impl::CacheStale(stats_);
327 }
328 }
329 impl::CacheMiss(stats_);
330
331 return std::nullopt;
332}
333
334template <typename Key, typename Value, typename Hash, typename Equal>
335std::optional<Value> ExpirableLruCache<Key, Value, Hash, Equal>::GetOptionalUnexpirable(const Key& key) {
336 auto old_value = lru_.Get(key);
337
338 if (old_value) {
339 impl::CacheHit(stats_);
340 return old_value->value;
341 }
342 impl::CacheMiss(stats_);
343
344 return std::nullopt;
345}
346
347template <typename Key, typename Value, typename Hash, typename Equal>
348std::optional<Value> ExpirableLruCache<
349 Key,
350 Value,
351 Hash,
352 Equal>::GetOptionalUnexpirableWithUpdate(const Key& key, const UpdateValueFunc& update_func) {
353 auto now = utils::datetime::SteadyNow();
354 auto old_value = lru_.Get(key);
355
356 if (old_value) {
357 impl::CacheHit(stats_);
358
359 if (ShouldUpdate(old_value->update_time, now)) {
360 UpdateInBackground(key, update_func);
361 }
362
363 return old_value->value;
364 }
365 impl::CacheMiss(stats_);
366
367 return std::nullopt;
368}
369
370template <typename Key, typename Value, typename Hash, typename Equal>
371std::optional<impl::ExpirableValue<Value>> ExpirableLruCache<
372 Key,
373 Value,
374 Hash,
376 const auto now = utils::datetime::SteadyNow();
377 const auto old_value = lru_.Get(key);
378 if (old_value) {
379 if (!IsExpired(old_value->update_time, now)) {
380 impl::CacheHit(stats_);
381 return old_value;
382 } else {
383 impl::CacheStale(stats_);
384 }
385 }
386 impl::CacheMiss(stats_);
387 return std::nullopt;
388}
389
390template <typename Key, typename Value, typename Hash, typename Equal>
391std::optional<Value> ExpirableLruCache<Key, Value, Hash, Equal>::GetOptionalNoUpdate(const Key& key) {
392 auto value_with_update_time = GetOptionalNoUpdateWithLastUpdateTime(key);
393 if (value_with_update_time.has_value()) {
394 return value_with_update_time->value;
395 }
396 return std::nullopt;
397}
398
399template <typename Key, typename Value, typename Hash, typename Equal>
400void ExpirableLruCache<Key, Value, Hash, Equal>::Put(const Key& key, const Value& value) {
401 lru_.Put(key, {value, utils::datetime::SteadyNow()});
402}
403
404template <typename Key, typename Value, typename Hash, typename Equal>
405void ExpirableLruCache<Key, Value, Hash, Equal>::Put(const Key& key, Value&& value) {
406 lru_.Put(key, {std::move(value), utils::datetime::SteadyNow()});
407}
408
409template <typename Key, typename Value, typename Hash, typename Equal>
410const impl::ExpirableLruCacheStatistics& ExpirableLruCache<Key, Value, Hash, Equal>::GetStatistics() const {
411 return stats_;
412}
413
414template <typename Key, typename Value, typename Hash, typename Equal>
415size_t ExpirableLruCache<Key, Value, Hash, Equal>::GetSizeApproximate() const {
416 return lru_.GetSize();
417}
418
419template <typename Key, typename Value, typename Hash, typename Equal>
420void ExpirableLruCache<Key, Value, Hash, Equal>::Invalidate() {
421 lru_.Invalidate();
422}
423
424template <typename Key, typename Value, typename Hash, typename Equal>
425void ExpirableLruCache<Key, Value, Hash, Equal>::InvalidateByKey(const Key& key) {
426 lru_.InvalidateByKey(key);
427}
428
429template <typename Key, typename Value, typename Hash, typename Equal>
430template <typename Predicate>
431void ExpirableLruCache<Key, Value, Hash, Equal>::InvalidateByKeyIf(const Key& key, Predicate pred) {
432 auto now = utils::datetime::SteadyNow();
433
434 auto mutex = mutex_set_.GetMutexForKey(key);
435 const std::lock_guard lock(mutex);
436 const auto cur_value = lru_.Get(key);
437
438 if (cur_value.has_value() && !IsExpired(cur_value->update_time, now) && pred(cur_value->value)) {
440 }
441}
442
443template <typename Key, typename Value, typename Hash, typename Equal>
444void ExpirableLruCache<Key, Value, Hash, Equal>::UpdateInBackground(const Key& key, UpdateValueFunc update_func) {
445 impl::CacheBackgroundUpdate(stats_);
446
447 // cache will wait for all detached tasks in ~ExpirableLruCache()
449 engine::AsyncNoSpan([token = wait_token_storage_.GetToken(), this, key, update_func = std::move(update_func)] {
450 auto mutex = mutex_set_.GetMutexForKey(key);
451 const std::unique_lock lock(mutex, std::try_to_lock);
452 if (!lock) {
453 // someone is updating the key right now
454 return;
455 }
456
457 auto now = utils::datetime::SteadyNow();
458 auto value = update_func(key);
459 lru_.Put(key, {value, now});
460 })
461 );
462}
463
464template <typename Key, typename Value, typename Hash, typename Equal>
465bool ExpirableLruCache<
466 Key,
467 Value,
468 Hash,
469 Equal>::IsExpired(std::chrono::steady_clock::time_point update_time, std::chrono::steady_clock::time_point now)
470 const {
471 auto max_lifetime = max_lifetime_.load();
472 return max_lifetime.count() != 0 && update_time + max_lifetime < now;
473}
474
475template <typename Key, typename Value, typename Hash, typename Equal>
476bool ExpirableLruCache<
477 Key,
478 Value,
479 Hash,
480 Equal>::ShouldUpdate(std::chrono::steady_clock::time_point update_time, std::chrono::steady_clock::time_point now)
481 const {
482 auto max_lifetime = max_lifetime_.load();
483 return (background_update_mode_.load() == BackgroundUpdateMode::kEnabled) && max_lifetime.count() != 0 &&
484 update_time + max_lifetime / 2 < now;
485}
486
487/// @brief Wrapper around @ref ExpirableLruCache that binds an update function
488/// for convenience.
489///
490/// @snippet core/src/cache/expirable_lru_cache_test.cpp Sample LruCacheWrapper
491template <typename Key, typename Value, typename Hash = std::hash<Key>, typename Equal = std::equal_to<Key>>
492class LruCacheWrapper final {
493public:
494 /// @brief The underlying cache type
495 using Cache = ExpirableLruCache<Key, Value, Hash, Equal>;
496 /// @brief Read mode for @ref Get (same as Cache::ReadMode)
497 using ReadMode = typename Cache::ReadMode;
498
499 /// @brief Constructs wrapper with shared cache and update function.
500 ///
501 /// The same \p update_func is used for all @ref Get and @ref GetOptional
502 /// calls.
503 ///
504 /// @param cache Shared pointer to @ref ExpirableLruCache.
505 /// @param update_func Function to compute value when key is missing or
506 /// expired.
507 LruCacheWrapper(std::shared_ptr<Cache> cache, typename Cache::UpdateValueFunc update_func)
508 : cache_(std::move(cache)),
509 update_func_(std::move(update_func))
510 {}
511
512 /// @brief Returns cached value or computes it if key is missing in cache.
513 ///
514 /// @param key Cache key.
515 /// @param read_mode If kUseCache, caches the computed value.
516 /// @return Cached or freshly computed value.
517 Value Get(const Key& key, ReadMode read_mode = ReadMode::kUseCache) {
518 return cache_->Get(key, update_func_, read_mode);
519 }
520
521 /// @brief Returns cached value or std::nullopt if key is missing in cache.
522 ///
523 /// @param key Cache key.
524 /// @return Value if key is in cache and not expired, otherwise std::nullopt.
525 std::optional<Value> GetOptional(const Key& key) { return cache_->GetOptional(key, update_func_); }
526
527 /// @brief Erases key from cache.
528 ///
529 /// @param key Cache key to erase.
530 void InvalidateByKey(const Key& key) { cache_->InvalidateByKey(key); }
531
532 /// @brief Erases key from cache if \p pred returns true for the current
533 /// value.
534 ///
535 /// @param key Cache key.
536 /// @param pred Predicate invoked with current value.
537 template <typename Predicate>
538 void InvalidateByKeyIf(const Key& key, Predicate pred) {
539 cache_->InvalidateByKeyIf(key, pred);
540 }
541
542 /// @brief Schedules background update of cached value for the key.
543 ///
544 /// @param key Cache key to update.
545 void UpdateInBackground(const Key& key) { cache_->UpdateInBackground(key, update_func_); }
546
547 /// @brief Returns raw cache pointer.
548 ///
549 /// @return Shared pointer to the underlying @ref ExpirableLruCache.
550 /// @note For internal use.
551 std::shared_ptr<Cache> GetCache() { return cache_; }
552
553private:
554 std::shared_ptr<Cache> cache_;
555 typename Cache::UpdateValueFunc update_func_;
556};
557
558template <typename Key, typename Value, typename Hash, typename Equal>
559void ExpirableLruCache<Key, Value, Hash, Equal>::Write(dump::Writer& writer) const {
560 utils::impl::UpdateGlobalTime();
561 lru_.Write(writer);
562}
563
564template <typename Key, typename Value, typename Hash, typename Equal>
565void ExpirableLruCache<Key, Value, Hash, Equal>::Read(dump::Reader& reader) {
566 utils::impl::UpdateGlobalTime();
567 lru_.Read(reader);
568}
569
570template <typename Key, typename Value, typename Hash, typename Equal>
571void ExpirableLruCache<Key, Value, Hash, Equal>::SetDumper(std::shared_ptr<dump::Dumper> dumper) {
572 lru_.SetDumper(std::move(dumper));
573}
574
575template <typename Key, typename Value, typename Hash, typename Equal>
576void DumpMetric(utils::statistics::Writer& writer, const ExpirableLruCache<Key, Value, Hash, Equal>& cache) {
577 writer["current-documents-count"] = cache.GetSizeApproximate();
578 writer = cache.GetStatistics();
579}
580
581} // namespace cache
582
583USERVER_NAMESPACE_END