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