userver: /data/code/userver/libraries/multi-index-lru/src/expirable_container_test.cpp Source File
Loading...
Searching...
No Matches
expirable_container_test.cpp
1#include <userver/engine/mutex.hpp>
2#include <userver/engine/sleep.hpp>
3#include <userver/multi-index-lru/expirable_container.hpp>
4#include <userver/utest/utest.hpp>
5#include <userver/utils/async.hpp>
6#include <userver/utils/mock_now.hpp>
7
8#include <iterator>
9#include <mutex>
10#include <string>
11
12#include <boost/multi_index/hashed_index.hpp>
13#include <boost/multi_index/member.hpp>
14#include <boost/multi_index/ordered_index.hpp>
15
16USERVER_NAMESPACE_BEGIN
17
18namespace {
19class ExpirableUsersTest : public ::testing::Test {
20protected:
21 void SetUp() override {}
22
23 struct IdTag {};
24 struct EmailTag {};
25 struct NameTag {};
26
27 struct User {
28 int id;
29 std::string email;
30 std::string name;
31
32 bool operator==(const User& other) const {
33 return id == other.id && email == other.email && name == other.name;
34 }
35 };
36
37 using UserCacheExpirable = multi_index_lru::ExpirableContainer<
38 User,
39 boost::multi_index::indexed_by<
40 boost::multi_index::ordered_unique<
41 boost::multi_index::tag<IdTag>,
42 boost::multi_index::member<User, int, &User::id>>,
43 boost::multi_index::ordered_unique<
44 boost::multi_index::tag<EmailTag>,
45 boost::multi_index::member<User, std::string, &User::email>>,
46 boost::multi_index::ordered_non_unique<
47 boost::multi_index::tag<NameTag>,
48 boost::multi_index::member<User, std::string, &User::name>>>>;
49
50 static_assert(std::bidirectional_iterator<decltype(std::declval<UserCacheExpirable&>().find<IdTag>(0))>);
51};
52
53UTEST_F(ExpirableUsersTest, BasicOperations) {
54 UserCacheExpirable cache(3, std::chrono::seconds(10)); // capacity=3, TTL=10s
55
56 // Test insertion
57 EXPECT_TRUE(cache.insert({.id = 1, .email = "alice@test.com", .name = "Alice"}));
58 EXPECT_TRUE(cache.insert({.id = 2, .email = "bob@test.com", .name = "Bob"}));
59 EXPECT_TRUE(cache.insert({.id = 3, .email = "charlie@test.com", .name = "Charlie"}));
60
61 EXPECT_EQ(cache.size(), 3);
62 EXPECT_EQ(cache.capacity(), 3);
63 EXPECT_FALSE(cache.empty());
64
65 // Test find by id (unique index)
66 auto alice_it = cache.find<IdTag>(1);
67 EXPECT_NE(alice_it, cache.end<IdTag>());
68 EXPECT_EQ(alice_it->name, "Alice");
69
70 // Test find by email (unique index)
71 auto bob_it = cache.find<EmailTag>("bob@test.com");
72 EXPECT_NE(bob_it, cache.end<EmailTag>());
73 EXPECT_EQ(bob_it->id, 2);
74
75 // Test find by name (non-unique index) - returns first match
76 auto charlie_it = cache.find<NameTag>("Charlie");
77 EXPECT_NE(charlie_it, cache.end<NameTag>());
78 EXPECT_EQ(charlie_it->email, "charlie@test.com");
79}
80
81UTEST_F(ExpirableUsersTest, IteratorIncrementDecrement) {
82 UserCacheExpirable cache(3, std::chrono::seconds(10));
83 EXPECT_TRUE(cache.insert({.id = 1, .email = "a@test.com", .name = "A"}));
84 EXPECT_TRUE(cache.insert({.id = 2, .email = "b@test.com", .name = "B"}));
85 EXPECT_TRUE(cache.insert({.id = 3, .email = "c@test.com", .name = "C"}));
86
87 auto it = cache.find<IdTag>(1);
88 ASSERT_NE(it, cache.end<IdTag>());
89
90 auto& prefix_ref = ++it;
91 EXPECT_EQ(&prefix_ref, &it);
92 EXPECT_EQ(it->id, 2);
93
94 auto prev = it++;
95 EXPECT_EQ(prev->id, 2);
96 EXPECT_EQ(it->id, 3);
97
98 auto& prefix_dec = --it;
99 EXPECT_EQ(&prefix_dec, &it);
100 EXPECT_EQ(it->id, 2);
101
102 auto next = it--;
103 EXPECT_EQ(next->id, 2);
104 EXPECT_EQ(it->id, 1);
105}
106
107UTEST_F(ExpirableUsersTest, FindNoUpdate) {
108 UserCacheExpirable cache(3, std::chrono::seconds(10));
109
110 cache.insert({.id = 1, .email = "alice@test.com", .name = "Alice"});
111 cache.insert({.id = 2, .email = "bob@test.com", .name = "Bob"});
112 cache.insert({.id = 3, .email = "charlie@test.com", .name = "Charlie"});
113
114 // Both finds should succeed
115 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
116 EXPECT_NE(cache.find_no_update<IdTag>(1), cache.end<IdTag>());
117}
118
119UTEST_F(ExpirableUsersTest, LRUEviction) {
120 UserCacheExpirable cache(3, std::chrono::seconds(10));
121
122 cache.insert({.id = 1, .email = "alice@test.com", .name = "Alice"});
123 cache.insert({.id = 2, .email = "bob@test.com", .name = "Bob"});
124 cache.insert({.id = 3, .email = "charlie@test.com", .name = "Charlie"});
125
126 // Access Alice and Charlie to make them recently used
127 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
128 EXPECT_NE(cache.find<IdTag>(3), cache.end<IdTag>());
129
130 // Add fourth element - Bob should be evicted (LRU)
131 cache.insert({.id = 4, .email = "david@test.com", .name = "David"});
132
133 EXPECT_EQ(cache.find<IdTag>(2), cache.end<IdTag>()); // Bob evicted (LRU)
134 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>()); // Alice remains
135 EXPECT_NE(cache.find<IdTag>(3), cache.end<IdTag>()); // Charlie remains
136 EXPECT_NE(cache.find<IdTag>(4), cache.end<IdTag>()); // David added
137 EXPECT_EQ(cache.size(), 3);
138}
139
140UTEST_F(ExpirableUsersTest, TTLExpiration) {
141 using namespace std::chrono_literals;
142 utils::datetime::MockNowSet(std::chrono::system_clock::now());
143
144 UserCacheExpirable cache(100, 100ms); // Very short TTL for testing
145
146 cache.insert({.id = 1, .email = "alice@test.com", .name = "Alice"});
147 cache.insert({.id = 2, .email = "bob@test.com", .name = "Bob"});
148
149 // Items should still exist
150 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
151 EXPECT_NE(cache.find<IdTag>(2), cache.end<IdTag>());
152 EXPECT_EQ(cache.size(), 2);
153
154 // Wait for TTL to expire
156
157 EXPECT_EQ(cache.find<IdTag>(1), cache.end<IdTag>());
158 EXPECT_EQ(cache.find<IdTag>(2), cache.end<IdTag>());
159 EXPECT_EQ(cache.size(), 0);
160}
161
162UTEST_F(ExpirableUsersTest, TTLRefreshOnAccess) {
163 using namespace std::chrono_literals;
164 utils::datetime::MockNowSet(std::chrono::system_clock::now());
165
166 UserCacheExpirable cache(100, 190ms);
167
168 cache.insert({.id = 1, .email = "alice@test.com", .name = "Alice"});
169
170 // Wait a bit but not enough to expire
172
173 // Access via find should refresh TTL
174 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
175
176 // Wait again - should still be alive due to refresh
178 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
179
180 // Wait for full TTL from last access
182 EXPECT_EQ(cache.find<IdTag>(1), cache.end<IdTag>());
183}
184
185UTEST_F(ExpirableUsersTest, EqualRangeOperations) {
186 using namespace std::chrono_literals;
187
188 UserCacheExpirable cache(10, 1h); // Long TTL to avoid expiration
189
190 // Insert multiple users with the same name
191 cache.insert({.id = 1, .email = "john1@test.com", .name = "John"});
192 cache.insert({.id = 2, .email = "john2@test.com", .name = "John"});
193 cache.insert({.id = 3, .email = "john3@test.com", .name = "John"});
194 cache.insert({.id = 4, .email = "alice@test.com", .name = "Alice"});
195
196 // Test equal_range for non-unique index
197 auto [begin, end] = cache.equal_range<NameTag>("John");
198
199 // Count matches
200 int count = 0;
201 for (auto it = begin; it != end; ++it) {
202 ++count;
203 EXPECT_EQ(it->name, "John");
204 }
205 EXPECT_EQ(count, 3);
206
207 // Test equal_range for non-existent key
208 auto [begin_empty, end_empty] = cache.equal_range<NameTag>("NonExistent");
209 EXPECT_EQ(begin_empty, end_empty);
210}
211
212UTEST_F(ExpirableUsersTest, EqualRangeNoUpdate) {
213 using namespace std::chrono_literals;
214
215 UserCacheExpirable cache(10, 1h);
216
217 cache.insert({.id = 1, .email = "john1@test.com", .name = "John"});
218 cache.insert({.id = 2, .email = "john2@test.com", .name = "John"});
219
220 // equal_range_no_update should work and find all matches
221 auto [begin, end] = cache.equal_range_no_update<NameTag>("John");
222
223 int count = 0;
224 for (auto it = begin; it != end; ++it) {
225 ++count;
226 EXPECT_TRUE(it->id == 1 || it->id == 2);
227 }
228 EXPECT_EQ(count, 2);
229}
230
231UTEST_F(ExpirableUsersTest, EraseOperations) {
232 UserCacheExpirable cache(3, std::chrono::seconds(10));
233
234 cache.insert({.id = 1, .email = "alice@test.com", .name = "Alice"});
235 cache.insert({.id = 2, .email = "bob@test.com", .name = "Bob"});
236
237 EXPECT_TRUE(cache.erase<IdTag>(1));
238 EXPECT_EQ(cache.find<IdTag>(1), cache.end<IdTag>());
239 EXPECT_NE(cache.find<IdTag>(2), cache.end<IdTag>());
240 EXPECT_EQ(cache.size(), 1);
241
242 EXPECT_FALSE(cache.erase<IdTag>(999)); // Non-existent
243 EXPECT_EQ(cache.size(), 1);
244}
245
246UTEST_F(ExpirableUsersTest, SetCapacity) {
247 UserCacheExpirable cache(5, std::chrono::seconds(10));
248
249 // Fill cache
250 for (int i = 1; i <= 5; ++i) {
251 cache.insert(User{.id = i, .email = std::to_string(i) + "@test.com", .name = "User" + std::to_string(i)});
252 }
253 EXPECT_EQ(cache.size(), 5);
254 EXPECT_EQ(cache.capacity(), 5);
255
256 // Reduce capacity - should evict LRU items
257 cache.set_capacity(3);
258 EXPECT_EQ(cache.capacity(), 3);
259
260 // Size should be <= new capacity
261 EXPECT_LE(cache.size(), 3);
262}
263
264UTEST_F(ExpirableUsersTest, Clear) {
265 UserCacheExpirable cache(5, std::chrono::seconds(10));
266
267 cache.insert({.id = 1, .email = "alice@test.com", .name = "Alice"});
268 cache.insert({.id = 2, .email = "bob@test.com", .name = "Bob"});
269
270 EXPECT_EQ(cache.size(), 2);
271 EXPECT_FALSE(cache.empty());
272
273 cache.clear();
274
275 EXPECT_EQ(cache.size(), 0);
276 EXPECT_TRUE(cache.empty());
277 EXPECT_EQ(cache.find<IdTag>(1), cache.end<IdTag>());
278 EXPECT_EQ(cache.find<IdTag>(2), cache.end<IdTag>());
279}
280
281UTEST_F(ExpirableUsersTest, CleanupExpired) {
282 using namespace std::chrono_literals;
283 utils::datetime::MockNowSet(std::chrono::system_clock::now());
284
285 UserCacheExpirable cache(5, 100ms);
286
287 cache.insert({.id = 1, .email = "alice@test.com", .name = "Alice"});
288 cache.insert({.id = 2, .email = "bob@test.com", .name = "Bob"});
289
290 // Wait for TTL to expire
292
293 // cleanup_expired should remove expired items
295
296 EXPECT_EQ(cache.size(), 0);
297}
298
299UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) {
300 // Container is not thread-safe; external synchronization required.
301 UserCacheExpirable cache(100, std::chrono::seconds(10));
302 engine::Mutex mutex;
303
304 constexpr int kCoroutines = 4;
305 constexpr int kIterations = 100;
306 std::vector<engine::TaskWithResult<void>> tasks;
307 tasks.reserve(kCoroutines);
308
309 for (int t = 0; t < kCoroutines; ++t) {
310 tasks.push_back(utils::Async("using cache", [&cache, &mutex, t]() {
311 for (int i = 0; i < kIterations; ++i) {
312 int id = t * kIterations + i;
313
314 {
315 const std::lock_guard lock{mutex};
316 cache.insert(User{
317 .id = id,
318 .email = std::to_string(id) + "@test.com",
319 .name = "User" + std::to_string(id)
320 });
321 }
322
323 if (id % 3 == 0) {
324 const std::lock_guard lock{mutex};
325 // Use find to check existence and update timestamp
326 cache.find<IdTag>(id);
327 }
328
329 if (id % 5 == 0) {
330 const std::lock_guard lock{mutex};
331 cache.erase<IdTag>(id - 1);
332 }
333 }
334 }));
335 }
336
337 for (auto& task : tasks) {
338 task.Get();
339 }
340
341 const std::lock_guard lock{mutex};
342 EXPECT_LE(cache.size(), 100);
343}
344
345#ifdef NDEBUG
346
347UTEST_F(ExpirableUsersTest, ZeroTTL) {
348 using namespace std::chrono_literals;
349
350 EXPECT_THROW({ UserCacheExpirable cache(10, 0ms); }, utils::InvariantError);
351}
352
353UTEST_F(ExpirableUsersTest, ZeroCapacity) {
354 using namespace std::chrono_literals;
355
356 EXPECT_THROW({ UserCacheExpirable cache(0, 10s); }, utils::InvariantError);
357}
358
359UTEST_F(ExpirableUsersTest, NegativeTTL) {
360 using namespace std::chrono_literals;
361
362 EXPECT_THROW({ UserCacheExpirable cache(10, -1ms); }, utils::InvariantError);
363}
364#endif
365
366} // namespace
367
368USERVER_NAMESPACE_END