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 /// The maximum total number of elements is `ways * way_size`.
35 NWayLRU(size_t ways, size_t way_size, const Hash& hash = Hash(), 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
69private:
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, const Eq& equal)
91 : caches_(), hash_fn_(hash) {
92 caches_.reserve(ways);
93 for (size_t i = 0; i < ways; ++i) caches_.emplace_back(hash, equal);
94 if (ways == 0) throw std::logic_error("Ways must be positive");
95
96 for (auto& way : caches_) way.cache.SetMaxSize(way_size);
97}
98
99template <typename T, typename U, typename Hash, typename Eq>
100void NWayLRU<T, U, Hash, Eq>::Put(const T& key, U value) {
101 auto& way = GetWay(key);
102 {
103 std::unique_lock<engine::Mutex> lock(way.mutex);
104 way.cache.Put(key, std::move(value));
105 }
106 NotifyDumper();
107}
108
109template <typename T, typename U, typename Hash, typename Eq>
110template <typename Validator>
111std::optional<U> NWayLRU<T, U, Hash, Eq>::Get(const T& key, Validator validator) {
112 auto& way = GetWay(key);
113 std::unique_lock<engine::Mutex> lock(way.mutex);
114 auto* value = way.cache.Get(key);
115
116 if (value) {
117 if (validator(*value)) return *value;
118 way.cache.Erase(key);
119 }
120
121 return std::nullopt;
122}
123
124template <typename T, typename U, typename Hash, typename Eq>
125void NWayLRU<T, U, Hash, Eq>::InvalidateByKey(const T& key) {
126 auto& way = GetWay(key);
127 {
128 std::unique_lock<engine::Mutex> lock(way.mutex);
129 way.cache.Erase(key);
130 }
131 NotifyDumper();
132}
133
134template <typename T, typename U, typename Hash, typename Eq>
135U NWayLRU<T, U, Hash, Eq>::GetOr(const T& key, const U& default_value) {
136 auto& way = GetWay(key);
137 std::unique_lock<engine::Mutex> lock(way.mutex);
138 return way.cache.GetOr(key, default_value);
139}
140
141template <typename T, typename U, typename Hash, typename Eq>
142void NWayLRU<T, U, Hash, Eq>::Invalidate() {
143 for (auto& way : caches_) {
144 std::unique_lock<engine::Mutex> lock(way.mutex);
145 way.cache.Clear();
146 }
147 NotifyDumper();
148}
149
150template <typename T, typename U, typename Hash, typename Eq>
151template <typename Function>
152void NWayLRU<T, U, Hash, Eq>::VisitAll(Function func) const {
153 for (const auto& way : caches_) {
154 std::unique_lock<engine::Mutex> lock(way.mutex);
155 way.cache.VisitAll(func);
156 }
157}
158
159template <typename T, typename U, typename Hash, typename Eq>
160size_t NWayLRU<T, U, Hash, Eq>::GetSize() const {
161 size_t size{0};
162 for (const auto& way : caches_) {
163 std::unique_lock<engine::Mutex> lock(way.mutex);
164 size += way.cache.GetSize();
165 }
166 return size;
167}
168
169template <typename T, typename U, typename Hash, typename Eq>
170void NWayLRU<T, U, Hash, Eq>::UpdateWaySize(size_t way_size) {
171 for (auto& way : caches_) {
172 std::unique_lock<engine::Mutex> lock(way.mutex);
173 way.cache.SetMaxSize(way_size);
174 }
175}
176
177template <typename T, typename U, typename Hash, typename Eq>
178typename NWayLRU<T, U, Hash, Eq>::Way& NWayLRU<T, U, Hash, Eq>::GetWay(const T& key) {
179 /// It is needed to twist hash because there is hash map in LruMap. Otherwise
180 /// nodes will fall into one bucket. According to
181 /// https://www.boost.org/doc/libs/1_83_0/libs/container_hash/doc/html/hash.html#notes_hash_combine
182 /// hash_combine can be treated as hash itself
183 auto seed = hash_fn_(key);
184 boost::hash_combine(seed, 0);
185 auto n = seed % caches_.size();
186 return caches_[n];
187}
188
189template <typename T, typename U, typename Hash, typename Equal>
190void NWayLRU<T, U, Hash, Equal>::Write(dump::Writer& writer) const {
191 writer.Write(caches_.size());
192
193 for (const Way& way : caches_) {
194 std::unique_lock<engine::Mutex> lock(way.mutex);
195
196 writer.Write(way.cache.GetSize());
197
198 way.cache.VisitAll([&writer](const T& key, const U& value) {
199 writer.Write(key);
200 writer.Write(value);
201 });
202 }
203}
204
205template <typename T, typename U, typename Hash, typename Equal>
206void NWayLRU<T, U, Hash, Equal>::Read(dump::Reader& reader) {
207 Invalidate();
208
209 const auto ways = reader.Read<std::size_t>();
210 for (std::size_t i = 0; i < ways; ++i) {
211 const auto elements_in_way = reader.Read<std::size_t>();
212 for (std::size_t j = 0; j < elements_in_way; ++j) {
213 auto key = reader.Read<T>();
214 auto value = reader.Read<U>();
215 Put(std::move(key), std::move(value));
216 }
217 }
218}
219
220template <typename T, typename U, typename Hash, typename Equal>
221void NWayLRU<T, U, Hash, Equal>::NotifyDumper() {
222 if (dumper_ != nullptr) {
223 dumper_->OnUpdateCompleted();
224 }
225}
226
227template <typename T, typename U, typename Hash, typename Equal>
228void NWayLRU<T, U, Hash, Equal>::SetDumper(std::shared_ptr<dump::Dumper> dumper) {
229 dumper_ = std::move(dumper);
230}
231
232} // namespace cache
233
234USERVER_NAMESPACE_END