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
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