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>
11#include <boost/multi_index/hashed_index.hpp>
12#include <boost/multi_index/member.hpp>
13#include <boost/multi_index/ordered_index.hpp>
15USERVER_NAMESPACE_BEGIN
18class ExpirableUsersTest :
public ::testing::Test {
20 void SetUp()
override {}
31 bool operator==(
const User& other)
const {
32 return id == other.id && email == other.email && name == other.name;
36 using UserCacheExpirable = multi_index_lru::ExpirableContainer<
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>>>>;
50UTEST_F(ExpirableUsersTest, BasicOperations) {
51 UserCacheExpirable cache(3, std::chrono::seconds(10));
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"}));
58 EXPECT_EQ(cache.size(), 3);
59 EXPECT_EQ(cache.capacity(), 3);
60 EXPECT_FALSE(cache.empty());
63 auto alice_it = cache.find<IdTag>(1);
64 EXPECT_NE(alice_it, cache.end<IdTag>());
65 EXPECT_EQ(alice_it->name,
"Alice");
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);
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");
78UTEST_F(ExpirableUsersTest, FindNoUpdate) {
79 UserCacheExpirable cache(3, std::chrono::seconds(10));
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"});
86 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
87 EXPECT_NE(cache.find_no_update<IdTag>(1), cache.end<IdTag>());
90UTEST_F(ExpirableUsersTest, LRUEviction) {
91 UserCacheExpirable cache(3, std::chrono::seconds(10));
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"});
98 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
99 EXPECT_NE(cache.find<IdTag>(3), cache.end<IdTag>());
102 cache.insert(User{4,
"david@test.com",
"David"});
104 EXPECT_EQ(cache.find<IdTag>(2), cache.end<IdTag>());
105 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
106 EXPECT_NE(cache.find<IdTag>(3), cache.end<IdTag>());
107 EXPECT_NE(cache.find<IdTag>(4), cache.end<IdTag>());
108 EXPECT_EQ(cache.size(), 3);
111UTEST_F(ExpirableUsersTest, TTLExpiration) {
112 using namespace std::chrono_literals;
115 UserCacheExpirable cache(100, 100ms);
117 cache.insert(User{1,
"alice@test.com",
"Alice"});
118 cache.insert(User{2,
"bob@test.com",
"Bob"});
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);
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);
133UTEST_F(ExpirableUsersTest, TTLRefreshOnAccess) {
134 using namespace std::chrono_literals;
137 UserCacheExpirable cache(100, 190ms);
139 cache.insert(User{1,
"alice@test.com",
"Alice"});
145 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
149 EXPECT_NE(cache.find<IdTag>(1), cache.end<IdTag>());
153 EXPECT_EQ(cache.find<IdTag>(1), cache.end<IdTag>());
156UTEST_F(ExpirableUsersTest, EqualRangeOperations) {
157 using namespace std::chrono_literals;
159 UserCacheExpirable cache(10, 1h);
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"});
168 auto [begin, end] = cache.equal_range<NameTag>(
"John");
172 for (
auto it = begin; it != end; ++it) {
174 EXPECT_EQ(it->name,
"John");
179 auto [begin_empty, end_empty] = cache.equal_range<NameTag>(
"NonExistent");
180 EXPECT_EQ(begin_empty, end_empty);
183UTEST_F(ExpirableUsersTest, EqualRangeNoUpdate) {
184 using namespace std::chrono_literals;
186 UserCacheExpirable cache(10, 1h);
188 cache.insert(User{1,
"john1@test.com",
"John"});
189 cache.insert(User{2,
"john2@test.com",
"John"});
192 auto [begin, end] = cache.equal_range_no_update<NameTag>(
"John");
195 for (
auto it = begin; it != end; ++it) {
197 EXPECT_TRUE(it->id == 1 || it->id == 2);
202UTEST_F(ExpirableUsersTest, EraseOperations) {
203 UserCacheExpirable cache(3, std::chrono::seconds(10));
205 cache.insert(User{1,
"alice@test.com",
"Alice"});
206 cache.insert(User{2,
"bob@test.com",
"Bob"});
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);
213 EXPECT_FALSE(cache.erase<IdTag>(999));
214 EXPECT_EQ(cache.size(), 1);
217UTEST_F(ExpirableUsersTest, SetCapacity) {
218 UserCacheExpirable cache(5, std::chrono::seconds(10));
221 for (
int i = 1; i <= 5; ++i) {
222 cache.insert(User{i, std::to_string(i) +
"@test.com",
"User" + std::to_string(i)});
224 EXPECT_EQ(cache.size(), 5);
225 EXPECT_EQ(cache.capacity(), 5);
228 cache.set_capacity(3);
229 EXPECT_EQ(cache.capacity(), 3);
232 EXPECT_LE(cache.size(), 3);
235UTEST_F(ExpirableUsersTest, Clear) {
236 UserCacheExpirable cache(5, std::chrono::seconds(10));
238 cache.insert(User{1,
"alice@test.com",
"Alice"});
239 cache.insert(User{2,
"bob@test.com",
"Bob"});
241 EXPECT_EQ(cache.size(), 2);
242 EXPECT_FALSE(cache.empty());
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>());
252UTEST_F(ExpirableUsersTest, CleanupExpired) {
253 using namespace std::chrono_literals;
256 UserCacheExpirable cache(5, 100ms);
258 cache.insert(User{1,
"alice@test.com",
"Alice"});
259 cache.insert(User{2,
"bob@test.com",
"Bob"});
267 EXPECT_EQ(cache.size(), 0);
270UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) {
272 UserCacheExpirable cache(100, std::chrono::seconds(10));
275 constexpr int kCoroutines = 4;
276 constexpr int kIterations = 100;
278 tasks.reserve(kCoroutines);
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;
286 const std::lock_guard lock{mutex};
287 cache.insert(User{id, std::to_string(id) +
"@test.com",
"User" + std::to_string(id)});
291 const std::lock_guard lock{mutex};
293 cache.find<IdTag>(id);
297 const std::lock_guard lock{mutex};
298 cache.erase<IdTag>(id - 1);
304 for (
auto& task : tasks) {
308 const std::lock_guard lock{mutex};
309 EXPECT_LE(cache.size(), 100);
314UTEST_F(ExpirableUsersTest, ZeroTTL) {
315 using namespace std::chrono_literals;
317 EXPECT_THROW({ UserCacheExpirable cache(10, 0ms); }, utils::InvariantError);
320UTEST_F(ExpirableUsersTest, ZeroCapacity) {
321 using namespace std::chrono_literals;
323 EXPECT_THROW({ UserCacheExpirable cache(0, 10s); }, utils::InvariantError);
326UTEST_F(ExpirableUsersTest, NegativeTTL) {
327 using namespace std::chrono_literals;
329 EXPECT_THROW({ UserCacheExpirable cache(10, -1ms); }, utils::InvariantError);