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>
10#include <boost/multi_index/hashed_index.hpp>
11#include <boost/multi_index/member.hpp>
12#include <boost/multi_index/ordered_index.hpp>
14USERVER_NAMESPACE_BEGIN
17class ExpirableUsersTest :
public ::testing::Test {
19 void SetUp()
override {}
30 bool operator==(
const User& other)
const {
31 return id == other.id && email == other.email && name == other.name;
35 using UserCacheExpirable = multi_index_lru::ExpirableContainer<
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>>>>;
49UTEST_F(ExpirableUsersTest, BasicOperations) {
50 UserCacheExpirable cache(3, std::chrono::seconds(10));
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"}));
57 EXPECT_EQ(cache.size(), 3);
58 EXPECT_EQ(cache.capacity(), 3);
59 EXPECT_FALSE(cache.empty());
62 auto alice_it = cache.find<IdTag>(1);
63 EXPECT_NE(alice_it, cache.end<IdTag>());
64 EXPECT_EQ(alice_it->name,
"Alice");
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);
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");
77UTEST_F(ExpirableUsersTest, FindNoUpdate) {
78 UserCacheExpirable cache(3, std::chrono::seconds(10));
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"});
85 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
86 EXPECT_NE(cache.find_no_update<IdTag>(1), cache.end<IdTag>());
89UTEST_F(ExpirableUsersTest, LRUEviction) {
90 UserCacheExpirable cache(3, std::chrono::seconds(10));
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"});
97 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
98 EXPECT_NE(cache.find<IdTag>(3), cache.end<IdTag>());
101 cache.insert(User{4,
"david@test.com",
"David"});
103 EXPECT_EQ(cache.find<IdTag>(2), cache.end<IdTag>());
104 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
105 EXPECT_NE(cache.find<IdTag>(3), cache.end<IdTag>());
106 EXPECT_NE(cache.find<IdTag>(4), cache.end<IdTag>());
107 EXPECT_EQ(cache.size(), 3);
110UTEST_F(ExpirableUsersTest, TTLExpiration) {
111 using namespace std::chrono_literals;
113 UserCacheExpirable cache(100, 100ms);
115 cache.insert(User{1,
"alice@test.com",
"Alice"});
116 cache.insert(User{2,
"bob@test.com",
"Bob"});
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);
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);
131UTEST_F(ExpirableUsersTest, TTLRefreshOnAccess) {
132 using namespace std::chrono_literals;
134 UserCacheExpirable cache(100, 190ms);
136 cache.insert(User{1,
"alice@test.com",
"Alice"});
142 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
146 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
150 EXPECT_EQ(cache.find<IdTag>(1), cache.end<IdTag>());
153UTEST_F(ExpirableUsersTest, EqualRangeOperations) {
154 using namespace std::chrono_literals;
156 UserCacheExpirable cache(10, 1h);
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"});
165 auto [begin, end] = cache.equal_range<NameTag>(
"John");
169 for (
auto it = begin; it != end; ++it) {
171 EXPECT_EQ(it->name,
"John");
176 auto [begin_empty, end_empty] = cache.equal_range<NameTag>(
"NonExistent");
177 EXPECT_EQ(begin_empty, end_empty);
180UTEST_F(ExpirableUsersTest, EqualRangeNoUpdate) {
181 using namespace std::chrono_literals;
183 UserCacheExpirable cache(10, 1h);
185 cache.insert(User{1,
"john1@test.com",
"John"});
186 cache.insert(User{2,
"john2@test.com",
"John"});
189 auto [begin, end] = cache.equal_range_no_update<NameTag>(
"John");
192 for (
auto it = begin; it != end; ++it) {
194 EXPECT_TRUE(it->id == 1 || it->id == 2);
199UTEST_F(ExpirableUsersTest, EraseOperations) {
200 UserCacheExpirable cache(3, std::chrono::seconds(10));
202 cache.insert(User{1,
"alice@test.com",
"Alice"});
203 cache.insert(User{2,
"bob@test.com",
"Bob"});
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);
210 EXPECT_FALSE(cache.erase<IdTag>(999));
211 EXPECT_EQ(cache.size(), 1);
214UTEST_F(ExpirableUsersTest, SetCapacity) {
215 UserCacheExpirable cache(5, std::chrono::seconds(10));
218 for (
int i = 1; i <= 5; ++i) {
219 cache.insert(User{i, std::to_string(i) +
"@test.com",
"User" + std::to_string(i)});
221 EXPECT_EQ(cache.size(), 5);
222 EXPECT_EQ(cache.capacity(), 5);
225 cache.set_capacity(3);
226 EXPECT_EQ(cache.capacity(), 3);
229 EXPECT_LE(cache.size(), 3);
232UTEST_F(ExpirableUsersTest, Clear) {
233 UserCacheExpirable cache(5, std::chrono::seconds(10));
235 cache.insert(User{1,
"alice@test.com",
"Alice"});
236 cache.insert(User{2,
"bob@test.com",
"Bob"});
238 EXPECT_EQ(cache.size(), 2);
239 EXPECT_FALSE(cache.empty());
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>());
249UTEST_F(ExpirableUsersTest, CleanupExpired) {
250 using namespace std::chrono_literals;
252 UserCacheExpirable cache(5, 100ms);
254 cache.insert(User{1,
"alice@test.com",
"Alice"});
255 cache.insert(User{2,
"bob@test.com",
"Bob"});
261 cache.cleanup_expired();
263 EXPECT_EQ(cache.size(), 0);
266UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) {
268 UserCacheExpirable cache(100, std::chrono::seconds(10));
271 constexpr int kCoroutines = 4;
272 constexpr int kIterations = 100;
274 tasks.reserve(kCoroutines);
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;
282 const std::lock_guard lock{mutex};
283 cache.insert(User{id, std::to_string(id) +
"@test.com",
"User" + std::to_string(id)});
287 const std::lock_guard lock{mutex};
289 cache.find<IdTag>(id);
293 const std::lock_guard lock{mutex};
294 cache.erase<IdTag>(id - 1);
300 for (
auto& task : tasks) {
304 const std::lock_guard lock{mutex};
305 EXPECT_LE(cache.size(), 100);