userver: userver/cache/expirable_lru_cache.hpp Source File
⚠️ This is the documentation for an old userver version. Click here to switch to the latest version.
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
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
21USERVER_NAMESPACE_BEGIN
22
23namespace cache {
24
25namespace impl {
26
27template <typename Value>
28struct ExpirableValue final {
29 Value value;
30 std::chrono::steady_clock::time_point update_time;
31};
32
33template <typename Value>
34void Write(dump::Writer& writer, const impl::ExpirableValue<Value>& value) {
35 const auto [now, steady_now] = utils::impl::GetGlobalTime();
36 writer.Write(value.value);
37 writer.Write(value.update_time - steady_now + now);
38}
39
40template <typename Value>
41impl::ExpirableValue<Value> Read(dump::Reader& reader,
42 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 reader.Read<Value>(),
47 reader.Read<std::chrono::system_clock::time_point>() - now + steady_now};
48}
49
50} // namespace impl
51
52/// @ingroup userver_containers
53/// @brief Class for expirable LRU cache. Use cache::LruMap for not expirable
54/// LRU Cache.
55///
56/// Example usage:
57///
58/// @snippet cache/expirable_lru_cache_test.cpp Sample ExpirableLruCache
59template <typename Key, typename Value, typename Hash = std::hash<Key>,
60 typename Equal = std::equal_to<Key>>
61class ExpirableLruCache final {
62 public:
63 using UpdateValueFunc = std::function<Value(const Key&)>;
64
65 /// Cache read mode
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 ExpirableLruCache(size_t ways, size_t way_size, const Hash& hash = Hash(),
72 const Equal& equal = Equal());
73
74 ~ExpirableLruCache();
75
76 void SetWaySize(size_t way_size);
77
78 std::chrono::milliseconds GetMaxLifetime() const noexcept;
79
80 void SetMaxLifetime(std::chrono::milliseconds max_lifetime);
81
82 /**
83 * Sets background update mode. If "background_update" mode is kDisabled,
84 * expiring values are not updated in background (asynchronously) or are
85 * updated if "background_update" is kEnabled.
86 */
87 void SetBackgroundUpdate(BackgroundUpdateMode background_update);
88
89 /**
90 * @returns GetOptional("key", update_func) if it is not std::nullopt.
91 * Otherwise the result of update_func(key) is returned, and additionally
92 * stored in cache if "read_mode" is kUseCache.
93 */
94 Value Get(const Key& key, const UpdateValueFunc& update_func,
95 ReadMode read_mode = ReadMode::kUseCache);
96
97 /**
98 * Update value in cache by "update_func" if background update mode is
99 * kEnabled and "key" is in cache and not expired but its lifetime ends soon.
100 * @returns value by key if key is in cache and not expired, or std::nullopt
101 * otherwise
102 */
103 std::optional<Value> GetOptional(const Key& key,
104 const UpdateValueFunc& update_func);
105
106 /**
107 * GetOptional, but without expiry checks and value updates.
108 *
109 * Used during fallback in FallbackELruCache.
110 */
111 std::optional<Value> GetOptionalUnexpirable(const Key& key);
112
113 /**
114 * GetOptional, but without expiry check.
115 *
116 * Used during fallback in FallbackELruCache.
117 */
119 const Key& key, const UpdateValueFunc& update_func);
120
121 /**
122 * GetOptional, but without value updates.
123 */
124 std::optional<Value> GetOptionalNoUpdate(const Key& key);
125
126 void Put(const Key& key, const Value& value);
127
128 void Put(const Key& key, Value&& value);
129
130 const impl::ExpirableLruCacheStatistics& GetStatistics() const;
131
132 size_t GetSizeApproximate() const;
133
134 /// Clear cache
136
137 /// Erase key from cache
138 void InvalidateByKey(const Key& key);
139
140 /// Add async task for updating value by update_func(key)
141 void UpdateInBackground(const Key& key, UpdateValueFunc update_func);
142
143 void Write(dump::Writer& writer) const;
144
145 void Read(dump::Reader& reader);
146
147 /// The dump::Dumper will be notified of any cache updates. This method is not
148 /// thread-safe.
149 void SetDumper(std::shared_ptr<dump::Dumper> dumper);
150
151 private:
152 bool IsExpired(std::chrono::steady_clock::time_point update_time,
153 std::chrono::steady_clock::time_point now) const;
154
155 bool ShouldUpdate(std::chrono::steady_clock::time_point update_time,
156 std::chrono::steady_clock::time_point now) const;
157
158 cache::NWayLRU<Key, impl::ExpirableValue<Value>, Hash, Equal> lru_;
159 std::atomic<std::chrono::milliseconds> max_lifetime_{
160 std::chrono::milliseconds(0)};
161 std::atomic<BackgroundUpdateMode> background_update_mode_{
162 BackgroundUpdateMode::kDisabled};
163 impl::ExpirableLruCacheStatistics stats_;
164 concurrent::MutexSet<Key, Hash, Equal> mutex_set_;
165 utils::impl::WaitTokenStorage wait_token_storage_;
166};
167
168template <typename Key, typename Value, typename Hash, typename Equal>
169ExpirableLruCache<Key, Value, Hash, Equal>::ExpirableLruCache(
170 size_t ways, size_t way_size, const Hash& hash, const Equal& equal)
171 : lru_(ways, way_size, hash, equal),
172 mutex_set_{ways, way_size, hash, equal} {}
173
174template <typename Key, typename Value, typename Hash, typename Equal>
175ExpirableLruCache<Key, Value, Hash, Equal>::~ExpirableLruCache() {
176 wait_token_storage_.WaitForAllTokens();
177}
178
179template <typename Key, typename Value, typename Hash, typename Equal>
180void ExpirableLruCache<Key, Value, Hash, Equal>::SetWaySize(size_t way_size) {
181 lru_.UpdateWaySize(way_size);
182}
183
184template <typename Key, typename Value, typename Hash, typename Equal>
185std::chrono::milliseconds
186ExpirableLruCache<Key, Value, Hash, Equal>::GetMaxLifetime() const noexcept {
187 return max_lifetime_.load();
188}
189
190template <typename Key, typename Value, typename Hash, typename Equal>
191void ExpirableLruCache<Key, Value, Hash, Equal>::SetMaxLifetime(
192 std::chrono::milliseconds max_lifetime) {
193 max_lifetime_ = max_lifetime;
194}
195
196template <typename Key, typename Value, typename Hash, typename Equal>
197void ExpirableLruCache<Key, Value, Hash, Equal>::SetBackgroundUpdate(
198 BackgroundUpdateMode background_update) {
199 background_update_mode_ = background_update;
200}
201
202template <typename Key, typename Value, typename Hash, typename Equal>
203Value ExpirableLruCache<Key, Value, Hash, Equal>::Get(
204 const Key& key, const UpdateValueFunc& update_func, ReadMode read_mode) {
205 auto now = utils::datetime::SteadyNow();
206 auto opt_old_value = GetOptional(key, update_func);
207 if (opt_old_value) {
208 return std::move(*opt_old_value);
209 }
210
211 auto mutex = mutex_set_.GetMutexForKey(key);
212 std::lock_guard lock(mutex);
213 // Test one more time - concurrent ExpirableLruCache::Get()
214 // might have put the value
215 auto old_value = lru_.Get(key);
216 if (old_value && !IsExpired(old_value->update_time, now)) {
217 return std::move(old_value->value);
218 }
219
220 auto value = update_func(key);
221 if (read_mode == ReadMode::kUseCache) {
222 lru_.Put(key, {value, now});
223 }
224 return value;
225}
226
227template <typename Key, typename Value, typename Hash, typename Equal>
228std::optional<Value> ExpirableLruCache<Key, Value, Hash, Equal>::GetOptional(
229 const Key& key, const UpdateValueFunc& update_func) {
230 auto now = utils::datetime::SteadyNow();
231 auto old_value = lru_.Get(key);
232
233 if (old_value) {
234 if (!IsExpired(old_value->update_time, now)) {
235 impl::CacheHit(stats_);
236
237 if (ShouldUpdate(old_value->update_time, now)) {
238 UpdateInBackground(key, update_func);
239 }
240
241 return std::move(old_value->value);
242 } else {
243 impl::CacheStale(stats_);
244 }
245 }
246 impl::CacheMiss(stats_);
247
248 return std::nullopt;
249}
250
251template <typename Key, typename Value, typename Hash, typename Equal>
252std::optional<Value>
253ExpirableLruCache<Key, Value, Hash, Equal>::GetOptionalUnexpirable(
254 const Key& key) {
255 auto old_value = lru_.Get(key);
256
257 if (old_value) {
258 impl::CacheHit(stats_);
259 return old_value->value;
260 }
261 impl::CacheMiss(stats_);
262
263 return std::nullopt;
264}
265
266template <typename Key, typename Value, typename Hash, typename Equal>
267std::optional<Value>
268ExpirableLruCache<Key, Value, Hash, Equal>::GetOptionalUnexpirableWithUpdate(
269 const Key& key, const UpdateValueFunc& update_func) {
270 auto now = utils::datetime::SteadyNow();
271 auto old_value = lru_.Get(key);
272
273 if (old_value) {
274 impl::CacheHit(stats_);
275
276 if (ShouldUpdate(old_value->update_time, now)) {
277 UpdateInBackground(key, update_func);
278 }
279
280 return old_value->value;
281 }
282 impl::CacheMiss(stats_);
283
284 return std::nullopt;
285}
286
287template <typename Key, typename Value, typename Hash, typename Equal>
288std::optional<Value>
289ExpirableLruCache<Key, Value, Hash, Equal>::GetOptionalNoUpdate(
290 const Key& key) {
291 auto now = utils::datetime::SteadyNow();
292 auto old_value = lru_.Get(key);
293
294 if (old_value) {
295 if (!IsExpired(old_value->update_time, now)) {
296 impl::CacheHit(stats_);
297
298 return old_value->value;
299 } else {
300 impl::CacheStale(stats_);
301 }
302 }
303 impl::CacheMiss(stats_);
304
305 return std::nullopt;
306}
307
308template <typename Key, typename Value, typename Hash, typename Equal>
309void ExpirableLruCache<Key, Value, Hash, Equal>::Put(const Key& key,
310 const Value& value) {
311 lru_.Put(key, {value, utils::datetime::SteadyNow()});
312}
313
314template <typename Key, typename Value, typename Hash, typename Equal>
315void ExpirableLruCache<Key, Value, Hash, Equal>::Put(const Key& key,
316 Value&& value) {
317 lru_.Put(key, {std::move(value), utils::datetime::SteadyNow()});
318}
319
320template <typename Key, typename Value, typename Hash, typename Equal>
321const impl::ExpirableLruCacheStatistics&
322ExpirableLruCache<Key, Value, Hash, Equal>::GetStatistics() const {
323 return stats_;
324}
325
326template <typename Key, typename Value, typename Hash, typename Equal>
327size_t ExpirableLruCache<Key, Value, Hash, Equal>::GetSizeApproximate() const {
328 return lru_.GetSize();
329}
330
331template <typename Key, typename Value, typename Hash, typename Equal>
332void ExpirableLruCache<Key, Value, Hash, Equal>::Invalidate() {
333 lru_.Invalidate();
334}
335
336template <typename Key, typename Value, typename Hash, typename Equal>
337void ExpirableLruCache<Key, Value, Hash, Equal>::InvalidateByKey(
338 const Key& key) {
339 lru_.InvalidateByKey(key);
340}
341
342template <typename Key, typename Value, typename Hash, typename Equal>
343void ExpirableLruCache<Key, Value, Hash, Equal>::UpdateInBackground(
344 const Key& key, UpdateValueFunc update_func) {
345 stats_.total.background_updates++;
346 stats_.recent.GetCurrentCounter().background_updates++;
347
348 // cache will wait for all detached tasks in ~ExpirableLruCache()
349 engine::AsyncNoSpan([token = wait_token_storage_.GetToken(), this, key,
350 update_func = std::move(update_func)] {
351 auto mutex = mutex_set_.GetMutexForKey(key);
352 std::unique_lock lock(mutex, std::try_to_lock);
353 if (!lock) {
354 // someone is updating the key right now
355 return;
356 }
357
358 auto now = utils::datetime::SteadyNow();
359 auto value = update_func(key);
360 lru_.Put(key, {value, now});
361 }).Detach();
362}
363
364template <typename Key, typename Value, typename Hash, typename Equal>
365bool ExpirableLruCache<Key, Value, Hash, Equal>::IsExpired(
366 std::chrono::steady_clock::time_point update_time,
367 std::chrono::steady_clock::time_point now) const {
368 auto max_lifetime = max_lifetime_.load();
369 return max_lifetime.count() != 0 && update_time + max_lifetime < now;
370}
371
372template <typename Key, typename Value, typename Hash, typename Equal>
373bool ExpirableLruCache<Key, Value, Hash, Equal>::ShouldUpdate(
374 std::chrono::steady_clock::time_point update_time,
375 std::chrono::steady_clock::time_point now) const {
376 auto max_lifetime = max_lifetime_.load();
377 return (background_update_mode_.load() == BackgroundUpdateMode::kEnabled) &&
378 max_lifetime.count() != 0 && update_time + max_lifetime / 2 < now;
379}
380
381template <typename Key, typename Value, typename Hash = std::hash<Key>,
382 typename Equal = std::equal_to<Key>>
383class LruCacheWrapper final {
384 public:
385 using Cache = ExpirableLruCache<Key, Value, Hash, Equal>;
386 using ReadMode = typename Cache::ReadMode;
387
388 LruCacheWrapper(std::shared_ptr<Cache> cache,
389 typename Cache::UpdateValueFunc update_func)
390 : cache_(std::move(cache)), update_func_(std::move(update_func)) {}
391
392 /// Get cached value or evaluates if "key" is missing in cache
393 Value Get(const Key& key, ReadMode read_mode = ReadMode::kUseCache) {
394 return cache_->Get(key, update_func_, read_mode);
395 }
396
397 /// Get cached value or "nullopt" if "key" is missing in cache
398 std::optional<Value> GetOptional(const Key& key) {
399 return cache_->GetOptional(key, update_func_);
400 }
401
402 void InvalidateByKey(const Key& key) { cache_->InvalidateByKey(key); }
403
404 /// Update cached value in background
405 void UpdateInBackground(const Key& key) {
406 cache_->UpdateInBackground(key, update_func_);
407 }
408
409 /// Get raw cache. For internal use.
410 std::shared_ptr<Cache> GetCache() { return cache_; }
411
412 private:
413 std::shared_ptr<Cache> cache_;
414 typename Cache::UpdateValueFunc update_func_;
415};
416
417template <typename Key, typename Value, typename Hash, typename Equal>
418void ExpirableLruCache<Key, Value, Hash, Equal>::Write(
419 dump::Writer& writer) const {
420 utils::impl::UpdateGlobalTime();
421 lru_.Write(writer);
422}
423
424template <typename Key, typename Value, typename Hash, typename Equal>
425void ExpirableLruCache<Key, Value, Hash, Equal>::Read(dump::Reader& reader) {
426 utils::impl::UpdateGlobalTime();
427 lru_.Read(reader);
428}
429
430template <typename Key, typename Value, typename Hash, typename Equal>
431void ExpirableLruCache<Key, Value, Hash, Equal>::SetDumper(
432 std::shared_ptr<dump::Dumper> dumper) {
433 lru_.SetDumper(std::move(dumper));
434}
435
436template <typename Key, typename Value, typename Hash, typename Equal>
437void DumpMetric(utils::statistics::Writer& writer,
438 const ExpirableLruCache<Key, Value, Hash, Equal>& cache) {
439 writer["current-documents-count"] = cache.GetSizeApproximate();
440 writer = cache.GetStatistics();
441}
442
443} // namespace cache
444
445USERVER_NAMESPACE_END