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