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