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