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>
12#include <boost/multi_index/hashed_index.hpp>
13#include <boost/multi_index/member.hpp>
14#include <boost/multi_index/ordered_index.hpp>
16USERVER_NAMESPACE_BEGIN
19class ExpirableUsersTest :
public ::
testing::Test {
21 void SetUp()
override {}
32 bool operator==(
const User& other)
const {
33 return id == other.id && email == other.email && name == other.name;
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>>>>;
50 static_assert(std::bidirectional_iterator<
decltype(std::declval<UserCacheExpirable&>().find<IdTag>(0))>);
53UTEST_F(ExpirableUsersTest, BasicOperations) {
54 UserCacheExpirable cache(3, std::chrono::seconds(10));
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"}));
61 EXPECT_EQ(cache.size(), 3);
62 EXPECT_EQ(cache.capacity(), 3);
63 EXPECT_FALSE(cache.empty());
66 auto alice_it = cache.find<IdTag>(1);
67 EXPECT_NE(alice_it, cache.end<IdTag>());
68 EXPECT_EQ(alice_it->name,
"Alice");
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);
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");
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"}));
87 auto it = cache.find<IdTag>(1);
88 ASSERT_NE(it, cache.end<IdTag>());
90 auto& prefix_ref = ++it;
91 EXPECT_EQ(&prefix_ref, &it);
95 EXPECT_EQ(prev->id, 2);
98 auto& prefix_dec = --it;
99 EXPECT_EQ(&prefix_dec, &it);
100 EXPECT_EQ(it->id, 2);
103 EXPECT_EQ(next->id, 2);
104 EXPECT_EQ(it->id, 1);
107UTEST_F(ExpirableUsersTest, FindNoUpdate) {
108 UserCacheExpirable cache(3, std::chrono::seconds(10));
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"});
115 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
116 EXPECT_NE(cache.find_no_update<IdTag>(1), cache.end<IdTag>());
119UTEST_F(ExpirableUsersTest, LRUEviction) {
120 UserCacheExpirable cache(3, std::chrono::seconds(10));
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"});
127 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
128 EXPECT_NE(cache.find<IdTag>(3), cache.end<IdTag>());
131 cache.insert({.id = 4, .email =
"david@test.com", .name =
"David"});
133 EXPECT_EQ(cache.find<IdTag>(2), cache.end<IdTag>());
134 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
135 EXPECT_NE(cache.find<IdTag>(3), cache.end<IdTag>());
136 EXPECT_NE(cache.find<IdTag>(4), cache.end<IdTag>());
137 EXPECT_EQ(cache.size(), 3);
140UTEST_F(ExpirableUsersTest, TTLExpiration) {
141 using namespace std::chrono_literals;
144 UserCacheExpirable cache(100, 100ms);
146 cache.insert({.id = 1, .email =
"alice@test.com", .name =
"Alice"});
147 cache.insert({.id = 2, .email =
"bob@test.com", .name =
"Bob"});
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);
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);
162UTEST_F(ExpirableUsersTest, TTLRefreshOnAccess) {
163 using namespace std::chrono_literals;
166 UserCacheExpirable cache(100, 190ms);
168 cache.insert({.id = 1, .email =
"alice@test.com", .name =
"Alice"});
174 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
178 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
182 EXPECT_EQ(cache.find<IdTag>(1), cache.end<IdTag>());
185UTEST_F(ExpirableUsersTest, EqualRangeOperations) {
186 using namespace std::chrono_literals;
188 UserCacheExpirable cache(10, 1h);
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"});
197 auto [begin, end] = cache.equal_range<NameTag>(
"John");
201 for (
auto it = begin; it != end; ++it) {
203 EXPECT_EQ(it->name,
"John");
208 auto [begin_empty, end_empty] = cache.equal_range<NameTag>(
"NonExistent");
209 EXPECT_EQ(begin_empty, end_empty);
212UTEST_F(ExpirableUsersTest, EqualRangeNoUpdate) {
213 using namespace std::chrono_literals;
215 UserCacheExpirable cache(10, 1h);
217 cache.insert({.id = 1, .email =
"john1@test.com", .name =
"John"});
218 cache.insert({.id = 2, .email =
"john2@test.com", .name =
"John"});
221 auto [begin, end] = cache.equal_range_no_update<NameTag>(
"John");
224 for (
auto it = begin; it != end; ++it) {
226 EXPECT_TRUE(it->id == 1 || it->id == 2);
231UTEST_F(ExpirableUsersTest, EraseOperations) {
232 UserCacheExpirable cache(3, std::chrono::seconds(10));
234 cache.insert({.id = 1, .email =
"alice@test.com", .name =
"Alice"});
235 cache.insert({.id = 2, .email =
"bob@test.com", .name =
"Bob"});
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);
242 EXPECT_FALSE(cache.erase<IdTag>(999));
243 EXPECT_EQ(cache.size(), 1);
246UTEST_F(ExpirableUsersTest, SetCapacity) {
247 UserCacheExpirable cache(5, std::chrono::seconds(10));
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)});
253 EXPECT_EQ(cache.size(), 5);
254 EXPECT_EQ(cache.capacity(), 5);
257 cache.set_capacity(3);
258 EXPECT_EQ(cache.capacity(), 3);
261 EXPECT_LE(cache.size(), 3);
264UTEST_F(ExpirableUsersTest, Clear) {
265 UserCacheExpirable cache(5, std::chrono::seconds(10));
267 cache.insert({.id = 1, .email =
"alice@test.com", .name =
"Alice"});
268 cache.insert({.id = 2, .email =
"bob@test.com", .name =
"Bob"});
270 EXPECT_EQ(cache.size(), 2);
271 EXPECT_FALSE(cache.empty());
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>());
281UTEST_F(ExpirableUsersTest, CleanupExpired) {
282 using namespace std::chrono_literals;
285 UserCacheExpirable cache(5, 100ms);
287 cache.insert({.id = 1, .email =
"alice@test.com", .name =
"Alice"});
288 cache.insert({.id = 2, .email =
"bob@test.com", .name =
"Bob"});
296 EXPECT_EQ(cache.size(), 0);
299UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) {
301 UserCacheExpirable cache(100, std::chrono::seconds(10));
304 constexpr int kCoroutines = 4;
305 constexpr int kIterations = 100;
307 tasks.reserve(kCoroutines);
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;
315 const std::lock_guard lock{mutex};
318 .email = std::to_string(id) +
"@test.com",
319 .name =
"User" + std::to_string(id)
324 const std::lock_guard lock{mutex};
326 cache.find<IdTag>(id);
330 const std::lock_guard lock{mutex};
331 cache.erase<IdTag>(id - 1);
337 for (
auto& task : tasks) {
341 const std::lock_guard lock{mutex};
342 EXPECT_LE(cache.size(), 100);
347UTEST_F(ExpirableUsersTest, ZeroTTL) {
348 using namespace std::chrono_literals;
350 EXPECT_THROW({ UserCacheExpirable cache(10, 0ms); }, utils::InvariantError);
353UTEST_F(ExpirableUsersTest, ZeroCapacity) {
354 using namespace std::chrono_literals;
356 EXPECT_THROW({ UserCacheExpirable cache(0, 10s); }, utils::InvariantError);
359UTEST_F(ExpirableUsersTest, NegativeTTL) {
360 using namespace std::chrono_literals;
362 EXPECT_THROW({ UserCacheExpirable cache(10, -1ms); }, utils::InvariantError);