From 2416150911027a83489a7d169ac16035c6278405 Mon Sep 17 00:00:00 2001 From: kungurtsev Date: Fri, 13 Sep 2024 15:22:20 +0200 Subject: [PATCH] Shared Cache S3FIFO implementation (#8692) --- .../run/kikimr_services_initializers.cpp | 5 +- ydb/core/protos/shared_cache.proto | 1 + ydb/core/tablet_flat/shared_cache_events.h | 12 + ydb/core/tablet_flat/shared_cache_s3fifo.h | 346 +++++++++++++++ .../tablet_flat/shared_cache_s3fifo_ut.cpp | 406 ++++++++++++++++++ ydb/core/tablet_flat/shared_sausagecache.cpp | 115 +++-- .../tablet_flat/ut/ut_shared_sausagecache.cpp | 269 +++++++++++- ydb/core/tablet_flat/ut/ya.make | 1 + ydb/core/util/cache_cache.h | 80 ++-- ydb/core/util/cache_cache_iface.h | 2 +- ydb/core/util/cache_cache_ut.cpp | 63 +-- 11 files changed, 1174 insertions(+), 126 deletions(-) create mode 100644 ydb/core/tablet_flat/shared_cache_s3fifo.h create mode 100644 ydb/core/tablet_flat/shared_cache_s3fifo_ut.cpp diff --git a/ydb/core/driver_lib/run/kikimr_services_initializers.cpp b/ydb/core/driver_lib/run/kikimr_services_initializers.cpp index 414cf6a0815f..62c60f623ce1 100644 --- a/ydb/core/driver_lib/run/kikimr_services_initializers.cpp +++ b/ydb/core/driver_lib/run/kikimr_services_initializers.cpp @@ -1093,10 +1093,7 @@ void TSharedCacheInitializer::InitializeServices( config->TotalAsyncQueueInFlyLimit = cfg.GetAsyncQueueInFlyLimit(); config->TotalScanQueueInFlyLimit = cfg.GetScanQueueInFlyLimit(); config->ReplacementPolicy = cfg.GetReplacementPolicy(); - - if (cfg.HasActivePagesReservationPercent()) { - config->ActivePagesReservationPercent = cfg.GetActivePagesReservationPercent(); - } + config->ActivePagesReservationPercent = cfg.GetActivePagesReservationPercent(); TIntrusivePtr<::NMonitoring::TDynamicCounters> tabletGroup = GetServiceCounters(appData->Counters, "tablets"); TIntrusivePtr<::NMonitoring::TDynamicCounters> sausageGroup = tabletGroup->GetSubgroup("type", "S_CACHE"); diff --git a/ydb/core/protos/shared_cache.proto b/ydb/core/protos/shared_cache.proto index d433c03b1cfd..5835055a9299 100644 --- a/ydb/core/protos/shared_cache.proto +++ b/ydb/core/protos/shared_cache.proto @@ -3,6 +3,7 @@ option java_package = "ru.yandex.kikimr.proto"; enum TReplacementPolicy { ThreeLeveledLRU = 0; + S3FIFO = 1; } message TSharedCacheConfig { diff --git a/ydb/core/tablet_flat/shared_cache_events.h b/ydb/core/tablet_flat/shared_cache_events.h index f71462d28229..2b7cb4bd37a6 100644 --- a/ydb/core/tablet_flat/shared_cache_events.h +++ b/ydb/core/tablet_flat/shared_cache_events.h @@ -3,6 +3,7 @@ #include "defs.h" #include "flat_bio_events.h" #include "shared_handle.h" +#include #include #include @@ -24,6 +25,7 @@ namespace NSharedCache { EvRequest, EvResult, EvUpdated, + EvReplacementPolicySwitch, EvEnd @@ -127,6 +129,16 @@ namespace NSharedCache { THashMap Actions; }; + + struct TEvReplacementPolicySwitch : public TEventLocal { + using TReplacementPolicy = NKikimrSharedCache::TReplacementPolicy; + + TReplacementPolicy ReplacementPolicy; + + TEvReplacementPolicySwitch(TReplacementPolicy replacementPolicy) + : ReplacementPolicy(replacementPolicy) + {} + }; } } diff --git a/ydb/core/tablet_flat/shared_cache_s3fifo.h b/ydb/core/tablet_flat/shared_cache_s3fifo.h new file mode 100644 index 000000000000..3644d1331180 --- /dev/null +++ b/ydb/core/tablet_flat/shared_cache_s3fifo.h @@ -0,0 +1,346 @@ +#pragma once +#include "defs.h" +#include +#include +#include +#include + +namespace NKikimr::NCache { + +// TODO: remove template args and make some page base class + +enum class ES3FIFOPageLocation { + None, + SmallQueue, + MainQueue +}; + +template +class TS3FIFOGhostPageQueue { + using TPageKey = typename TPageTraits::TPageKey; + + struct TGhostPage { + TPageKey Key; + ui64 Size; // zero size is tombstone + + TGhostPage(const TPageKey& key, ui64 size) + : Key(key) + , Size(size) + {} + }; + + struct TGhostPageHash { + using is_transparent = void; + + inline size_t operator()(const TGhostPage* ghost) const { + return TPageTraits::GetHash(ghost->Key); + } + + inline size_t operator()(const TPageKey& key) const { + return TPageTraits::GetHash(key); + } + }; + + struct TGhostPageEqual { + using is_transparent = void; + + inline bool operator()(const TGhostPage* left, const TGhostPage* right) const { + return TPageTraits::Equals(left->Key, right->Key); + } + + inline bool operator()(const TGhostPage* left, const TPageKey& right) const { + return TPageTraits::Equals(left->Key, right); + } + }; + +public: + TS3FIFOGhostPageQueue(ui64 limit) + : Limit(limit) + {} + + void Add(const TPageKey& key, ui64 size) { + if (Y_UNLIKELY(size == 0)) { + Y_DEBUG_ABORT_S("Empty " << TPageTraits::ToString(key) << " page"); + return; + } + + TGhostPage* ghost = &GhostsQueue.emplace_back(key, size); + if (Y_UNLIKELY(!GhostsSet.emplace(ghost).second)) { + GhostsQueue.pop_back(); + Y_DEBUG_ABORT_S("Duplicated " << TPageTraits::ToString(key) << " page"); + return; + } + + Size += ghost->Size; + + EvictWhileFull(); + } + + bool Erase(const TPageKey& key, ui64 size) { + if (auto it = GhostsSet.find(key); it != GhostsSet.end()) { + TGhostPage* ghost = *it; + Y_DEBUG_ABORT_UNLESS(ghost->Size == size); + Y_ABORT_UNLESS(Size >= ghost->Size); + Size -= ghost->Size; + ghost->Size = 0; // mark as deleted + GhostsSet.erase(it); + return true; + } + return false; + } + + void UpdateLimit(ui64 limit) { + Limit = limit; + EvictWhileFull(); + } + + TString Dump() const { + TStringBuilder result; + size_t count = 0; + ui64 size = 0; + for (auto it = GhostsQueue.begin(); it != GhostsQueue.end(); it++) { + const TGhostPage* ghost = &*it; + if (ghost->Size) { // isn't deleted + Y_DEBUG_ABORT_UNLESS(GhostsSet.contains(ghost)); + if (count != 0) result << ", "; + result << "{" << TPageTraits::ToString(ghost->Key) << " " << ghost->Size << "b}"; + count++; + size += ghost->Size; + } + } + Y_DEBUG_ABORT_UNLESS(GhostsSet.size() == count); + Y_DEBUG_ABORT_UNLESS(Size == size); + return result; + } + +private: + void EvictWhileFull() { + while (!GhostsQueue.empty() && Size > Limit) { + TGhostPage* ghost = &GhostsQueue.front(); + if (ghost->Size) { // isn't deleted + Y_ABORT_UNLESS(Size >= ghost->Size); + Size -= ghost->Size; + bool erased = GhostsSet.erase(ghost); + Y_ABORT_UNLESS(erased); + } + GhostsQueue.pop_front(); + } + } + + ui64 Limit; + ui64 Size = 0; + // TODO: store ghost withing PageMap + THashSet GhostsSet; + TDeque GhostsQueue; +}; + +template +class TS3FIFOCache : public ICacheCache { + using TPageKey = typename TPageTraits::TPageKey; + + struct TLimit { + ui64 SmallQueueLimit; + ui64 MainQueueLimit; + + TLimit(ui64 limit) + : SmallQueueLimit(limit / 10) + , MainQueueLimit(limit - SmallQueueLimit) + {} + }; + + struct TQueue { + TQueue(ES3FIFOPageLocation location) + : Location(location) + {} + + ES3FIFOPageLocation Location; + TIntrusiveList Queue; + ui64 Size = 0; + }; + +public: + TS3FIFOCache(ui64 limit) + : Limit(limit) + , SmallQueue(ES3FIFOPageLocation::SmallQueue) + , MainQueue(ES3FIFOPageLocation::MainQueue) + , GhostQueue(limit) + {} + + TPage* EvictNext() override { + if (SmallQueue.Queue.Empty() && MainQueue.Queue.Empty()) { + return nullptr; + } + + // TODO: account passive pages inside the cache + TLimit savedLimit = std::exchange(Limit, TLimit(SmallQueue.Size + MainQueue.Size - 1)); + + TPage* evictedPage = EvictOneIfFull(); + Y_DEBUG_ABORT_UNLESS(evictedPage); + + Limit = savedLimit; + + return evictedPage; + } + + TIntrusiveList Touch(TPage* page) override { + const ES3FIFOPageLocation location = TPageTraits::GetLocation(page); + switch (location) { + case ES3FIFOPageLocation::SmallQueue: + case ES3FIFOPageLocation::MainQueue: { + TouchFast(page); + return {}; + } + case ES3FIFOPageLocation::None: + return Insert(page); + default: + Y_ABORT("Unknown page location"); + } + } + + void Erase(TPage* page) override { + const ES3FIFOPageLocation location = TPageTraits::GetLocation(page); + switch (location) { + case ES3FIFOPageLocation::None: + EraseGhost(page); + break; + case ES3FIFOPageLocation::SmallQueue: + Erase(SmallQueue, page); + break; + case ES3FIFOPageLocation::MainQueue: + Erase(MainQueue, page); + break; + default: + Y_ABORT("Unknown page location"); + } + + TPageTraits::SetFrequency(page, 0); + } + + void UpdateLimit(ui64 limit) override { + Limit = limit; + GhostQueue.UpdateLimit(limit); + } + + TString Dump() const { + TStringBuilder result; + + auto dump = [&](const TQueue& queue) { + size_t count = 0; + ui64 size = 0; + for (auto it = queue.Queue.begin(); it != queue.Queue.end(); it++) { + const TPage* page = &*it; + if (count != 0) result << ", "; + result << "{" << TPageTraits::GetKeyToString(page) << " " << TPageTraits::GetFrequency(page) << "f " << TPageTraits::GetSize(page) << "b}"; + count++; + size += TPageTraits::GetSize(page); + } + Y_DEBUG_ABORT_UNLESS(queue.Size == size); + }; + + result << "SmallQueue: "; + dump(SmallQueue); + result << Endl << "MainQueue: "; + dump(MainQueue); + result << Endl << "GhostQueue: "; + result << GhostQueue.Dump(); + + return result; + } + +private: + TPage* EvictOneIfFull() { + while (true) { + if (!SmallQueue.Queue.Empty() && SmallQueue.Size > Limit.SmallQueueLimit) { + TPage* page = Pop(SmallQueue); + if (ui32 frequency = TPageTraits::GetFrequency(page); frequency > 1) { // load inserts, first read touches, second read touches + Push(MainQueue, page); + } else { + if (frequency) TPageTraits::SetFrequency(page, 0); + AddGhost(page); + return page; + } + } else if (!MainQueue.Queue.Empty() && MainQueue.Size > Limit.MainQueueLimit) { + TPage* page = Pop(MainQueue); + if (ui32 frequency = TPageTraits::GetFrequency(page); frequency > 0) { + TPageTraits::SetFrequency(page, frequency - 1); + Push(MainQueue, page); + } else { + return page; + } + } else { + break; + } + } + + return nullptr; + } + + void TouchFast(TPage* page) { + Y_DEBUG_ABORT_UNLESS(TPageTraits::GetLocation(page) != ES3FIFOPageLocation::None); + + ui32 frequency = TPageTraits::GetFrequency(page); + if (frequency < 3) { + TPageTraits::SetFrequency(page, frequency + 1); + } + } + + TIntrusiveList Insert(TPage* page) { + Y_DEBUG_ABORT_UNLESS(TPageTraits::GetLocation(page) == ES3FIFOPageLocation::None); + + Push(EraseGhost(page) ? MainQueue : SmallQueue, page); + TPageTraits::SetFrequency(page, 0); + + TIntrusiveList evictedList; + while (TPage* evictedPage = EvictOneIfFull()) { + evictedList.PushBack(evictedPage); + } + + return evictedList; + } + + TPage* Pop(TQueue& queue) { + Y_DEBUG_ABORT_UNLESS(!queue.Queue.Empty()); + Y_ABORT_UNLESS(TPageTraits::GetLocation(queue.Queue.Front()) == queue.Location); + Y_ABORT_UNLESS(queue.Size >= TPageTraits::GetSize(queue.Queue.Front())); + + TPage* page = queue.Queue.PopFront(); + queue.Size -= TPageTraits::GetSize(page); + TPageTraits::SetLocation(page, ES3FIFOPageLocation::None); + + return page; + } + + void Push(TQueue& queue, TPage* page) { + Y_ABORT_UNLESS(TPageTraits::GetLocation(page) == ES3FIFOPageLocation::None); + + queue.Queue.PushBack(page); + queue.Size += TPageTraits::GetSize(page); + TPageTraits::SetLocation(page, queue.Location); + } + + void Erase(TQueue& queue, TPage* page) { + Y_ABORT_UNLESS(TPageTraits::GetLocation(page) == queue.Location); + Y_ABORT_UNLESS(queue.Size >= TPageTraits::GetSize(page)); + + page->Unlink(); + queue.Size -= TPageTraits::GetSize(page); + TPageTraits::SetLocation(page, ES3FIFOPageLocation::None); + } + + void AddGhost(const TPage* page) { + GhostQueue.Add(TPageTraits::GetKey(page), TPageTraits::GetSize(page)); + } + + bool EraseGhost(const TPage* page) { + return GhostQueue.Erase(TPageTraits::GetKey(page), TPageTraits::GetSize(page)); + } + +private: + TLimit Limit; + TQueue SmallQueue; + TQueue MainQueue; + TS3FIFOGhostPageQueue GhostQueue; + +}; + +} diff --git a/ydb/core/tablet_flat/shared_cache_s3fifo_ut.cpp b/ydb/core/tablet_flat/shared_cache_s3fifo_ut.cpp new file mode 100644 index 000000000000..96082a2811b3 --- /dev/null +++ b/ydb/core/tablet_flat/shared_cache_s3fifo_ut.cpp @@ -0,0 +1,406 @@ +#include +#include "shared_cache_s3fifo.h" + +namespace NKikimr::NCache { + +namespace { + + struct TPage : public TIntrusiveListItem { + ui32 Id; + size_t Size; + + TPage(ui32 id, size_t size) + : Id(id), Size(size) + {} + + ui32 CacheFlags1 : 4 = 0; + ui32 CacheFlags2 : 4 = 0; + }; + + struct TPageTraits { + struct TPageKey { + ui32 Id; + + TPageKey(ui32 id) + : Id(id) + {} + }; + + static ui64 GetSize(const TPage* page) { + return page->Size; + } + + static TPageKey GetKey(const TPage* page) { + return {page->Id}; + } + + static size_t GetHash(const TPageKey& key) { + return std::hash()(key.Id); + } + + static bool Equals(const TPageKey& left, const TPageKey& right) { + return left.Id == right.Id; + } + + static TString ToString(const TPageKey& key) { + return std::to_string(key.Id); + } + + static TString GetKeyToString(const TPage* page) { + return ToString(GetKey(page)); + } + + static ES3FIFOPageLocation GetLocation(const TPage* page) { + return static_cast(page->CacheFlags1); + } + + static void SetLocation(TPage* page, ES3FIFOPageLocation location) { + ui32 location_ = static_cast(location); + Y_ABORT_UNLESS(location_ < (1 << 4)); + page->CacheFlags1 = location_; + } + + static ui32 GetFrequency(const TPage* page) { + return page->CacheFlags2; + } + + static void SetFrequency(TPage* page, ui32 frequency) { + Y_ABORT_UNLESS(frequency < (1 << 4)); + page->CacheFlags2 = frequency; + } + }; + +} + +Y_UNIT_TEST_SUITE(TS3FIFOGhostQueue) { + + Y_UNIT_TEST(Add) { + TS3FIFOGhostPageQueue queue(100); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), ""); + + queue.Add(1, 10); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}"); + + queue.Add(2, 30); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {2 30b}"); + + queue.Add(3, 60); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {2 30b}, {3 60b}"); + + queue.Add(4, 1); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{2 30b}, {3 60b}, {4 1b}"); + + queue.Add(1, 3); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{2 30b}, {3 60b}, {4 1b}, {1 3b}"); + } + + Y_UNIT_TEST(Erase) { + TS3FIFOGhostPageQueue queue(100); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), ""); + + queue.Add(1, 10); + queue.Add(2, 30); + queue.Add(3, 60); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {2 30b}, {3 60b}"); + + UNIT_ASSERT_VALUES_EQUAL(queue.Erase(5, 42), false); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {2 30b}, {3 60b}"); + + UNIT_ASSERT_VALUES_EQUAL(queue.Erase(2, 30), true); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {3 60b}"); + + queue.Add(4, 30); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {3 60b}, {4 30b}"); + + queue.Add(5, 1); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{3 60b}, {4 30b}, {5 1b}"); + } + + Y_UNIT_TEST(Erase_Add) { + TS3FIFOGhostPageQueue queue(100); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), ""); + + queue.Add(1, 10); + queue.Add(2, 30); + queue.Add(3, 60); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {2 30b}, {3 60b}"); + + UNIT_ASSERT_VALUES_EQUAL(queue.Erase(2, 30), true); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {3 60b}"); + + queue.Add(2, 30); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {3 60b}, {2 30b}"); + + queue.Add(4, 70); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{2 30b}, {4 70b}"); + } + + Y_UNIT_TEST(Add_Big) { + TS3FIFOGhostPageQueue queue(100); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), ""); + + queue.Add(1, 101); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), ""); + } + + Y_UNIT_TEST(UpdateLimit) { + TS3FIFOGhostPageQueue queue(100); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), ""); + + queue.Add(1, 10); + queue.Add(2, 20); + queue.Add(3, 30); + queue.Add(4, 40); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{1 10b}, {2 20b}, {3 30b}, {4 40b}"); + + queue.UpdateLimit(80); + UNIT_ASSERT_VALUES_EQUAL(queue.Dump(), "{3 30b}, {4 40b}"); + } + +} + +Y_UNIT_TEST_SUITE(TS3FIFOCache) { + + TVector Touch(auto& cache, TPage& page) { + auto evicted = cache.Touch(&page); + TVector result; + for (auto& p : evicted) { + UNIT_ASSERT_VALUES_EQUAL(p.CacheFlags1, 0); + UNIT_ASSERT_VALUES_EQUAL(p.CacheFlags2, 0); + result.push_back(p.Id); + } + return result; + } + + std::optional EvictNext(auto& cache) { + auto evicted = cache.EvictNext(); + if (evicted) { + UNIT_ASSERT_VALUES_EQUAL(evicted->CacheFlags1, 0); + UNIT_ASSERT_VALUES_EQUAL(evicted->CacheFlags2, 0); + return evicted->Id; + } + return {}; + } + + Y_UNIT_TEST(Touch) { + TS3FIFOCache cache(100); + + TPage page1{1, 2}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {1 0f 2b}" << Endl + << "MainQueue: " << Endl + << "GhostQueue: ")); + + TPage page2{2, 3}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {1 0f 2b}, {2 0f 3b}" << Endl + << "MainQueue: " << Endl + << "GhostQueue: ")); + + TPage page3{3, 4}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {1 0f 2b}, {2 0f 3b}, {3 0f 4b}" << Endl + << "MainQueue: " << Endl + << "GhostQueue: ")); + + TPage page4{4, 1}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page4), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {1 0f 2b}, {2 0f 3b}, {3 0f 4b}, {4 0f 1b}" << Endl + << "MainQueue: " << Endl + << "GhostQueue: ")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {1 2f 2b}, {2 1f 3b}, {3 2f 4b}, {4 0f 1b}" << Endl + << "MainQueue: " << Endl + << "GhostQueue: ")); + + TPage page5{5, 8}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page5), TVector{2}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {4 0f 1b}, {5 0f 8b}" << Endl + << "MainQueue: {1 2f 2b}, {3 2f 4b}" << Endl + << "GhostQueue: {2 3b}")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {4 0f 1b}, {5 0f 8b}" << Endl + << "MainQueue: {1 2f 2b}, {3 3f 4b}" << Endl + << "GhostQueue: {2 3b}")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {4 0f 1b}, {5 0f 8b}" << Endl + << "MainQueue: {1 2f 2b}, {3 3f 4b}, {2 0f 3b}" << Endl + << "GhostQueue: ")); + } + + Y_UNIT_TEST(Touch_MainQueue) { + TS3FIFOCache cache(100); + + TPage page1{1, 20}; + TPage page2{2, 30}; + TPage page3{3, 40}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{1}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{2}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{3}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: " << Endl + << "GhostQueue: {1 20b}, {2 30b}, {3 40b}")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: {2 0f 30b}, {1 0f 20b}, {3 0f 40b}" << Endl + << "GhostQueue: ")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: {2 2f 30b}, {1 1f 20b}, {3 0f 40b}" << Endl + << "GhostQueue: ")); + + TPage page4{4, 20}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page4), TVector{4}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: {2 2f 30b}, {1 1f 20b}, {3 0f 40b}" << Endl + << "GhostQueue: {4 20b}")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page4), TVector{3}); + // MainQueue: {2 2f 30b}, {1 1f 20b}, {3 0f 40b}, {4 0f 20b} + // MainQueue: {1 1f 20b}, {3 0f 40b}, {4 0f 20b}, {2 1f 30b} + // MainQueue: {3 0f 40b}, {4 0f 20b}, {2 1f 30b}, {1 0f 20b} + // MainQueue: {4 0f 20b}, {2 1f 30b}, {1 0f 20b} + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: {4 0f 20b}, {2 1f 30b}, {1 0f 20b}" << Endl + << "GhostQueue: ")); + } + + Y_UNIT_TEST(EvictNext) { + TS3FIFOCache cache(100); + + TPage page1{1, 20}; + TPage page2{2, 30}; + TPage page3{3, 40}; + TPage page4{4, 10}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{1}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{2}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{3}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: " << Endl + << "GhostQueue: {1 20b}, {2 30b}, {3 40b}")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: {2 0f 30b}, {1 1f 20b}, {3 0f 40b}" << Endl + << "GhostQueue: ")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page4), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {4 0f 10b}" << Endl + << "MainQueue: {2 0f 30b}, {1 1f 20b}, {3 0f 40b}" << Endl + << "GhostQueue: ")); + + UNIT_ASSERT_VALUES_EQUAL(EvictNext(cache), 4); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: {2 0f 30b}, {1 1f 20b}, {3 0f 40b}" << Endl + << "GhostQueue: {4 10b}")); + + UNIT_ASSERT_VALUES_EQUAL(EvictNext(cache), 2); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: {1 1f 20b}, {3 0f 40b}" << Endl + << "GhostQueue: {4 10b}")); + + UNIT_ASSERT_VALUES_EQUAL(EvictNext(cache), 3); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: {1 0f 20b}" << Endl + << "GhostQueue: {4 10b}")); + + UNIT_ASSERT_VALUES_EQUAL(EvictNext(cache), 1); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: " << Endl + << "GhostQueue: {4 10b}")); + + UNIT_ASSERT_VALUES_EQUAL(EvictNext(cache), std::optional{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: " << Endl + << "GhostQueue: {4 10b}")); + } + + Y_UNIT_TEST(UpdateLimit) { + TS3FIFOCache cache(100); + + TPage page1{1, 20}; + TPage page2{2, 30}; + TPage page3{3, 40}; + TPage page4{4, 10}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{1}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{2}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{3}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: " << Endl + << "GhostQueue: {1 20b}, {2 30b}, {3 40b}")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page2), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page1), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page3), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: {2 0f 30b}, {1 0f 20b}, {3 0f 40b}" << Endl + << "GhostQueue: ")); + + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page4), TVector{}); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {4 0f 10b}" << Endl + << "MainQueue: {2 0f 30b}, {1 0f 20b}, {3 0f 40b}" << Endl + << "GhostQueue: ")); + + cache.UpdateLimit(45); + TPage page5{5, 1}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page5), (TVector{4, 2, 1})); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: {5 0f 1b}" << Endl + << "MainQueue: {3 0f 40b}" << Endl + << "GhostQueue: {4 10b}")); + + cache.UpdateLimit(0); + TPage page6{6, 1}; + UNIT_ASSERT_VALUES_EQUAL(Touch(cache, page6), (TVector{5, 6, 3})); + UNIT_ASSERT_VALUES_EQUAL(cache.Dump(), (TString)(TStringBuilder() + << "SmallQueue: " << Endl + << "MainQueue: " << Endl + << "GhostQueue: ")); + } + +} + +} diff --git a/ydb/core/tablet_flat/shared_sausagecache.cpp b/ydb/core/tablet_flat/shared_sausagecache.cpp index cd74754bdfe1..8b200181d815 100644 --- a/ydb/core/tablet_flat/shared_sausagecache.cpp +++ b/ydb/core/tablet_flat/shared_sausagecache.cpp @@ -3,6 +3,7 @@ #include "flat_bio_events.h" #include "flat_bio_actor.h" #include "util_fmt_logger.h" +#include #include #include #include @@ -52,24 +53,6 @@ static bool Satisfies(NLog::EPriority priority = NLog::PRI_DEBUG) { return false; } -struct TEvPrivate { - enum EEv { - EvReplacementPolicySwitch = EventSpaceBegin(TKikimrEvents::ES_PRIVATE), - - EvEnd - }; - - static_assert(EvEnd < EventSpaceEnd(TKikimrEvents::ES_PRIVATE)); - - struct TEvReplacementPolicySwitch : public TEventLocal { - TSharedPageCacheConfig::TReplacementPolicy ReplacementPolicy; - - TEvReplacementPolicySwitch(TSharedPageCacheConfig::TReplacementPolicy replacementPolicy) - : ReplacementPolicy(replacementPolicy) - {} - }; -}; - class TSharedPageCache : public TActorBootstrapped { using ELnLev = NUtil::ELnLev; using TBlocks = TVector; @@ -92,7 +75,8 @@ class TSharedPageCache : public TActorBootstrapped { , public TIntrusiveListItem { ui32 State : 4 = PageStateNo; - ui32 CacheFlags : 16 = 0; + ui32 CacheFlags1 : 4 = 0; + ui32 CacheFlags2 : 4 = 0; const ui32 PageId; const size_t Size; @@ -125,24 +109,75 @@ class TSharedPageCache : public TActorBootstrapped { } void EnsureNoCacheFlags() { - Y_VERIFY_S(CacheFlags == 0, "Unexpected " << CacheFlags << " page cache flags"); + Y_VERIFY_S(CacheFlags1 == 0, "Unexpected page " << CacheFlags1 << " cache flags 1"); + Y_VERIFY_S(CacheFlags2 == 0, "Unexpected page " << CacheFlags2 << " cache flags 2"); + } + }; + + struct TCacheCachePageTraits { + static ui64 GetWeight(const TPage* page) { + return sizeof(TPage) + page->Size; } - struct TWeight { - static ui64 Get(TPage *x) { - return sizeof(TPage) + (x->State == PageStateLoaded ? x->Size : 0); - } - }; + static ECacheCacheGeneration GetGeneration(const TPage *page) { + return static_cast(page->CacheFlags1); + } - struct TCacheFlags { - static ui32 Get(TPage *x) { - return x->CacheFlags; - } - static void Set(TPage *x, ui32 flags) { - Y_ABORT_UNLESS(flags < Max()); - x->CacheFlags = flags; - } + static void SetGeneration(TPage *page, ECacheCacheGeneration generation) { + ui32 generation_ = static_cast(generation); + Y_ABORT_UNLESS(generation_ < (1 << 4)); + page->CacheFlags1 = generation_; + } + }; + + struct TS3FIFOPageTraits { + struct TPageKey { + TLogoBlobID LogoBlobID; + ui32 PageId; }; + + static ui64 GetSize(const TPage* page) { + return sizeof(TPage) + page->Size; + } + + static TPageKey GetKey(const TPage* page) { + return {page->Collection->MetaId, page->PageId}; + } + + static size_t GetHash(const TPageKey& key) { + return MultiHash(key.LogoBlobID.Hash(), key.PageId); + } + + static bool Equals(const TPageKey& left, const TPageKey& right) { + return left.PageId == right.PageId && left.LogoBlobID == right.LogoBlobID; + } + + static TString ToString(const TPageKey& key) { + return TStringBuilder() << "LogoBlobID: " << key.LogoBlobID.ToString() << " PageId: " << key.PageId; + } + + static TString GetKeyToString(const TPage* page) { + return ToString(GetKey(page)); + } + + static ES3FIFOPageLocation GetLocation(const TPage* page) { + return static_cast(page->CacheFlags1); + } + + static void SetLocation(TPage* page, ES3FIFOPageLocation location) { + ui32 location_ = static_cast(location); + Y_ABORT_UNLESS(location_ < (1 << 4)); + page->CacheFlags1 = location_; + } + + static ui32 GetFrequency(const TPage* page) { + return page->CacheFlags2; + } + + static void SetFrequency(TPage* page, ui32 frequency) { + Y_ABORT_UNLESS(frequency < (1 << 4)); + page->CacheFlags2 = frequency; + } }; struct TRequest : public TSimpleRefCount { @@ -233,10 +268,12 @@ class TSharedPageCache : public TActorBootstrapped { // now it will be fixed by ActualizeCacheSizeLimit call switch (Config->ReplacementPolicy) { + case NKikimrSharedCache::S3FIFO: + return MakeHolder>(1); case NKikimrSharedCache::ThreeLeveledLRU: default: { TCacheCacheConfig cacheCacheConfig(1, Config->Counters->FreshBytes, Config->Counters->StagingBytes, Config->Counters->WarmBytes); - return MakeHolder>(std::move(cacheCacheConfig)); + return MakeHolder>(std::move(cacheCacheConfig)); } } } @@ -247,7 +284,7 @@ class TSharedPageCache : public TActorBootstrapped { // limit of cache depends only on config and mem because passive pages may go in and out arbitrary // we may have some passive bytes, so if we fully fill this Cache we may exceed the limit // because of that DoGC should be called to ensure limits - Cache->UpdateCacheSize(limitBytes); + Cache->UpdateLimit(limitBytes); if (Config->Counters) { Config->Counters->ConfigLimitBytes->Set(Config->LimitBytes.value_or(0)); @@ -299,7 +336,7 @@ class TSharedPageCache : public TActorBootstrapped { DoGC(); } - void Handle(TEvPrivate::TEvReplacementPolicySwitch::TPtr &ev) { + void Handle(NSharedCache::TEvReplacementPolicySwitch::TPtr &ev) { auto *msg = ev->Get(); if (msg->ReplacementPolicy == Config->ReplacementPolicy) { @@ -322,7 +359,7 @@ class TSharedPageCache : public TActorBootstrapped { page->EnsureNoCacheFlags(); // touch each page multiple times to make it warm - for (ui32 touchTimes = 0; touchTimes < 2; touchTimes++) { + for (ui32 touchTimes = 0; touchTimes < 3; touchTimes++) { Evict(Cache->Touch(page)); } } @@ -1123,7 +1160,7 @@ class TSharedPageCache : public TActorBootstrapped { if (msg->Record.GetReplacementPolicy() != Config->ReplacementPolicy) { // Note: use random delay to prevent the whole cluster lag and storage ddos ui32 delaySeconds = RandomNumber(msg->Record.GetReplacementPolicySwitchUniformDelaySeconds() + 1); - Schedule(TDuration::Seconds(delaySeconds), new TEvPrivate::TEvReplacementPolicySwitch(msg->Record.GetReplacementPolicy())); + Schedule(TDuration::Seconds(delaySeconds), new NSharedCache::TEvReplacementPolicySwitch(msg->Record.GetReplacementPolicy())); } } @@ -1219,7 +1256,7 @@ class TSharedPageCache : public TActorBootstrapped { hFunc(NMemory::TEvConsumerRegistered, Handle); hFunc(NMemory::TEvConsumerLimit, Handle); - hFunc(TEvPrivate::TEvReplacementPolicySwitch, Handle); + hFunc(NSharedCache::TEvReplacementPolicySwitch, Handle); } } diff --git a/ydb/core/tablet_flat/ut/ut_shared_sausagecache.cpp b/ydb/core/tablet_flat/ut/ut_shared_sausagecache.cpp index f18c08fe9320..af7a405c3006 100644 --- a/ydb/core/tablet_flat/ut/ut_shared_sausagecache.cpp +++ b/ydb/core/tablet_flat/ut/ut_shared_sausagecache.cpp @@ -13,16 +13,30 @@ enum : ui32 { ValueColumnId = 2, }; +using TRetriedCounters = TVector; +using namespace NSharedCache; + +void Increment(TRetriedCounters& retried, ui32 attempts) { + if (attempts >= retried.size()) { + retried.resize(attempts + 1); + } + retried.at(attempts)++; +} + struct TTxInitSchema : public ITransaction { bool Execute(TTransactionContext& txc, const TActorContext&) override { if (txc.DB.GetScheme().GetTableInfo(TableId)) return true; + TCompactionPolicy policy; + policy.MinBTreeIndexNodeSize = 128; + txc.DB.Alter() .AddTable("test" + ToString(ui32(TableId)), TableId) .AddColumn(TableId, "key", KeyColumnId, NScheme::TInt64::TypeId, false) .AddColumn(TableId, "value", ValueColumnId, NScheme::TString::TypeId, false) - .AddColumnToKey(TableId, KeyColumnId); + .AddColumnToKey(TableId, KeyColumnId) + .SetCompactionPolicy(TableId, policy); return true; } @@ -59,12 +73,18 @@ struct TTxWriteRow : public ITransaction { struct TTxReadRow : public ITransaction { i64 Key; + TRetriedCounters& Retried; + ui32 Attempts = 0; - explicit TTxReadRow(i64 key) + explicit TTxReadRow(i64 key, TRetriedCounters& retried) : Key(key) + , Retried(retried) { } bool Execute(TTransactionContext& txc, const TActorContext&) override { + Increment(Retried, Attempts); + Attempts++; + TVector rawKey; rawKey.emplace_back(&Key, sizeof(Key), NScheme::TTypeInfo(NScheme::TInt64::TypeId)); @@ -99,6 +119,15 @@ void WaitEvent(TMyEnvBase& env, ui32 eventType, ui32 requiredCount = 1) { env->DispatchEvents(options); } +void RestartAndClearCache(TMyEnvBase& env) { + env.SendSync(new TEvents::TEvPoison, false, true); + env->Send(MakeSharedPageCacheId(), TActorId{}, new NMemory::TEvConsumerLimit(0_MB)); + WaitEvent(env, NMemory::EvConsumerLimit); + env->Send(MakeSharedPageCacheId(), TActorId{}, new NMemory::TEvConsumerLimit(8_MB)); + WaitEvent(env, NMemory::EvConsumerLimit); + env.FireDummyTablet(ui32(NFake::TDummy::EFlg::Comp)); +} + Y_UNIT_TEST(Limits) { TMyEnvBase env; auto counters = MakeIntrusive(env->GetDynamicCounters()); @@ -109,7 +138,7 @@ Y_UNIT_TEST(Limits) { env.FireDummyTablet(ui32(NFake::TDummy::EFlg::Comp)); env.SendSync(new NFake::TEvExecute{ new TTxInitSchema() }); - // write 300 rows, each ~100KB + // write 300 rows, each ~100KB (~30MB) for (i64 key = 0; key < 300; ++key) { TString value(size_t(100 * 1024), char('a' + key % 26)); env.SendSync(new NFake::TEvExecute{ new TTxWriteRow(key, std::move(value)) }); @@ -120,8 +149,9 @@ Y_UNIT_TEST(Limits) { Cerr << "...waiting until compacted" << Endl; env.WaitFor(); + TRetriedCounters retried; for (i64 key = 0; key < 100; ++key) { - env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key) }); + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }); } LogCounters(counters); UNIT_ASSERT_VALUES_EQUAL(counters->LoadInFlyBytes->Val(), 0); @@ -161,6 +191,237 @@ Y_UNIT_TEST(Limits) { UNIT_ASSERT_VALUES_EQUAL(counters->MemLimitBytes->Val(), counters->ActiveLimitBytes->Val()); } +Y_UNIT_TEST(ThreeLeveledLRU) { + TMyEnvBase env; + auto counters = MakeIntrusive(env->GetDynamicCounters()); + + env.FireDummyTablet(ui32(NFake::TDummy::EFlg::Comp)); + env.SendSync(new NFake::TEvExecute{ new TTxInitSchema() }); + + // write 100 rows, each ~100KB (~10MB) + for (i64 key = 0; key < 100; ++key) { + TString value(size_t(100 * 1024), char('a' + key % 26)); + env.SendSync(new NFake::TEvExecute{ new TTxWriteRow(key, std::move(value)) }); + } + + Cerr << "...compacting" << Endl; + env.SendSync(new NFake::TEvCompact(TableId)); + Cerr << "...waiting until compacted" << Endl; + env.WaitFor(); + + TRetriedCounters retried; + for (i64 key = 99; key >= 0; --key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 45, 5})); + + RestartAndClearCache(env); + + retried = {}; + for (i64 key = 0; key < 100; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB / 3 * 2), static_cast(1_MB / 3)); // 2 full layers (fresh & staging) + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 100, 14, 2})); + + retried = {}; + for (i64 key = 99; key >= 0; --key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 44, 6})); + + retried = {}; + for (i64 key = 99; key >= 0; --key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 73, 10, 1})); + + RestartAndClearCache(env); + + // read some key twice + retried = {}; + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(0, retried) }, true); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{1, 1, 1, 1})); + retried = {}; + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(0, retried) }, true); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{1})); + + // simulate scan + retried = {}; + for (i64 key = 1; key < 100; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB / 3 * 2), static_cast(1_MB / 3)); // 2 full layers (fresh & staging) + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{99, 99, 13, 1})); + + // read the key again + retried = {}; + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(0, retried) }, true); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{1, 1, 1})); + + RestartAndClearCache(env); + + retried = {}; + for (i64 key = 0; key < 100; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB / 3 * 2), static_cast(1_MB / 3)); // 2 full layers (fresh & staging) + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{200, 100, 14, 2})); + + retried = {}; + for (i64 key = 0; key < 100; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB / 3 * 2), static_cast(1_MB / 3)); // 2 full layers (fresh & staging) + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 100, 14})); +} + +Y_UNIT_TEST(S3FIFO) { + TMyEnvBase env; + auto counters = MakeIntrusive(env->GetDynamicCounters()); + + env.FireDummyTablet(ui32(NFake::TDummy::EFlg::Comp)); + env.SendSync(new NFake::TEvExecute{ new TTxInitSchema() }); + + env->Send(MakeSharedPageCacheId(), TActorId{}, new TEvReplacementPolicySwitch(NKikimrSharedCache::S3FIFO)); + WaitEvent(env, NSharedCache::EvReplacementPolicySwitch); + + // write 100 rows, each ~100KB (~10MB) + for (i64 key = 0; key < 100; ++key) { + TString value(size_t(100 * 1024), char('a' + key % 26)); + env.SendSync(new NFake::TEvExecute{ new TTxWriteRow(key, std::move(value)) }); + } + + Cerr << "...compacting" << Endl; + env.SendSync(new NFake::TEvCompact(TableId)); + Cerr << "...waiting until compacted" << Endl; + env.WaitFor(); + + TRetriedCounters retried; + for (i64 key = 99; key >= 0; --key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 92, 12})); + + RestartAndClearCache(env); + + retried = {}; + for (i64 key = 0; key < 100; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB / 10), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 100, 14, 2})); + + retried = {}; + for (i64 key = 99; key >= 0; --key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 92})); + + retried = {}; + for (i64 key = 99; key >= 0; --key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 28})); + + RestartAndClearCache(env); + + // read some key twice + retried = {}; + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(0, retried) }, true); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{1, 1, 1, 1})); + retried = {}; + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(0, retried) }, true); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{1})); + + // simulate scan + retried = {}; + for (i64 key = 1; key < 100; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB / 10), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{99, 99, 13, 1})); + + // read the key again + retried = {}; + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(0, retried) }, true); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{1})); + + RestartAndClearCache(env); + + retried = {}; + for (i64 key = 0; key < 100; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{200, 100, 14, 2})); + + retried = {}; + for (i64 key = 0; key < 100; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + LogCounters(counters); + UNIT_ASSERT_DOUBLES_EQUAL(counters->ActiveBytes->Val(), static_cast(8_MB), static_cast(1_MB / 3)); + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{100, 28})); +} + +Y_UNIT_TEST(ReplacementPolicySwitch) { + TMyEnvBase env; + auto counters = MakeIntrusive(env->GetDynamicCounters()); + + env.FireDummyTablet(ui32(NFake::TDummy::EFlg::Comp)); + env.SendSync(new NFake::TEvExecute{ new TTxInitSchema() }); + + // write 100 rows, each ~100KB (~10MB) + for (i64 key = 0; key < 100; ++key) { + TString value(size_t(100 * 1024), char('a' + key % 26)); + env.SendSync(new NFake::TEvExecute{ new TTxWriteRow(key, std::move(value)) }); + } + + Cerr << "...compacting" << Endl; + env.SendSync(new NFake::TEvCompact(TableId)); + Cerr << "...waiting until compacted" << Endl; + env.WaitFor(); + + RestartAndClearCache(env); + + TRetriedCounters retried = {}; + for (i64 key = 0; key < 3; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{3, 3, 1, 1})); + + env->Send(MakeSharedPageCacheId(), TActorId{}, new TEvReplacementPolicySwitch(NKikimrSharedCache::S3FIFO)); + WaitEvent(env, NSharedCache::EvReplacementPolicySwitch); + + retried = {}; + for (i64 key = 0; key < 3; ++key) { + env.SendSync(new NFake::TEvExecute{ new TTxReadRow(key, retried) }, true); + } + UNIT_ASSERT_VALUES_EQUAL(retried, (TVector{3})); +} + } // Y_UNIT_TEST_SUITE(TSharedPageCache) } // namespace NTabletFlatExecutor diff --git a/ydb/core/tablet_flat/ut/ya.make b/ydb/core/tablet_flat/ut/ya.make index 39017baf6954..a547bb527935 100644 --- a/ydb/core/tablet_flat/ut/ya.make +++ b/ydb/core/tablet_flat/ut/ya.make @@ -27,6 +27,7 @@ SRCS( flat_test_db.h flat_test_db.cpp flat_test_db_helpers.h + shared_cache_s3fifo_ut.cpp shared_handle_ut.cpp ut_btree_index_nodes.cpp ut_btree_index_iter_charge.cpp diff --git a/ydb/core/util/cache_cache.h b/ydb/core/util/cache_cache.h index 325d80f252c1..f5493abfdc61 100644 --- a/ydb/core/util/cache_cache.h +++ b/ydb/core/util/cache_cache.h @@ -9,16 +9,16 @@ namespace NKikimr::NCache { +enum class ECacheCacheGeneration { + None, + Fresh, + Staging, + Warm, +}; + struct TCacheCacheConfig : public TAtomicRefCount { using TCounterPtr = ::NMonitoring::TDynamicCounters::TCounterPtr; - enum ECacheGeneration { - CacheGenNone, - CacheGenFresh, - CacheGenStaging, - CacheGenWarm, - }; - ui64 Limit; ui64 FreshLimit; @@ -50,10 +50,7 @@ struct TCacheCacheConfig : public TAtomicRefCount { } }; -template +template class TCacheCache : public ICacheCache { public: TCacheCache(const TCacheCacheConfig &config) @@ -88,19 +85,19 @@ class TCacheCache : public ICacheCache { TIntrusiveList evictedList; TIntrusiveListItem *xitem = item; - const TCacheCacheConfig::ECacheGeneration cacheGen = GetGeneration(item); + const ECacheCacheGeneration cacheGen = TItemTraits::GetGeneration(item); switch (cacheGen) { - case TCacheCacheConfig::CacheGenNone: // place in fresh + case ECacheCacheGeneration::None: // place in fresh AddToFresh(item, evictedList); [[fallthrough]]; - case TCacheCacheConfig::CacheGenFresh: // just update inside fresh + case ECacheCacheGeneration::Fresh: // just update inside fresh xitem->Unlink(); FreshList.PushFront(xitem); break; - case TCacheCacheConfig::CacheGenStaging: // move to warm + case ECacheCacheGeneration::Staging: // move to warm MoveToWarm(item, evictedList); break; - case TCacheCacheConfig::CacheGenWarm: // just update inside warm + case ECacheCacheGeneration::Warm: // just update inside warm xitem->Unlink(); WarmList.PushFront(xitem); break; @@ -113,21 +110,21 @@ class TCacheCache : public ICacheCache { } void Erase(TItem *item) override { - const TCacheCacheConfig::ECacheGeneration cacheGen = GetGeneration(item); + const ECacheCacheGeneration cacheGen = TItemTraits::GetGeneration(item); switch (cacheGen) { - case TCacheCacheConfig::CacheGenNone: + case ECacheCacheGeneration::None: break; - case TCacheCacheConfig::CacheGenFresh: + case ECacheCacheGeneration::Fresh: Unlink(item, FreshWeight); if (Config.ReportedFresh) *Config.ReportedFresh = FreshWeight; break; - case TCacheCacheConfig::CacheGenStaging: + case ECacheCacheGeneration::Staging: Unlink(item, StagingWeight); if (Config.ReportedStaging) *Config.ReportedStaging = StagingWeight; break; - case TCacheCacheConfig::CacheGenWarm: + case ECacheCacheGeneration::Warm: Unlink(item, WarmWeight); if (Config.ReportedWarm) *Config.ReportedWarm = WarmWeight; @@ -135,18 +132,18 @@ class TCacheCache : public ICacheCache { default: Y_DEBUG_ABORT("unknown cache generation"); } - SetGeneration(item, TCacheCacheConfig::CacheGenNone); + TItemTraits::SetGeneration(item, ECacheCacheGeneration::None); } - void UpdateCacheSize(ui64 cacheSize) override { - Config.SetLimit(cacheSize); + void UpdateLimit(ui64 limit) override { + Config.SetLimit(limit); } private: void Unlink(TItem *item, ui64 &weight) { item->Unlink(); - const ui64 elementWeight = WeightOp.Get(item); + const ui64 elementWeight = TItemTraits::GetWeight(item); Y_DEBUG_ABORT_UNLESS(elementWeight <= weight); weight -= elementWeight; } @@ -154,9 +151,9 @@ class TCacheCache : public ICacheCache { void AddToFresh(TItem *item, TIntrusiveList& evictedList) { LimitFresh(evictedList); item->Unlink(); - FreshWeight += WeightOp.Get(item); + FreshWeight += TItemTraits::GetWeight(item); FreshList.PushFront(item); - SetGeneration(item, TCacheCacheConfig::CacheGenFresh); + TItemTraits::SetGeneration(item, ECacheCacheGeneration::Fresh); if (Config.ReportedStaging) *Config.ReportedStaging = StagingWeight; @@ -168,9 +165,9 @@ class TCacheCache : public ICacheCache { // Note: unlink first, so item is not evicted by LimitWarm call below Unlink(item, StagingWeight); LimitWarm(evictedList); - WarmWeight += WeightOp.Get(item); + WarmWeight += TItemTraits::GetWeight(item); WarmList.PushFront(item); - SetGeneration(item, TCacheCacheConfig::CacheGenWarm); + TItemTraits::SetGeneration(item, ECacheCacheGeneration::Warm); if (Config.ReportedStaging) *Config.ReportedStaging = StagingWeight; @@ -180,16 +177,16 @@ class TCacheCache : public ICacheCache { void AddToStaging(TItem *item, TIntrusiveList& evictedList) { LimitStaging(evictedList); - StagingWeight += WeightOp.Get(item); + StagingWeight += TItemTraits::GetWeight(item); StagingList.PushFront(item); - SetGeneration(item, TCacheCacheConfig::CacheGenStaging); + TItemTraits::SetGeneration(item, ECacheCacheGeneration::Staging); } void LimitFresh(TIntrusiveList& evictedList) { while (FreshWeight > Config.FreshLimit) { Y_DEBUG_ABORT_UNLESS(!FreshList.Empty()); TItem *x = FreshList.PopBack(); - Y_ABORT_UNLESS(GetGeneration(x) == TCacheCacheConfig::CacheGenFresh, "malformed entry in fresh cache. %" PRIu32, (ui32)GetGeneration(x)); + Y_ABORT_UNLESS(TItemTraits::GetGeneration(x) == ECacheCacheGeneration::Fresh, "malformed entry in fresh cache. %" PRIu32, (ui32)TItemTraits::GetGeneration(x)); Unlink(x, FreshWeight); AddToStaging(x, evictedList); } @@ -199,7 +196,7 @@ class TCacheCache : public ICacheCache { while (WarmWeight > Config.WarmLimit) { Y_DEBUG_ABORT_UNLESS(!WarmList.Empty()); TItem *x = WarmList.PopBack(); - Y_ABORT_UNLESS(GetGeneration(x) == TCacheCacheConfig::CacheGenWarm, "malformed entry in warm cache. %" PRIu32, (ui32)GetGeneration(x)); + Y_ABORT_UNLESS(TItemTraits::GetGeneration(x) == ECacheCacheGeneration::Warm, "malformed entry in warm cache. %" PRIu32, (ui32)TItemTraits::GetGeneration(x)); Unlink(x, WarmWeight); AddToStaging(x, evictedList); } @@ -209,9 +206,9 @@ class TCacheCache : public ICacheCache { while (StagingWeight > Config.StagingLimit) { Y_DEBUG_ABORT_UNLESS(!StagingList.Empty()); TItem *evicted = StagingList.PopBack(); - Y_ABORT_UNLESS(GetGeneration(evicted) == TCacheCacheConfig::CacheGenStaging, "malformed entry in staging cache %" PRIu32, (ui32)GetGeneration(evicted)); + Y_ABORT_UNLESS(TItemTraits::GetGeneration(evicted) == ECacheCacheGeneration::Staging, "malformed entry in staging cache %" PRIu32, (ui32)TItemTraits::GetGeneration(evicted)); Unlink(evicted, StagingWeight); - SetGeneration(evicted, TCacheCacheConfig::CacheGenNone); + TItemTraits::SetGeneration(evicted, ECacheCacheGeneration::None); evictedList.PushBack(evicted); } } @@ -221,19 +218,11 @@ class TCacheCache : public ICacheCache { TItem *evicted = list.PopBack(); Unlink(evicted, weight); - SetGeneration(evicted, TCacheCacheConfig::CacheGenNone); + TItemTraits::SetGeneration(evicted, ECacheCacheGeneration::None); return evicted; } - void SetGeneration(TItem *item, TCacheCacheConfig::ECacheGeneration gen) { - GenerationOp.Set(item, static_cast(gen)); - } - - TCacheCacheConfig::ECacheGeneration GetGeneration(TItem *item) { - return static_cast(GenerationOp.Get(item)); - } - private: TCacheCacheConfig Config; @@ -244,9 +233,6 @@ class TCacheCache : public ICacheCache { ui64 FreshWeight; ui64 StagingWeight; ui64 WarmWeight; - - TWeight WeightOp; - TCacheFlags GenerationOp; }; } diff --git a/ydb/core/util/cache_cache_iface.h b/ydb/core/util/cache_cache_iface.h index 49a996bac452..89d5798ef6cc 100644 --- a/ydb/core/util/cache_cache_iface.h +++ b/ydb/core/util/cache_cache_iface.h @@ -13,7 +13,7 @@ struct ICacheCache { virtual void Erase(TItem *item) = 0; // WARN: do not evict items - virtual void UpdateCacheSize(ui64 cacheSize) = 0; + virtual void UpdateLimit(ui64 limit) = 0; virtual ~ICacheCache() = default; }; diff --git a/ydb/core/util/cache_cache_ut.cpp b/ydb/core/util/cache_cache_ut.cpp index abfb5eb25170..03e83a2f10a5 100644 --- a/ydb/core/util/cache_cache_ut.cpp +++ b/ydb/core/util/cache_cache_ut.cpp @@ -6,21 +6,22 @@ namespace NKikimr::NCache { Y_UNIT_TEST_SUITE(TCacheCacheTest) { struct TPage : public TIntrusiveListItem { - ui32 CacheGeneration : 16 = 0; + ECacheCacheGeneration CacheGeneration; }; - struct TWeight { - static ui64 Get(TPage *) { + struct TCacheCachePageTraits { + static ui64 GetWeight(const TPage*) { return 1; } - }; - struct TGeneration { - static ui32 Get(TPage *x) { - return x->CacheGeneration; + static ECacheCacheGeneration GetGeneration(const TPage *page) { + return static_cast(page->CacheGeneration); } - static void Set(TPage *x, ui32 gen) { - x->CacheGeneration = gen; + + static void SetGeneration(TPage *page, ECacheCacheGeneration generation) { + ui32 generation_ = static_cast(generation); + Y_ABORT_UNLESS(generation_ < (1 << 4)); + page->CacheGeneration = generation; } }; @@ -32,14 +33,14 @@ Y_UNIT_TEST_SUITE(TCacheCacheTest) { // this should mean nothing is cacheable, but currently we will // place 1 page on a level until it is inspected again. TCacheCacheConfig config(1, fresh, staging, warm); - TCacheCache cache(config); + TCacheCache cache(config); TVector pages(3); TIntrusiveList evicted; // page 0 added to fresh evicted = cache.Touch(&pages[0]); - UNIT_ASSERT(pages[0].CacheGeneration == TCacheCacheConfig::CacheGenFresh); + UNIT_ASSERT(pages[0].CacheGeneration == ECacheCacheGeneration::Fresh); UNIT_ASSERT_VALUES_EQUAL(fresh->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(staging->Val(), 0ULL); UNIT_ASSERT_VALUES_EQUAL(warm->Val(), 0ULL); @@ -47,8 +48,8 @@ Y_UNIT_TEST_SUITE(TCacheCacheTest) { // page 1 added to fresh first bumps page 0 to staging evicted = cache.Touch(&pages[1]); - UNIT_ASSERT(pages[1].CacheGeneration == TCacheCacheConfig::CacheGenFresh); - UNIT_ASSERT(pages[0].CacheGeneration == TCacheCacheConfig::CacheGenStaging); + UNIT_ASSERT(pages[1].CacheGeneration == ECacheCacheGeneration::Fresh); + UNIT_ASSERT(pages[0].CacheGeneration == ECacheCacheGeneration::Staging); UNIT_ASSERT_VALUES_EQUAL(fresh->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(staging->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(warm->Val(), 0ULL); @@ -56,7 +57,7 @@ Y_UNIT_TEST_SUITE(TCacheCacheTest) { // page 0 is moved to warm from staging evicted = cache.Touch(&pages[0]); - UNIT_ASSERT(pages[0].CacheGeneration == TCacheCacheConfig::CacheGenWarm); + UNIT_ASSERT(pages[0].CacheGeneration == ECacheCacheGeneration::Warm); UNIT_ASSERT_VALUES_EQUAL(fresh->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(staging->Val(), 0ULL); UNIT_ASSERT_VALUES_EQUAL(warm->Val(), 1ULL); @@ -64,8 +65,8 @@ Y_UNIT_TEST_SUITE(TCacheCacheTest) { // page 2 added to fresh first bumps page 1 to staging evicted = cache.Touch(&pages[2]); - UNIT_ASSERT(pages[2].CacheGeneration == TCacheCacheConfig::CacheGenFresh); - UNIT_ASSERT(pages[1].CacheGeneration == TCacheCacheConfig::CacheGenStaging); + UNIT_ASSERT(pages[2].CacheGeneration == ECacheCacheGeneration::Fresh); + UNIT_ASSERT(pages[1].CacheGeneration == ECacheCacheGeneration::Staging); UNIT_ASSERT_VALUES_EQUAL(fresh->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(staging->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(warm->Val(), 1ULL); @@ -73,8 +74,8 @@ Y_UNIT_TEST_SUITE(TCacheCacheTest) { // page 1 moves to warm, but first it bumps page 0 to staging evicted = cache.Touch(&pages[1]); - UNIT_ASSERT(pages[1].CacheGeneration == TCacheCacheConfig::CacheGenWarm); - UNIT_ASSERT(pages[0].CacheGeneration == TCacheCacheConfig::CacheGenStaging); + UNIT_ASSERT(pages[1].CacheGeneration == ECacheCacheGeneration::Warm); + UNIT_ASSERT(pages[0].CacheGeneration == ECacheCacheGeneration::Staging); UNIT_ASSERT_VALUES_EQUAL(fresh->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(staging->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(warm->Val(), 1ULL); @@ -88,7 +89,7 @@ Y_UNIT_TEST_SUITE(TCacheCacheTest) { // 2 pages per layer TCacheCacheConfig config(3, fresh, staging, warm); - TCacheCache cache(config); + TCacheCache cache(config); TVector pages(6); @@ -100,38 +101,38 @@ Y_UNIT_TEST_SUITE(TCacheCacheTest) { cache.Touch(&pages[1]); cache.Touch(&pages[4]); cache.Touch(&pages[5]); - UNIT_ASSERT(pages[0].CacheGeneration == TCacheCacheConfig::CacheGenWarm); - UNIT_ASSERT(pages[1].CacheGeneration == TCacheCacheConfig::CacheGenWarm); - UNIT_ASSERT(pages[2].CacheGeneration == TCacheCacheConfig::CacheGenStaging); - UNIT_ASSERT(pages[3].CacheGeneration == TCacheCacheConfig::CacheGenStaging); - UNIT_ASSERT(pages[4].CacheGeneration == TCacheCacheConfig::CacheGenFresh); - UNIT_ASSERT(pages[5].CacheGeneration == TCacheCacheConfig::CacheGenFresh); + UNIT_ASSERT(pages[0].CacheGeneration == ECacheCacheGeneration::Warm); + UNIT_ASSERT(pages[1].CacheGeneration == ECacheCacheGeneration::Warm); + UNIT_ASSERT(pages[2].CacheGeneration == ECacheCacheGeneration::Staging); + UNIT_ASSERT(pages[3].CacheGeneration == ECacheCacheGeneration::Staging); + UNIT_ASSERT(pages[4].CacheGeneration == ECacheCacheGeneration::Fresh); + UNIT_ASSERT(pages[5].CacheGeneration == ECacheCacheGeneration::Fresh); UNIT_ASSERT_VALUES_EQUAL(fresh->Val(), 2ULL); UNIT_ASSERT_VALUES_EQUAL(warm->Val(), 2ULL); UNIT_ASSERT_VALUES_EQUAL(staging->Val(), 2ULL); UNIT_ASSERT_VALUES_EQUAL(cache.EvictNext(), &pages[2]); - UNIT_ASSERT(pages[2].CacheGeneration == 0); + UNIT_ASSERT(pages[2].CacheGeneration == ECacheCacheGeneration::None); UNIT_ASSERT_VALUES_EQUAL(staging->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(cache.EvictNext(), &pages[3]); - UNIT_ASSERT(pages[3].CacheGeneration == 0); + UNIT_ASSERT(pages[3].CacheGeneration == ECacheCacheGeneration::None); UNIT_ASSERT_VALUES_EQUAL(staging->Val(), 0ULL); UNIT_ASSERT_VALUES_EQUAL(cache.EvictNext(), &pages[4]); - UNIT_ASSERT(pages[4].CacheGeneration == 0); + UNIT_ASSERT(pages[4].CacheGeneration == ECacheCacheGeneration::None); UNIT_ASSERT_VALUES_EQUAL(fresh->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(cache.EvictNext(), &pages[5]); - UNIT_ASSERT(pages[5].CacheGeneration == 0); + UNIT_ASSERT(pages[5].CacheGeneration == ECacheCacheGeneration::None); UNIT_ASSERT_VALUES_EQUAL(fresh->Val(), 0ULL); UNIT_ASSERT_VALUES_EQUAL(cache.EvictNext(), &pages[0]); - UNIT_ASSERT(pages[0].CacheGeneration == 0); + UNIT_ASSERT(pages[0].CacheGeneration == ECacheCacheGeneration::None); UNIT_ASSERT_VALUES_EQUAL(warm->Val(), 1ULL); UNIT_ASSERT_VALUES_EQUAL(cache.EvictNext(), &pages[1]); - UNIT_ASSERT(pages[1].CacheGeneration == 0); + UNIT_ASSERT(pages[1].CacheGeneration == ECacheCacheGeneration::None); UNIT_ASSERT_VALUES_EQUAL(warm->Val(), 0ULL); UNIT_ASSERT_VALUES_EQUAL(cache.EvictNext(), nullptr);