userver: userver/cache/nway_lru_cache.hpp Source File
Loading...
Searching...
No Matches
nway_lru_cache.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/cache/nway_lru_cache.hpp
4/// @brief @copybrief cache::NWayLRU
5
6#include <functional>
7#include <optional>
8#include <vector>
9
10#include <boost/container_hash/hash.hpp>
11
12#include <userver/cache/lru_map.hpp>
13#include <userver/dump/dumper.hpp>
14#include <userver/dump/operations.hpp>
15#include <userver/engine/mutex.hpp>
16
17USERVER_NAMESPACE_BEGIN
18
19namespace cache {
20
21/// @brief N-way LRU cache with per-way mutexes and optional dump support.
22/// @ingroup userver_containers
23///
24/// @snippet core/src/cache/nway_lru_cache_test.cpp NWayLRU basic
25template <typename T, typename U, typename Hash = std::hash<T>, typename Equal = std::equal_to<T>>
26class NWayLRU final {
27public:
28 /// @brief Constructs an N-way LRU cache with the given number of ways and
29 /// maximum elements per way.
30 ///
31 /// The maximum total number of elements is `ways * way_size`.
32 ///
33 /// @param ways Number of ways (a.k.a. shards, internal hash-maps), into
34 /// which elements are distributed based on their hash. Each shard is
35 /// protected by an individual mutex. Larger \p ways means more internal
36 /// hash-map instances and more memory usage, but less contention. A good
37 /// starting point is `ways=16`. If you encounter contention, you can
38 /// increase \p ways to something on the order of `256` or whatever your
39 /// RAM constraints allow.
40 /// @param way_size Maximum allowed amount of elements per way. When the
41 /// size of a way reaches this number, existing elements are deleted
42 /// according to the LRU policy.
43 /// @param hash Instance of \p Hash function to use, in case of a custom
44 /// stateful Hash.
45 /// @param equal Instance of \p Equal function to use, in case of a custom
46 /// stateful Equal.
47 /// @throws std::logic_error if \p ways is zero.
48 NWayLRU(size_t ways, size_t way_size, const Hash& hash = Hash(), const Equal& equal = Equal());
49
50 /// @brief Stores value by key.
51 /// @param key Key to store the value under.
52 /// @param value Value to store.
53 void Put(const T& key, U value);
54
55 /// @brief Returns cached value by key if validator returns true for it.
56 /// @param key Key to look up.
57 /// @param validator Callable that takes the cached value and returns false
58 /// to invalidate the entry.
59 /// @return Cached value if present and validator returned true, otherwise
60 /// std::nullopt.
61 /// @note If validator returns false, the entry is removed from the cache.
62 template <typename Validator>
63 std::optional<U> Get(const T& key, Validator validator);
64
65 /// @brief Returns cached value by key if present; otherwise std::nullopt.
66 /// @param key Key to look up.
67 /// @return Cached value if present, otherwise std::nullopt.
68 /// @note Equivalent to Get(key, [](const U&) { return true; }).
69 std::optional<U> Get(const T& key) {
70 return Get(key, [](const U&) { return true; });
71 }
72
73 /// @brief Returns cached value by key if present; otherwise returns
74 /// default_value.
75 /// @param key Key to look up.
76 /// @param default_value Value to return when key is not found.
77 /// @return Cached value if present, otherwise \p default_value.
78 U GetOr(const T& key, const U& default_value);
79
80 /// @brief Removes all entries from the cache.
81 void Invalidate();
82
83 /// @brief Removes the entry for the given key if present.
84 /// @param key Key whose entry is to be removed.
85 void InvalidateByKey(const T& key);
86
87 /// @brief Iterates over all items, invoking \p func for each (key, value).
88 /// @param func Callable to invoke for each item (e.g. void(const T&, const U&)).
89 /// @note May be slow for big caches.
90 template <typename Function>
91 void VisitAll(Function func) const;
92
93 /// @brief Returns the total number of elements in the cache across all ways.
94 /// @return Total number of elements.
95 size_t GetSize() const;
96
97 /// @brief Updates maximum elements per way.
98 /// @param way_size New maximum elements per way; see
99 /// @ref cache::NWayLRU::NWayLRU "NWayLRU constructor" for the semantics.
100 void UpdateWaySize(size_t way_size);
101
102 /// @brief Serializes the cache contents to the dump writer.
103 /// @param writer Target @ref dump::Writer.
104 void Write(dump::Writer& writer) const;
105
106 /// @brief Deserializes the cache contents from the dump reader.
107 /// @param reader Source @ref dump::Reader.
108 /// @note Clears existing entries before loading.
109 void Read(dump::Reader& reader);
110
111 /// @brief Sets the dumper; it will be notified of any cache updates.
112 /// @param dumper Instance of @ref dump::Dumper, or nullptr to clear.
113 /// @note This method is not thread-safe.
114 void SetDumper(std::shared_ptr<dump::Dumper> dumper);
115
116private:
117 struct Way {
118 Way(Way&& other) noexcept : cache(std::move(other.cache)) {}
119
120 // max_size is not used, will be reset by Resize() in NWayLRU::NWayLRU
121 Way(const Hash& hash, const Equal& equal)
122 : cache(1, hash, equal)
123 {}
124
125 mutable engine::Mutex mutex;
126 LruMap<T, U, Hash, Equal> cache;
127 };
128
129 Way& GetWay(const T& key);
130
131 void NotifyDumper();
132
133 std::vector<Way> caches_;
134 Hash hash_fn_;
135 std::shared_ptr<dump::Dumper> dumper_{nullptr};
136};
137
138template <typename T, typename U, typename Hash, typename Eq>
139NWayLRU<T, U, Hash, Eq>::NWayLRU(size_t ways, size_t way_size, const Hash& hash, const Eq& equal)
140 : caches_(),
141 hash_fn_(hash)
142{
143 caches_.reserve(ways);
144 for (size_t i = 0; i < ways; ++i) {
145 caches_.emplace_back(hash, equal);
146 }
147 if (ways == 0) {
148 throw std::logic_error("Ways must be positive");
149 }
150
151 for (auto& way : caches_) {
152 way.cache.SetMaxSize(way_size);
153 }
154}
155
156template <typename T, typename U, typename Hash, typename Eq>
157void NWayLRU<T, U, Hash, Eq>::Put(const T& key, U value) {
158 auto& way = GetWay(key);
159 {
160 const std::unique_lock lock{way.mutex};
161 way.cache.Put(key, std::move(value));
162 }
163 NotifyDumper();
164}
165
166template <typename T, typename U, typename Hash, typename Eq>
167template <typename Validator>
168std::optional<U> NWayLRU<T, U, Hash, Eq>::Get(const T& key, Validator validator) {
169 auto& way = GetWay(key);
170 const std::unique_lock lock{way.mutex};
171 auto* value = way.cache.Get(key);
172
173 if (value) {
174 if (validator(*value)) {
175 return *value;
176 }
177 way.cache.Erase(key);
178 }
179
180 return std::nullopt;
181}
182
183template <typename T, typename U, typename Hash, typename Eq>
184void NWayLRU<T, U, Hash, Eq>::InvalidateByKey(const T& key) {
185 auto& way = GetWay(key);
186 {
187 const std::unique_lock lock{way.mutex};
188 way.cache.Erase(key);
189 }
190 NotifyDumper();
191}
192
193template <typename T, typename U, typename Hash, typename Eq>
194U NWayLRU<T, U, Hash, Eq>::GetOr(const T& key, const U& default_value) {
195 auto& way = GetWay(key);
196 const std::unique_lock lock{way.mutex};
197 return way.cache.GetOr(key, default_value);
198}
199
200template <typename T, typename U, typename Hash, typename Eq>
201void NWayLRU<T, U, Hash, Eq>::Invalidate() {
202 for (auto& way : caches_) {
203 const std::unique_lock lock{way.mutex};
204 way.cache.Clear();
205 }
206 NotifyDumper();
207}
208
209template <typename T, typename U, typename Hash, typename Eq>
210template <typename Function>
211void NWayLRU<T, U, Hash, Eq>::VisitAll(Function func) const {
212 for (const auto& way : caches_) {
213 const std::unique_lock lock{way.mutex};
214 way.cache.VisitAll(func);
215 }
216}
217
218template <typename T, typename U, typename Hash, typename Eq>
219size_t NWayLRU<T, U, Hash, Eq>::GetSize() const {
220 size_t size{0};
221 for (const auto& way : caches_) {
222 const std::unique_lock lock{way.mutex};
223 size += way.cache.GetSize();
224 }
225 return size;
226}
227
228template <typename T, typename U, typename Hash, typename Eq>
229void NWayLRU<T, U, Hash, Eq>::UpdateWaySize(size_t way_size) {
230 for (auto& way : caches_) {
231 const std::unique_lock lock{way.mutex};
232 way.cache.SetMaxSize(way_size);
233 }
234}
235
236template <typename T, typename U, typename Hash, typename Eq>
237typename NWayLRU<T, U, Hash, Eq>::Way& NWayLRU<T, U, Hash, Eq>::GetWay(const T& key) {
238 /// It is needed to twist hash because there is hash map in LruMap. Otherwise
239 /// nodes will fall into one bucket. According to
240 /// https://www.boost.org/doc/libs/1_83_0/libs/container_hash/doc/html/hash.html#notes_hash_combine
241 /// hash_combine can be treated as hash itself
242 auto seed = hash_fn_(key);
243 boost::hash_combine(seed, 0);
244 auto n = seed % caches_.size();
245 return caches_[n];
246}
247
248template <typename T, typename U, typename Hash, typename Equal>
249void NWayLRU<T, U, Hash, Equal>::Write(dump::Writer& writer) const {
250 writer.Write(caches_.size());
251
252 for (const Way& way : caches_) {
253 const std::unique_lock lock{way.mutex};
254
255 writer.Write(way.cache.GetSize());
256
257 way.cache.VisitAll([&writer](const T& key, const U& value) {
258 writer.Write(key);
259 writer.Write(value);
260 });
261 }
262}
263
264template <typename T, typename U, typename Hash, typename Equal>
265void NWayLRU<T, U, Hash, Equal>::Read(dump::Reader& reader) {
267
268 const auto ways = reader.Read<std::size_t>();
269 for (std::size_t i = 0; i < ways; ++i) {
270 const auto elements_in_way = reader.Read<std::size_t>();
271 for (std::size_t j = 0; j < elements_in_way; ++j) {
272 auto key = reader.Read<T>();
273 auto value = reader.Read<U>();
274 Put(std::move(key), std::move(value));
275 }
276 }
277}
278
279template <typename T, typename U, typename Hash, typename Equal>
280void NWayLRU<T, U, Hash, Equal>::NotifyDumper() {
281 if (dumper_ != nullptr) {
282 dumper_->OnUpdateCompleted();
283 }
284}
285
286template <typename T, typename U, typename Hash, typename Equal>
287void NWayLRU<T, U, Hash, Equal>::SetDumper(std::shared_ptr<dump::Dumper> dumper) {
288 dumper_ = std::move(dumper);
289}
290
291} // namespace cache
292
293USERVER_NAMESPACE_END