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