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/// @ingroup userver_containers
19template <typename T, typename U, typename Hash = std::hash<T>, typename Equal = std::equal_to<T>>
20class NWayLRU final {
21public:
22 /// @param ways is the number of ways (a.k.a. shards, internal hash-maps),
23 /// into which elements are distributed based on their hash. Each shard is
24 /// protected by an individual mutex. Larger `ways` means more internal
25 /// hash-map instances and more memory usage, but less contention. A good
26 /// starting point is `ways=16`. If you encounter contention, you can increase
27 /// `ways` to something on the order of `256` or whatever your RAM constraints
28 /// allow.
29 ///
30 /// @param way_size is the maximum allowed amount of elements per way. When
31 /// the size of a way reaches this number, existing elements are deleted
32 /// according to the LRU policy.
33 ///
34 /// @param hash is the instance of `Hash` function to use, in case of a custom stateful `Hash`.
35 /// @param equal is the instance of `Equal` function to use, in case of a custom stateful `Equal`.
36 ///
37 /// The maximum total number of elements is `ways * way_size`.
38 NWayLRU(size_t ways, size_t way_size, const Hash& hash = Hash(), const Equal& equal = Equal());
39
40 void Put(const T& key, U value);
41
42 template <typename Validator>
43 std::optional<U> Get(const T& key, Validator validator);
44
45 std::optional<U> Get(const T& key) {
46 return Get(key, [](const U&) { return true; });
47 }
48
49 U GetOr(const T& key, const U& default_value);
50
51 void Invalidate();
52
53 void InvalidateByKey(const T& key);
54
55 /// Iterates over all items. May be slow for big caches.
56 template <typename Function>
57 void VisitAll(Function func) const;
58
59 size_t GetSize() const;
60
61 /// For the description of `way_size`,
62 /// see the cache::NWayLRU::NWayLRU constructor.
63 void UpdateWaySize(size_t way_size);
64
65 void Write(dump::Writer& writer) const;
66 void Read(dump::Reader& reader);
67
68 /// The dump::Dumper will be notified of any cache updates. This method is not
69 /// thread-safe.
70 void SetDumper(std::shared_ptr<dump::Dumper> dumper);
71
72private:
73 struct Way {
74 Way(Way&& other) noexcept : cache(std::move(other.cache)) {}
75
76 // max_size is not used, will be reset by Resize() in NWayLRU::NWayLRU
77 Way(const Hash& hash, const Equal& equal)
78 : cache(1, hash, equal)
79 {}
80
81 mutable engine::Mutex mutex;
82 LruMap<T, U, Hash, Equal> cache;
83 };
84
85 Way& GetWay(const T& key);
86
87 void NotifyDumper();
88
89 std::vector<Way> caches_;
90 Hash hash_fn_;
91 std::shared_ptr<dump::Dumper> dumper_{nullptr};
92};
93
94template <typename T, typename U, typename Hash, typename Eq>
95NWayLRU<T, U, Hash, Eq>::NWayLRU(size_t ways, size_t way_size, const Hash& hash, const Eq& equal)
96 : caches_(),
97 hash_fn_(hash)
98{
99 caches_.reserve(ways);
100 for (size_t i = 0; i < ways; ++i) {
101 caches_.emplace_back(hash, equal);
102 }
103 if (ways == 0) {
104 throw std::logic_error("Ways must be positive");
105 }
106
107 for (auto& way : caches_) {
108 way.cache.SetMaxSize(way_size);
109 }
110}
111
112template <typename T, typename U, typename Hash, typename Eq>
113void NWayLRU<T, U, Hash, Eq>::Put(const T& key, U value) {
114 auto& way = GetWay(key);
115 {
116 const std::unique_lock<engine::Mutex> lock(way.mutex);
117 way.cache.Put(key, std::move(value));
118 }
119 NotifyDumper();
120}
121
122template <typename T, typename U, typename Hash, typename Eq>
123template <typename Validator>
124std::optional<U> NWayLRU<T, U, Hash, Eq>::Get(const T& key, Validator validator) {
125 auto& way = GetWay(key);
126 const std::unique_lock<engine::Mutex> lock(way.mutex);
127 auto* value = way.cache.Get(key);
128
129 if (value) {
130 if (validator(*value)) {
131 return *value;
132 }
133 way.cache.Erase(key);
134 }
135
136 return std::nullopt;
137}
138
139template <typename T, typename U, typename Hash, typename Eq>
140void NWayLRU<T, U, Hash, Eq>::InvalidateByKey(const T& key) {
141 auto& way = GetWay(key);
142 {
143 const std::unique_lock<engine::Mutex> lock(way.mutex);
144 way.cache.Erase(key);
145 }
146 NotifyDumper();
147}
148
149template <typename T, typename U, typename Hash, typename Eq>
150U NWayLRU<T, U, Hash, Eq>::GetOr(const T& key, const U& default_value) {
151 auto& way = GetWay(key);
152 std::unique_lock<engine::Mutex> lock(way.mutex);
153 return way.cache.GetOr(key, default_value);
154}
155
156template <typename T, typename U, typename Hash, typename Eq>
157void NWayLRU<T, U, Hash, Eq>::Invalidate() {
158 for (auto& way : caches_) {
159 const std::unique_lock<engine::Mutex> lock(way.mutex);
160 way.cache.Clear();
161 }
162 NotifyDumper();
163}
164
165template <typename T, typename U, typename Hash, typename Eq>
166template <typename Function>
167void NWayLRU<T, U, Hash, Eq>::VisitAll(Function func) const {
168 for (const auto& way : caches_) {
169 std::unique_lock<engine::Mutex> lock(way.mutex);
170 way.cache.VisitAll(func);
171 }
172}
173
174template <typename T, typename U, typename Hash, typename Eq>
175size_t NWayLRU<T, U, Hash, Eq>::GetSize() const {
176 size_t size{0};
177 for (const auto& way : caches_) {
178 const std::unique_lock<engine::Mutex> lock(way.mutex);
179 size += way.cache.GetSize();
180 }
181 return size;
182}
183
184template <typename T, typename U, typename Hash, typename Eq>
185void NWayLRU<T, U, Hash, Eq>::UpdateWaySize(size_t way_size) {
186 for (auto& way : caches_) {
187 const std::unique_lock<engine::Mutex> lock(way.mutex);
188 way.cache.SetMaxSize(way_size);
189 }
190}
191
192template <typename T, typename U, typename Hash, typename Eq>
193typename NWayLRU<T, U, Hash, Eq>::Way& NWayLRU<T, U, Hash, Eq>::GetWay(const T& key) {
194 /// It is needed to twist hash because there is hash map in LruMap. Otherwise
195 /// nodes will fall into one bucket. According to
196 /// https://www.boost.org/doc/libs/1_83_0/libs/container_hash/doc/html/hash.html#notes_hash_combine
197 /// hash_combine can be treated as hash itself
198 auto seed = hash_fn_(key);
199 boost::hash_combine(seed, 0);
200 auto n = seed % caches_.size();
201 return caches_[n];
202}
203
204template <typename T, typename U, typename Hash, typename Equal>
205void NWayLRU<T, U, Hash, Equal>::Write(dump::Writer& writer) const {
206 writer.Write(caches_.size());
207
208 for (const Way& way : caches_) {
209 const std::unique_lock<engine::Mutex> lock(way.mutex);
210
211 writer.Write(way.cache.GetSize());
212
213 way.cache.VisitAll([&writer](const T& key, const U& value) {
214 writer.Write(key);
215 writer.Write(value);
216 });
217 }
218}
219
220template <typename T, typename U, typename Hash, typename Equal>
221void NWayLRU<T, U, Hash, Equal>::Read(dump::Reader& reader) {
222 Invalidate();
223
224 const auto ways = reader.Read<std::size_t>();
225 for (std::size_t i = 0; i < ways; ++i) {
226 const auto elements_in_way = reader.Read<std::size_t>();
227 for (std::size_t j = 0; j < elements_in_way; ++j) {
228 auto key = reader.Read<T>();
229 auto value = reader.Read<U>();
230 Put(std::move(key), std::move(value));
231 }
232 }
233}
234
235template <typename T, typename U, typename Hash, typename Equal>
236void NWayLRU<T, U, Hash, Equal>::NotifyDumper() {
237 if (dumper_ != nullptr) {
238 dumper_->OnUpdateCompleted();
239 }
240}
241
242template <typename T, typename U, typename Hash, typename Equal>
243void NWayLRU<T, U, Hash, Equal>::SetDumper(std::shared_ptr<dump::Dumper> dumper) {
244 dumper_ = std::move(dumper);
245}
246
247} // namespace cache
248
249USERVER_NAMESPACE_END