From e67bba2adc597d9378e8c0a97788752a49c334f3 Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Fri, 23 Jan 2026 14:25:17 +0300 Subject: [PATCH 01/14] added expirable version of multi-index-lru --- .../userver/multi-index-lru/container.hpp | 51 +---- .../multi-index-lru/container_impl.hpp | 74 ++++++ .../multi-index-lru/expirable_container.hpp | 181 +++++++++++++++ libraries/multi-index-lru/src/main_test.cpp | 214 ++++++++++++++++++ 4 files changed, 470 insertions(+), 50 deletions(-) create mode 100644 libraries/multi-index-lru/include/userver/multi-index-lru/container_impl.hpp create mode 100644 libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp index 628d853895cc..c3cbc8a0de05 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp @@ -3,61 +3,12 @@ /// @file userver/multi-index-lru/container.hpp /// @brief @copybrief multi_index_lru::Container -#include -#include -#include -#include - -#include -#include -#include +#include "container_impl.hpp" USERVER_NAMESPACE_BEGIN namespace multi_index_lru { -namespace impl { -template > -inline constexpr bool is_mpl_na = false; - -template -inline constexpr bool is_mpl_na().~na())>> = true; - -template -struct lazy_add_seq { - using type = boost::multi_index::indexed_by, Indices...>; -}; - -template -struct lazy_add_seq_no_last { -private: - template - static auto makeWithoutLast(std::index_sequence) { - using Tuple = std::tuple; - return boost::multi_index::indexed_by, std::tuple_element_t...>{}; - } - -public: - using type = decltype(makeWithoutLast(std::make_index_sequence{})); -}; - -template -struct add_seq_index {}; - -template -struct add_seq_index> { - using LastType = decltype((Indices{}, ...)); - - using type = typename std::conditional_t< - is_mpl_na, - lazy_add_seq_no_last, - lazy_add_seq>::type; -}; - -template -using add_seq_index_t = typename add_seq_index::type; -} // namespace impl - /// @ingroup userver_containers /// /// @brief MultiIndex LRU container diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container_impl.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/container_impl.hpp new file mode 100644 index 000000000000..6e34d6223eba --- /dev/null +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/container_impl.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace multi_index_lru { + +namespace impl { +template > +inline constexpr bool is_mpl_na = false; + +template +inline constexpr bool is_mpl_na().~na())>> = true; + +template +struct lazy_add_seq { + using type = boost::multi_index::indexed_by, Indices...>; +}; + +template +struct lazy_add_seq_no_last { +private: + template + static auto makeWithoutLast(std::index_sequence) { + using Tuple = std::tuple; + return boost::multi_index::indexed_by, std::tuple_element_t...>{}; + } + +public: + using type = decltype(makeWithoutLast(std::make_index_sequence{})); +}; + +template +struct add_seq_index {}; + +template +struct add_seq_index> { + using LastType = decltype((Indices{}, ...)); + + using type = typename std::conditional_t< + is_mpl_na, + lazy_add_seq_no_last, + lazy_add_seq>::type; +}; + +template +using add_seq_index_t = typename add_seq_index::type; + + +template +struct TimestampedValue : public Value { + std::chrono::steady_clock::time_point last_accessed; + + TimestampedValue() = default; + + explicit TimestampedValue(const Value& val) + : Value(val), + last_accessed(std::chrono::steady_clock::now()) {} + + explicit TimestampedValue(Value&& val) + : Value(std::move(val)), + last_accessed(std::chrono::steady_clock::now()) {} +}; +} // namespace impl +} // namespace multi_index_lru + +USERVER_NAMESPACE_END \ No newline at end of file diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp new file mode 100644 index 000000000000..0c09ec4a5a01 --- /dev/null +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -0,0 +1,181 @@ +#pragma once + +/// @file userver/multi-index-lru/container.hpp +/// @brief @copybrief multi_index_lru::ExpirableContainer + +#include +#include +#include +#include +#include + +#include "container_impl.hpp" + +USERVER_NAMESPACE_BEGIN + +namespace multi_index_lru { + +/// @ingroup userver_containers +/// +/// @brief MultiIndex LRU expirable container +template > +class ExpirableContainer { +public: + explicit ExpirableContainer(size_t max_size, + std::chrono::milliseconds ttl, + std::chrono::milliseconds cleanup_interval = std::chrono::milliseconds(60)) + : max_size_(max_size), ttl_(ttl), cleanup_interval_(cleanup_interval), cleanup_thread_running_(false) + { + assert(ttl.count() > 0 && "ttl must be positive"); + assert(cleanup_interval.count() > 0 && "cleanup_interval must be positive"); + } + + ~ExpirableContainer() { + stop_cleanup(); + } + + template + bool emplace(Args&&... args) { + std::lock_guard lock(mutex_); + + auto& seq_index = container_.template get<0>(); + auto result = seq_index.emplace_front(std::forward(args)...); + + if (!result.second) { + seq_index.relocate(seq_index.begin(), result.first); + seq_index.modify(result.first, [](CacheItem& item) { + item.last_accessed = std::chrono::steady_clock::now(); + }); + } else if (seq_index.size() > max_size_) { + seq_index.pop_back(); + } + + if (!cleanup_thread_running_.load(std::memory_order_relaxed)) { + start_cleanup(); + } + + return result.second; + } + + bool insert(const Value& value) { return emplace(value); } + + bool insert(Value&& value) { return emplace(std::move(value)); } + + template + auto find(const Key& key) { + std::lock_guard lock(mutex_); + auto& primary_index = container_.template get(); + auto it = primary_index.find(key); + + if (it != primary_index.end()) { + if (std::chrono::steady_clock::now() > it->last_accessed + ttl_) { + primary_index.erase(it); + return primary_index.end(); + } + + auto& seq_index = container_.template get<0>(); + auto seq_it = container_.template project<0>(it); + seq_index.relocate(seq_index.begin(), seq_it); + + primary_index.modify(it, [](CacheItem& item) { + item.last_accessed = std::chrono::steady_clock::now(); + }); + } + + return it; + } + + template + bool contains(const Key& key) { + return this->template find(key) != container_.template get().end(); + } + + template + bool erase(const Key& key) { + std::lock_guard lock(mutex_); + return container_.template get().erase(key) > 0; + } + + std::size_t size() const { + std::lock_guard lock(mutex_); + return container_.size(); + } + bool empty() const { + std::lock_guard lock(mutex_); + return container_.empty(); + } + std::size_t capacity() const { return max_size_; } + + void set_capacity(std::size_t new_capacity) { + max_size_ = new_capacity; + auto& seq_index = container_.template get<0>(); + + std::lock_guard lock(mutex_); + while (container_.size() > max_size_) { + seq_index.pop_back(); + } + } + + void clear() { + std::lock_guard lock(mutex_); + container_.clear(); + } + + template + auto end() { + return container_.template get().end(); + } + +private: + using CacheItem = impl::TimestampedValue; + using ExtendedIndexSpecifierList = impl::add_seq_index_t; + using BoostContainer = boost::multi_index::multi_index_container; + + void cleanup() { + std::lock_guard lock(mutex_); + auto now = std::chrono::steady_clock::now(); + + auto& seq_index = container_.template get<0>(); + for (auto it = seq_index.begin(); it != seq_index.end(); ) { + if (now > it->last_accessed + ttl_) { + it = seq_index.erase(it); + } else { + ++it; + } + } + } + + void start_cleanup() { + std::lock_guard lock(start_thread_mutex_); + if (cleanup_thread_running_.load(std::memory_order_relaxed)) { + return; + } + + cleanup_thread_running_.store(true, std::memory_order_release); + cleanup_thread_ = std::thread([this]() { + while (cleanup_thread_running_.load(std::memory_order_acquire)) { + std::this_thread::sleep_for(this->cleanup_interval_); + this->cleanup(); + } + }); + } + + void stop_cleanup() { + cleanup_thread_running_.store(false, std::memory_order_release); + if (cleanup_thread_.joinable()) { + cleanup_thread_.join(); + } + } + + BoostContainer container_; + std::size_t max_size_; + std::chrono::milliseconds ttl_; + std::chrono::milliseconds cleanup_interval_; + mutable std::mutex mutex_; + mutable std::mutex start_thread_mutex_; + std::atomic cleanup_thread_running_; + std::thread cleanup_thread_; +}; +} // namespace multi_index_lru + +USERVER_NAMESPACE_END diff --git a/libraries/multi-index-lru/src/main_test.cpp b/libraries/multi-index-lru/src/main_test.cpp index 8adc2ca79a5f..ae2ccb66cd7c 100644 --- a/libraries/multi-index-lru/src/main_test.cpp +++ b/libraries/multi-index-lru/src/main_test.cpp @@ -1,10 +1,14 @@ #include +#include #include #include #include #include +#include +#include +#include USERVER_NAMESPACE_BEGIN @@ -92,6 +96,216 @@ TEST_F(LRUUsersTest, LRUEviction) { EXPECT_TRUE((cache.contains(4))); // David added } +class ExpirableUsersTest : public ::testing::Test { +protected: + void SetUp() override {} + + struct IdTag {}; + struct EmailTag {}; + struct NameTag {}; + + struct User { + int id; + std::string email; + std::string name; + + bool operator==(const User& other) const { + return id == other.id && email == other.email && name == other.name; + } + }; + + using UserCacheExpirable = multi_index_lru::ExpirableContainer< + User, + boost::multi_index::indexed_by< + boost::multi_index::ordered_unique< + boost::multi_index::tag, + boost::multi_index::member>, + boost::multi_index::ordered_unique< + boost::multi_index::tag, + boost::multi_index::member>, + boost::multi_index::ordered_non_unique< + boost::multi_index::tag, + boost::multi_index::member>>>; +}; + + +TEST_F(ExpirableUsersTest, BasicOperations) { + UserCacheExpirable cache(3, std::chrono::seconds(10)); // capacity=3, TTL=10s + + // Test insertion + EXPECT_TRUE(cache.insert(User{1, "alice@test.com", "Alice"})); + EXPECT_TRUE(cache.insert(User{2, "bob@test.com", "Bob"})); + EXPECT_TRUE(cache.insert(User{3, "charlie@test.com", "Charlie"})); + + EXPECT_EQ(cache.size(), 3); + EXPECT_EQ(cache.capacity(), 3); + EXPECT_FALSE(cache.empty()); + + // Test find by id + auto by_id = cache.find(1); + ASSERT_NE(by_id, cache.end()); + EXPECT_EQ(by_id->name, "Alice"); + + // Test find by email + auto by_email = cache.find("bob@test.com"); + ASSERT_NE(by_email, cache.end()); + EXPECT_EQ(by_email->id, 2); + + // Test find by name + auto by_name = cache.find("Charlie"); + ASSERT_NE(by_name, cache.end()); + EXPECT_EQ(by_name->email, "charlie@test.com"); +} + +TEST_F(ExpirableUsersTest, LRUEviction) { + UserCacheExpirable cache(3, std::chrono::seconds(10)); + + cache.insert(User{1, "alice@test.com", "Alice"}); + cache.insert(User{2, "bob@test.com", "Bob"}); + cache.insert(User{3, "charlie@test.com", "Charlie"}); + + // Access Alice and Charlie to make them recently used + cache.find(1); + cache.find(3); + + // Add fourth element - Bob should be evicted (LRU) + cache.insert(User{4, "david@test.com", "David"}); + + EXPECT_FALSE(cache.contains(2)); // Bob evicted (LRU) + EXPECT_TRUE(cache.contains(1)); // Alice remains + EXPECT_TRUE(cache.contains(3)); // Charlie remains + EXPECT_TRUE(cache.contains(4)); // David added + EXPECT_EQ(cache.size(), 3); +} + +TEST_F(ExpirableUsersTest, TTLExpiration) { + using namespace std::chrono_literals; + + UserCacheExpirable cache(100, 100ms); // Very short TTL for testing + + cache.insert(User{1, "alice@test.com", "Alice"}); + cache.insert(User{2, "bob@test.com", "Bob"}); + + // Items should still exist + EXPECT_TRUE(cache.contains(1)); + EXPECT_TRUE(cache.contains(2)); + EXPECT_EQ(cache.size(), 2); + + // Wait for TTL to expire + std::this_thread::sleep_for(150ms); + + EXPECT_FALSE(cache.contains(1)); + EXPECT_FALSE(cache.contains(2)); + EXPECT_EQ(cache.size(), 0); +} + +TEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { + using namespace std::chrono_literals; + + UserCacheExpirable cache(100, 200ms); + + cache.insert(User{1, "alice@test.com", "Alice"}); + + // Wait a bit but not enough to expire + std::this_thread::sleep_for(80ms); + + // Access should refresh TTL + EXPECT_TRUE(cache.contains(1)); + + // Wait again - should still be alive due to refresh + std::this_thread::sleep_for(100ms); + EXPECT_TRUE(cache.contains(1)); + + // Wait for full TTL from last access + std::this_thread::sleep_for(200ms); + EXPECT_FALSE(cache.contains(1)); +} + +TEST_F(ExpirableUsersTest, EraseOperations) { + UserCacheExpirable cache(3, std::chrono::seconds(10)); + + cache.insert(User{1, "alice@test.com", "Alice"}); + cache.insert(User{2, "bob@test.com", "Bob"}); + + EXPECT_TRUE(cache.erase(1)); + EXPECT_FALSE(cache.contains(1)); + EXPECT_TRUE(cache.contains(2)); + EXPECT_EQ(cache.size(), 1); + + EXPECT_FALSE(cache.erase(999)); // Non-existent + EXPECT_EQ(cache.size(), 1); +} + +TEST_F(ExpirableUsersTest, SetCapacity) { + UserCacheExpirable cache(5, std::chrono::seconds(10)); + + // Fill cache + for (int i = 1; i <= 5; ++i) { + cache.insert(User{i, std::to_string(i) + "@test.com", "User" + std::to_string(i)}); + } + EXPECT_EQ(cache.size(), 5); + EXPECT_EQ(cache.capacity(), 5); + + // Reduce capacity - should evict LRU items + cache.set_capacity(3); + EXPECT_EQ(cache.capacity(), 3); + + // Size should be <= new capacity + EXPECT_LE(cache.size(), 3); +} + +TEST_F(ExpirableUsersTest, Clear) { + UserCacheExpirable cache(5, std::chrono::seconds(10)); + + cache.insert(User{1, "alice@test.com", "Alice"}); + cache.insert(User{2, "bob@test.com", "Bob"}); + + EXPECT_EQ(cache.size(), 2); + EXPECT_FALSE(cache.empty()); + + cache.clear(); + + EXPECT_EQ(cache.size(), 0); + EXPECT_TRUE(cache.empty()); + EXPECT_FALSE(cache.contains(1)); + EXPECT_FALSE(cache.contains(2)); +} + +TEST_F(ExpirableUsersTest, ThreadSafetyBasic) { + UserCacheExpirable cache(100, std::chrono::seconds(10)); + + constexpr int kThreads = 4; + constexpr int kIterations = 100; + std::vector threads; + + for (int t = 0; t < kThreads; ++t) { + threads.emplace_back([&cache, t]() { + for (int i = 0; i < kIterations; ++i) { + int id = t * kIterations + i; + cache.insert(User{id, std::to_string(id) + "@test.com", "User" + std::to_string(id)}); + + // Concurrent reads + if (id % 3 == 0) { + cache.find(id); + cache.contains(id); + } + + // Concurrent erase + if (id % 5 == 0) { + cache.erase(id - 1); + } + } + }); + } + + for (auto& t : threads) { + t.join(); + } + + // Should not crash and size should be reasonable + EXPECT_LE(cache.size(), 100); // Due to capacity limit +} + class ProductsTest : public ::testing::Test { protected: struct SkuTag {}; From 639b1c7239f637de1a1b1bb6dccc868cf0e66c02 Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Fri, 23 Jan 2026 14:35:13 +0300 Subject: [PATCH 02/14] includex fix --- libraries/multi-index-lru/src/main_benchmark.cpp | 3 +++ libraries/multi-index-lru/src/main_test.cpp | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/multi-index-lru/src/main_benchmark.cpp b/libraries/multi-index-lru/src/main_benchmark.cpp index 8304e92a7709..ed91ddf6e7fb 100644 --- a/libraries/multi-index-lru/src/main_benchmark.cpp +++ b/libraries/multi-index-lru/src/main_benchmark.cpp @@ -7,6 +7,9 @@ #include #include +#include +#include +#include USERVER_NAMESPACE_BEGIN diff --git a/libraries/multi-index-lru/src/main_test.cpp b/libraries/multi-index-lru/src/main_test.cpp index ae2ccb66cd7c..8740078d7ec7 100644 --- a/libraries/multi-index-lru/src/main_test.cpp +++ b/libraries/multi-index-lru/src/main_test.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include USERVER_NAMESPACE_BEGIN From dc533ab03081ff640281137b8f450ddb488aa7d4 Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Fri, 30 Jan 2026 13:16:31 +0300 Subject: [PATCH 03/14] mutexes array for expirable cache --- .../userver/multi-index-lru/container.hpp | 2 +- .../multi-index-lru/expirable_container.hpp | 73 +++++-- .../mpl_helpers.hpp} | 0 ..._benchmark.cpp => container_benchmark.cpp} | 0 .../src/container_test.cpp.cpp | 178 ++++++++++++++++++ ..._test.cpp => expirable_container_test.cpp} | 164 +--------------- 6 files changed, 236 insertions(+), 181 deletions(-) rename libraries/multi-index-lru/include/userver/multi-index-lru/{container_impl.hpp => impl/mpl_helpers.hpp} (100%) rename libraries/multi-index-lru/src/{main_benchmark.cpp => container_benchmark.cpp} (100%) create mode 100644 libraries/multi-index-lru/src/container_test.cpp.cpp rename libraries/multi-index-lru/src/{main_test.cpp => expirable_container_test.cpp} (57%) diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp index c3cbc8a0de05..bd62df5ba672 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp @@ -3,7 +3,7 @@ /// @file userver/multi-index-lru/container.hpp /// @brief @copybrief multi_index_lru::Container -#include "container_impl.hpp" +#include "impl/mpl_helpers.hpp" USERVER_NAMESPACE_BEGIN diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index 0c09ec4a5a01..61914e26c801 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -8,8 +8,10 @@ #include #include #include +#include -#include "container_impl.hpp" +#include "impl/mpl_helpers.hpp" +#define _USERVER_MULTIINDEX_LRU_MUTEX_COUNT 100 USERVER_NAMESPACE_BEGIN @@ -36,7 +38,8 @@ class ExpirableContainer { template bool emplace(Args&&... args) { - std::lock_guard lock(mutex_); + + lock_all_mutexes(); auto& seq_index = container_.template get<0>(); auto result = seq_index.emplace_front(std::forward(args)...); @@ -50,10 +53,13 @@ class ExpirableContainer { seq_index.pop_back(); } + unlock_all_mutexes(); + if (!cleanup_thread_running_.load(std::memory_order_relaxed)) { start_cleanup(); } + return result.second; } @@ -63,10 +69,14 @@ class ExpirableContainer { template auto find(const Key& key) { - std::lock_guard lock(mutex_); auto& primary_index = container_.template get(); auto it = primary_index.find(key); + std::size_t element_ptr = (std::size_t) it.operator->(); + std::lock_guard lock(mutexes_[element_ptr % _USERVER_MULTIINDEX_LRU_MUTEX_COUNT]); + + it = primary_index.find(key); + if (it != primary_index.end()) { if (std::chrono::steady_clock::now() > it->last_accessed + ttl_) { primary_index.erase(it); @@ -92,17 +102,30 @@ class ExpirableContainer { template bool erase(const Key& key) { - std::lock_guard lock(mutex_); - return container_.template get().erase(key) > 0; + auto& primary_index = container_.template get(); + auto it = primary_index.find(key); + + if (it == primary_index.end()) { + return false; + } + + std::size_t element_ptr = (std::size_t) it.operator->(); + std::lock_guard lock(mutexes_[element_ptr % _USERVER_MULTIINDEX_LRU_MUTEX_COUNT]); + + return primary_index.erase(key) > 0; } std::size_t size() const { - std::lock_guard lock(mutex_); - return container_.size(); + lock_all_mutexes(); + std::size_t size = container_.size(); + unlock_all_mutexes(); + return size; } bool empty() const { - std::lock_guard lock(mutex_); - return container_.empty(); + lock_all_mutexes(); + auto res = container_.empty(); + unlock_all_mutexes(); + return res; } std::size_t capacity() const { return max_size_; } @@ -110,15 +133,17 @@ class ExpirableContainer { max_size_ = new_capacity; auto& seq_index = container_.template get<0>(); - std::lock_guard lock(mutex_); + lock_all_mutexes(); while (container_.size() > max_size_) { seq_index.pop_back(); } + unlock_all_mutexes(); } void clear() { - std::lock_guard lock(mutex_); + lock_all_mutexes(); container_.clear(); + unlock_all_mutexes(); } template @@ -132,19 +157,33 @@ class ExpirableContainer { using BoostContainer = boost::multi_index::multi_index_container; void cleanup() { - std::lock_guard lock(mutex_); + lock_all_mutexes(); auto now = std::chrono::steady_clock::now(); auto& seq_index = container_.template get<0>(); - for (auto it = seq_index.begin(); it != seq_index.end(); ) { + while(!seq_index.empty()) { + auto it = seq_index.rbegin(); if (now > it->last_accessed + ttl_) { - it = seq_index.erase(it); + seq_index.pop_back(); } else { - ++it; + break; } } + unlock_all_mutexes(); + } + + void lock_all_mutexes() const { + for (int i = 0; i < _USERVER_MULTIINDEX_LRU_MUTEX_COUNT; ++i) { + mutexes_[i].lock(); + } } - + + void unlock_all_mutexes() const { + for (int i = 0; i < _USERVER_MULTIINDEX_LRU_MUTEX_COUNT; ++i) { + mutexes_[i].unlock(); + } + } + void start_cleanup() { std::lock_guard lock(start_thread_mutex_); if (cleanup_thread_running_.load(std::memory_order_relaxed)) { @@ -171,10 +210,10 @@ class ExpirableContainer { std::size_t max_size_; std::chrono::milliseconds ttl_; std::chrono::milliseconds cleanup_interval_; - mutable std::mutex mutex_; mutable std::mutex start_thread_mutex_; std::atomic cleanup_thread_running_; std::thread cleanup_thread_; + mutable std::array mutexes_; }; } // namespace multi_index_lru diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container_impl.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp similarity index 100% rename from libraries/multi-index-lru/include/userver/multi-index-lru/container_impl.hpp rename to libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp diff --git a/libraries/multi-index-lru/src/main_benchmark.cpp b/libraries/multi-index-lru/src/container_benchmark.cpp similarity index 100% rename from libraries/multi-index-lru/src/main_benchmark.cpp rename to libraries/multi-index-lru/src/container_benchmark.cpp diff --git a/libraries/multi-index-lru/src/container_test.cpp.cpp b/libraries/multi-index-lru/src/container_test.cpp.cpp new file mode 100644 index 000000000000..3f9800503be6 --- /dev/null +++ b/libraries/multi-index-lru/src/container_test.cpp.cpp @@ -0,0 +1,178 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace { + +class LRUUsersTest : public ::testing::Test { +protected: + void SetUp() override {} + + struct IdTag {}; + struct EmailTag {}; + struct NameTag {}; + + struct User { + int id; + std::string email; + std::string name; + + bool operator==(const User& other) const { + return id == other.id && email == other.email && name == other.name; + } + }; + + using UserCache = multi_index_lru::Container< + User, + boost::multi_index::indexed_by< + boost::multi_index::ordered_unique< + boost::multi_index::tag, + boost::multi_index::member>, + boost::multi_index::ordered_unique< + boost::multi_index::tag, + boost::multi_index::member>, + boost::multi_index::ordered_non_unique< + boost::multi_index::tag, + boost::multi_index::member>>>; +}; + +TEST_F(LRUUsersTest, BasicOperations) { + UserCache cache(3); // capacity == 3 + + // Test insertion + cache.emplace(User{1, "alice@test.com", "Alice"}); + cache.emplace(User{2, "bob@test.com", "Bob"}); + cache.emplace(User{3, "charlie@test.com", "Charlie"}); + + EXPECT_EQ(cache.size(), 3); + + // Test find by id + auto by_id = cache.find(1); + ASSERT_NE(by_id, cache.end()); + EXPECT_EQ(by_id->name, "Alice"); + + // Test find by email + auto by_email = cache.find("bob@test.com"); + ASSERT_NE(by_email, cache.end()); + EXPECT_EQ(by_email->id, 2); + + // Test find by name + auto by_name = cache.find("Charlie"); + ASSERT_NE(by_name, cache.end()); + EXPECT_EQ(by_name->email, "charlie@test.com"); + + // Test template find method + auto it = cache.find("alice@test.com"); + EXPECT_NE(it, cache.end()); +} + +TEST_F(LRUUsersTest, LRUEviction) { + UserCache cache(3); + + cache.emplace(User{1, "alice@test.com", "Alice"}); + cache.emplace(User{2, "bob@test.com", "Bob"}); + cache.emplace(User{3, "charlie@test.com", "Charlie"}); + + // Access Alice and Charlie to make them recently used + cache.find(1); + cache.find(3); + + // Add fourth element - Bob should be evicted + cache.emplace(User{4, "david@test.com", "David"}); + + EXPECT_FALSE((cache.contains(2))); // Bob evicted + EXPECT_TRUE((cache.contains(1))); // Alice remains + EXPECT_TRUE((cache.contains(3))); // Charlie remains + EXPECT_TRUE((cache.contains(4))); // David added +} + +class ProductsTest : public ::testing::Test { +protected: + struct SkuTag {}; + struct NameTag {}; + + struct Product { + std::string sku; + std::string name; + double price; + + bool operator==(const Product& other) const { + return sku == other.sku && name == other.name && price == other.price; + } + }; + + using ProductCache = multi_index_lru::Container< + Product, + boost::multi_index::indexed_by< + boost::multi_index::ordered_unique< + boost::multi_index::tag, + boost::multi_index::member>, + boost::multi_index::ordered_unique< + boost::multi_index::tag, + boost::multi_index::member>>>; +}; + +TEST_F(ProductsTest, BasicProductOperations) { + ProductCache cache(2); + + cache.emplace(Product{"A1", "Laptop", 999.99}); + cache.emplace(Product{"A2", "Mouse", 29.99}); + + auto laptop = cache.find("A1"); + ASSERT_NE(laptop, cache.end()); + EXPECT_EQ(laptop->name, "Laptop"); +} + +TEST_F(ProductsTest, ProductEviction) { + ProductCache cache(2); + + cache.emplace(Product{"A1", "Laptop", 999.99}); + cache.emplace(Product{"A2", "Mouse", 29.99}); + + // A1 was used, so A2 should be ousted when adding A3 + cache.find("A1"); + cache.emplace(Product{"A3", "Keyboard", 79.99}); + + EXPECT_TRUE((cache.contains("A1"))); // used + EXPECT_TRUE((cache.contains("A3"))); // new + EXPECT_FALSE((cache.contains("A2"))); // ousted + + EXPECT_NE(cache.find("Keyboard"), cache.end()); + EXPECT_EQ(cache.find("Mouse"), cache.end()); +} + +TEST(Snippet, SimpleUsage) { + struct MyValueT { + std::string key; + int val; + }; + + struct MyTag {}; + + MyValueT my_value{"some_key", 1}; + /// [Usage] + using MyLruCache = multi_index_lru::Container< + MyValueT, + boost::multi_index::indexed_by, + boost::multi_index::member>>>; + + MyLruCache cache(1000); // Capacity of 1000 items + cache.insert(my_value); + auto it = cache.find("some_key"); + EXPECT_NE(it, cache.end()); + /// [Usage] +} + +} // namespace + +USERVER_NAMESPACE_END diff --git a/libraries/multi-index-lru/src/main_test.cpp b/libraries/multi-index-lru/src/expirable_container_test.cpp similarity index 57% rename from libraries/multi-index-lru/src/main_test.cpp rename to libraries/multi-index-lru/src/expirable_container_test.cpp index 8740078d7ec7..3153cbff9fd4 100644 --- a/libraries/multi-index-lru/src/main_test.cpp +++ b/libraries/multi-index-lru/src/expirable_container_test.cpp @@ -12,89 +12,6 @@ USERVER_NAMESPACE_BEGIN namespace { - -class LRUUsersTest : public ::testing::Test { -protected: - void SetUp() override {} - - struct IdTag {}; - struct EmailTag {}; - struct NameTag {}; - - struct User { - int id; - std::string email; - std::string name; - - bool operator==(const User& other) const { - return id == other.id && email == other.email && name == other.name; - } - }; - - using UserCache = multi_index_lru::Container< - User, - boost::multi_index::indexed_by< - boost::multi_index::ordered_unique< - boost::multi_index::tag, - boost::multi_index::member>, - boost::multi_index::ordered_unique< - boost::multi_index::tag, - boost::multi_index::member>, - boost::multi_index::ordered_non_unique< - boost::multi_index::tag, - boost::multi_index::member>>>; -}; - -TEST_F(LRUUsersTest, BasicOperations) { - UserCache cache(3); // capacity == 3 - - // Test insertion - cache.emplace(User{1, "alice@test.com", "Alice"}); - cache.emplace(User{2, "bob@test.com", "Bob"}); - cache.emplace(User{3, "charlie@test.com", "Charlie"}); - - EXPECT_EQ(cache.size(), 3); - - // Test find by id - auto by_id = cache.find(1); - ASSERT_NE(by_id, cache.end()); - EXPECT_EQ(by_id->name, "Alice"); - - // Test find by email - auto by_email = cache.find("bob@test.com"); - ASSERT_NE(by_email, cache.end()); - EXPECT_EQ(by_email->id, 2); - - // Test find by name - auto by_name = cache.find("Charlie"); - ASSERT_NE(by_name, cache.end()); - EXPECT_EQ(by_name->email, "charlie@test.com"); - - // Test template find method - auto it = cache.find("alice@test.com"); - EXPECT_NE(it, cache.end()); -} - -TEST_F(LRUUsersTest, LRUEviction) { - UserCache cache(3); - - cache.emplace(User{1, "alice@test.com", "Alice"}); - cache.emplace(User{2, "bob@test.com", "Bob"}); - cache.emplace(User{3, "charlie@test.com", "Charlie"}); - - // Access Alice and Charlie to make them recently used - cache.find(1); - cache.find(3); - - // Add fourth element - Bob should be evicted - cache.emplace(User{4, "david@test.com", "David"}); - - EXPECT_FALSE((cache.contains(2))); // Bob evicted - EXPECT_TRUE((cache.contains(1))); // Alice remains - EXPECT_TRUE((cache.contains(3))); // Charlie remains - EXPECT_TRUE((cache.contains(4))); // David added -} - class ExpirableUsersTest : public ::testing::Test { protected: void SetUp() override {} @@ -304,85 +221,6 @@ TEST_F(ExpirableUsersTest, ThreadSafetyBasic) { // Should not crash and size should be reasonable EXPECT_LE(cache.size(), 100); // Due to capacity limit } - -class ProductsTest : public ::testing::Test { -protected: - struct SkuTag {}; - struct NameTag {}; - - struct Product { - std::string sku; - std::string name; - double price; - - bool operator==(const Product& other) const { - return sku == other.sku && name == other.name && price == other.price; - } - }; - - using ProductCache = multi_index_lru::Container< - Product, - boost::multi_index::indexed_by< - boost::multi_index::ordered_unique< - boost::multi_index::tag, - boost::multi_index::member>, - boost::multi_index::ordered_unique< - boost::multi_index::tag, - boost::multi_index::member>>>; -}; - -TEST_F(ProductsTest, BasicProductOperations) { - ProductCache cache(2); - - cache.emplace(Product{"A1", "Laptop", 999.99}); - cache.emplace(Product{"A2", "Mouse", 29.99}); - - auto laptop = cache.find("A1"); - ASSERT_NE(laptop, cache.end()); - EXPECT_EQ(laptop->name, "Laptop"); -} - -TEST_F(ProductsTest, ProductEviction) { - ProductCache cache(2); - - cache.emplace(Product{"A1", "Laptop", 999.99}); - cache.emplace(Product{"A2", "Mouse", 29.99}); - - // A1 was used, so A2 should be ousted when adding A3 - cache.find("A1"); - cache.emplace(Product{"A3", "Keyboard", 79.99}); - - EXPECT_TRUE((cache.contains("A1"))); // used - EXPECT_TRUE((cache.contains("A3"))); // new - EXPECT_FALSE((cache.contains("A2"))); // ousted - - EXPECT_NE(cache.find("Keyboard"), cache.end()); - EXPECT_EQ(cache.find("Mouse"), cache.end()); -} - -TEST(Snippet, SimpleUsage) { - struct MyValueT { - std::string key; - int val; - }; - - struct MyTag {}; - - MyValueT my_value{"some_key", 1}; - /// [Usage] - using MyLruCache = multi_index_lru::Container< - MyValueT, - boost::multi_index::indexed_by, - boost::multi_index::member>>>; - - MyLruCache cache(1000); // Capacity of 1000 items - cache.insert(my_value); - auto it = cache.find("some_key"); - EXPECT_NE(it, cache.end()); - /// [Usage] -} - } // namespace -USERVER_NAMESPACE_END +USERVER_NAMESPACE_END \ No newline at end of file From 3466289b9d23c780a6a881c60310744252289afb Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Fri, 30 Jan 2026 15:53:23 +0300 Subject: [PATCH 04/14] TimestampedValue without inheritance --- .../multi-index-lru/expirable_container.hpp | 4 +- .../multi-index-lru/impl/mpl_helpers.hpp | 39 +++++++++++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index 61914e26c801..5ed9c79d7b0a 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -80,7 +80,7 @@ class ExpirableContainer { if (it != primary_index.end()) { if (std::chrono::steady_clock::now() > it->last_accessed + ttl_) { primary_index.erase(it); - return primary_index.end(); + return impl::TimestampedIteratorWrapper{primary_index.end()}; } auto& seq_index = container_.template get<0>(); @@ -92,7 +92,7 @@ class ExpirableContainer { }); } - return it; + return impl::TimestampedIteratorWrapper{it}; } template diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp index 6e34d6223eba..64c0d27cb674 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp @@ -55,18 +55,41 @@ using add_seq_index_t = typename add_seq_index::type; template -struct TimestampedValue : public Value { +struct TimestampedValue { + Value value; std::chrono::steady_clock::time_point last_accessed; - + TimestampedValue() = default; + + explicit TimestampedValue(const Value& val) + : value(val), last_accessed(std::chrono::steady_clock::now()) {} + + explicit TimestampedValue(Value&& val) + : value(std::move(val)), last_accessed(std::chrono::steady_clock::now()) {} + + operator Value&() { return value; } + operator const Value&() const { return value; } + + Value* operator->() { return &value; } + const Value* operator->() const { return &value; } + + Value& get() { return value; } + const Value& get() const { return value; } +}; - explicit TimestampedValue(const Value& val) - : Value(val), - last_accessed(std::chrono::steady_clock::now()) {} +template +class TimestampedIteratorWrapper : public Iterator { +public: + using Iterator::Iterator; + TimestampedIteratorWrapper(Iterator iter) : Iterator(std::move(iter)) {} + + auto operator->() { + return *(this->Iterator::operator->()); + } - explicit TimestampedValue(Value&& val) - : Value(std::move(val)), - last_accessed(std::chrono::steady_clock::now()) {} + auto operator->() const { + return *(this->Iterator::operator->()); + } }; } // namespace impl } // namespace multi_index_lru From 984e239bbfe82ebd6644934f71378e5276d8ed7e Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Fri, 6 Feb 2026 22:07:37 +0300 Subject: [PATCH 05/14] using userver's sync primitives --- .../userver/multi-index-lru/container.hpp | 4 +- .../multi-index-lru/expirable_container.hpp | 137 +++++------- .../multi-index-lru/impl/mpl_helpers.hpp | 31 +-- .../src/expirable_container_benchmark.cpp | 211 ++++++++++++++++++ .../src/expirable_container_test.cpp | 41 +++- 5 files changed, 317 insertions(+), 107 deletions(-) create mode 100644 libraries/multi-index-lru/src/expirable_container_benchmark.cpp diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp index bd62df5ba672..8d948ddd1646 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp @@ -80,7 +80,9 @@ class Container { } private: - using ExtendedIndexSpecifierList = impl::add_seq_index_t; + using ExtendedIndexSpecifierList = impl::add_index_t< + boost::multi_index::sequenced<>, + IndexSpecifierList>; using BoostContainer = boost::multi_index::multi_index_container; diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index 5ed9c79d7b0a..7ac6bb9e96d3 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -3,15 +3,20 @@ /// @file userver/multi-index-lru/container.hpp /// @brief @copybrief multi_index_lru::ExpirableContainer -#include -#include #include -#include #include -#include +#include +#include #include "impl/mpl_helpers.hpp" -#define _USERVER_MULTIINDEX_LRU_MUTEX_COUNT 100 + +#include +#include +#include +#include +#include +#include +#include USERVER_NAMESPACE_BEGIN @@ -26,7 +31,7 @@ class ExpirableContainer { explicit ExpirableContainer(size_t max_size, std::chrono::milliseconds ttl, std::chrono::milliseconds cleanup_interval = std::chrono::milliseconds(60)) - : max_size_(max_size), ttl_(ttl), cleanup_interval_(cleanup_interval), cleanup_thread_running_(false) + : max_size_(max_size), ttl_(ttl), cleanup_interval_(cleanup_interval) { assert(ttl.count() > 0 && "ttl must be positive"); assert(cleanup_interval.count() > 0 && "cleanup_interval must be positive"); @@ -38,8 +43,8 @@ class ExpirableContainer { template bool emplace(Args&&... args) { - - lock_all_mutexes(); + std::lock_guard read_lock(read_mutex_); + std::lock_guard write_lock(write_mutex_); auto& seq_index = container_.template get<0>(); auto result = seq_index.emplace_front(std::forward(args)...); @@ -53,48 +58,43 @@ class ExpirableContainer { seq_index.pop_back(); } - unlock_all_mutexes(); - - if (!cleanup_thread_running_.load(std::memory_order_relaxed)) { - start_cleanup(); - } - + start_cleanup(); return result.second; } - bool insert(const Value& value) { return emplace(value); } - - bool insert(Value&& value) { return emplace(std::move(value)); } - template auto find(const Key& key) { + std::shared_lock read_lock(read_mutex_); auto& primary_index = container_.template get(); auto it = primary_index.find(key); - std::size_t element_ptr = (std::size_t) it.operator->(); - std::lock_guard lock(mutexes_[element_ptr % _USERVER_MULTIINDEX_LRU_MUTEX_COUNT]); - - it = primary_index.find(key); - if (it != primary_index.end()) { if (std::chrono::steady_clock::now() > it->last_accessed + ttl_) { + std::lock_guard write_lock(write_mutex_); primary_index.erase(it); return impl::TimestampedIteratorWrapper{primary_index.end()}; } auto& seq_index = container_.template get<0>(); auto seq_it = container_.template project<0>(it); - seq_index.relocate(seq_index.begin(), seq_it); + { + std::lock_guard write_lock(write_mutex_); + seq_index.relocate(seq_index.begin(), seq_it); - primary_index.modify(it, [](CacheItem& item) { - item.last_accessed = std::chrono::steady_clock::now(); - }); + primary_index.modify(it, [](CacheItem& item) { + item.last_accessed = std::chrono::steady_clock::now(); + }); + } } return impl::TimestampedIteratorWrapper{it}; } + bool insert(const Value& value) { return emplace(value); } + + bool insert(Value&& value) { return emplace(std::move(value)); } + template bool contains(const Key& key) { return this->template find(key) != container_.template get().end(); @@ -102,30 +102,18 @@ class ExpirableContainer { template bool erase(const Key& key) { - auto& primary_index = container_.template get(); - auto it = primary_index.find(key); - - if (it == primary_index.end()) { - return false; - } - - std::size_t element_ptr = (std::size_t) it.operator->(); - std::lock_guard lock(mutexes_[element_ptr % _USERVER_MULTIINDEX_LRU_MUTEX_COUNT]); - - return primary_index.erase(key) > 0; + std::lock_guard read_lock(read_mutex_); + std::lock_guard write_lock(write_mutex_); + return container_.template get().erase(key) > 0; } std::size_t size() const { - lock_all_mutexes(); - std::size_t size = container_.size(); - unlock_all_mutexes(); - return size; + std::shared_lock read_lock(read_mutex_); + return container_.size(); } bool empty() const { - lock_all_mutexes(); - auto res = container_.empty(); - unlock_all_mutexes(); - return res; + std::shared_lock read_lock(read_mutex_); + return container_.empty(); } std::size_t capacity() const { return max_size_; } @@ -133,17 +121,17 @@ class ExpirableContainer { max_size_ = new_capacity; auto& seq_index = container_.template get<0>(); - lock_all_mutexes(); + std::lock_guard read_lock(read_mutex_); + std::lock_guard write_lock(write_mutex_); while (container_.size() > max_size_) { seq_index.pop_back(); } - unlock_all_mutexes(); } void clear() { - lock_all_mutexes(); + std::lock_guard read_lock(read_mutex_); + std::lock_guard write_lock(write_mutex_); container_.clear(); - unlock_all_mutexes(); } template @@ -153,11 +141,14 @@ class ExpirableContainer { private: using CacheItem = impl::TimestampedValue; - using ExtendedIndexSpecifierList = impl::add_seq_index_t; + using ExtendedIndexSpecifierList = impl::add_index_t< + boost::multi_index::sequenced<>, + IndexSpecifierList>; using BoostContainer = boost::multi_index::multi_index_container; void cleanup() { - lock_all_mutexes(); + std::lock_guard read_lock(read_mutex_); + std::lock_guard write_lock(write_mutex_); auto now = std::chrono::steady_clock::now(); auto& seq_index = container_.template get<0>(); @@ -169,40 +160,25 @@ class ExpirableContainer { break; } } - unlock_all_mutexes(); } - - void lock_all_mutexes() const { - for (int i = 0; i < _USERVER_MULTIINDEX_LRU_MUTEX_COUNT; ++i) { - mutexes_[i].lock(); - } - } - - void unlock_all_mutexes() const { - for (int i = 0; i < _USERVER_MULTIINDEX_LRU_MUTEX_COUNT; ++i) { - mutexes_[i].unlock(); - } - } - + void start_cleanup() { - std::lock_guard lock(start_thread_mutex_); - if (cleanup_thread_running_.load(std::memory_order_relaxed)) { + if (cleanup_task_.IsValid() && !cleanup_task_.IsFinished()) { return; } - cleanup_thread_running_.store(true, std::memory_order_release); - cleanup_thread_ = std::thread([this]() { - while (cleanup_thread_running_.load(std::memory_order_acquire)) { - std::this_thread::sleep_for(this->cleanup_interval_); + cleanup_task_ = userver::utils::Async("lru_cleanup", [this] { + while (!userver::engine::current_task::ShouldCancel()) { + userver::engine::SleepFor(cleanup_interval_); this->cleanup(); } }); } void stop_cleanup() { - cleanup_thread_running_.store(false, std::memory_order_release); - if (cleanup_thread_.joinable()) { - cleanup_thread_.join(); + if (cleanup_task_.IsValid()) { + cleanup_task_.RequestCancel(); + cleanup_task_.Wait(); } } @@ -210,11 +186,12 @@ class ExpirableContainer { std::size_t max_size_; std::chrono::milliseconds ttl_; std::chrono::milliseconds cleanup_interval_; - mutable std::mutex start_thread_mutex_; - std::atomic cleanup_thread_running_; - std::thread cleanup_thread_; - mutable std::array mutexes_; + mutable userver::engine::SharedMutex read_mutex_; + mutable userver::engine::Mutex write_mutex_; + userver::engine::Task cleanup_task_; }; + + } // namespace multi_index_lru -USERVER_NAMESPACE_END +USERVER_NAMESPACE_END \ No newline at end of file diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp index 64c0d27cb674..fd890a38ae42 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp @@ -19,39 +19,39 @@ inline constexpr bool is_mpl_na = false; template inline constexpr bool is_mpl_na().~na())>> = true; -template -struct lazy_add_seq { - using type = boost::multi_index::indexed_by, Indices...>; +template +struct lazy_add_index { + using type = boost::multi_index::indexed_by; }; -template -struct lazy_add_seq_no_last { +template +struct lazy_add_index_no_last { private: template static auto makeWithoutLast(std::index_sequence) { using Tuple = std::tuple; - return boost::multi_index::indexed_by, std::tuple_element_t...>{}; + return boost::multi_index::indexed_by...>{}; } public: using type = decltype(makeWithoutLast(std::make_index_sequence{})); }; -template -struct add_seq_index {}; +template +struct add_index {}; -template -struct add_seq_index> { +template +struct add_index> { using LastType = decltype((Indices{}, ...)); using type = typename std::conditional_t< is_mpl_na, - lazy_add_seq_no_last, - lazy_add_seq>::type; + lazy_add_index_no_last, + lazy_add_index>::type; }; -template -using add_seq_index_t = typename add_seq_index::type; +template +using add_index_t = typename add_index::type; template @@ -72,6 +72,9 @@ struct TimestampedValue { Value* operator->() { return &value; } const Value* operator->() const { return &value; } + + Value& operator*() {return value; } + const Value& operator*() const {return value; } Value& get() { return value; } const Value& get() const { return value; } diff --git a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp new file mode 100644 index 000000000000..5a7b28179cdb --- /dev/null +++ b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp @@ -0,0 +1,211 @@ +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace benchmarks { + +const std::size_t kOperationsNumber = 100000; +const int kMaxIdSize = 50000; + +struct IdTag {}; +struct EmailTag {}; +struct NameTag {}; + +struct User { + int id; + std::string email; + std::string name; + + bool operator==(const User& other) const { + return id == other.id && email == other.email && name == other.name; + } +}; + +namespace { + +User GenerateUser() { + return User{ + utils::RandRange(0, kMaxIdSize), + "email" + std::to_string(utils::RandRange(0, kMaxIdSize)), + "name" + std::to_string(utils::RandRange(0, kMaxIdSize)) + }; +} + +int GenerateId() { + return utils::RandRange(0, kMaxIdSize); +} + +std::string GenerateName() { + return "name" + std::to_string(utils::RandRange(0, kMaxIdSize)); +} + +std::string GenerateEmail() { + return "email" + std::to_string(utils::RandRange(0, kMaxIdSize)); +} + +} // namespace + +using UserCache = multi_index_lru::ExpirableContainer< + User, + boost::multi_index::indexed_by< + boost::multi_index::ordered_unique< + boost::multi_index::tag, + boost::multi_index::member>, + boost::multi_index::ordered_unique< + boost::multi_index::tag, + boost::multi_index::member>, + boost::multi_index::ordered_non_unique< + boost::multi_index::tag, + boost::multi_index::member>>>; + +static void ExpirableFindEmplaceMix(benchmark::State& state) { + userver::engine::RunStandalone([&] { + const std::size_t cache_size = state.range(0); + + UserCache cache( + cache_size, + std::chrono::minutes(10), // ttl — big to not cause interference + std::chrono::seconds(1) // cleanup_interval + ); + + for (std::size_t i = 0; i < cache_size; ++i) { + cache.insert(GenerateUser()); + } + + const std::size_t read_ops = kOperationsNumber * 4 / 5; + const std::size_t write_ops = kOperationsNumber / 5; + + std::vector names(read_ops); + std::vector emails(read_ops); + std::vector ids(read_ops); + std::vector users(write_ops); + + for (std::size_t i = 0; i < read_ops; ++i) { + names[i] = GenerateName(); + emails[i] = GenerateEmail(); + ids[i] = GenerateId(); + } + + for (std::size_t i = 0; i < write_ops; ++i) { + users[i] = GenerateUser(); + } + + for (auto _ : state) { + for (std::size_t i = 0; i < read_ops; ++i) { + cache.find(names[i]); + cache.find(emails[i]); + cache.find(ids[i]); + } + + for (std::size_t i = 0; i < write_ops; ++i) { + cache.insert(users[i]); + } + } + }); +} + +BENCHMARK(ExpirableFindEmplaceMix) + ->RangeMultiplier(10) + ->Range(10, 1'000'000); + + +static void PrepareCache(UserCache& cache, std::size_t size) { + for (std::size_t i = 0; i < size; ++i) { + cache.insert(GenerateUser()); + } +} + +static void ExpirableGetOperations(benchmark::State& state) { + userver::engine::RunStandalone([&] { + const std::size_t cache_size = state.range(0); + + UserCache cache( + cache_size, + std::chrono::minutes(10), + std::chrono::minutes(1) + ); + PrepareCache(cache, cache_size); + + + for (auto _ : state) { + state.PauseTiming(); + + std::vector names(kOperationsNumber); + std::vector emails(kOperationsNumber); + std::vector ids(kOperationsNumber); + + for (std::size_t i = 0; i < kOperationsNumber; ++i) { + names[i] = GenerateName(); + emails[i] = GenerateEmail(); + ids[i] = GenerateId(); + } + + state.ResumeTiming(); + + for (std::size_t i = 0; i < kOperationsNumber; ++i) { + benchmark::DoNotOptimize(cache.find(names[i])); + benchmark::DoNotOptimize(cache.find(emails[i])); + benchmark::DoNotOptimize(cache.find(ids[i])); + } + } + + state.SetItemsProcessed(state.iterations() * kOperationsNumber * 3); + state.SetComplexityN(cache_size); + }); +} + +BENCHMARK(ExpirableGetOperations) + ->RangeMultiplier(10) + ->Range(100, 1'000'000); + + +static void ExpirableEmplaceOperations(benchmark::State& state) { + userver::engine::RunStandalone([&] { + const std::size_t cache_size = state.range(0); + + UserCache cache( + cache_size, + std::chrono::minutes(10), + std::chrono::minutes(1) + ); + PrepareCache(cache, cache_size); + + for (auto _ : state) { + state.PauseTiming(); + + std::vector users(kOperationsNumber); + for (std::size_t i = 0; i < kOperationsNumber; ++i) { + users[i] = GenerateUser(); + } + + state.ResumeTiming(); + + for (std::size_t i = 0; i < kOperationsNumber; ++i) { + cache.insert(users[i]); + } + } + + state.SetItemsProcessed(state.iterations() * kOperationsNumber); + state.SetComplexityN(cache_size); + }); +} + +BENCHMARK(ExpirableEmplaceOperations) + ->RangeMultiplier(10) + ->Range(100, 1'000'000); + +} // namespace benchmarks + +USERVER_NAMESPACE_END \ No newline at end of file diff --git a/libraries/multi-index-lru/src/expirable_container_test.cpp b/libraries/multi-index-lru/src/expirable_container_test.cpp index 3153cbff9fd4..bc3e5d14091e 100644 --- a/libraries/multi-index-lru/src/expirable_container_test.cpp +++ b/libraries/multi-index-lru/src/expirable_container_test.cpp @@ -1,5 +1,8 @@ #include #include +#include +#include +#include #include @@ -44,8 +47,8 @@ class ExpirableUsersTest : public ::testing::Test { boost::multi_index::member>>>; }; - TEST_F(ExpirableUsersTest, BasicOperations) { + userver::engine::RunStandalone([&] { UserCacheExpirable cache(3, std::chrono::seconds(10)); // capacity=3, TTL=10s // Test insertion @@ -71,9 +74,11 @@ TEST_F(ExpirableUsersTest, BasicOperations) { auto by_name = cache.find("Charlie"); ASSERT_NE(by_name, cache.end()); EXPECT_EQ(by_name->email, "charlie@test.com"); + }); } TEST_F(ExpirableUsersTest, LRUEviction) { + userver::engine::RunStandalone([&] { UserCacheExpirable cache(3, std::chrono::seconds(10)); cache.insert(User{1, "alice@test.com", "Alice"}); @@ -92,9 +97,11 @@ TEST_F(ExpirableUsersTest, LRUEviction) { EXPECT_TRUE(cache.contains(3)); // Charlie remains EXPECT_TRUE(cache.contains(4)); // David added EXPECT_EQ(cache.size(), 3); + }); } TEST_F(ExpirableUsersTest, TTLExpiration) { + userver::engine::RunStandalone([&] { using namespace std::chrono_literals; UserCacheExpirable cache(100, 100ms); // Very short TTL for testing @@ -113,9 +120,11 @@ TEST_F(ExpirableUsersTest, TTLExpiration) { EXPECT_FALSE(cache.contains(1)); EXPECT_FALSE(cache.contains(2)); EXPECT_EQ(cache.size(), 0); + }); } TEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { + userver::engine::RunStandalone([&] { using namespace std::chrono_literals; UserCacheExpirable cache(100, 200ms); @@ -135,9 +144,11 @@ TEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { // Wait for full TTL from last access std::this_thread::sleep_for(200ms); EXPECT_FALSE(cache.contains(1)); + }); } TEST_F(ExpirableUsersTest, EraseOperations) { + userver::engine::RunStandalone([&] { UserCacheExpirable cache(3, std::chrono::seconds(10)); cache.insert(User{1, "alice@test.com", "Alice"}); @@ -150,9 +161,11 @@ TEST_F(ExpirableUsersTest, EraseOperations) { EXPECT_FALSE(cache.erase(999)); // Non-existent EXPECT_EQ(cache.size(), 1); + }); } TEST_F(ExpirableUsersTest, SetCapacity) { + userver::engine::RunStandalone([&] { UserCacheExpirable cache(5, std::chrono::seconds(10)); // Fill cache @@ -168,9 +181,11 @@ TEST_F(ExpirableUsersTest, SetCapacity) { // Size should be <= new capacity EXPECT_LE(cache.size(), 3); + }); } TEST_F(ExpirableUsersTest, Clear) { + userver::engine::RunStandalone([&] { UserCacheExpirable cache(5, std::chrono::seconds(10)); cache.insert(User{1, "alice@test.com", "Alice"}); @@ -185,41 +200,43 @@ TEST_F(ExpirableUsersTest, Clear) { EXPECT_TRUE(cache.empty()); EXPECT_FALSE(cache.contains(1)); EXPECT_FALSE(cache.contains(2)); + }); } TEST_F(ExpirableUsersTest, ThreadSafetyBasic) { + userver::engine::RunStandalone([&] { UserCacheExpirable cache(100, std::chrono::seconds(10)); - constexpr int kThreads = 4; + constexpr int kCoroutines = 4; constexpr int kIterations = 100; - std::vector threads; + std::vector> tasks; + tasks.reserve(kCoroutines); - for (int t = 0; t < kThreads; ++t) { - threads.emplace_back([&cache, t]() { + for (int t = 0; t < kCoroutines; ++t) { + tasks.push_back(utils::Async("using cache", [&cache, t]() { for (int i = 0; i < kIterations; ++i) { int id = t * kIterations + i; + cache.insert(User{id, std::to_string(id) + "@test.com", "User" + std::to_string(id)}); - // Concurrent reads if (id % 3 == 0) { cache.find(id); cache.contains(id); } - // Concurrent erase if (id % 5 == 0) { cache.erase(id - 1); } } - }); + })); } - for (auto& t : threads) { - t.join(); + for (auto& task : tasks) { + task.Get(); } - // Should not crash and size should be reasonable - EXPECT_LE(cache.size(), 100); // Due to capacity limit + EXPECT_LE(cache.size(), 100); + }); } } // namespace From a0c39d8f134339a64f44108cb7b677eabacdabf7 Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Sun, 8 Feb 2026 13:55:08 +0300 Subject: [PATCH 06/14] reusing multi_index_lru::Container --- .../userver/multi-index-lru/container.hpp | 32 ++++++- .../multi-index-lru/expirable_container.hpp | 90 +++++++------------ .../multi-index-lru/impl/mpl_helpers.hpp | 3 +- .../src/expirable_container_benchmark.cpp | 2 +- .../src/expirable_container_test.cpp | 4 +- 5 files changed, 63 insertions(+), 68 deletions(-) diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp index 8d948ddd1646..a1d82526025b 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp @@ -9,6 +9,9 @@ USERVER_NAMESPACE_BEGIN namespace multi_index_lru { +template > +class ExpirableContainer; + /// @ingroup userver_containers /// /// @brief MultiIndex LRU container @@ -20,7 +23,7 @@ class Container { {} template - bool emplace(Args&&... args) { + auto emplace(Args&&... args) { auto& seq_index = container_.template get<0>(); auto result = seq_index.emplace_front(std::forward(args)...); @@ -29,12 +32,12 @@ class Container { } else if (seq_index.size() > max_size_) { seq_index.pop_back(); } - return result.second; + return result; } - bool insert(const Value& value) { return emplace(value); } + bool insert(const Value& value) { return emplace(value).second; } - bool insert(Value&& value) { return emplace(std::move(value)); } + bool insert(Value&& value) { return emplace(std::move(value)).second; } template auto find(const Key& key) { @@ -88,6 +91,27 @@ class Container { BoostContainer container_; std::size_t max_size_; + + auto &get_sequensed() { + return container_.template get<0>(); + } + + const auto& get_sequensed() const { + return container_.template get<0>(); + } + + template + auto& get() { + return container_.template get(); + } + + template + const auto& get() const { + return container_.template get(); + } + + template + friend class ExpirableContainer; }; } // namespace multi_index_lru diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index 7ac6bb9e96d3..bfe7f54dbe3c 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -9,6 +9,7 @@ #include #include "impl/mpl_helpers.hpp" +#include "container.hpp" #include #include @@ -25,13 +26,13 @@ namespace multi_index_lru { /// @ingroup userver_containers /// /// @brief MultiIndex LRU expirable container -template > +template class ExpirableContainer { public: explicit ExpirableContainer(size_t max_size, std::chrono::milliseconds ttl, std::chrono::milliseconds cleanup_interval = std::chrono::milliseconds(60)) - : max_size_(max_size), ttl_(ttl), cleanup_interval_(cleanup_interval) + : container_(max_size), ttl_(ttl), cleanup_interval_(cleanup_interval) { assert(ttl.count() > 0 && "ttl must be positive"); assert(cleanup_interval.count() > 0 && "cleanup_interval must be positive"); @@ -42,101 +43,75 @@ class ExpirableContainer { } template - bool emplace(Args&&... args) { - std::lock_guard read_lock(read_mutex_); - std::lock_guard write_lock(write_mutex_); + auto emplace(Args&&... args) { + std::lock_guard lock(mutex_); - auto& seq_index = container_.template get<0>(); - auto result = seq_index.emplace_front(std::forward(args)...); + auto result = container_.emplace(std::forward(args)...); if (!result.second) { - seq_index.relocate(seq_index.begin(), result.first); - seq_index.modify(result.first, [](CacheItem& item) { - item.last_accessed = std::chrono::steady_clock::now(); - }); - } else if (seq_index.size() > max_size_) { - seq_index.pop_back(); + result.first->last_accessed = std::chrono::steady_clock::now(); } start_cleanup(); - return result.second; + return result; } template auto find(const Key& key) { - std::shared_lock read_lock(read_mutex_); - auto& primary_index = container_.template get(); - auto it = primary_index.find(key); - - if (it != primary_index.end()) { + std::lock_guard lock(mutex_); + auto it = container_.template find(key); + + if (it != container_.template end()) { if (std::chrono::steady_clock::now() > it->last_accessed + ttl_) { - std::lock_guard write_lock(write_mutex_); - primary_index.erase(it); - return impl::TimestampedIteratorWrapper{primary_index.end()}; + container_.template get().erase(it); + return impl::TimestampedIteratorWrapper{container_.template end()}; } - auto& seq_index = container_.template get<0>(); - auto seq_it = container_.template project<0>(it); - { - std::lock_guard write_lock(write_mutex_); - seq_index.relocate(seq_index.begin(), seq_it); - - primary_index.modify(it, [](CacheItem& item) { - item.last_accessed = std::chrono::steady_clock::now(); - }); - } + it->last_accessed = std::chrono::steady_clock::now(); } return impl::TimestampedIteratorWrapper{it}; } - bool insert(const Value& value) { return emplace(value); } + bool insert(const Value& value) { return emplace(value).second; } - bool insert(Value&& value) { return emplace(std::move(value)); } + bool insert(Value&& value) { return emplace(std::move(value)).second; } template bool contains(const Key& key) { - return this->template find(key) != container_.template get().end(); + return this->template find(key) != container_.template end(); } template bool erase(const Key& key) { - std::lock_guard read_lock(read_mutex_); - std::lock_guard write_lock(write_mutex_); - return container_.template get().erase(key) > 0; + std::lock_guard lock(mutex_); + return container_.template erase(key); } std::size_t size() const { - std::shared_lock read_lock(read_mutex_); + std::shared_lock lock(mutex_); return container_.size(); } bool empty() const { - std::shared_lock read_lock(read_mutex_); + std::shared_lock lock(mutex_); return container_.empty(); } - std::size_t capacity() const { return max_size_; } + std::size_t capacity() const { return container_.capacity(); } void set_capacity(std::size_t new_capacity) { - max_size_ = new_capacity; - auto& seq_index = container_.template get<0>(); - - std::lock_guard read_lock(read_mutex_); - std::lock_guard write_lock(write_mutex_); - while (container_.size() > max_size_) { - seq_index.pop_back(); - } + std::lock_guard lock(mutex_); + container_.set_capacity(new_capacity); } void clear() { - std::lock_guard read_lock(read_mutex_); - std::lock_guard write_lock(write_mutex_); + std::lock_guard lock(mutex_); container_.clear(); } template auto end() { - return container_.template get().end(); + return container_.template end(); } private: @@ -144,14 +119,13 @@ class ExpirableContainer { using ExtendedIndexSpecifierList = impl::add_index_t< boost::multi_index::sequenced<>, IndexSpecifierList>; - using BoostContainer = boost::multi_index::multi_index_container; + using BoostContainer = Container; void cleanup() { - std::lock_guard read_lock(read_mutex_); - std::lock_guard write_lock(write_mutex_); + std::lock_guard lock(mutex_); auto now = std::chrono::steady_clock::now(); - auto& seq_index = container_.template get<0>(); + auto& seq_index = container_.get_sequensed(); while(!seq_index.empty()) { auto it = seq_index.rbegin(); if (now > it->last_accessed + ttl_) { @@ -183,11 +157,9 @@ class ExpirableContainer { } BoostContainer container_; - std::size_t max_size_; std::chrono::milliseconds ttl_; std::chrono::milliseconds cleanup_interval_; - mutable userver::engine::SharedMutex read_mutex_; - mutable userver::engine::Mutex write_mutex_; + mutable userver::engine::SharedMutex mutex_; userver::engine::Task cleanup_task_; }; diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp index fd890a38ae42..0b8d49add72b 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp @@ -53,11 +53,10 @@ struct add_index> { template using add_index_t = typename add_index::type; - template struct TimestampedValue { Value value; - std::chrono::steady_clock::time_point last_accessed; + mutable std::chrono::steady_clock::time_point last_accessed; TimestampedValue() = default; diff --git a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp index 5a7b28179cdb..bd3d40d8d863 100644 --- a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp +++ b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp @@ -76,7 +76,7 @@ static void ExpirableFindEmplaceMix(benchmark::State& state) { UserCache cache( cache_size, - std::chrono::minutes(10), // ttl — big to not cause interference + std::chrono::seconds(5), // ttl — big to not cause interference std::chrono::seconds(1) // cleanup_interval ); diff --git a/libraries/multi-index-lru/src/expirable_container_test.cpp b/libraries/multi-index-lru/src/expirable_container_test.cpp index bc3e5d14091e..26c5b8d359e2 100644 --- a/libraries/multi-index-lru/src/expirable_container_test.cpp +++ b/libraries/multi-index-lru/src/expirable_container_test.cpp @@ -127,12 +127,12 @@ TEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { userver::engine::RunStandalone([&] { using namespace std::chrono_literals; - UserCacheExpirable cache(100, 200ms); + UserCacheExpirable cache(100, 190ms); cache.insert(User{1, "alice@test.com", "Alice"}); // Wait a bit but not enough to expire - std::this_thread::sleep_for(80ms); + std::this_thread::sleep_for(100ms); // Access should refresh TTL EXPECT_TRUE(cache.contains(1)); From 6edff0d55468f968ab8f6a02018ef3db711909fa Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Tue, 10 Feb 2026 17:21:10 +0300 Subject: [PATCH 07/14] formatting and fixes --- .../userver/multi-index-lru/container.hpp | 4 +- .../multi-index-lru/expirable_container.hpp | 51 +++++++++------ ...tainer_test.cpp.cpp => container_test.cpp} | 11 ++-- .../src/expirable_container_benchmark.cpp | 12 ++-- .../src/expirable_container_test.cpp | 65 ++++++++----------- 5 files changed, 70 insertions(+), 73 deletions(-) rename libraries/multi-index-lru/src/{container_test.cpp.cpp => container_test.cpp} (95%) diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp index a1d82526025b..09fdde334e18 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp @@ -101,12 +101,12 @@ class Container { } template - auto& get() { + auto& get_index() { return container_.template get(); } template - const auto& get() const { + const auto& get_index() const { return container_.template get(); } diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index bfe7f54dbe3c..e498a249c5d1 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "impl/mpl_helpers.hpp" #include "container.hpp" @@ -57,30 +58,24 @@ class ExpirableContainer { return result; } + bool insert(const Value& value) { return emplace(value).second; } + + bool insert(Value&& value) { return emplace(std::move(value)).second; } + template - auto find(const Key& key) { + std::optional get(const Key& key) { std::lock_guard lock(mutex_); - auto it = container_.template find(key); - - if (it != container_.template end()) { - if (std::chrono::steady_clock::now() > it->last_accessed + ttl_) { - container_.template get().erase(it); - return impl::TimestampedIteratorWrapper{container_.template end()}; - } - - it->last_accessed = std::chrono::steady_clock::now(); + auto it = find(lock, key); + if (it == end()) { + return std::nullopt; } - - return impl::TimestampedIteratorWrapper{it}; + return *it; } - bool insert(const Value& value) { return emplace(value).second; } - - bool insert(Value&& value) { return emplace(std::move(value)).second; } - template bool contains(const Key& key) { - return this->template find(key) != container_.template end(); + std::lock_guard lock(mutex_); + return this->template find(lock, key) != container_.template end(); } template @@ -119,11 +114,27 @@ class ExpirableContainer { using ExtendedIndexSpecifierList = impl::add_index_t< boost::multi_index::sequenced<>, IndexSpecifierList>; - using BoostContainer = Container; + using CacheContainer = Container; + + template + auto find(std::lock_guard&, const Key& key) { + auto it = container_.template find(key); + + if (it != container_.template end()) { + if (std::chrono::steady_clock::now() > it->last_accessed + ttl_) { + container_.template get_index().erase(it); + return impl::TimestampedIteratorWrapper{container_.template end()}; + } + + it->last_accessed = std::chrono::steady_clock::now(); + } + + return impl::TimestampedIteratorWrapper{it}; + } void cleanup() { - std::lock_guard lock(mutex_); auto now = std::chrono::steady_clock::now(); + std::lock_guard lock(mutex_); auto& seq_index = container_.get_sequensed(); while(!seq_index.empty()) { @@ -156,7 +167,7 @@ class ExpirableContainer { } } - BoostContainer container_; + CacheContainer container_; std::chrono::milliseconds ttl_; std::chrono::milliseconds cleanup_interval_; mutable userver::engine::SharedMutex mutex_; diff --git a/libraries/multi-index-lru/src/container_test.cpp.cpp b/libraries/multi-index-lru/src/container_test.cpp similarity index 95% rename from libraries/multi-index-lru/src/container_test.cpp.cpp rename to libraries/multi-index-lru/src/container_test.cpp index 3f9800503be6..278936ab099b 100644 --- a/libraries/multi-index-lru/src/container_test.cpp.cpp +++ b/libraries/multi-index-lru/src/container_test.cpp @@ -1,9 +1,8 @@ #include -#include +#include #include -#include #include #include #include @@ -45,7 +44,7 @@ class LRUUsersTest : public ::testing::Test { boost::multi_index::member>>>; }; -TEST_F(LRUUsersTest, BasicOperations) { +UTEST_F(LRUUsersTest, BasicOperations) { UserCache cache(3); // capacity == 3 // Test insertion @@ -75,7 +74,7 @@ TEST_F(LRUUsersTest, BasicOperations) { EXPECT_NE(it, cache.end()); } -TEST_F(LRUUsersTest, LRUEviction) { +UTEST_F(LRUUsersTest, LRUEviction) { UserCache cache(3); cache.emplace(User{1, "alice@test.com", "Alice"}); @@ -121,7 +120,7 @@ class ProductsTest : public ::testing::Test { boost::multi_index::member>>>; }; -TEST_F(ProductsTest, BasicProductOperations) { +UTEST_F(ProductsTest, BasicProductOperations) { ProductCache cache(2); cache.emplace(Product{"A1", "Laptop", 999.99}); @@ -132,7 +131,7 @@ TEST_F(ProductsTest, BasicProductOperations) { EXPECT_EQ(laptop->name, "Laptop"); } -TEST_F(ProductsTest, ProductEviction) { +UTEST_F(ProductsTest, ProductEviction) { ProductCache cache(2); cache.emplace(Product{"A1", "Laptop", 999.99}); diff --git a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp index bd3d40d8d863..2363b2e36301 100644 --- a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp +++ b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp @@ -104,9 +104,9 @@ static void ExpirableFindEmplaceMix(benchmark::State& state) { for (auto _ : state) { for (std::size_t i = 0; i < read_ops; ++i) { - cache.find(names[i]); - cache.find(emails[i]); - cache.find(ids[i]); + cache.get(names[i]); + cache.get(emails[i]); + cache.get(ids[i]); } for (std::size_t i = 0; i < write_ops; ++i) { @@ -155,9 +155,9 @@ static void ExpirableGetOperations(benchmark::State& state) { state.ResumeTiming(); for (std::size_t i = 0; i < kOperationsNumber; ++i) { - benchmark::DoNotOptimize(cache.find(names[i])); - benchmark::DoNotOptimize(cache.find(emails[i])); - benchmark::DoNotOptimize(cache.find(ids[i])); + benchmark::DoNotOptimize(cache.get(names[i])); + benchmark::DoNotOptimize(cache.get(emails[i])); + benchmark::DoNotOptimize(cache.get(ids[i])); } } diff --git a/libraries/multi-index-lru/src/expirable_container_test.cpp b/libraries/multi-index-lru/src/expirable_container_test.cpp index 26c5b8d359e2..0cdac1fea474 100644 --- a/libraries/multi-index-lru/src/expirable_container_test.cpp +++ b/libraries/multi-index-lru/src/expirable_container_test.cpp @@ -1,12 +1,10 @@ -#include #include #include -#include #include +#include #include -#include #include #include #include @@ -47,8 +45,7 @@ class ExpirableUsersTest : public ::testing::Test { boost::multi_index::member>>>; }; -TEST_F(ExpirableUsersTest, BasicOperations) { - userver::engine::RunStandalone([&] { +UTEST_F(ExpirableUsersTest, BasicOperations) { UserCacheExpirable cache(3, std::chrono::seconds(10)); // capacity=3, TTL=10s // Test insertion @@ -60,25 +57,23 @@ TEST_F(ExpirableUsersTest, BasicOperations) { EXPECT_EQ(cache.capacity(), 3); EXPECT_FALSE(cache.empty()); - // Test find by id - auto by_id = cache.find(1); - ASSERT_NE(by_id, cache.end()); + // Test get by id + auto by_id = cache.get(1); + EXPECT_TRUE(by_id.has_value()); EXPECT_EQ(by_id->name, "Alice"); - // Test find by email - auto by_email = cache.find("bob@test.com"); - ASSERT_NE(by_email, cache.end()); + // Test get by email + auto by_email = cache.get("bob@test.com"); + EXPECT_TRUE(by_email.has_value()); EXPECT_EQ(by_email->id, 2); - // Test find by name - auto by_name = cache.find("Charlie"); - ASSERT_NE(by_name, cache.end()); + // Test get by name + auto by_name = cache.get("Charlie"); + EXPECT_TRUE(by_name.has_value()); EXPECT_EQ(by_name->email, "charlie@test.com"); - }); } -TEST_F(ExpirableUsersTest, LRUEviction) { - userver::engine::RunStandalone([&] { +UTEST_F(ExpirableUsersTest, LRUEviction) { UserCacheExpirable cache(3, std::chrono::seconds(10)); cache.insert(User{1, "alice@test.com", "Alice"}); @@ -86,8 +81,8 @@ TEST_F(ExpirableUsersTest, LRUEviction) { cache.insert(User{3, "charlie@test.com", "Charlie"}); // Access Alice and Charlie to make them recently used - cache.find(1); - cache.find(3); + cache.get(1); + cache.get(3); // Add fourth element - Bob should be evicted (LRU) cache.insert(User{4, "david@test.com", "David"}); @@ -97,11 +92,9 @@ TEST_F(ExpirableUsersTest, LRUEviction) { EXPECT_TRUE(cache.contains(3)); // Charlie remains EXPECT_TRUE(cache.contains(4)); // David added EXPECT_EQ(cache.size(), 3); - }); } -TEST_F(ExpirableUsersTest, TTLExpiration) { - userver::engine::RunStandalone([&] { +UTEST_F(ExpirableUsersTest, TTLExpiration) { using namespace std::chrono_literals; UserCacheExpirable cache(100, 100ms); // Very short TTL for testing @@ -120,11 +113,10 @@ TEST_F(ExpirableUsersTest, TTLExpiration) { EXPECT_FALSE(cache.contains(1)); EXPECT_FALSE(cache.contains(2)); EXPECT_EQ(cache.size(), 0); - }); } -TEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { - userver::engine::RunStandalone([&] { +UTEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { + using namespace std::chrono_literals; UserCacheExpirable cache(100, 190ms); @@ -144,11 +136,10 @@ TEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { // Wait for full TTL from last access std::this_thread::sleep_for(200ms); EXPECT_FALSE(cache.contains(1)); - }); } -TEST_F(ExpirableUsersTest, EraseOperations) { - userver::engine::RunStandalone([&] { +UTEST_F(ExpirableUsersTest, EraseOperations) { + UserCacheExpirable cache(3, std::chrono::seconds(10)); cache.insert(User{1, "alice@test.com", "Alice"}); @@ -161,11 +152,10 @@ TEST_F(ExpirableUsersTest, EraseOperations) { EXPECT_FALSE(cache.erase(999)); // Non-existent EXPECT_EQ(cache.size(), 1); - }); } -TEST_F(ExpirableUsersTest, SetCapacity) { - userver::engine::RunStandalone([&] { +UTEST_F(ExpirableUsersTest, SetCapacity) { + UserCacheExpirable cache(5, std::chrono::seconds(10)); // Fill cache @@ -181,11 +171,10 @@ TEST_F(ExpirableUsersTest, SetCapacity) { // Size should be <= new capacity EXPECT_LE(cache.size(), 3); - }); } -TEST_F(ExpirableUsersTest, Clear) { - userver::engine::RunStandalone([&] { +UTEST_F(ExpirableUsersTest, Clear) { + UserCacheExpirable cache(5, std::chrono::seconds(10)); cache.insert(User{1, "alice@test.com", "Alice"}); @@ -200,11 +189,10 @@ TEST_F(ExpirableUsersTest, Clear) { EXPECT_TRUE(cache.empty()); EXPECT_FALSE(cache.contains(1)); EXPECT_FALSE(cache.contains(2)); - }); } -TEST_F(ExpirableUsersTest, ThreadSafetyBasic) { - userver::engine::RunStandalone([&] { +UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) { + UserCacheExpirable cache(100, std::chrono::seconds(10)); constexpr int kCoroutines = 4; @@ -220,7 +208,7 @@ TEST_F(ExpirableUsersTest, ThreadSafetyBasic) { cache.insert(User{id, std::to_string(id) + "@test.com", "User" + std::to_string(id)}); if (id % 3 == 0) { - cache.find(id); + cache.get(id); cache.contains(id); } @@ -236,7 +224,6 @@ TEST_F(ExpirableUsersTest, ThreadSafetyBasic) { } EXPECT_LE(cache.size(), 100); - }); } } // namespace From 7e3904f9e647ed3f6a66842caba53ca21464225e Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Sat, 14 Feb 2026 22:42:29 +0300 Subject: [PATCH 08/14] refactoring --- .../userver/multi-index-lru/container.hpp | 5 ++ .../multi-index-lru/expirable_container.hpp | 79 +++++++++++++++---- .../multi-index-lru/impl/mpl_helpers.hpp | 14 ++++ .../src/expirable_container_test.cpp | 12 +-- 4 files changed, 89 insertions(+), 21 deletions(-) diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp index 09fdde334e18..f5dfeefcc566 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp @@ -110,6 +110,11 @@ class Container { return container_.template get(); } + template + auto project_to_sequenced(IterT it) { + return container_.template project<0>(it); + } + template friend class ExpirableContainer; }; diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index e498a249c5d1..7be97f8ce2dd 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include "impl/mpl_helpers.hpp" #include "container.hpp" @@ -63,19 +63,33 @@ class ExpirableContainer { bool insert(Value&& value) { return emplace(std::move(value)).second; } template - std::optional get(const Key& key) { + std::vector get(const Key& key) { + auto now = std::chrono::steady_clock::now(); std::lock_guard lock(mutex_); - auto it = find(lock, key); - if (it == end()) { - return std::nullopt; + + std::vector result; + auto& index = container_.template get_index(); + + if constexpr (impl::is_unique_index::value) { + auto it = find(lock, key, now); + if (it != container_.template end()) { + result.push_back(it->value); + } + } else { + auto range = find_range(lock, key, now); + for (auto it = range.first; it != range.second; ++it) { + result.push_back(it->value); + } } - return *it; + + return result; } template bool contains(const Key& key) { + auto now = std::chrono::steady_clock::now(); std::lock_guard lock(mutex_); - return this->template find(lock, key) != container_.template end(); + return find(lock, key, now) != container_.template end(); } template @@ -117,19 +131,55 @@ class ExpirableContainer { using CacheContainer = Container; template - auto find(std::lock_guard&, const Key& key) { + auto find(std::lock_guard&, + const Key& key, + std::chrono::steady_clock::time_point now) { auto it = container_.template find(key); - if (it != container_.template end()) { - if (std::chrono::steady_clock::now() > it->last_accessed + ttl_) { + if (it != end()) { + if (now > it->last_accessed + ttl_) { container_.template get_index().erase(it); - return impl::TimestampedIteratorWrapper{container_.template end()}; + return end(); + } else { + it->last_accessed = now; } - - it->last_accessed = std::chrono::steady_clock::now(); } + + return it; + } - return impl::TimestampedIteratorWrapper{it}; + template + auto find_range(std::lock_guard&, + const Key& key, + std::chrono::steady_clock::time_point now) { + auto& index = container_.template get_index(); + auto [begin, end] = index.equal_range(key); + + auto it = begin; + std::vector to_erase; + std::vector to_move; + + while (it != end) { + if (now > it->last_accessed + ttl_) { + to_erase.push_back(it); + ++it; + } else { + it->last_accessed = now; + to_move.push_back(it); + ++it; + } + } + + for (auto erase_it : to_erase) { + index.erase(erase_it); + } + + auto& seq_index = container_.get_sequensed(); + for (auto move_it : to_move) { + seq_index.relocate(seq_index.begin(), container_.project_to_sequenced(move_it)); + } + + return index.equal_range(key); } void cleanup() { @@ -174,7 +224,6 @@ class ExpirableContainer { userver::engine::Task cleanup_task_; }; - } // namespace multi_index_lru USERVER_NAMESPACE_END \ No newline at end of file diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp index 0b8d49add72b..f38c76ac0c6b 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp @@ -93,6 +93,20 @@ class TimestampedIteratorWrapper : public Iterator { return *(this->Iterator::operator->()); } }; + +template +struct is_unique_index { +private: + template + static auto test(int) -> decltype(std::declval().find(std::declval()), + std::true_type{}); + + template + static std::false_type test(...); + +public: + static constexpr bool value = decltype(test(0))::value; +}; } // namespace impl } // namespace multi_index_lru diff --git a/libraries/multi-index-lru/src/expirable_container_test.cpp b/libraries/multi-index-lru/src/expirable_container_test.cpp index 0cdac1fea474..9701ca21531f 100644 --- a/libraries/multi-index-lru/src/expirable_container_test.cpp +++ b/libraries/multi-index-lru/src/expirable_container_test.cpp @@ -59,18 +59,18 @@ UTEST_F(ExpirableUsersTest, BasicOperations) { // Test get by id auto by_id = cache.get(1); - EXPECT_TRUE(by_id.has_value()); - EXPECT_EQ(by_id->name, "Alice"); + EXPECT_FALSE(by_id.empty()); + EXPECT_EQ(by_id.begin()->name, "Alice"); // Test get by email auto by_email = cache.get("bob@test.com"); - EXPECT_TRUE(by_email.has_value()); - EXPECT_EQ(by_email->id, 2); + EXPECT_FALSE(by_email.empty()); + EXPECT_EQ(by_email.begin()->id, 2); // Test get by name auto by_name = cache.get("Charlie"); - EXPECT_TRUE(by_name.has_value()); - EXPECT_EQ(by_name->email, "charlie@test.com"); + EXPECT_FALSE(by_name.empty()); + EXPECT_EQ(by_name.begin()->email, "charlie@test.com"); } UTEST_F(ExpirableUsersTest, LRUEviction) { From 6e1b3b4b08d19e5755c7a4b49315d625ab235761 Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Thu, 19 Feb 2026 16:04:37 +0300 Subject: [PATCH 09/14] large refactoring -- no syncronisation inside of expirable container methods, giving it to user --- .../userver/multi-index-lru/container.hpp | 42 ++++-- .../multi-index-lru/expirable_container.hpp | 124 +++++++----------- .../multi-index-lru/impl/mpl_helpers.hpp | 15 --- .../src/container_benchmark.cpp | 12 +- .../multi-index-lru/src/container_test.cpp | 113 +++++++++++++--- .../src/expirable_container_test.cpp | 68 +++++----- 6 files changed, 221 insertions(+), 153 deletions(-) diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp index f5dfeefcc566..f92e5361fc69 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp @@ -24,7 +24,7 @@ class Container { template auto emplace(Args&&... args) { - auto& seq_index = container_.template get<0>(); + auto& seq_index = get_sequensed(); auto result = seq_index.emplace_front(std::forward(args)...); if (!result.second) { @@ -40,12 +40,12 @@ class Container { bool insert(Value&& value) { return emplace(std::move(value)).second; } template - auto find(const Key& key) { - auto& primary_index = container_.template get(); + auto get(const Key& key) { + auto& primary_index = get_index(); auto it = primary_index.find(key); if (it != primary_index.end()) { - auto& seq_index = container_.template get<0>(); + auto& seq_index = get_sequensed(); auto seq_it = container_.template project<0>(it); seq_index.relocate(seq_index.begin(), seq_it); } @@ -53,14 +53,40 @@ class Container { return it; } + template + auto get_no_update(const Key& key) { + return get_index().find(key); + } + + template + auto equal_range(const Key& key) { + auto& primary_index = get_index(); + + auto [begin, end] = primary_index.equal_range(key); + auto it = begin; + + auto& seq_index = get_sequensed(); + while (it != end) { + seq_index.relocate(seq_index.begin(), project_to_sequenced(it)); + ++it; + } + + return std::pair{begin, end}; + } + + template + auto equal_range_no_update(const Key& key) { + return get_index().equal_range(key); + } + template bool contains(const Key& key) { - return this->template find(key) != container_.template get().end(); + return this->template get(key) != get_index().end(); } template bool erase(const Key& key) { - return container_.template get().erase(key) > 0; + return get_index().erase(key) > 0; } std::size_t size() const { return container_.size(); } @@ -69,7 +95,7 @@ class Container { void set_capacity(std::size_t new_capacity) { max_size_ = new_capacity; - auto& seq_index = container_.template get<0>(); + auto& seq_index = get_sequensed(); while (container_.size() > max_size_) { seq_index.pop_back(); } @@ -79,7 +105,7 @@ class Container { template auto end() { - return container_.template get().end(); + return get_index().end(); } private: diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index 7be97f8ce2dd..035ac999b00d 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "impl/mpl_helpers.hpp" #include "container.hpp" @@ -31,30 +32,20 @@ template class ExpirableContainer { public: explicit ExpirableContainer(size_t max_size, - std::chrono::milliseconds ttl, - std::chrono::milliseconds cleanup_interval = std::chrono::milliseconds(60)) - : container_(max_size), ttl_(ttl), cleanup_interval_(cleanup_interval) + std::chrono::milliseconds ttl) + : container_(max_size), ttl_(ttl) { assert(ttl.count() > 0 && "ttl must be positive"); - assert(cleanup_interval.count() > 0 && "cleanup_interval must be positive"); - } - - ~ExpirableContainer() { - stop_cleanup(); } template auto emplace(Args&&... args) { - std::lock_guard lock(mutex_); - auto result = container_.emplace(std::forward(args)...); if (!result.second) { result.first->last_accessed = std::chrono::steady_clock::now(); } - start_cleanup(); - return result; } @@ -63,20 +54,37 @@ class ExpirableContainer { bool insert(Value&& value) { return emplace(std::move(value)).second; } template - std::vector get(const Key& key) { - auto now = std::chrono::steady_clock::now(); - std::lock_guard lock(mutex_); + auto get(const Key& key) { + std::vector result; + auto& index = container_.template get_index(); + + if constexpr (impl::is_unique_index::value) { + auto it = find(key); + if (it != container_.template end()) { + result.push_back(it->value); + } + } else { + auto range = find_range(key); + for (auto it = range.first; it != range.second; ++it) { + result.push_back(it->value); + } + } + return result; + } + + template + auto get_no_update(const Key& key) { std::vector result; auto& index = container_.template get_index(); if constexpr (impl::is_unique_index::value) { - auto it = find(lock, key, now); + auto it = container_.template get_no_update(key); if (it != container_.template end()) { result.push_back(it->value); } } else { - auto range = find_range(lock, key, now); + auto range = container_.template equal_range_no_update(key); for (auto it = range.first; it != range.second; ++it) { result.push_back(it->value); } @@ -87,34 +95,27 @@ class ExpirableContainer { template bool contains(const Key& key) { - auto now = std::chrono::steady_clock::now(); - std::lock_guard lock(mutex_); - return find(lock, key, now) != container_.template end(); + return this->template find(key) != container_.template end(); } template bool erase(const Key& key) { - std::lock_guard lock(mutex_); return container_.template erase(key); } std::size_t size() const { - std::shared_lock lock(mutex_); return container_.size(); } bool empty() const { - std::shared_lock lock(mutex_); return container_.empty(); } std::size_t capacity() const { return container_.capacity(); } void set_capacity(std::size_t new_capacity) { - std::lock_guard lock(mutex_); container_.set_capacity(new_capacity); } void clear() { - std::lock_guard lock(mutex_); container_.clear(); } @@ -123,6 +124,20 @@ class ExpirableContainer { return container_.template end(); } + void cleanup_expired() { + auto now = std::chrono::steady_clock::now(); + auto& seq_index = container_.get_sequensed(); + + while(!seq_index.empty()) { + auto it = seq_index.rbegin(); + if (now > it->last_accessed + ttl_) { + seq_index.pop_back(); + } else { + break; + } + } + } + private: using CacheItem = impl::TimestampedValue; using ExtendedIndexSpecifierList = impl::add_index_t< @@ -131,10 +146,9 @@ class ExpirableContainer { using CacheContainer = Container; template - auto find(std::lock_guard&, - const Key& key, - std::chrono::steady_clock::time_point now) { - auto it = container_.template find(key); + auto find(const Key& key) { + auto now = std::chrono::steady_clock::now(); + auto it = container_.template get(key); if (it != end()) { if (now > it->last_accessed + ttl_) { @@ -149,15 +163,13 @@ class ExpirableContainer { } template - auto find_range(std::lock_guard&, - const Key& key, - std::chrono::steady_clock::time_point now) { + auto find_range(const Key& key) { + auto now = std::chrono::steady_clock::now(); auto& index = container_.template get_index(); - auto [begin, end] = index.equal_range(key); + auto [begin, end] = container_.template equal_range(key); auto it = begin; std::vector to_erase; - std::vector to_move; while (it != end) { if (now > it->last_accessed + ttl_) { @@ -165,7 +177,6 @@ class ExpirableContainer { ++it; } else { it->last_accessed = now; - to_move.push_back(it); ++it; } } @@ -174,54 +185,11 @@ class ExpirableContainer { index.erase(erase_it); } - auto& seq_index = container_.get_sequensed(); - for (auto move_it : to_move) { - seq_index.relocate(seq_index.begin(), container_.project_to_sequenced(move_it)); - } - return index.equal_range(key); } - void cleanup() { - auto now = std::chrono::steady_clock::now(); - std::lock_guard lock(mutex_); - - auto& seq_index = container_.get_sequensed(); - while(!seq_index.empty()) { - auto it = seq_index.rbegin(); - if (now > it->last_accessed + ttl_) { - seq_index.pop_back(); - } else { - break; - } - } - } - - void start_cleanup() { - if (cleanup_task_.IsValid() && !cleanup_task_.IsFinished()) { - return; - } - - cleanup_task_ = userver::utils::Async("lru_cleanup", [this] { - while (!userver::engine::current_task::ShouldCancel()) { - userver::engine::SleepFor(cleanup_interval_); - this->cleanup(); - } - }); - } - - void stop_cleanup() { - if (cleanup_task_.IsValid()) { - cleanup_task_.RequestCancel(); - cleanup_task_.Wait(); - } - } - CacheContainer container_; std::chrono::milliseconds ttl_; - std::chrono::milliseconds cleanup_interval_; - mutable userver::engine::SharedMutex mutex_; - userver::engine::Task cleanup_task_; }; } // namespace multi_index_lru diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp index f38c76ac0c6b..bdbe0f5a0a61 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp @@ -79,21 +79,6 @@ struct TimestampedValue { const Value& get() const { return value; } }; -template -class TimestampedIteratorWrapper : public Iterator { -public: - using Iterator::Iterator; - TimestampedIteratorWrapper(Iterator iter) : Iterator(std::move(iter)) {} - - auto operator->() { - return *(this->Iterator::operator->()); - } - - auto operator->() const { - return *(this->Iterator::operator->()); - } -}; - template struct is_unique_index { private: diff --git a/libraries/multi-index-lru/src/container_benchmark.cpp b/libraries/multi-index-lru/src/container_benchmark.cpp index ed91ddf6e7fb..956a394752a8 100644 --- a/libraries/multi-index-lru/src/container_benchmark.cpp +++ b/libraries/multi-index-lru/src/container_benchmark.cpp @@ -84,9 +84,9 @@ void LruFindEmplaceMix(benchmark::State& state) { for ([[maybe_unused]] auto _ : state) { for (std::size_t i = 0; i < reading_kOperationsNumber; ++i) { - cache.find(names[i]); - cache.find(emails[i]); - cache.find(ids[i]); + cache.get(names[i]); + cache.get(emails[i]); + cache.get(ids[i]); } for (std::size_t i = 0; i < writing_kOperationsNumber; ++i) { @@ -122,9 +122,9 @@ static void GetOperations(::benchmark::State& state) { state.ResumeTiming(); for (std::size_t i = 0; i < operations_count; ++i) { - ::benchmark::DoNotOptimize(cache.find(names[i])); - ::benchmark::DoNotOptimize(cache.find(emails[i])); - ::benchmark::DoNotOptimize(cache.find(ids[i])); + ::benchmark::DoNotOptimize(cache.get(names[i])); + ::benchmark::DoNotOptimize(cache.get(emails[i])); + ::benchmark::DoNotOptimize(cache.get(ids[i])); } } diff --git a/libraries/multi-index-lru/src/container_test.cpp b/libraries/multi-index-lru/src/container_test.cpp index 278936ab099b..91dde16e18b0 100644 --- a/libraries/multi-index-lru/src/container_test.cpp +++ b/libraries/multi-index-lru/src/container_test.cpp @@ -54,23 +54,23 @@ UTEST_F(LRUUsersTest, BasicOperations) { EXPECT_EQ(cache.size(), 3); - // Test find by id - auto by_id = cache.find(1); + // Test get by id + auto by_id = cache.get(1); ASSERT_NE(by_id, cache.end()); EXPECT_EQ(by_id->name, "Alice"); - // Test find by email - auto by_email = cache.find("bob@test.com"); + // Test get by email + auto by_email = cache.get("bob@test.com"); ASSERT_NE(by_email, cache.end()); EXPECT_EQ(by_email->id, 2); - // Test find by name - auto by_name = cache.find("Charlie"); + // Test get by name + auto by_name = cache.get("Charlie"); ASSERT_NE(by_name, cache.end()); EXPECT_EQ(by_name->email, "charlie@test.com"); - // Test template find method - auto it = cache.find("alice@test.com"); + // Test template get method + auto it = cache.get("alice@test.com"); EXPECT_NE(it, cache.end()); } @@ -82,8 +82,8 @@ UTEST_F(LRUUsersTest, LRUEviction) { cache.emplace(User{3, "charlie@test.com", "Charlie"}); // Access Alice and Charlie to make them recently used - cache.find(1); - cache.find(3); + cache.get(1); + cache.get(3); // Add fourth element - Bob should be evicted cache.emplace(User{4, "david@test.com", "David"}); @@ -94,6 +94,89 @@ UTEST_F(LRUUsersTest, LRUEviction) { EXPECT_TRUE((cache.contains(4))); // David added } +UTEST_F(LRUUsersTest, GetNoUpdateDoesNotChangeLru) { + UserCache cache(3); + + cache.emplace(User{1, "alice@test.com", "Alice"}); + cache.emplace(User{2, "bob@test.com", "Bob"}); + cache.emplace(User{3, "charlie@test.com", "Charlie"}); + + auto it = cache.get_no_update(1); // without updating + ASSERT_NE(it, cache.end()); + EXPECT_EQ(it->name, "Alice"); + + cache.emplace(User{4, "david@test.com", "David"}); + + EXPECT_FALSE((cache.contains(1))); // evicted + EXPECT_TRUE((cache.contains(2))); // remains + EXPECT_TRUE((cache.contains(3))); // remains + EXPECT_TRUE((cache.contains(4))); // added +} + +UTEST_F(LRUUsersTest, EqualRangeUpdatesLruForAllMatches) { + UserCache cache(4); + + cache.emplace(User{1, "john1@test.com", "John"}); + cache.emplace(User{2, "john2@test.com", "John"}); + cache.emplace(User{3, "alice@test.com", "Alice"}); + cache.emplace(User{4, "bob@test.com", "Bob"}); + + auto [begin, end] = cache.equal_range("John"); + int count = 0; + for (auto it = begin; it != end; ++it) { + ++count; + } + EXPECT_EQ(count, 2); + + cache.emplace(User{5, "eve@test.com", "Eve"}); + + EXPECT_TRUE((cache.contains(1))); // remains + EXPECT_TRUE((cache.contains(2))); // remains + EXPECT_FALSE((cache.contains(3))); // evicted + EXPECT_TRUE((cache.contains(4))); // remains + EXPECT_TRUE((cache.contains(5))); // added +} + +UTEST_F(LRUUsersTest, EqualRangeNoUpdateDoesNotChangeLru) { + UserCache cache(4); + + cache.emplace(User{1, "john1@test.com", "John"}); + cache.emplace(User{2, "john2@test.com", "John"}); + cache.emplace(User{3, "alice@test.com", "Alice"}); + cache.emplace(User{4, "bob@test.com", "Bob"}); + + auto [begin, end] = cache.equal_range_no_update("John"); + int count = 0; + for (auto it = begin; it != end; ++it) { + ++count; + } + EXPECT_EQ(count, 2); + + cache.emplace(User{5, "eve@test.com", "Eve"}); + + EXPECT_FALSE((cache.contains(1))); // evicted + EXPECT_TRUE((cache.contains(2))); // remains + EXPECT_TRUE((cache.contains(3))); // remains + EXPECT_TRUE((cache.contains(4))); // remains + EXPECT_TRUE((cache.contains(5))); // added +} + +UTEST_F(LRUUsersTest, EqualRangeWorksWithEmptyRange) { + UserCache cache(3); + cache.emplace(User{1, "alice@test.com", "Alice"}); + + auto [begin, end] = cache.equal_range("Nonexistent"); + EXPECT_EQ(begin, end); +} + +UTEST_F(LRUUsersTest, EqualRangeNoUpdateWorksWithEmptyRange) { + UserCache cache(3); + cache.emplace(User{1, "alice@test.com", "Alice"}); + + auto [begin, end] = cache.equal_range_no_update("Nonexistent"); + EXPECT_EQ(begin, end); +} + class ProductsTest : public ::testing::Test { protected: struct SkuTag {}; @@ -126,7 +209,7 @@ UTEST_F(ProductsTest, BasicProductOperations) { cache.emplace(Product{"A1", "Laptop", 999.99}); cache.emplace(Product{"A2", "Mouse", 29.99}); - auto laptop = cache.find("A1"); + auto laptop = cache.get("A1"); ASSERT_NE(laptop, cache.end()); EXPECT_EQ(laptop->name, "Laptop"); } @@ -138,15 +221,15 @@ UTEST_F(ProductsTest, ProductEviction) { cache.emplace(Product{"A2", "Mouse", 29.99}); // A1 was used, so A2 should be ousted when adding A3 - cache.find("A1"); + cache.get("A1"); cache.emplace(Product{"A3", "Keyboard", 79.99}); EXPECT_TRUE((cache.contains("A1"))); // used EXPECT_TRUE((cache.contains("A3"))); // new EXPECT_FALSE((cache.contains("A2"))); // ousted - EXPECT_NE(cache.find("Keyboard"), cache.end()); - EXPECT_EQ(cache.find("Mouse"), cache.end()); + EXPECT_NE(cache.get("Keyboard"), cache.end()); + EXPECT_EQ(cache.get("Mouse"), cache.end()); } TEST(Snippet, SimpleUsage) { @@ -167,7 +250,7 @@ TEST(Snippet, SimpleUsage) { MyLruCache cache(1000); // Capacity of 1000 items cache.insert(my_value); - auto it = cache.find("some_key"); + auto it = cache.get("some_key"); EXPECT_NE(it, cache.end()); /// [Usage] } diff --git a/libraries/multi-index-lru/src/expirable_container_test.cpp b/libraries/multi-index-lru/src/expirable_container_test.cpp index 9701ca21531f..976b12db7ca9 100644 --- a/libraries/multi-index-lru/src/expirable_container_test.cpp +++ b/libraries/multi-index-lru/src/expirable_container_test.cpp @@ -1,9 +1,12 @@ #include #include #include +#include +#include #include #include +#include #include #include @@ -57,20 +60,20 @@ UTEST_F(ExpirableUsersTest, BasicOperations) { EXPECT_EQ(cache.capacity(), 3); EXPECT_FALSE(cache.empty()); - // Test get by id - auto by_id = cache.get(1); - EXPECT_FALSE(by_id.empty()); - EXPECT_EQ(by_id.begin()->name, "Alice"); + // Test get by id (unique index) – returns vector + auto alice_vec = cache.get(1); + ASSERT_EQ(alice_vec.size(), 1); + EXPECT_EQ(alice_vec[0].name, "Alice"); - // Test get by email - auto by_email = cache.get("bob@test.com"); - EXPECT_FALSE(by_email.empty()); - EXPECT_EQ(by_email.begin()->id, 2); + // Test get by email (unique index) + auto bob_vec = cache.get("bob@test.com"); + ASSERT_EQ(bob_vec.size(), 1); + EXPECT_EQ(bob_vec[0].id, 2); - // Test get by name - auto by_name = cache.get("Charlie"); - EXPECT_FALSE(by_name.empty()); - EXPECT_EQ(by_name.begin()->email, "charlie@test.com"); + // Test get by name (non‑unique index) – returns all with that name + auto charlie_vec = cache.get("Charlie"); + ASSERT_EQ(charlie_vec.size(), 1); + EXPECT_EQ(charlie_vec[0].email, "charlie@test.com"); } UTEST_F(ExpirableUsersTest, LRUEviction) { @@ -80,9 +83,9 @@ UTEST_F(ExpirableUsersTest, LRUEviction) { cache.insert(User{2, "bob@test.com", "Bob"}); cache.insert(User{3, "charlie@test.com", "Charlie"}); - // Access Alice and Charlie to make them recently used - cache.get(1); - cache.get(3); + // Access Alice and Charlie to make them recently used (contains updates timestamp) + EXPECT_TRUE(cache.contains(1)); + EXPECT_TRUE(cache.contains(3)); // Add fourth element - Bob should be evicted (LRU) cache.insert(User{4, "david@test.com", "David"}); @@ -98,7 +101,7 @@ UTEST_F(ExpirableUsersTest, TTLExpiration) { using namespace std::chrono_literals; UserCacheExpirable cache(100, 100ms); // Very short TTL for testing - + cache.insert(User{1, "alice@test.com", "Alice"}); cache.insert(User{2, "bob@test.com", "Bob"}); @@ -108,7 +111,7 @@ UTEST_F(ExpirableUsersTest, TTLExpiration) { EXPECT_EQ(cache.size(), 2); // Wait for TTL to expire - std::this_thread::sleep_for(150ms); + userver::engine::SleepFor(150ms); EXPECT_FALSE(cache.contains(1)); EXPECT_FALSE(cache.contains(2)); @@ -116,30 +119,27 @@ UTEST_F(ExpirableUsersTest, TTLExpiration) { } UTEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { - using namespace std::chrono_literals; UserCacheExpirable cache(100, 190ms); - + cache.insert(User{1, "alice@test.com", "Alice"}); // Wait a bit but not enough to expire - std::this_thread::sleep_for(100ms); - - // Access should refresh TTL + userver::engine::SleepFor(99ms); + // Access via contains should refresh TTL EXPECT_TRUE(cache.contains(1)); // Wait again - should still be alive due to refresh - std::this_thread::sleep_for(100ms); + userver::engine::SleepFor(99ms); EXPECT_TRUE(cache.contains(1)); // Wait for full TTL from last access - std::this_thread::sleep_for(200ms); + userver::engine::SleepFor(200ms); EXPECT_FALSE(cache.contains(1)); } UTEST_F(ExpirableUsersTest, EraseOperations) { - UserCacheExpirable cache(3, std::chrono::seconds(10)); cache.insert(User{1, "alice@test.com", "Alice"}); @@ -155,7 +155,6 @@ UTEST_F(ExpirableUsersTest, EraseOperations) { } UTEST_F(ExpirableUsersTest, SetCapacity) { - UserCacheExpirable cache(5, std::chrono::seconds(10)); // Fill cache @@ -174,7 +173,6 @@ UTEST_F(ExpirableUsersTest, SetCapacity) { } UTEST_F(ExpirableUsersTest, Clear) { - UserCacheExpirable cache(5, std::chrono::seconds(10)); cache.insert(User{1, "alice@test.com", "Alice"}); @@ -192,8 +190,9 @@ UTEST_F(ExpirableUsersTest, Clear) { } UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) { - + // Container is not thread-safe; external synchronization required. UserCacheExpirable cache(100, std::chrono::seconds(10)); + engine::Mutex mutex; constexpr int kCoroutines = 4; constexpr int kIterations = 100; @@ -201,18 +200,23 @@ UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) { tasks.reserve(kCoroutines); for (int t = 0; t < kCoroutines; ++t) { - tasks.push_back(utils::Async("using cache", [&cache, t]() { + tasks.push_back(utils::Async("using cache", [&cache, &mutex, t]() { for (int i = 0; i < kIterations; ++i) { int id = t * kIterations + i; - cache.insert(User{id, std::to_string(id) + "@test.com", "User" + std::to_string(id)}); + { + std::lock_guard lock(mutex); + cache.insert(User{id, std::to_string(id) + "@test.com", "User" + std::to_string(id)}); + } if (id % 3 == 0) { - cache.get(id); + std::lock_guard lock(mutex); + // Use contains to check existence and update timestamp cache.contains(id); } if (id % 5 == 0) { + std::lock_guard lock(mutex); cache.erase(id - 1); } } @@ -223,8 +227,10 @@ UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) { task.Get(); } + std::lock_guard lock(mutex); EXPECT_LE(cache.size(), 100); } + } // namespace USERVER_NAMESPACE_END \ No newline at end of file From 879621999aea6038c6553ae9ccf1f3a48e13d1f0 Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Sat, 21 Feb 2026 18:32:57 +0300 Subject: [PATCH 10/14] fixes --- .../multi-index-lru/expirable_container.hpp | 9 +++------ .../src/expirable_container_benchmark.cpp | 18 +++--------------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index 035ac999b00d..873e0393e2df 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -1,20 +1,17 @@ #pragma once -/// @file userver/multi-index-lru/container.hpp +/// @file userver/multi-index-lru/expirable_container.hpp /// @brief @copybrief multi_index_lru::ExpirableContainer #include -#include #include -#include -#include -#include #include "impl/mpl_helpers.hpp" #include "container.hpp" #include #include +#include #include #include #include @@ -35,7 +32,7 @@ class ExpirableContainer { std::chrono::milliseconds ttl) : container_(max_size), ttl_(ttl) { - assert(ttl.count() > 0 && "ttl must be positive"); + UASSERT_MSG(ttl.count() > 0, "ttl must be positive"); } template diff --git a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp index 2363b2e36301..0814b64938b5 100644 --- a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp +++ b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp @@ -74,11 +74,7 @@ static void ExpirableFindEmplaceMix(benchmark::State& state) { userver::engine::RunStandalone([&] { const std::size_t cache_size = state.range(0); - UserCache cache( - cache_size, - std::chrono::seconds(5), // ttl — big to not cause interference - std::chrono::seconds(1) // cleanup_interval - ); + UserCache cache(cache_size, std::chrono::seconds(5)); for (std::size_t i = 0; i < cache_size; ++i) { cache.insert(GenerateUser()); @@ -131,11 +127,7 @@ static void ExpirableGetOperations(benchmark::State& state) { userver::engine::RunStandalone([&] { const std::size_t cache_size = state.range(0); - UserCache cache( - cache_size, - std::chrono::minutes(10), - std::chrono::minutes(1) - ); + UserCache cache(cache_size, std::chrono::minutes(10)); PrepareCache(cache, cache_size); @@ -175,11 +167,7 @@ static void ExpirableEmplaceOperations(benchmark::State& state) { userver::engine::RunStandalone([&] { const std::size_t cache_size = state.range(0); - UserCache cache( - cache_size, - std::chrono::minutes(10), - std::chrono::minutes(1) - ); + UserCache cache(cache_size, std::chrono::minutes(10)); PrepareCache(cache, cache_size); for (auto _ : state) { From 7841df53c0a7a92773fcb68311a7c05bddd71d1f Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Sun, 22 Feb 2026 00:56:36 +0300 Subject: [PATCH 11/14] returned the find methods and the return of iterators --- .../userver/multi-index-lru/container.hpp | 6 +- .../multi-index-lru/expirable_container.hpp | 118 ++++++-------- .../multi-index-lru/impl/mpl_helpers.hpp | 15 ++ .../src/container_benchmark.cpp | 12 +- .../multi-index-lru/src/container_test.cpp | 41 +++-- .../src/expirable_container_benchmark.cpp | 12 +- .../src/expirable_container_test.cpp | 146 +++++++++++++----- 7 files changed, 206 insertions(+), 144 deletions(-) diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp index f92e5361fc69..c576ebd8956e 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/container.hpp @@ -40,7 +40,7 @@ class Container { bool insert(Value&& value) { return emplace(std::move(value)).second; } template - auto get(const Key& key) { + auto find(const Key& key) { auto& primary_index = get_index(); auto it = primary_index.find(key); @@ -54,7 +54,7 @@ class Container { } template - auto get_no_update(const Key& key) { + auto find_no_update(const Key& key) { return get_index().find(key); } @@ -81,7 +81,7 @@ class Container { template bool contains(const Key& key) { - return this->template get(key) != get_index().end(); + return this->template find(key) != end(); } template diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index 873e0393e2df..40c9c1cdf0d6 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -51,48 +51,66 @@ class ExpirableContainer { bool insert(Value&& value) { return emplace(std::move(value)).second; } template - auto get(const Key& key) { - std::vector result; - auto& index = container_.template get_index(); + auto find(const Key& key) { + auto now = std::chrono::steady_clock::now(); + auto it = container_.template find(key); - if constexpr (impl::is_unique_index::value) { - auto it = find(key); - if (it != container_.template end()) { - result.push_back(it->value); - } - } else { - auto range = find_range(key); - for (auto it = range.first; it != range.second; ++it) { - result.push_back(it->value); + if (it != container_.template end()) { + if (now > it->last_accessed + ttl_) { + container_.template get_index().erase(it); + return end(); + } else { + it->last_accessed = now; } } - return result; + return impl::TimestampedIteratorWrapper{it}; } template - auto get_no_update(const Key& key) { - std::vector result; + auto find_no_update(const Key& key) { + auto it = container_.template find_no_update(key); + return impl::TimestampedIteratorWrapper{it}; + } + + template + auto equal_range(const Key& key) { + auto now = std::chrono::steady_clock::now(); auto& index = container_.template get_index(); + auto [begin, end] = container_.template equal_range(key); - if constexpr (impl::is_unique_index::value) { - auto it = container_.template get_no_update(key); - if (it != container_.template end()) { - result.push_back(it->value); - } - } else { - auto range = container_.template equal_range_no_update(key); - for (auto it = range.first; it != range.second; ++it) { - result.push_back(it->value); + auto it = begin; + std::vector to_erase; + + while (it != end) { + if (now > it->last_accessed + ttl_) { + to_erase.push_back(it); + ++it; + } else { + it->last_accessed = now; + ++it; } } - return result; + for (auto erase_it : to_erase) { + index.erase(erase_it); + } + + auto ret = index.equal_range(key); + return std::pair{impl::TimestampedIteratorWrapper{ret.first}, + impl::TimestampedIteratorWrapper{ret.second}}; + } + + template + auto equal_range_no_update(const Key& key) { + auto [begin, end] = container_.template equal_range_no_update(key); + return std::pair{impl::TimestampedIteratorWrapper{begin}, + impl::TimestampedIteratorWrapper{end}}; } template bool contains(const Key& key) { - return this->template find(key) != container_.template end(); + return this->template find(key) != this->template end(); } template @@ -118,7 +136,7 @@ class ExpirableContainer { template auto end() { - return container_.template end(); + return impl::TimestampedIteratorWrapper{container_.template end()}; } void cleanup_expired() { @@ -137,54 +155,8 @@ class ExpirableContainer { private: using CacheItem = impl::TimestampedValue; - using ExtendedIndexSpecifierList = impl::add_index_t< - boost::multi_index::sequenced<>, - IndexSpecifierList>; using CacheContainer = Container; - template - auto find(const Key& key) { - auto now = std::chrono::steady_clock::now(); - auto it = container_.template get(key); - - if (it != end()) { - if (now > it->last_accessed + ttl_) { - container_.template get_index().erase(it); - return end(); - } else { - it->last_accessed = now; - } - } - - return it; - } - - template - auto find_range(const Key& key) { - auto now = std::chrono::steady_clock::now(); - auto& index = container_.template get_index(); - auto [begin, end] = container_.template equal_range(key); - - auto it = begin; - std::vector to_erase; - - while (it != end) { - if (now > it->last_accessed + ttl_) { - to_erase.push_back(it); - ++it; - } else { - it->last_accessed = now; - ++it; - } - } - - for (auto erase_it : to_erase) { - index.erase(erase_it); - } - - return index.equal_range(key); - } - CacheContainer container_; std::chrono::milliseconds ttl_; }; diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp index bdbe0f5a0a61..f38c76ac0c6b 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/impl/mpl_helpers.hpp @@ -79,6 +79,21 @@ struct TimestampedValue { const Value& get() const { return value; } }; +template +class TimestampedIteratorWrapper : public Iterator { +public: + using Iterator::Iterator; + TimestampedIteratorWrapper(Iterator iter) : Iterator(std::move(iter)) {} + + auto operator->() { + return *(this->Iterator::operator->()); + } + + auto operator->() const { + return *(this->Iterator::operator->()); + } +}; + template struct is_unique_index { private: diff --git a/libraries/multi-index-lru/src/container_benchmark.cpp b/libraries/multi-index-lru/src/container_benchmark.cpp index 956a394752a8..ed91ddf6e7fb 100644 --- a/libraries/multi-index-lru/src/container_benchmark.cpp +++ b/libraries/multi-index-lru/src/container_benchmark.cpp @@ -84,9 +84,9 @@ void LruFindEmplaceMix(benchmark::State& state) { for ([[maybe_unused]] auto _ : state) { for (std::size_t i = 0; i < reading_kOperationsNumber; ++i) { - cache.get(names[i]); - cache.get(emails[i]); - cache.get(ids[i]); + cache.find(names[i]); + cache.find(emails[i]); + cache.find(ids[i]); } for (std::size_t i = 0; i < writing_kOperationsNumber; ++i) { @@ -122,9 +122,9 @@ static void GetOperations(::benchmark::State& state) { state.ResumeTiming(); for (std::size_t i = 0; i < operations_count; ++i) { - ::benchmark::DoNotOptimize(cache.get(names[i])); - ::benchmark::DoNotOptimize(cache.get(emails[i])); - ::benchmark::DoNotOptimize(cache.get(ids[i])); + ::benchmark::DoNotOptimize(cache.find(names[i])); + ::benchmark::DoNotOptimize(cache.find(emails[i])); + ::benchmark::DoNotOptimize(cache.find(ids[i])); } } diff --git a/libraries/multi-index-lru/src/container_test.cpp b/libraries/multi-index-lru/src/container_test.cpp index 91dde16e18b0..4ed680202073 100644 --- a/libraries/multi-index-lru/src/container_test.cpp +++ b/libraries/multi-index-lru/src/container_test.cpp @@ -54,23 +54,22 @@ UTEST_F(LRUUsersTest, BasicOperations) { EXPECT_EQ(cache.size(), 3); - // Test get by id - auto by_id = cache.get(1); - ASSERT_NE(by_id, cache.end()); + // Test find by id + auto by_id = cache.find(1); + EXPECT_NE(by_id, cache.end()); EXPECT_EQ(by_id->name, "Alice"); - // Test get by email - auto by_email = cache.get("bob@test.com"); - ASSERT_NE(by_email, cache.end()); + // Test find by email + auto by_email = cache.find("bob@test.com"); + EXPECT_NE(by_email, cache.end()); EXPECT_EQ(by_email->id, 2); - // Test get by name - auto by_name = cache.get("Charlie"); - ASSERT_NE(by_name, cache.end()); + // Test find by name + auto by_name = cache.find("Charlie"); + EXPECT_NE(by_name, cache.end()); EXPECT_EQ(by_name->email, "charlie@test.com"); - // Test template get method - auto it = cache.get("alice@test.com"); + auto it = cache.find("alice@test.com"); EXPECT_NE(it, cache.end()); } @@ -82,8 +81,8 @@ UTEST_F(LRUUsersTest, LRUEviction) { cache.emplace(User{3, "charlie@test.com", "Charlie"}); // Access Alice and Charlie to make them recently used - cache.get(1); - cache.get(3); + cache.find(1); + cache.find(3); // Add fourth element - Bob should be evicted cache.emplace(User{4, "david@test.com", "David"}); @@ -101,8 +100,8 @@ UTEST_F(LRUUsersTest, GetNoUpdateDoesNotChangeLru) { cache.emplace(User{2, "bob@test.com", "Bob"}); cache.emplace(User{3, "charlie@test.com", "Charlie"}); - auto it = cache.get_no_update(1); // without updating - ASSERT_NE(it, cache.end()); + auto it = cache.find_no_update(1); // without updating + EXPECT_NE(it, cache.end()); EXPECT_EQ(it->name, "Alice"); cache.emplace(User{4, "david@test.com", "David"}); @@ -209,8 +208,8 @@ UTEST_F(ProductsTest, BasicProductOperations) { cache.emplace(Product{"A1", "Laptop", 999.99}); cache.emplace(Product{"A2", "Mouse", 29.99}); - auto laptop = cache.get("A1"); - ASSERT_NE(laptop, cache.end()); + auto laptop = cache.find("A1"); + EXPECT_NE(laptop, cache.end()); EXPECT_EQ(laptop->name, "Laptop"); } @@ -221,15 +220,15 @@ UTEST_F(ProductsTest, ProductEviction) { cache.emplace(Product{"A2", "Mouse", 29.99}); // A1 was used, so A2 should be ousted when adding A3 - cache.get("A1"); + cache.find("A1"); cache.emplace(Product{"A3", "Keyboard", 79.99}); EXPECT_TRUE((cache.contains("A1"))); // used EXPECT_TRUE((cache.contains("A3"))); // new EXPECT_FALSE((cache.contains("A2"))); // ousted - EXPECT_NE(cache.get("Keyboard"), cache.end()); - EXPECT_EQ(cache.get("Mouse"), cache.end()); + EXPECT_NE(cache.find("Keyboard"), cache.end()); + EXPECT_EQ(cache.find("Mouse"), cache.end()); } TEST(Snippet, SimpleUsage) { @@ -250,7 +249,7 @@ TEST(Snippet, SimpleUsage) { MyLruCache cache(1000); // Capacity of 1000 items cache.insert(my_value); - auto it = cache.get("some_key"); + auto it = cache.find("some_key"); EXPECT_NE(it, cache.end()); /// [Usage] } diff --git a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp index 0814b64938b5..077941fd3272 100644 --- a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp +++ b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp @@ -100,9 +100,9 @@ static void ExpirableFindEmplaceMix(benchmark::State& state) { for (auto _ : state) { for (std::size_t i = 0; i < read_ops; ++i) { - cache.get(names[i]); - cache.get(emails[i]); - cache.get(ids[i]); + cache.find(names[i]); + cache.find(emails[i]); + cache.find(ids[i]); } for (std::size_t i = 0; i < write_ops; ++i) { @@ -147,9 +147,9 @@ static void ExpirableGetOperations(benchmark::State& state) { state.ResumeTiming(); for (std::size_t i = 0; i < kOperationsNumber; ++i) { - benchmark::DoNotOptimize(cache.get(names[i])); - benchmark::DoNotOptimize(cache.get(emails[i])); - benchmark::DoNotOptimize(cache.get(ids[i])); + benchmark::DoNotOptimize(cache.find(names[i])); + benchmark::DoNotOptimize(cache.find(emails[i])); + benchmark::DoNotOptimize(cache.find(ids[i])); } } diff --git a/libraries/multi-index-lru/src/expirable_container_test.cpp b/libraries/multi-index-lru/src/expirable_container_test.cpp index 976b12db7ca9..c9cec3842899 100644 --- a/libraries/multi-index-lru/src/expirable_container_test.cpp +++ b/libraries/multi-index-lru/src/expirable_container_test.cpp @@ -60,20 +60,32 @@ UTEST_F(ExpirableUsersTest, BasicOperations) { EXPECT_EQ(cache.capacity(), 3); EXPECT_FALSE(cache.empty()); - // Test get by id (unique index) – returns vector - auto alice_vec = cache.get(1); - ASSERT_EQ(alice_vec.size(), 1); - EXPECT_EQ(alice_vec[0].name, "Alice"); - - // Test get by email (unique index) - auto bob_vec = cache.get("bob@test.com"); - ASSERT_EQ(bob_vec.size(), 1); - EXPECT_EQ(bob_vec[0].id, 2); - - // Test get by name (non‑unique index) – returns all with that name - auto charlie_vec = cache.get("Charlie"); - ASSERT_EQ(charlie_vec.size(), 1); - EXPECT_EQ(charlie_vec[0].email, "charlie@test.com"); + // Test find by id (unique index) + auto alice_it = cache.find(1); + EXPECT_NE(alice_it, cache.end()); + EXPECT_EQ(alice_it->name, "Alice"); + + // Test find by email (unique index) + auto bob_it = cache.find("bob@test.com"); + EXPECT_NE(bob_it, cache.end()); + EXPECT_EQ(bob_it->id, 2); + + // Test find by name (non‑unique index) - returns first match + auto charlie_it = cache.find("Charlie"); + EXPECT_NE(charlie_it, cache.end()); + EXPECT_EQ(charlie_it->email, "charlie@test.com"); +} + +UTEST_F(ExpirableUsersTest, FindNoUpdate) { + UserCacheExpirable cache(3, std::chrono::seconds(10)); + + cache.insert(User{1, "alice@test.com", "Alice"}); + cache.insert(User{2, "bob@test.com", "Bob"}); + cache.insert(User{3, "charlie@test.com", "Charlie"}); + + // Both finds should succeed + EXPECT_NE(cache.find(1), cache.end()); + EXPECT_NE(cache.find_no_update(1), cache.end()); } UTEST_F(ExpirableUsersTest, LRUEviction) { @@ -83,17 +95,17 @@ UTEST_F(ExpirableUsersTest, LRUEviction) { cache.insert(User{2, "bob@test.com", "Bob"}); cache.insert(User{3, "charlie@test.com", "Charlie"}); - // Access Alice and Charlie to make them recently used (contains updates timestamp) - EXPECT_TRUE(cache.contains(1)); - EXPECT_TRUE(cache.contains(3)); + // Access Alice and Charlie to make them recently used + EXPECT_NE(cache.find(1), cache.end()); + EXPECT_NE(cache.find(3), cache.end()); // Add fourth element - Bob should be evicted (LRU) cache.insert(User{4, "david@test.com", "David"}); - EXPECT_FALSE(cache.contains(2)); // Bob evicted (LRU) - EXPECT_TRUE(cache.contains(1)); // Alice remains - EXPECT_TRUE(cache.contains(3)); // Charlie remains - EXPECT_TRUE(cache.contains(4)); // David added + EXPECT_EQ(cache.find(2), cache.end()); // Bob evicted (LRU) + EXPECT_NE(cache.find(1), cache.end()); // Alice remains + EXPECT_NE(cache.find(3), cache.end()); // Charlie remains + EXPECT_NE(cache.find(4), cache.end()); // David added EXPECT_EQ(cache.size(), 3); } @@ -106,15 +118,15 @@ UTEST_F(ExpirableUsersTest, TTLExpiration) { cache.insert(User{2, "bob@test.com", "Bob"}); // Items should still exist - EXPECT_TRUE(cache.contains(1)); - EXPECT_TRUE(cache.contains(2)); + EXPECT_NE(cache.find(1), cache.end()); + EXPECT_NE(cache.find(2), cache.end()); EXPECT_EQ(cache.size(), 2); // Wait for TTL to expire userver::engine::SleepFor(150ms); - EXPECT_FALSE(cache.contains(1)); - EXPECT_FALSE(cache.contains(2)); + EXPECT_EQ(cache.find(1), cache.end()); + EXPECT_EQ(cache.find(2), cache.end()); EXPECT_EQ(cache.size(), 0); } @@ -127,16 +139,63 @@ UTEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { // Wait a bit but not enough to expire userver::engine::SleepFor(99ms); - // Access via contains should refresh TTL - EXPECT_TRUE(cache.contains(1)); + + // Access via find should refresh TTL + EXPECT_NE(cache.find(1), cache.end()); // Wait again - should still be alive due to refresh userver::engine::SleepFor(99ms); - EXPECT_TRUE(cache.contains(1)); + EXPECT_NE(cache.find(1), cache.end()); // Wait for full TTL from last access userver::engine::SleepFor(200ms); - EXPECT_FALSE(cache.contains(1)); + EXPECT_EQ(cache.find(1), cache.end()); +} + +UTEST_F(ExpirableUsersTest, EqualRangeOperations) { + using namespace std::chrono_literals; + + UserCacheExpirable cache(10, 1h); // Long TTL to avoid expiration + + // Insert multiple users with the same name + cache.insert(User{1, "john1@test.com", "John"}); + cache.insert(User{2, "john2@test.com", "John"}); + cache.insert(User{3, "john3@test.com", "John"}); + cache.insert(User{4, "alice@test.com", "Alice"}); + + // Test equal_range for non-unique index + auto [begin, end] = cache.equal_range("John"); + + // Count matches + int count = 0; + for (auto it = begin; it != end; ++it) { + ++count; + EXPECT_EQ(it->name, "John"); + } + EXPECT_EQ(count, 3); + + // Test equal_range for non-existent key + auto [begin_empty, end_empty] = cache.equal_range("NonExistent"); + EXPECT_EQ(begin_empty, end_empty); +} + +UTEST_F(ExpirableUsersTest, EqualRangeNoUpdate) { + using namespace std::chrono_literals; + + UserCacheExpirable cache(10, 1h); + + cache.insert(User{1, "john1@test.com", "John"}); + cache.insert(User{2, "john2@test.com", "John"}); + + // equal_range_no_update should work and find all matches + auto [begin, end] = cache.equal_range_no_update("John"); + + int count = 0; + for (auto it = begin; it != end; ++it) { + ++count; + EXPECT_TRUE(it->id == 1 || it->id == 2); + } + EXPECT_EQ(count, 2); } UTEST_F(ExpirableUsersTest, EraseOperations) { @@ -146,8 +205,8 @@ UTEST_F(ExpirableUsersTest, EraseOperations) { cache.insert(User{2, "bob@test.com", "Bob"}); EXPECT_TRUE(cache.erase(1)); - EXPECT_FALSE(cache.contains(1)); - EXPECT_TRUE(cache.contains(2)); + EXPECT_EQ(cache.find(1), cache.end()); + EXPECT_NE(cache.find(2), cache.end()); EXPECT_EQ(cache.size(), 1); EXPECT_FALSE(cache.erase(999)); // Non-existent @@ -185,8 +244,25 @@ UTEST_F(ExpirableUsersTest, Clear) { EXPECT_EQ(cache.size(), 0); EXPECT_TRUE(cache.empty()); - EXPECT_FALSE(cache.contains(1)); - EXPECT_FALSE(cache.contains(2)); + EXPECT_EQ(cache.find(1), cache.end()); + EXPECT_EQ(cache.find(2), cache.end()); +} + +UTEST_F(ExpirableUsersTest, CleanupExpired) { + using namespace std::chrono_literals; + + UserCacheExpirable cache(5, 100ms); + + cache.insert(User{1, "alice@test.com", "Alice"}); + cache.insert(User{2, "bob@test.com", "Bob"}); + + // Wait for TTL to expire + userver::engine::SleepFor(150ms); + + // cleanup_expired should remove expired items + cache.cleanup_expired(); + + EXPECT_EQ(cache.size(), 0); } UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) { @@ -211,8 +287,8 @@ UTEST_F(ExpirableUsersTest, ThreadSafetyBasic) { if (id % 3 == 0) { std::lock_guard lock(mutex); - // Use contains to check existence and update timestamp - cache.contains(id); + // Use find to check existence and update timestamp + cache.find(id); } if (id % 5 == 0) { From 814568311ad4c6adf471d03bcd3f69a364c37c2e Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Sun, 22 Feb 2026 15:37:55 +0300 Subject: [PATCH 12/14] suggestions --- .../multi-index-lru/expirable_container.hpp | 33 ++++++------------- .../src/expirable_container_benchmark.cpp | 10 ++---- .../src/expirable_container_test.cpp | 12 +++---- 3 files changed, 18 insertions(+), 37 deletions(-) diff --git a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp index 40c9c1cdf0d6..3642a11f7d4a 100644 --- a/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp +++ b/libraries/multi-index-lru/include/userver/multi-index-lru/expirable_container.hpp @@ -3,20 +3,10 @@ /// @file userver/multi-index-lru/expirable_container.hpp /// @brief @copybrief multi_index_lru::ExpirableContainer -#include -#include - #include "impl/mpl_helpers.hpp" #include "container.hpp" -#include -#include #include -#include -#include -#include -#include -#include USERVER_NAMESPACE_BEGIN @@ -77,28 +67,25 @@ class ExpirableContainer { auto equal_range(const Key& key) { auto now = std::chrono::steady_clock::now(); auto& index = container_.template get_index(); - auto [begin, end] = container_.template equal_range(key); + auto range = container_.template equal_range(key); - auto it = begin; - std::vector to_erase; + auto it = range.first; + bool changed = false; - while (it != end) { + while (it != range.second) { if (now > it->last_accessed + ttl_) { - to_erase.push_back(it); - ++it; + it = index.erase(it); + changed = true; } else { it->last_accessed = now; ++it; } } - - for (auto erase_it : to_erase) { - index.erase(erase_it); + if (changed) { + range = index.equal_range(key); } - - auto ret = index.equal_range(key); - return std::pair{impl::TimestampedIteratorWrapper{ret.first}, - impl::TimestampedIteratorWrapper{ret.second}}; + return std::pair{impl::TimestampedIteratorWrapper{range.first}, + impl::TimestampedIteratorWrapper{range.second}}; } template diff --git a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp index 077941fd3272..de71b6af7b5b 100644 --- a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp +++ b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp @@ -1,14 +1,10 @@ #include #include -#include -#include #include #include #include -#include -#include #include #include @@ -71,7 +67,7 @@ using UserCache = multi_index_lru::ExpirableContainer< boost::multi_index::member>>>; static void ExpirableFindEmplaceMix(benchmark::State& state) { - userver::engine::RunStandalone([&] { + engine::RunStandalone([&] { const std::size_t cache_size = state.range(0); UserCache cache(cache_size, std::chrono::seconds(5)); @@ -124,7 +120,7 @@ static void PrepareCache(UserCache& cache, std::size_t size) { } static void ExpirableGetOperations(benchmark::State& state) { - userver::engine::RunStandalone([&] { + engine::RunStandalone([&] { const std::size_t cache_size = state.range(0); UserCache cache(cache_size, std::chrono::minutes(10)); @@ -164,7 +160,7 @@ BENCHMARK(ExpirableGetOperations) static void ExpirableEmplaceOperations(benchmark::State& state) { - userver::engine::RunStandalone([&] { + engine::RunStandalone([&] { const std::size_t cache_size = state.range(0); UserCache cache(cache_size, std::chrono::minutes(10)); diff --git a/libraries/multi-index-lru/src/expirable_container_test.cpp b/libraries/multi-index-lru/src/expirable_container_test.cpp index c9cec3842899..11699546ffd9 100644 --- a/libraries/multi-index-lru/src/expirable_container_test.cpp +++ b/libraries/multi-index-lru/src/expirable_container_test.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include @@ -11,7 +10,6 @@ #include #include #include -#include USERVER_NAMESPACE_BEGIN @@ -123,7 +121,7 @@ UTEST_F(ExpirableUsersTest, TTLExpiration) { EXPECT_EQ(cache.size(), 2); // Wait for TTL to expire - userver::engine::SleepFor(150ms); + engine::SleepFor(150ms); EXPECT_EQ(cache.find(1), cache.end()); EXPECT_EQ(cache.find(2), cache.end()); @@ -138,17 +136,17 @@ UTEST_F(ExpirableUsersTest, TTLRefreshOnAccess) { cache.insert(User{1, "alice@test.com", "Alice"}); // Wait a bit but not enough to expire - userver::engine::SleepFor(99ms); + engine::SleepFor(99ms); // Access via find should refresh TTL EXPECT_NE(cache.find(1), cache.end()); // Wait again - should still be alive due to refresh - userver::engine::SleepFor(99ms); + engine::SleepFor(99ms); EXPECT_NE(cache.find(1), cache.end()); // Wait for full TTL from last access - userver::engine::SleepFor(200ms); + engine::SleepFor(200ms); EXPECT_EQ(cache.find(1), cache.end()); } @@ -257,7 +255,7 @@ UTEST_F(ExpirableUsersTest, CleanupExpired) { cache.insert(User{2, "bob@test.com", "Bob"}); // Wait for TTL to expire - userver::engine::SleepFor(150ms); + engine::SleepFor(150ms); // cleanup_expired should remove expired items cache.cleanup_expired(); From ca6c41c6e1914ff827f291e462c26225f2dce6f0 Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Sun, 22 Feb 2026 17:22:35 +0300 Subject: [PATCH 13/14] fix --- libraries/multi-index-lru/src/expirable_container_benchmark.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp index de71b6af7b5b..5b237592dfb1 100644 --- a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp +++ b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include From dabf5b1658c571228cac4fb171a030017d6cc330 Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Sun, 22 Feb 2026 18:40:35 +0300 Subject: [PATCH 14/14] fix --- libraries/multi-index-lru/src/expirable_container_benchmark.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp index 5b237592dfb1..39a4bfae6a90 100644 --- a/libraries/multi-index-lru/src/expirable_container_benchmark.cpp +++ b/libraries/multi-index-lru/src/expirable_container_benchmark.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include