From ec4efb5c9a4451fd9c90c4a0a3b29b2c8c83cd23 Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Sun, 5 Apr 2026 23:33:51 +0500 Subject: [PATCH 1/2] test with custom allocator --- .../userver/multi-index-lru/container.hpp | 4 ++ .../multi-index-lru/expirable_container.hpp | 1 + .../multi-index-lru/src/container_test.cpp | 66 ++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) 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 afed3ffaada5..847703a331e5 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,6 +24,7 @@ class Container { : max_size_(max_size) {} + // Constructs element in-place, returns pair template auto emplace(Args&&... args) { auto& seq_index = get_sequenced(); @@ -71,6 +72,7 @@ class Container { return get_index().find(key); } + // Returns range of matching elements, updates all template auto equal_range(const Key& key) { auto& primary_index = get_index(); @@ -87,6 +89,7 @@ class Container { return std::pair{begin, end}; } + // Returns range of matching elements without updates template auto equal_range_no_update(const Key& key) const { return get_index().equal_range(key); @@ -158,6 +161,7 @@ class Container { } } + // Returns end iterator for specified index template auto end() const { return get_index().end(); 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 47fdc86cb4ce..bd3471b9da92 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 @@ -137,6 +137,7 @@ class ExpirableContainer { return impl::IteratorToValue{container_.template end()}; } + // Removes all expired items from container void cleanup_expired() { const auto now = std::chrono::steady_clock::now(); diff --git a/libraries/multi-index-lru/src/container_test.cpp b/libraries/multi-index-lru/src/container_test.cpp index a4f43a87c2a0..6d9215ed0721 100644 --- a/libraries/multi-index-lru/src/container_test.cpp +++ b/libraries/multi-index-lru/src/container_test.cpp @@ -231,7 +231,71 @@ UTEST_F(ProductsTest, ProductEviction) { EXPECT_EQ(cache.find("Mouse"), cache.end()); } -TEST(Snippet, SimpleUsage) { +class ProductsTestWithAllocator : public ProductsTest { +protected: + + class Counter { + public: + static std::atomic count; + static void increment() { count++; } + static size_t get() { return count.load(); } + static void reset() { count = 0; } + }; + + template + class CountingAllocator : public std::allocator { + public: + CountingAllocator() = default; + template + CountingAllocator(const CountingAllocator&) {} + + T* allocate(size_t n) { + Counter::increment(); + return std::allocator::allocate(n); + } + + static size_t get_count() {return Counter::get();} + static void reset_count() {Counter::reset();} + + template + struct rebind { + using other = CountingAllocator; + }; + }; + + 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>>, + CountingAllocator>; +}; + +std::atomic ProductsTestWithAllocator::Counter::count{0}; + +UTEST_F(ProductsTestWithAllocator, AllocationsCheck) { + ProductCache cache(20); + + for (int i = 0; i < 1000; ++i) { + cache.insert(Product{"A" + std::to_string(i), "Laptop_" + std::to_string(i), 999.99}); + } + auto firstAllocationsCount = ProductsTestWithAllocator::CountingAllocator::get_count(); + + cache.clear(); + for (int i = 0; i < 1000; ++i) { + cache.insert(Product{"A" + std::to_string(i), "Laptop_" + std::to_string(i), 999.99}); + } + auto secondAllocationsCount = ProductsTestWithAllocator::CountingAllocator::get_count(); + + // no extra allocations since nodes are being reused + EXPECT_EQ(firstAllocationsCount, secondAllocationsCount); +} + +UTEST(Snippet, SimpleUsage) { struct MyValueT { std::string key; int val; From 019ca7482d0ac377f404fc96d71039f2603894be Mon Sep 17 00:00:00 2001 From: Vadim Leonov Date: Sun, 5 Apr 2026 23:38:56 +0500 Subject: [PATCH 2/2] docs update --- .../en/userver/libraries/multi_index_lru.md | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/scripts/docs/en/userver/libraries/multi_index_lru.md b/scripts/docs/en/userver/libraries/multi_index_lru.md index bbe6c105735c..aeb504d98911 100644 --- a/scripts/docs/en/userver/libraries/multi_index_lru.md +++ b/scripts/docs/en/userver/libraries/multi_index_lru.md @@ -2,11 +2,26 @@ ## Introduction -Generic LRU (Least Recently Used) cache container implementation that combines Boost.MultiIndex for flexible indexing -for efficient LRU tracking. It uses @ref multi_index_lru. +Generic LRU (Least Recently Used) cache container implementation that combines Boost.MultiIndex for flexible indexing with efficient LRU tracking. The container maintains elements in access order while supporting multiple indexing strategies through Boost.MultiIndex. The LRU eviction policy automatically removes the least recently accessed items when capacity is reached. + +Two container variants are provided: +- **Container** - Basic LRU cache with capacity management +- **ExpirableContainer** - Extended version with time-based expiration (TTL) + +## Implementation Notes + +### Node Reuse Strategy +The container maintains a free list of allocated nodes to reduce memory allocations. When items are evicted or erased, their nodes are moved to the free list and reused for future insertions. + +### Thread Safety +This container is **not thread-safe**. External synchronization is required for concurrent access. + +### Iterator Invalidation +- Insertions may invalidate iterators if capacity is exceeded and eviction occurs +- Erasures invalidate iterators to the erased element only +- Find operations do not invalidate iterators +- `set_capacity()` may invalidate iterators when reducing capacity -The container maintains elements in access order while supporting multiple indexing strategies through Boost.MultiIndex. - The LRU eviction policy automatically removes the least recently accessed items when capacity is reached. ## Usage