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