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