// Copyright Epic Games, Inc. All Rights Reserved. #include "zenstore/cache/structuredcachestore.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if ZEN_PLATFORM_WINDOWS # include #endif ZEN_THIRD_PARTY_INCLUDES_START #include #include #include ZEN_THIRD_PARTY_INCLUDES_END #if ZEN_WITH_TESTS # include # include # include # include # include # include # include #endif #include ////////////////////////////////////////////////////////////////////////// namespace zen { const FLLMTag& GetCacheStoreTag() { static FLLMTag _("store", FLLMTag("cache")); return _; } bool IsKnownBadBucketName(std::string_view Bucket) { if (Bucket.size() == 32) { uint8_t BucketHex[16]; if (ParseHexBytes(Bucket, BucketHex)) { return true; } } return false; } bool ValidateIoBuffer(ZenContentType ContentType, IoBuffer Buffer) { ZEN_ASSERT_SLOW(Buffer.GetContentType() == ContentType); if (ContentType == ZenContentType::kCbObject) { CbValidateError Error = ValidateCompactBinary(Buffer, CbValidateMode::All); if (Error == CbValidateError::None) { return true; } ZEN_SCOPED_ERROR("compact binary validation failed: '{}'", ToString(Error)); return false; } else if (ContentType == ZenContentType::kCompressedBinary) { IoBuffer MemoryBuffer = IoBufferBuilder::ReadFromFileMaybe(Buffer); IoHash HeaderRawHash; uint64_t RawSize = 0; if (!CompressedBuffer::ValidateCompressedHeader(MemoryBuffer, /* out */ HeaderRawHash, /* out */ RawSize)) { ZEN_SCOPED_ERROR("compressed buffer header validation failed"); return false; } CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(MemoryBuffer), /* out */ HeaderRawHash, /* out */ RawSize); CompositeBuffer Decompressed = Compressed.DecompressToComposite(); IoHash DecompressedHash = IoHash::HashBuffer(Decompressed); if (HeaderRawHash != DecompressedHash) { ZEN_SCOPED_ERROR("decompressed hash {} differs from header hash {}", DecompressedHash, HeaderRawHash); return false; } } else { // No way to verify this kind of content (what is it exactly?) static int Once = [&] { ZEN_WARN("ValidateIoBuffer called with unknown content type ({})", ToString(ContentType)); return 42; }(); } return true; }; ZenCacheNamespace::ZenCacheNamespace(GcManager& Gc, JobQueue& JobQueue, const std::filesystem::path& RootDir, const Configuration& Config) : m_Gc(Gc) , m_JobQueue(JobQueue) , m_RootDir(RootDir) , m_Configuration(Config) , m_DiskLayer(m_Gc, m_JobQueue, m_RootDir, m_Configuration.DiskLayerConfig) { ZEN_MEMSCOPE(GetCacheStoreTag()); ZEN_INFO("initializing structured cache at '{}'", m_RootDir); CreateDirectories(m_RootDir); m_DiskLayer.DiscoverBuckets(); m_Gc.AddGcStorage(this); } ZenCacheNamespace::~ZenCacheNamespace() { m_Gc.RemoveGcStorage(this); } struct ZenCacheNamespace::PutBatchHandle { ZenCacheDiskLayer::PutBatchHandle* DiskLayerHandle = nullptr; }; ZenCacheNamespace::PutBatchHandle* ZenCacheNamespace::BeginPutBatch(std::vector& OutResult) { ZenCacheNamespace::PutBatchHandle* Handle = new ZenCacheNamespace::PutBatchHandle; Handle->DiskLayerHandle = m_DiskLayer.BeginPutBatch(OutResult); return Handle; } void ZenCacheNamespace::EndPutBatch(PutBatchHandle* Batch) noexcept { try { ZEN_ASSERT(Batch); m_DiskLayer.EndPutBatch(Batch->DiskLayerHandle); delete Batch; } catch (const std::exception& Ex) { ZEN_ERROR("Exception in ZenCacheNamespace::EndPutBatch: '{}'", Ex.what()); } } struct ZenCacheNamespace::GetBatchHandle { GetBatchHandle(ZenCacheValueVec_t& OutResult) : Results(OutResult) {} ZenCacheValueVec_t& Results; ZenCacheDiskLayer::GetBatchHandle* DiskLayerHandle = nullptr; }; ZenCacheNamespace::GetBatchHandle* ZenCacheNamespace::BeginGetBatch(ZenCacheValueVec_t& OutResult) { ZenCacheNamespace::GetBatchHandle* Handle = new ZenCacheNamespace::GetBatchHandle(OutResult); Handle->DiskLayerHandle = m_DiskLayer.BeginGetBatch(OutResult); return Handle; } void ZenCacheNamespace::EndGetBatch(GetBatchHandle* Batch) noexcept { try { ZEN_ASSERT(Batch); m_DiskLayer.EndGetBatch(Batch->DiskLayerHandle); metrics::RequestStats::Scope StatsScope(m_GetOps, 0); for (const ZenCacheValue& Result : Batch->Results) { if (Result.Value) { m_HitCount++; StatsScope.SetBytes(Result.Value.Size()); } else { m_MissCount++; } } delete Batch; } catch (const std::exception& Ex) { ZEN_ERROR("Exception in ZenCacheNamespace::EndGetBatch: '{}'", Ex.what()); } } bool ZenCacheNamespace::Get(std::string_view InBucket, const IoHash& HashKey, ZenCacheValue& OutValue) { ZEN_TRACE_CPU("Z$::Namespace::Get"); metrics::RequestStats::Scope StatsScope(m_GetOps, 0); bool Ok = m_DiskLayer.Get(InBucket, HashKey, OutValue); if (Ok) { ZEN_ASSERT(OutValue.Value.Size()); StatsScope.SetBytes(OutValue.Value.Size()); m_HitCount++; return true; } m_MissCount++; return false; } void ZenCacheNamespace::Get(std::string_view InBucket, const IoHash& HashKey, GetBatchHandle& BatchHandle) { ZEN_TRACE_CPU("Z$::Namespace::Get(Batched)"); metrics::RequestStats::Scope StatsScope(m_GetOps, 0); m_DiskLayer.Get(InBucket, HashKey, *BatchHandle.DiskLayerHandle); return; } void ZenCacheNamespace::Put(std::string_view InBucket, const IoHash& HashKey, const ZenCacheValue& Value, std::span References, PutBatchHandle* OptionalBatchHandle) { ZEN_TRACE_CPU(OptionalBatchHandle ? "Z$::Namespace::Put(Batched)" : "Z$::Namespace::Put"); metrics::RequestStats::Scope $(m_PutOps, Value.Value.Size()); // Store value and index ZEN_ASSERT(Value.Value.Size()); ZenCacheDiskLayer::PutBatchHandle* DiskLayerBatchHandle = OptionalBatchHandle ? OptionalBatchHandle->DiskLayerHandle : nullptr; m_DiskLayer.Put(InBucket, HashKey, Value, References, DiskLayerBatchHandle); m_WriteCount++; } bool ZenCacheNamespace::DropBucket(std::string_view Bucket) { ZEN_INFO("dropping bucket '{}'", Bucket); const bool Dropped = m_DiskLayer.DropBucket(Bucket); ZEN_INFO("bucket '{}' was {}", Bucket, Dropped ? "dropped" : "not found"); return Dropped; } void ZenCacheNamespace::EnumerateBucketContents(std::string_view Bucket, std::function& Fn) const { m_DiskLayer.EnumerateBucketContents(Bucket, Fn); } bool ZenCacheNamespace::Drop() { return m_DiskLayer.Drop(); } void ZenCacheNamespace::Flush() { m_DiskLayer.Flush(); } void ZenCacheNamespace::ScrubStorage(ScrubContext& Ctx) { if (m_LastScrubTime == Ctx.ScrubTimestamp()) { return; } ZEN_INFO("scrubbing '{}'", m_RootDir); m_LastScrubTime = Ctx.ScrubTimestamp(); m_DiskLayer.ScrubStorage(Ctx); } GcStorageSize ZenCacheNamespace::StorageSize() const { return m_DiskLayer.StorageSize(); } ZenCacheNamespace::Info ZenCacheNamespace::GetInfo() const { ZenCacheNamespace::Info Info = {.RootDir = m_RootDir, .Config = m_Configuration, .DiskLayerInfo = m_DiskLayer.GetInfo()}; std::unordered_set BucketNames; for (const std::string& BucketName : Info.DiskLayerInfo.BucketNames) { BucketNames.insert(BucketName); } Info.BucketNames.insert(Info.BucketNames.end(), BucketNames.begin(), BucketNames.end()); return Info; } std::optional ZenCacheNamespace::GetBucketInfo(std::string_view Bucket) const { std::optional DiskBucketInfo = m_DiskLayer.GetBucketInfo(Bucket); if (!DiskBucketInfo.has_value()) { return {}; } ZenCacheNamespace::BucketInfo Info = {.DiskLayerInfo = *DiskBucketInfo}; return Info; } ZenCacheNamespace::NamespaceStats ZenCacheNamespace::Stats() { return ZenCacheNamespace::NamespaceStats{.HitCount = m_HitCount, .MissCount = m_MissCount, .WriteCount = m_WriteCount, .PutOps = m_PutOps.Snapshot(), .GetOps = m_GetOps.Snapshot(), .DiskStats = m_DiskLayer.Stats()}; } CacheValueDetails::NamespaceDetails ZenCacheNamespace::GetValueDetails(const std::string_view BucketFilter, const std::string_view ValueFilter) const { return m_DiskLayer.GetValueDetails(BucketFilter, ValueFilter); } std::vector ZenCacheNamespace::GetGcReferencerLocks() { return m_DiskLayer.GetGcReferencerLocks(); } void ZenCacheNamespace::EnableUpdateCapture() { m_DiskLayer.EnableUpdateCapture(); } void ZenCacheNamespace::DisableUpdateCapture() { m_DiskLayer.DisableUpdateCapture(); } bool ZenCacheNamespace::GetContentStats(std::string_view BucketName, CacheContentStats& OutContentStats) const { return m_DiskLayer.GetContentStats(BucketName, OutContentStats); } #if ZEN_WITH_TESTS void ZenCacheNamespace::SetAccessTime(std::string_view Bucket, const IoHash& HashKey, GcClock::TimePoint Time) { m_DiskLayer.SetAccessTime(Bucket, HashKey, Time); } #endif // ZEN_WITH_TESTS //////////////////////////// ZenCacheStore ZEN_DEFINE_LOG_CATEGORY_STATIC(LogCacheActivity, "z$"); static constinit std::string_view UE4DDCNamespaceName = "ue4.ddc"; ZenCacheStore::ZenCacheStore(GcManager& Gc, JobQueue& JobQueue, const std::filesystem::path& BasePath, const Configuration& Configuration, const DiskWriteBlocker* InDiskWriteBlocker) : m_DiskWriteBlocker(InDiskWriteBlocker) , m_Gc(Gc) , m_JobQueue(JobQueue) , m_BasePath(BasePath) , m_Configuration(Configuration) , m_ExitLogging(false) { ZEN_MEMSCOPE(GetCacheStoreTag()); SetLoggingConfig(m_Configuration.Logging); CreateDirectories(m_BasePath); ZEN_INFO("initializing cache store at '{}'", m_BasePath); DirectoryContent DirContent; GetDirectoryContent(m_BasePath, DirectoryContentFlags::IncludeDirs, DirContent); std::vector Namespaces; for (const std::filesystem::path& DirPath : DirContent.Directories) { std::string DirName = PathToUtf8(DirPath.filename()); if (DirName.starts_with(NamespaceDiskPrefix)) { Namespaces.push_back(DirName.substr(NamespaceDiskPrefix.length())); continue; } } ZEN_INFO("Found {} namespaces in '{}'", Namespaces.size(), m_BasePath); if (std::find(Namespaces.begin(), Namespaces.end(), UE4DDCNamespaceName) == Namespaces.end()) { // default (unspecified) and ue4-ddc namespace points to the same namespace instance std::filesystem::path DefaultNamespaceFolder = m_BasePath / fmt::format("{}{}", NamespaceDiskPrefix, UE4DDCNamespaceName); CreateDirectories(DefaultNamespaceFolder); Namespaces.push_back(std::string(UE4DDCNamespaceName)); } for (const std::string& NamespaceName : Namespaces) { m_Namespaces[NamespaceName] = std::make_unique(Gc, m_JobQueue, m_BasePath / fmt::format("{}{}", NamespaceDiskPrefix, NamespaceName), m_Configuration.NamespaceConfig); } m_Gc.AddGcReferencer(*this); m_Gc.AddGcReferenceLocker(*this); } ZenCacheStore::~ZenCacheStore() { ZEN_INFO("closing cache store at '{}'", m_BasePath); m_Gc.RemoveGcReferenceLocker(*this); m_Gc.RemoveGcReferencer(*this); SetLoggingConfig({.EnableWriteLog = false, .EnableAccessLog = false}); m_Namespaces.clear(); } void ZenCacheStore::LogWorker() { SetCurrentThreadName("ZenCacheStore::LogWorker"); ZEN_MEMSCOPE(GetCacheStoreTag()); LoggerRef ZCacheLog(logging::Get("z$")); auto Log = [&ZCacheLog]() -> LoggerRef { return ZCacheLog; }; std::vector Items; while (true) { try { m_LogQueueLock.WithExclusiveLock([this, &Items]() { Items.swap(m_LogQueue); }); if (m_DiskWriteBlocker == nullptr || m_DiskWriteBlocker->AreDiskWritesAllowed()) { for (const auto& Item : Items) { if (Item.Value.Value) { const bool IsCbObject = Item.Value.Value.GetContentType() == ZenContentType::kCbObject; try { const IoHash ObjectHash = IsCbObject ? IoHash::HashBuffer(Item.Value.Value.GetView()) : Item.Value.RawHash; const size_t ObjectSize = IsCbObject ? Item.Value.Value.GetSize() : Item.Value.RawSize; ZEN_LOG_INFO(LogCacheActivity, "{} [{}] {}/{}/{} -> {} {} {}", Item.Op, Item.Context, Item.Namespace, Item.Bucket, Item.HashKey, ObjectHash, ObjectSize, ToString(Item.Value.Value.GetContentType())) } catch (const std::exception& Ex) { ZEN_LOG_INFO(LogCacheActivity, "{} [{}] {}/{}/{} failed: Reason: '{}'", Item.Op, Item.Context, Item.Namespace, Item.Bucket, Item.HashKey, Ex.what()) } } else { ZEN_LOG_INFO(LogCacheActivity, "{} [{}] {}/{}/{}", Item.Op, Item.Context, Item.Namespace, Item.Bucket, Item.HashKey); } } } if (!Items.empty()) { Items.resize(0); continue; } if (m_ExitLogging) { break; } m_LogEvent.Wait(); m_LogEvent.Reset(); } catch (const std::exception& Ex) { ZEN_WARN("Log writer failed: '{}'", Ex.what()); } } } ZenCacheStore::PutBatch::PutBatch(ZenCacheStore& CacheStore, std::string_view InNamespace, std::vector& OutResult) : m_CacheStore(CacheStore) { ZEN_MEMSCOPE(GetCacheStoreTag()); if (m_Store = m_CacheStore.GetNamespace(InNamespace); m_Store) { m_NamespaceBatchHandle = m_Store->BeginPutBatch(OutResult); } } ZenCacheStore::PutBatch::~PutBatch() { try { if (m_Store) { ZEN_MEMSCOPE(GetCacheStoreTag()); ZEN_ASSERT(m_NamespaceBatchHandle); m_Store->EndPutBatch(m_NamespaceBatchHandle); } } catch (const std::exception& Ex) { ZEN_ERROR("Exception in cache store when ending batch put operation: '{}'", Ex.what()); } } ZenCacheStore::GetBatch::GetBatch(ZenCacheStore& CacheStore, std::string_view InNamespace, ZenCacheValueVec_t& OutResult) : m_CacheStore(CacheStore) , Results(OutResult) { ZEN_MEMSCOPE(GetCacheStoreTag()); if (m_Store = m_CacheStore.GetNamespace(InNamespace); m_Store) { m_NamespaceBatchHandle = m_Store->BeginGetBatch(OutResult); } } ZenCacheStore::GetBatch::~GetBatch() { try { if (m_Store) { ZEN_MEMSCOPE(GetCacheStoreTag()); ZEN_ASSERT(m_NamespaceBatchHandle); m_Store->EndGetBatch(m_NamespaceBatchHandle); metrics::RequestStats::Scope OpScope(m_CacheStore.m_GetOps, 0); for (const ZenCacheValue& Result : Results) { if (Result.Value) { m_CacheStore.m_HitCount++; OpScope.SetBytes(Result.Value.GetSize()); } m_CacheStore.m_MissCount++; } } } catch (const std::exception& Ex) { ZEN_ERROR("Exception in cache store when ending batch get operation: '{}'", Ex.what()); } } bool ZenCacheStore::Get(const CacheRequestContext& Context, std::string_view Namespace, std::string_view Bucket, const IoHash& HashKey, ZenCacheValue& OutValue) { // Ad hoc rejection of known bad usage patterns for DDC bucket names if (IsKnownBadBucketName(Bucket)) { m_RejectedReadCount++; return false; } ZEN_MEMSCOPE(GetCacheStoreTag()); ZEN_TRACE_CPU("Z$::Get"); metrics::RequestStats::Scope OpScope(m_GetOps, 0); if (ZenCacheNamespace* Store = GetNamespace(Namespace); Store) { bool Result = Store->Get(Bucket, HashKey, OutValue); if (m_AccessLogEnabled) { ZEN_TRACE_CPU("Z$::Get::AccessLog"); bool Signal = false; m_LogQueueLock.WithExclusiveLock([&]() { Signal = m_LogQueue.empty(); m_LogQueue.emplace_back(AccessLogItem{.Op = Result ? "GET HIT " : "GET MISS", .Context = Context, .Namespace = std::string(Namespace), .Bucket = std::string(Bucket), .HashKey = HashKey, .Value = OutValue}); }); if (Signal) { m_LogEvent.Set(); } } if (Result) { m_HitCount++; OpScope.SetBytes(OutValue.Value.GetSize()); return true; } m_MissCount++; return false; } ZEN_WARN("request for unknown namespace '{}' in ZenCacheStore::Get [{}], bucket '{}', key '{}'", Context, Namespace, Bucket, HashKey.ToHexString()); m_MissCount++; return false; } void ZenCacheStore::Get(const CacheRequestContext& Context, std::string_view Namespace, std::string_view Bucket, const IoHash& HashKey, GetBatch& BatchHandle) { // Ad hoc rejection of known bad usage patterns for DDC bucket names if (IsKnownBadBucketName(Bucket)) { m_RejectedReadCount++; return; } ZEN_MEMSCOPE(GetCacheStoreTag()); ZEN_TRACE_CPU("Z$::Get"); metrics::RequestStats::Scope OpScope(m_GetOps, 0); if (ZenCacheNamespace* Store = GetNamespace(Namespace); Store) { Store->Get(Bucket, HashKey, *BatchHandle.m_NamespaceBatchHandle); return; } ZEN_WARN("request for unknown namespace '{}' in ZenCacheStore::Get [{}], bucket '{}', key '{}'", Context, Namespace, Bucket, HashKey.ToHexString()); m_MissCount++; } void ZenCacheStore::Put(const CacheRequestContext& Context, std::string_view Namespace, std::string_view Bucket, const IoHash& HashKey, const ZenCacheValue& Value, std::span References, PutBatch* OptionalBatchHandle) { // Ad hoc rejection of known bad usage patterns for DDC bucket names if (IsKnownBadBucketName(Bucket)) { m_RejectedWriteCount++; return; } ZEN_MEMSCOPE(GetCacheStoreTag()); ZEN_TRACE_CPU("Z$::Put"); metrics::RequestStats::Scope $(m_PutOps, Value.Value.GetSize()); if (m_WriteLogEnabled) { ZEN_TRACE_CPU("Z$::Get::WriteLog"); bool Signal = false; m_LogQueueLock.WithExclusiveLock([&]() { Signal = m_LogQueue.empty(); m_LogQueue.emplace_back(AccessLogItem{.Op = "PUT ", .Context = Context, .Namespace = std::string(Namespace), .Bucket = std::string(Bucket), .HashKey = HashKey, .Value = Value}); }); if (Signal) { m_LogEvent.Set(); } } if (ZenCacheNamespace* Store = GetNamespace(Namespace); Store) { ZenCacheNamespace::PutBatchHandle* BatchHandle = OptionalBatchHandle ? OptionalBatchHandle->m_NamespaceBatchHandle : nullptr; Store->Put(Bucket, HashKey, Value, References, BatchHandle); m_WriteCount++; return; } ZEN_WARN("request for unknown namespace '{}' in ZenCacheStore::Put [{}] bucket '{}', key '{}'", Context, Namespace, Bucket, HashKey.ToHexString()); } bool ZenCacheStore::DropBucket(std::string_view Namespace, std::string_view Bucket) { if (ZenCacheNamespace* Store = GetNamespace(Namespace); Store) { return Store->DropBucket(Bucket); } ZEN_WARN("request for unknown namespace '{}' in ZenCacheStore::DropBucket, bucket '{}'", Namespace, Bucket); return false; } bool ZenCacheStore::DropNamespace(std::string_view InNamespace) { RwLock::SharedLockScope _(m_NamespacesLock); if (auto It = m_Namespaces.find(std::string(InNamespace)); It != m_Namespaces.end()) { ZenCacheNamespace& Namespace = *It->second; m_DroppedNamespaces.push_back(std::move(It->second)); m_Namespaces.erase(It); return Namespace.Drop(); } ZEN_WARN("request for unknown namespace '{}' in ZenCacheStore::DropNamespace", InNamespace); return false; } void ZenCacheStore::Flush() { ZEN_MEMSCOPE(GetCacheStoreTag()); ZEN_INFO("flushing cache store at '{}'", m_BasePath); IterateNamespaces([&](std::string_view, ZenCacheNamespace& Store) { Store.Flush(); }); } void ZenCacheStore::ScrubStorage(ScrubContext& Ctx) { IterateNamespaces([&](std::string_view, ZenCacheNamespace& Store) { Store.ScrubStorage(Ctx); }); } CacheValueDetails ZenCacheStore::GetValueDetails(const std::string_view NamespaceFilter, const std::string_view BucketFilter, const std::string_view ValueFilter) const { CacheValueDetails Details; if (NamespaceFilter.empty()) { IterateNamespaces([&](std::string_view Namespace, ZenCacheNamespace& Store) { Details.Namespaces[std::string(Namespace)] = Store.GetValueDetails(BucketFilter, ValueFilter); }); } else if (const ZenCacheNamespace* Store = FindNamespace(NamespaceFilter); Store != nullptr) { Details.Namespaces[std::string(NamespaceFilter)] = Store->GetValueDetails(BucketFilter, ValueFilter); } return Details; } void ZenCacheStore::EnumerateBucketContents(std::string_view Namespace, std::string_view Bucket, std::function&& Fn) { if (const ZenCacheNamespace* Ns = FindNamespace(Namespace)) { Ns->EnumerateBucketContents(Bucket, Fn); } } ZenCacheNamespace* ZenCacheStore::GetNamespace(std::string_view Namespace) { RwLock::SharedLockScope _(m_NamespacesLock); if (auto It = m_Namespaces.find(std::string(Namespace)); It != m_Namespaces.end()) { return It->second.get(); } if (Namespace == DefaultNamespace) { if (auto It = m_Namespaces.find(std::string(UE4DDCNamespaceName)); It != m_Namespaces.end()) { return It->second.get(); } } _.ReleaseNow(); if (!m_Configuration.AllowAutomaticCreationOfNamespaces) { return nullptr; } RwLock::ExclusiveLockScope __(m_NamespacesLock); if (auto It = m_Namespaces.find(std::string(Namespace)); It != m_Namespaces.end()) { return It->second.get(); } auto NewNamespace = m_Namespaces.insert_or_assign(std::string(Namespace), std::make_unique(m_Gc, m_JobQueue, m_BasePath / fmt::format("{}{}", NamespaceDiskPrefix, Namespace), m_Configuration.NamespaceConfig)); if (m_CapturedNamespaces) { m_CapturedNamespaces->push_back(std::string(Namespace)); } return NewNamespace.first->second.get(); } const ZenCacheNamespace* ZenCacheStore::FindNamespace(std::string_view Namespace) const { RwLock::SharedLockScope _(m_NamespacesLock); if (auto It = m_Namespaces.find(std::string(Namespace)); It != m_Namespaces.end()) { return It->second.get(); } if (Namespace == DefaultNamespace) { if (auto It = m_Namespaces.find(std::string(UE4DDCNamespaceName)); It != m_Namespaces.end()) { return It->second.get(); } } return nullptr; } std::vector ZenCacheStore::GetNamespaces() { std::vector Namespaces; IterateNamespaces([&](std::string_view Namespace, ZenCacheNamespace&) { Namespaces.push_back(std::string(Namespace)); }); return Namespaces; } void ZenCacheStore::IterateNamespaces(const std::function& Callback) const { std::vector> Namespaces; { RwLock::SharedLockScope _(m_NamespacesLock); Namespaces.reserve(m_Namespaces.size()); for (const auto& Entry : m_Namespaces) { if (Entry.first == DefaultNamespace) { continue; } Namespaces.push_back({Entry.first, *Entry.second}); } } for (auto& Entry : Namespaces) { Callback(Entry.first, Entry.second); } } GcStorageSize ZenCacheStore::StorageSize() const { GcStorageSize Size; IterateNamespaces([&](std::string_view, ZenCacheNamespace& Store) { GcStorageSize StoreSize = Store.StorageSize(); Size.MemorySize += StoreSize.MemorySize; Size.DiskSize += StoreSize.DiskSize; }); return Size; } ZenCacheStore::CacheStoreStats ZenCacheStore::Stats(bool IncludeNamespaceStats) { ZenCacheStore::CacheStoreStats Result{.HitCount = m_HitCount, .MissCount = m_MissCount, .WriteCount = m_WriteCount, .RejectedWriteCount = m_RejectedWriteCount, .RejectedReadCount = m_RejectedReadCount, .PutOps = m_PutOps.Snapshot(), .GetOps = m_GetOps.Snapshot()}; if (IncludeNamespaceStats) { IterateNamespaces([&](std::string_view NamespaceName, ZenCacheNamespace& Store) { Result.NamespaceStats.emplace_back(NamedNamespaceStats{.NamespaceName = std::string(NamespaceName), .Stats = Store.Stats()}); }); } return Result; } void ZenCacheStore::ReportMetrics(StatsMetrics& Statsd) { const bool IncludeNamespaceStats = false; const CacheStoreStats Now = Stats(IncludeNamespaceStats); const CacheStoreStats& Old = m_LastReportedMetrics; Statsd.Meter("zen.cache_hits", Now.HitCount - Old.HitCount); Statsd.Meter("zen.cache_misses", Now.MissCount - Old.MissCount); Statsd.Meter("zen.cache_writes", Now.WriteCount - Old.WriteCount); m_LastReportedMetrics = Now; } void ZenCacheStore::SetLoggingConfig(const Configuration::LogConfig& Loggingconfig) { if (!Loggingconfig.EnableAccessLog && !Loggingconfig.EnableWriteLog) { m_AccessLogEnabled.store(false); m_WriteLogEnabled.store(false); m_ExitLogging.store(true); m_LogEvent.Set(); if (m_AsyncLoggingThread.joinable()) { m_AsyncLoggingThread.join(); } m_Configuration.Logging = Loggingconfig; return; } if (!m_AccessLogEnabled.load() && !m_WriteLogEnabled.load()) { m_AsyncLoggingThread = std::thread(&ZenCacheStore::LogWorker, this); } m_WriteLogEnabled.store(Loggingconfig.EnableWriteLog); m_AccessLogEnabled.store(Loggingconfig.EnableAccessLog); m_Configuration.Logging = Loggingconfig; } ZenCacheStore::Info ZenCacheStore::GetInfo() const { ZenCacheStore::Info Info = {.Config = m_Configuration, .StorageSize = StorageSize()}; IterateNamespaces([&Info](std::string_view NamespaceName, ZenCacheNamespace& Namespace) { Info.NamespaceNames.push_back(std::string(NamespaceName)); ZenCacheNamespace::Info NamespaceInfo = Namespace.GetInfo(); Info.DiskEntryCount += NamespaceInfo.DiskLayerInfo.EntryCount; }); return Info; } std::optional ZenCacheStore::GetNamespaceInfo(std::string_view NamespaceName) { if (const ZenCacheNamespace* Namespace = FindNamespace(NamespaceName); Namespace) { return Namespace->GetInfo(); } return {}; } std::optional ZenCacheStore::GetBucketInfo(std::string_view NamespaceName, std::string_view BucketName) { if (const ZenCacheNamespace* Namespace = FindNamespace(NamespaceName); Namespace) { return Namespace->GetBucketInfo(BucketName); } return {}; } std::vector ZenCacheStore::LockState(GcCtx& Ctx) { ZEN_TRACE_CPU("CacheStore::LockState"); auto Log = [&Ctx]() { return Ctx.Logger; }; std::vector Locks; Locks.emplace_back(RwLock::SharedLockScope(m_NamespacesLock)); for (auto& NamespaceIt : m_Namespaces) { std::vector NamespaceLocks = NamespaceIt.second->GetGcReferencerLocks(); for (auto It = std::make_move_iterator(NamespaceLocks.begin()); It != std::make_move_iterator(NamespaceLocks.end()); It++) { Locks.emplace_back(std::move(*It)); } } return Locks; } void ZenCacheStore::EnableUpdateCapture() { std::vector Namespaces; m_NamespacesLock.WithExclusiveLock([&]() { if (m_UpdateCaptureRefCounter == 0) { ZEN_ASSERT(!m_CapturedNamespaces); m_CapturedNamespaces = std::make_unique>(); } else { ZEN_ASSERT(m_CapturedNamespaces); } m_UpdateCaptureRefCounter++; Namespaces.reserve(m_Namespaces.size()); for (auto& NamespaceIt : m_Namespaces) { Namespaces.push_back(NamespaceIt.second.get()); } }); for (ZenCacheNamespace* Namespace : Namespaces) { Namespace->EnableUpdateCapture(); } } void ZenCacheStore::DisableUpdateCapture() { std::vector Namespaces; m_NamespacesLock.WithExclusiveLock([&]() { ZEN_ASSERT(m_CapturedNamespaces); ZEN_ASSERT(m_UpdateCaptureRefCounter > 0); m_UpdateCaptureRefCounter--; if (m_UpdateCaptureRefCounter == 0) { m_CapturedNamespaces.reset(); } Namespaces.reserve(m_Namespaces.size()); for (auto& NamespaceIt : m_Namespaces) { Namespaces.push_back(NamespaceIt.second.get()); } }); for (ZenCacheNamespace* Namespace : Namespaces) { Namespace->DisableUpdateCapture(); } } std::vector ZenCacheStore::GetCapturedNamespacesLocked() { if (m_CapturedNamespaces) { return *m_CapturedNamespaces; } return {}; } bool ZenCacheStore::GetContentStats(std::string_view NamespaceName, std::string_view BucketName, CacheContentStats& OutContentStats) const { if (const ZenCacheNamespace* Namespace = FindNamespace(NamespaceName); Namespace) { return Namespace->GetContentStats(BucketName, OutContentStats); } return false; } std::string ZenCacheStore::GetGcName(GcCtx&) { return fmt::format("zencachestore: '{}'", m_BasePath.string()); } GcStoreCompactor* ZenCacheStore::RemoveExpiredData(GcCtx&, GcStats&) { return nullptr; } class CacheStoreReferenceChecker : public GcReferenceChecker { public: CacheStoreReferenceChecker(ZenCacheStore& InCacheStore) : m_CacheStore(InCacheStore) { m_CacheStore.EnableUpdateCapture(); } virtual ~CacheStoreReferenceChecker() { try { m_CacheStore.DisableUpdateCapture(); } catch (const std::exception& Ex) { ZEN_ERROR("~CacheStoreReferenceChecker threw exception: '{}'", Ex.what()); } } virtual std::string GetGcName(GcCtx&) override { return "cachestore"; } virtual void PreCache(GcCtx&) override {} virtual void UpdateLockedState(GcCtx& Ctx) override { ZEN_TRACE_CPU("Z$::UpdateLockedState"); auto Log = [&Ctx]() { return Ctx.Logger; }; Stopwatch Timer; std::vector AddedBuckets; const auto _ = MakeGuard([&] { if (!Ctx.Settings.Verbose) { return; } ZEN_INFO("GCV2: cachestore [LOCKSTATE] '{}': found {} references in {} in {} new buckets", "cachestore", m_References.size(), NiceTimeSpanMs(Timer.GetElapsedTimeMs()), AddedBuckets.size()); }); std::vector AddedNamespaces = m_CacheStore.GetCapturedNamespacesLocked(); for (const std::string& AddedNamespace : AddedNamespaces) { if (auto It = m_CacheStore.m_Namespaces.find(AddedNamespace); It != m_CacheStore.m_Namespaces.end()) { ZenCacheNamespace& Namespace = *It->second; for (auto& BucketKV : Namespace.m_DiskLayer.m_Buckets) { AddedBuckets.push_back(BucketKV.second.get()); } } } for (auto& NamepaceKV : m_CacheStore.m_Namespaces) { ZenCacheNamespace& Namespace = *NamepaceKV.second; std::vector NamespaceAddedBuckets = Namespace.m_DiskLayer.GetCapturedBucketsLocked(); for (const std::string& AddedBucketName : NamespaceAddedBuckets) { if (auto It = Namespace.m_DiskLayer.m_Buckets.find(AddedBucketName); It != Namespace.m_DiskLayer.m_Buckets.end()) { AddedBuckets.push_back(It->second.get()); } } } for (ZenCacheDiskLayer::CacheBucket* Bucket : AddedBuckets) { bool Continue = Bucket->GetReferences(Ctx.Logger, Ctx.IsCancelledFlag, /*StateIsAlreadyLocked*/ true, Ctx.Settings.StoreCacheAttachmentMetaData, Ctx.Settings.StoreCacheAttachmentMetaData, m_References, nullptr); if (!Continue) { break; } } FilterReferences(Ctx, fmt::format("cachestore [LOCKSTATE] '{}'", "cachestore"), m_References); } virtual std::span GetUnusedReferences(GcCtx& Ctx, std::span IoCids) override { ZEN_TRACE_CPU("Z$::GetUnusedReferences"); auto Log = [&Ctx]() { return Ctx.Logger; }; const size_t InitialCount = IoCids.size(); size_t UsedCount = InitialCount; Stopwatch Timer; const auto _ = MakeGuard([&] { if (!Ctx.Settings.Verbose) { return; } ZEN_INFO("GCV2: projectstore [FILTER REFERENCES] '{}': filtered out {} used references out of {} in {}", "projectstore", UsedCount, InitialCount, NiceTimeSpanMs(Timer.GetElapsedTimeMs())); }); std::span UnusedReferences = KeepUnusedReferences(m_References, IoCids); UsedCount = IoCids.size() - UnusedReferences.size(); return UnusedReferences; } private: ZenCacheStore& m_CacheStore; std::vector m_References; }; std::vector ZenCacheStore::CreateReferenceCheckers(GcCtx& Ctx) { ZEN_TRACE_CPU("CacheStore::CreateReferenceCheckers"); auto Log = [&Ctx]() { return Ctx.Logger; }; Stopwatch Timer; const auto _ = MakeGuard([&] { if (!Ctx.Settings.Verbose) { return; } ZEN_INFO("GCV2: cachestore [CREATE CHECKERS] '{}': completed in {}", m_BasePath, NiceTimeSpanMs(Timer.GetElapsedTimeMs())); }); std::vector Checkers; Checkers.emplace_back(new CacheStoreReferenceChecker(*this)); return Checkers; } std::vector ZenCacheStore::CreateReferenceValidators(GcCtx& /*Ctx*/) { return {}; } ////////////////////////////////////////////////////////////////////////// #if ZEN_WITH_TESTS using namespace std::literals; namespace testutils { IoHash CreateKey(size_t KeyValue) { return IoHash::HashBuffer(&KeyValue, sizeof(size_t)); } IoHash ToIoHash(const Oid& Id) { char OIdString[24 + 1]; Id.ToString(OIdString); IoHash Key = IoHash::HashBuffer(OIdString, 24); return Key; } std::pair CreateBinaryBlob(size_t Size) { return {Oid::NewOid(), CreateRandomBlob(Size)}; } std::vector> CreateCompressedAttachment(CidStore& Store, const std::span& Sizes) { std::vector> Result; Result.reserve(Sizes.size()); for (size_t Size : Sizes) { auto Blob = CreateBinaryBlob(Size); CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(Blob.second.Data(), Blob.second.Size())); CHECK(!Store.ContainsChunk(Compressed.DecodeRawHash())); Result.emplace_back(std::pair(Blob.first, Compressed)); } return Result; } std::pair CreateRecord(std::span> Attachments) { Oid Id = Oid::NewOid(); IoHash Key = ToIoHash(Id); CbObjectWriter Record; Record << "Key"sv << Id; for (size_t Idx = 0; auto& Cid : Attachments) { Record.AddBinaryAttachment(fmt::format("attachment-{}", Idx++), Cid.second.DecodeRawHash()); } IoBuffer Buffer = Record.Save().GetBuffer().AsIoBuffer(); Buffer.SetContentType(ZenContentType::kCbObject); return {Key, Buffer}; } } // namespace testutils TEST_CASE("cachestore.store") { ScopedTemporaryDirectory TempDir; GcManager Gc; auto JobQueue = MakeJobQueue(1, "testqueue"); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); const int kIterationCount = 100; for (int i = 0; i < kIterationCount; ++i) { const IoHash Key = IoHash::HashBuffer(&i, sizeof i); CbObjectWriter Cbo; Cbo << "hey" << i; CbObject Obj = Cbo.Save(); ZenCacheValue Value; Value.Value = Obj.GetBuffer().AsIoBuffer(); Value.Value.SetContentType(ZenContentType::kCbObject); Zcs.Put("test_bucket"sv, Key, Value, {}); } for (int i = 0; i < kIterationCount; ++i) { const IoHash Key = IoHash::HashBuffer(&i, sizeof i); ZenCacheValue Value; Zcs.Get("test_bucket"sv, Key, /* out */ Value); REQUIRE(Value.Value); CHECK(Value.Value.GetContentType() == ZenContentType::kCbObject); CHECK_EQ(ValidateCompactBinary(Value.Value, CbValidateMode::All), CbValidateError::None); CbObject Obj = LoadCompactBinaryObject(Value.Value); CHECK_EQ(Obj["hey"].AsInt32(), i); } } TEST_CASE("cachestore.size") { auto JobQueue = MakeJobQueue(1, "testqueue"); const auto CreateCacheValue = [](size_t Size) -> CbObject { std::vector Buf; Buf.resize(Size); CbObjectWriter Writer; Writer.AddBinary("Binary"sv, Buf.data(), Buf.size()); return Writer.Save(); }; SUBCASE("mem/disklayer") { const size_t Count = 16; ScopedTemporaryDirectory TempDir; GcStorageSize CacheSize; { GcManager Gc; ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CbObject CacheValue = CreateCacheValue(Zcs.GetConfig().DiskLayerConfig.BucketConfig.MemCacheSizeThreshold - 256); IoBuffer Buffer = CacheValue.GetBuffer().AsIoBuffer(); Buffer.SetContentType(ZenContentType::kCbObject); std::vector> Keys; for (size_t Key = 0; Key < Count; ++Key) { const size_t Bucket = Key % 4; std::string BucketName = fmt::format("test_bucket-{}", Bucket); IoHash Hash = IoHash::HashBuffer(&Key, sizeof(uint32_t)); Zcs.Put(BucketName, Hash, ZenCacheValue{.Value = Buffer}, {}); Keys.push_back({BucketName, Hash}); } CacheSize = Zcs.StorageSize(); CHECK_LE(CacheValue.GetSize() * Count, CacheSize.DiskSize); CHECK_EQ(0, CacheSize.MemorySize); for (const auto& Key : Keys) { ZenCacheValue _; Zcs.Get(Key.first, Key.second, _); } CacheSize = Zcs.StorageSize(); CHECK_LE(CacheValue.GetSize() * Count, CacheSize.DiskSize); CHECK_LE(CacheValue.GetSize() * Count, CacheSize.MemorySize); } { GcManager Gc; ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); const GcStorageSize SerializedSize = Zcs.StorageSize(); CHECK_EQ(SerializedSize.MemorySize, 0); CHECK_LE(SerializedSize.DiskSize, CacheSize.DiskSize); for (size_t Bucket = 0; Bucket < 4; ++Bucket) { Zcs.DropBucket(fmt::format("test_bucket-{}", Bucket)); } CHECK_EQ(0, Zcs.StorageSize().DiskSize); CHECK_EQ(0, Zcs.StorageSize().MemorySize); } } SUBCASE("disklayer") { const size_t Count = 16; ScopedTemporaryDirectory TempDir; GcStorageSize CacheSize; { GcManager Gc; ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CbObject CacheValue = CreateCacheValue(Zcs.GetConfig().DiskLayerConfig.BucketConfig.MemCacheSizeThreshold + 64); IoBuffer Buffer = CacheValue.GetBuffer().AsIoBuffer(); Buffer.SetContentType(ZenContentType::kCbObject); for (size_t Key = 0; Key < Count; ++Key) { const size_t Bucket = Key % 4; Zcs.Put(fmt::format("test_bucket-{}", Bucket), IoHash::HashBuffer(&Key, sizeof(uint32_t)), {.Value = Buffer}, {}); } CacheSize = Zcs.StorageSize(); CHECK_LE(CacheValue.GetSize() * Count, CacheSize.DiskSize); CHECK_EQ(0, CacheSize.MemorySize); } { GcManager Gc; ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); const GcStorageSize SerializedSize = Zcs.StorageSize(); CHECK_EQ(SerializedSize.MemorySize, 0); CHECK_LE(SerializedSize.DiskSize, CacheSize.DiskSize); for (size_t Bucket = 0; Bucket < 4; ++Bucket) { Zcs.DropBucket(fmt::format("test_bucket-{}", Bucket)); } CHECK_EQ(0, Zcs.StorageSize().DiskSize); } } } TEST_CASE("cachestore.threadedinsert") // * doctest::skip(true)) { // for (uint32_t i = 0; i < 100; ++i) { ScopedTemporaryDirectory TempDir; const uint64_t kChunkSize = 1048; const int32_t kChunkCount = 8192; struct Chunk { std::string Bucket; IoBuffer Buffer; }; std::unordered_map Chunks; Chunks.reserve(kChunkCount); const std::string Bucket1 = "rightinone"; const std::string Bucket2 = "rightintwo"; for (int32_t Idx = 0; Idx < kChunkCount; ++Idx) { while (true) { IoBuffer Chunk = CreateRandomBlob(kChunkSize); IoHash Hash = IoHash::HashBuffer(Chunk); if (Chunks.contains(Hash)) { continue; } Chunks[Hash] = {.Bucket = Bucket1, .Buffer = Chunk}; break; } while (true) { IoBuffer Chunk = CreateRandomBlob(kChunkSize); IoHash Hash = IoHash::HashBuffer(Chunk); if (Chunks.contains(Hash)) { continue; } Chunks[Hash] = {.Bucket = Bucket2, .Buffer = Chunk}; break; } } CreateDirectories(TempDir.Path()); WorkerThreadPool ThreadPool(4); GcManager Gc; auto JobQueue = MakeJobQueue(1, "testqueue"); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path(), {}); { std::atomic WorkCompleted = 0; for (const auto& Chunk : Chunks) { ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, &Chunk]() { Zcs.Put(Chunk.second.Bucket, Chunk.first, {.Value = Chunk.second.Buffer}, {}); WorkCompleted.fetch_add(1); }); } while (WorkCompleted < Chunks.size()) { Sleep(1); } } auto DoGC = [](GcManager& Gc, ZenCacheNamespace& Zcs, std::unordered_map& GcChunkHashes) { GcSettings Settings = {.CacheExpireTime = GcClock::Now() - std::chrono::hours(24), .ProjectStoreExpireTime = GcClock::Now() - std::chrono::hours(24), .CollectSmallObjects = true, .IsDeleteMode = true, .CompactBlockUsageThresholdPercent = 100}; Gc.CollectGarbage(Settings); // Cheating as we don't get the list of deleted hashes back from this call std::unordered_map RemainingChunkHashes; for (const auto& It : GcChunkHashes) { ZenCacheValue Tmp; if (Zcs.Get(It.second, It.first, Tmp)) { RemainingChunkHashes.insert(It); } } GcChunkHashes.swap(RemainingChunkHashes); }; const uint64_t TotalSize = Zcs.StorageSize().DiskSize; CHECK_LE(kChunkSize * Chunks.size(), TotalSize); { std::atomic WorkCompleted = 0; for (const auto& Chunk : Chunks) { ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, &Chunk]() { std::string Bucket = Chunk.second.Bucket; IoHash ChunkHash = Chunk.first; ZenCacheValue CacheValue; CHECK(Zcs.Get(Bucket, ChunkHash, CacheValue)); IoHash Hash = IoHash::HashBuffer(CacheValue.Value); CHECK(ChunkHash == Hash); WorkCompleted.fetch_add(1); }); } while (WorkCompleted < Chunks.size()) { Sleep(1); } } std::unordered_map GcChunkHashes; GcChunkHashes.reserve(Chunks.size()); for (const auto& Chunk : Chunks) { GcChunkHashes[Chunk.first] = Chunk.second.Bucket; } { std::unordered_map NewChunks; for (int32_t Idx = 0; Idx < kChunkCount; ++Idx) { { IoBuffer Chunk = CreateRandomBlob(kChunkSize); IoHash Hash = IoHash::HashBuffer(Chunk); NewChunks[Hash] = {.Bucket = Bucket1, .Buffer = Chunk}; } { IoBuffer Chunk = CreateRandomBlob(kChunkSize); IoHash Hash = IoHash::HashBuffer(Chunk); NewChunks[Hash] = {.Bucket = Bucket2, .Buffer = Chunk}; } } std::atomic WorkCompleted = 0; std::atomic_uint32_t AddedChunkCount = 0; for (const auto& Chunk : NewChunks) { ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, Chunk, &AddedChunkCount]() { Zcs.Put(Chunk.second.Bucket, Chunk.first, {.Value = Chunk.second.Buffer}, {}); AddedChunkCount.fetch_add(1); WorkCompleted.fetch_add(1); }); } for (const auto& Chunk : Chunks) { ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, Chunk]() { ZenCacheValue CacheValue; if (Zcs.Get(Chunk.second.Bucket, Chunk.first, CacheValue)) { CHECK(Chunk.first == IoHash::HashBuffer(CacheValue.Value)); } WorkCompleted.fetch_add(1); }); } while (AddedChunkCount.load() < NewChunks.size()) { // Need to be careful since we might GC blocks we don't know outside of RwLock::ExclusiveLockScope for (const auto& Chunk : NewChunks) { ZenCacheValue CacheValue; if (Zcs.Get(Chunk.second.Bucket, Chunk.first, CacheValue)) { GcChunkHashes[Chunk.first] = Chunk.second.Bucket; } } DoGC(Gc, Zcs, GcChunkHashes); } while (WorkCompleted < NewChunks.size() + Chunks.size()) { Sleep(1); } { // Need to be careful since we might GC blocks we don't know outside of RwLock::ExclusiveLockScope for (const auto& Chunk : NewChunks) { ZenCacheValue CacheValue; if (Zcs.Get(Chunk.second.Bucket, Chunk.first, CacheValue)) { GcChunkHashes[Chunk.first] = Chunk.second.Bucket; } } DoGC(Gc, Zcs, GcChunkHashes); } } { { std::atomic WorkCompleted = 0; for (const auto& Chunk : GcChunkHashes) { ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, Chunk]() { ZenCacheValue CacheValue; CHECK(Zcs.Get(Chunk.second, Chunk.first, CacheValue)); CHECK(Chunk.first == IoHash::HashBuffer(CacheValue.Value)); WorkCompleted.fetch_add(1); }); } while (WorkCompleted < GcChunkHashes.size()) { Sleep(1); } } } } } TEST_CASE("cachestore.namespaces") { using namespace testutils; const auto CreateCacheValue = [](size_t Size) -> CbObject { std::vector Buf; Buf.resize(Size); CbObjectWriter Writer; Writer.AddBinary("Binary"sv, Buf.data(), Buf.size()); return Writer.Save(); }; auto JobQueue = MakeJobQueue(1, "testqueue"); ScopedTemporaryDirectory TempDir; CreateDirectories(TempDir.Path()); const CacheRequestContext Context; IoHash Key1; IoHash Key2; { GcManager Gc; ZenCacheStore Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {.AllowAutomaticCreationOfNamespaces = false}, nullptr); const auto Bucket = "teardrinker"sv; const auto CustomNamespace = "mynamespace"sv; // Create a cache record Key1 = CreateKey(42); CbObject CacheValue = CreateCacheValue(4096); IoBuffer Buffer = CacheValue.GetBuffer().AsIoBuffer(); Buffer.SetContentType(ZenContentType::kCbObject); ZenCacheValue PutValue = {.Value = Buffer}; Zcs.Put(Context, ZenCacheStore::DefaultNamespace, Bucket, Key1, PutValue, {}); ZenCacheValue GetValue; CHECK(Zcs.Get(Context, ZenCacheStore::DefaultNamespace, Bucket, Key1, GetValue)); CHECK(!Zcs.Get(Context, CustomNamespace, Bucket, Key1, GetValue)); // This should just be dropped as we don't allow creating of namespaces on the fly Zcs.Put(Context, CustomNamespace, Bucket, Key1, PutValue, {}); CHECK(!Zcs.Get(Context, CustomNamespace, Bucket, Key1, GetValue)); } { GcManager Gc; ZenCacheStore Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {.AllowAutomaticCreationOfNamespaces = true}, nullptr); const auto Bucket = "teardrinker"sv; const auto CustomNamespace = "mynamespace"sv; Key2 = CreateKey(43); CbObject CacheValue2 = CreateCacheValue(4096); IoBuffer Buffer2 = CacheValue2.GetBuffer().AsIoBuffer(); Buffer2.SetContentType(ZenContentType::kCbObject); ZenCacheValue PutValue2 = {.Value = Buffer2}; Zcs.Put(Context, CustomNamespace, Bucket, Key2, PutValue2, {}); ZenCacheValue GetValue; CHECK(!Zcs.Get(Context, ZenCacheStore::DefaultNamespace, Bucket, Key2, GetValue)); CHECK(Zcs.Get(Context, ZenCacheStore::DefaultNamespace, Bucket, Key1, GetValue)); CHECK(!Zcs.Get(Context, CustomNamespace, Bucket, Key1, GetValue)); CHECK(Zcs.Get(Context, CustomNamespace, Bucket, Key2, GetValue)); } } TEST_CASE("cachestore.drop.bucket") { using namespace testutils; const auto CreateCacheValue = [](size_t Size) -> CbObject { std::vector Buf; Buf.resize(Size); CbObjectWriter Writer; Writer.AddBinary("Binary"sv, Buf.data(), Buf.size()); return Writer.Save(); }; auto JobQueue = MakeJobQueue(1, "testqueue"); ScopedTemporaryDirectory TempDir; CreateDirectories(TempDir.Path()); const CacheRequestContext Context; IoHash Key1; IoHash Key2; auto PutValue = [&CreateCacheValue, &Context](ZenCacheStore& Zcs, std::string_view Namespace, std::string_view Bucket, size_t KeyIndex, size_t Size) { // Create a cache record IoHash Key = CreateKey(KeyIndex); CbObject CacheValue = CreateCacheValue(Size); IoBuffer Buffer = CacheValue.GetBuffer().AsIoBuffer(); Buffer.SetContentType(ZenContentType::kCbObject); ZenCacheValue PutValue = {.Value = Buffer}; Zcs.Put(Context, Namespace, Bucket, Key, PutValue, {}); return Key; }; auto GetValue = [&Context](ZenCacheStore& Zcs, std::string_view Namespace, std::string_view Bucket, const IoHash& Key) { ZenCacheValue GetValue; Zcs.Get(Context, Namespace, Bucket, Key, GetValue); return GetValue; }; WorkerThreadPool Workers(1); { GcManager Gc; ZenCacheStore Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {.AllowAutomaticCreationOfNamespaces = true}, nullptr); const auto Bucket = "teardrinker"sv; const auto Namespace = "mynamespace"sv; Key1 = PutValue(Zcs, Namespace, Bucket, 42, 4096); Key2 = PutValue(Zcs, Namespace, Bucket, 43, 2048); ZenCacheValue Value1 = GetValue(Zcs, Namespace, Bucket, Key1); CHECK(Value1.Value); std::atomic_bool WorkComplete = false; Workers.ScheduleWork([&]() { zen::Sleep(100); Value1.Value = IoBuffer{}; WorkComplete = true; }); // On Windows, DropBucket() will be blocked as long as we hold a reference to a buffer in the bucket // Our DropBucket execution blocks any incoming request from completing until we are done with the drop CHECK(Zcs.DropBucket(Namespace, Bucket)); while (!WorkComplete) { zen::Sleep(1); } // Entire bucket should be dropped, but doing a request should will re-create the namespace but it must still be empty Value1 = GetValue(Zcs, Namespace, Bucket, Key1); CHECK(!Value1.Value); ZenCacheValue Value2 = GetValue(Zcs, Namespace, Bucket, Key2); CHECK(!Value2.Value); } } TEST_CASE("cachestore.drop.namespace") { using namespace testutils; const CacheRequestContext Context; const auto CreateCacheValue = [](size_t Size) -> CbObject { std::vector Buf; Buf.resize(Size); CbObjectWriter Writer; Writer.AddBinary("Binary"sv, Buf.data(), Buf.size()); return Writer.Save(); }; auto JobQueue = MakeJobQueue(1, "testqueue"); ScopedTemporaryDirectory TempDir; CreateDirectories(TempDir.Path()); auto PutValue = [&CreateCacheValue, &Context](ZenCacheStore& Zcs, std::string_view Namespace, std::string_view Bucket, size_t KeyIndex, size_t Size) { // Create a cache record IoHash Key = CreateKey(KeyIndex); CbObject CacheValue = CreateCacheValue(Size); IoBuffer Buffer = CacheValue.GetBuffer().AsIoBuffer(); Buffer.SetContentType(ZenContentType::kCbObject); ZenCacheValue PutValue = {.Value = Buffer}; Zcs.Put(Context, Namespace, Bucket, Key, PutValue, {}); return Key; }; auto GetValue = [&Context](ZenCacheStore& Zcs, std::string_view Namespace, std::string_view Bucket, const IoHash& Key) { ZenCacheValue GetValue; Zcs.Get(Context, Namespace, Bucket, Key, GetValue); return GetValue; }; WorkerThreadPool Workers(1); { GcManager Gc; ZenCacheStore Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {.AllowAutomaticCreationOfNamespaces = true}, nullptr); const auto Bucket1 = "teardrinker1"sv; const auto Bucket2 = "teardrinker2"sv; const auto Namespace1 = "mynamespace1"sv; const auto Namespace2 = "mynamespace2"sv; IoHash Key1 = PutValue(Zcs, Namespace1, Bucket1, 42, 4096); IoHash Key2 = PutValue(Zcs, Namespace1, Bucket2, 43, 2048); IoHash Key3 = PutValue(Zcs, Namespace2, Bucket1, 44, 4096); IoHash Key4 = PutValue(Zcs, Namespace2, Bucket2, 45, 2048); ZenCacheValue Value1 = GetValue(Zcs, Namespace1, Bucket1, Key1); CHECK(Value1.Value); ZenCacheValue Value2 = GetValue(Zcs, Namespace1, Bucket2, Key2); CHECK(Value2.Value); ZenCacheValue Value3 = GetValue(Zcs, Namespace2, Bucket1, Key3); CHECK(Value3.Value); ZenCacheValue Value4 = GetValue(Zcs, Namespace2, Bucket2, Key4); CHECK(Value4.Value); std::atomic_bool WorkComplete = false; Workers.ScheduleWork([&]() { zen::Sleep(100); Value1.Value = IoBuffer{}; Value2.Value = IoBuffer{}; Value3.Value = IoBuffer{}; Value4.Value = IoBuffer{}; WorkComplete = true; }); // On Windows, DropBucket() will be blocked as long as we hold a reference to a buffer in the bucket // Our DropBucket execution blocks any incoming request from completing until we are done with the drop CHECK(Zcs.DropNamespace(Namespace1)); while (!WorkComplete) { zen::Sleep(1); } // Entire namespace should be dropped, but doing a request should will re-create the namespace but it must still be empty Value1 = GetValue(Zcs, Namespace1, Bucket1, Key1); CHECK(!Value1.Value); Value2 = GetValue(Zcs, Namespace1, Bucket2, Key2); CHECK(!Value2.Value); Value3 = GetValue(Zcs, Namespace2, Bucket1, Key3); CHECK(Value3.Value); Value4 = GetValue(Zcs, Namespace2, Bucket2, Key4); CHECK(Value4.Value); } } TEST_CASE("cachestore.blocked.disklayer.put") { ScopedTemporaryDirectory TempDir; GcStorageSize CacheSize; const auto CreateCacheValue = [](size_t Size) -> CbObject { std::vector Buf; Buf.resize(Size, Size & 0xff); CbObjectWriter Writer; Writer.AddBinary("Binary"sv, Buf.data(), Buf.size()); return Writer.Save(); }; GcManager Gc; auto JobQueue = MakeJobQueue(1, "testqueue"); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CbObject CacheValue = CreateCacheValue(64 * 1024 + 64); IoBuffer Buffer = CacheValue.GetBuffer().AsIoBuffer(); Buffer.SetContentType(ZenContentType::kCbObject); size_t Key = Buffer.Size(); IoHash HashKey = IoHash::HashBuffer(&Key, sizeof(uint32_t)); Zcs.Put("test_bucket", HashKey, {.Value = Buffer}, {}); ZenCacheValue BufferGet; CHECK(Zcs.Get("test_bucket", HashKey, BufferGet)); CbObject CacheValue2 = CreateCacheValue(64 * 1024 + 64 + 1); IoBuffer Buffer2 = CacheValue2.GetBuffer().AsIoBuffer(); Buffer2.SetContentType(ZenContentType::kCbObject); // We should be able to overwrite even if the file is open for read Zcs.Put("test_bucket", HashKey, {.Value = Buffer2}, {}); MemoryView OldView = BufferGet.Value.GetView(); ZenCacheValue BufferGet2; CHECK(Zcs.Get("test_bucket", HashKey, BufferGet2)); MemoryView NewView = BufferGet2.Value.GetView(); // Make sure file openend for read before we wrote it still have old data CHECK(OldView.GetSize() == Buffer.GetSize()); CHECK(memcmp(OldView.GetData(), Buffer.GetData(), OldView.GetSize()) == 0); // Make sure we get the new data when reading after we write new data CHECK(NewView.GetSize() == Buffer2.GetSize()); CHECK(memcmp(NewView.GetData(), Buffer2.GetData(), NewView.GetSize()) == 0); } TEST_CASE("cachestore.scrub") { ScopedTemporaryDirectory TempDir; using namespace testutils; struct CacheRecord { IoBuffer Record; std::vector Attachments; }; auto CreateCacheRecord = [](bool Structured, std::string_view Bucket, const IoHash& Key, const std::vector& AttachmentSizes) { CacheRecord Result; if (Structured) { Result.Attachments.resize(AttachmentSizes.size()); CbObjectWriter Record; Record.BeginObject("Key"sv); { Record << "Bucket"sv << Bucket; Record << "Hash"sv << Key; } Record.EndObject(); for (size_t Index = 0; Index < AttachmentSizes.size(); Index++) { IoBuffer AttachmentData = CreateRandomBlob(AttachmentSizes[Index]); CompressedBuffer CompressedAttachmentData = CompressedBuffer::Compress(SharedBuffer(AttachmentData)); Record.AddBinaryAttachment(fmt::format("attachment-{}", Index), CompressedAttachmentData.DecodeRawHash()); Result.Attachments[Index] = CompressedAttachmentData; } Result.Record = Record.Save().GetBuffer().AsIoBuffer(); Result.Record.SetContentType(ZenContentType::kCbObject); } else { std::string RecordData = fmt::format("{}:{}", Bucket, Key.ToHexString()); size_t TotalSize = RecordData.length() + 1; for (size_t AttachmentSize : AttachmentSizes) { TotalSize += AttachmentSize; } Result.Record = IoBuffer(TotalSize); char* DataPtr = (char*)Result.Record.MutableData(); memcpy(DataPtr, RecordData.c_str(), RecordData.length() + 1); DataPtr += RecordData.length() + 1; for (size_t AttachmentSize : AttachmentSizes) { IoBuffer AttachmentData = CreateRandomBlob(AttachmentSize); memcpy(DataPtr, AttachmentData.GetData(), AttachmentData.GetSize()); DataPtr += AttachmentData.GetSize(); } } return Result; }; GcManager Gc; CidStore CidStore(Gc); auto JobQueue = MakeJobQueue(1, "testqueue"); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; CidStore.Initialize(CidConfig); auto CreateRecords = [&](bool IsStructured, std::string_view BucketName, const std::vector& Cids, const std::vector& AttachmentSizes) { for (const IoHash& Cid : Cids) { CacheRecord Record = CreateCacheRecord(IsStructured, BucketName, Cid, AttachmentSizes); std::vector AttachmentHashes; for (const CompressedBuffer& Attachment : Record.Attachments) { AttachmentHashes.push_back(Attachment.DecodeRawHash()); CidStore.AddChunk(Attachment.GetCompressed().Flatten().AsIoBuffer(), AttachmentHashes.back()); } Zcs.Put("mybucket", Cid, {.Value = Record.Record}, AttachmentHashes); } }; std::vector AttachmentSizes = {16, 1000, 2000, 4000, 8000, 64000, 80000}; std::vector UnstructuredCids{CreateKey(4), CreateKey(5), CreateKey(6)}; CreateRecords(false, "mybucket"sv, UnstructuredCids, AttachmentSizes); std::vector StructuredCids{CreateKey(1), CreateKey(2), CreateKey(3)}; CreateRecords(true, "mybucket"sv, StructuredCids, AttachmentSizes); WorkerThreadPool ThreadPool{1}; ScrubContext ScrubCtx{ThreadPool}; Zcs.ScrubStorage(ScrubCtx); CidStore.ScrubStorage(ScrubCtx); CHECK(ScrubCtx.ScrubbedChunks() == (StructuredCids.size() + StructuredCids.size() * AttachmentSizes.size()) + UnstructuredCids.size()); CHECK(ScrubCtx.BadCids().GetSize() == 0); } TEST_CASE("cachestore.newgc.basics") { using namespace testutils; ScopedTemporaryDirectory TempDir; auto JobQueue = MakeJobQueue(1, "testqueue"); struct CacheEntry { IoBuffer Data; std::vector> Attachments; }; std::unordered_map CacheEntries; auto CreateCacheRecord = [&](ZenCacheNamespace& Zcs, CidStore& CidStore, std::string_view Bucket, std::span> Attachments) { std::vector AttachmentKeys; for (const auto& Attachment : Attachments) { AttachmentKeys.push_back(Attachment.second.DecodeRawHash()); } auto Record = CreateRecord(Attachments); Zcs.Put(Bucket, Record.first, {.Value = Record.second, .RawSize = Record.second.GetSize(), .RawHash = IoHash::HashBuffer(Record.second.GetData(), Record.second.GetSize())}, AttachmentKeys); for (const auto& Attachment : Attachments) { CidStore.AddChunk(Attachment.second.GetCompressed().Flatten().AsIoBuffer(), Attachment.second.DecodeRawHash()); } CacheEntries.insert({Record.first, CacheEntry{.Data = Record.second, .Attachments = {Attachments.begin(), Attachments.end()}}}); return Record.first; }; auto CreateCacheValue = [&](ZenCacheNamespace& Zcs, std::string_view Bucket, size_t Size) { std::pair CacheValue = CreateBinaryBlob(Size); IoHash Key = ToIoHash(CacheValue.first); Zcs.Put(Bucket, Key, {.Value = CacheValue.second, .RawSize = CacheValue.second.GetSize(), .RawHash = IoHash::HashBuffer(CacheValue.second.GetData(), CacheValue.second.GetSize())}, {}); CacheEntries.insert({Key, CacheEntry{CacheValue.second, {}}}); return Key; }; auto ValidateCacheEntry = [&](ZenCacheNamespace& Zcs, CidStore& CidStore, std::string_view Bucket, const IoHash& Key, bool ExpectCacheEntry, bool ExpectAttachments) { const CacheEntry& Entry = CacheEntries[Key]; ZenCacheValue Value; bool CacheExists = Zcs.Get(Bucket, Key, Value); if (ExpectCacheEntry) { if (!CacheExists) { return false; } if (Value.Value.GetSize() != Entry.Data.GetSize()) { return false; } if (!Value.Value.GetView().EqualBytes(Entry.Data.GetView())) { return false; } } else if (CacheExists) { return false; } if (ExpectAttachments) { for (const auto& Attachment : Entry.Attachments) { IoHash AttachmentHash = Attachment.second.DecodeRawHash(); IoBuffer StoredData = CidStore.FindChunkByCid(AttachmentHash); if (!StoredData) { return false; } if (!StoredData.GetView().EqualBytes(Attachment.second.GetCompressed().Flatten().GetView())) { return false; } } } else { for (const auto& Attachment : Entry.Attachments) { IoHash AttachmentHash = Attachment.second.DecodeRawHash(); if (CidStore.ContainsChunk(AttachmentHash)) { return false; } } } return true; }; std::vector CacheRecords; std::vector UnstructuredCacheValues; const auto TearDrinkerBucket = "teardrinker"sv; { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); // Create some basic data { // Structured record with attachments auto Attachments1 = CreateCompressedAttachment(CidStore, std::vector{77, 1024 * 1024 * 2, 99, 1024 * 1024 * 2 + 87}); CacheRecords.emplace_back(CreateCacheRecord(Zcs, CidStore, TearDrinkerBucket, Attachments1)); // Structured record with reuse of attachments auto Attachments2 = CreateCompressedAttachment(CidStore, std::vector{971}); Attachments2.push_back(Attachments1[0]); Attachments2.push_back(Attachments1[1]); CacheRecords.emplace_back(CreateCacheRecord(Zcs, CidStore, TearDrinkerBucket, Attachments2)); } CacheRecords.emplace_back(CreateCacheRecord(Zcs, CidStore, TearDrinkerBucket, {})); { // Unstructured cache values UnstructuredCacheValues.push_back(CreateCacheValue(Zcs, TearDrinkerBucket, 84)); UnstructuredCacheValues.push_back(CreateCacheValue(Zcs, TearDrinkerBucket, 591)); UnstructuredCacheValues.push_back(CreateCacheValue(Zcs, TearDrinkerBucket, 1024 * 1024 * 3 + 7)); UnstructuredCacheValues.push_back(CreateCacheValue(Zcs, TearDrinkerBucket, 71)); } } SUBCASE("expire nothing") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() - std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() - std::chrono::hours(1), .CollectSmallObjects = false, .IsDeleteMode = false, .Verbose = true}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_EQ(0u, Result.CompactStoresStatSum.RemovedDisk); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], true, true)); } SUBCASE("expire all large objects, delete nothing") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = false, .IsDeleteMode = false, .Verbose = true}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_EQ(0u, Result.CompactStoresStatSum.RemovedDisk); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], true, true)); } SUBCASE("expire all object, delete nothing") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = true, .IsDeleteMode = false, .Verbose = true}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_EQ(0u, Result.CompactStoresStatSum.RemovedDisk); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], true, true)); } SUBCASE("expire all large objects, skip cid") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = false, .IsDeleteMode = true, .SkipCidDelete = true, .Verbose = true}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_EQ(CacheEntries[UnstructuredCacheValues[2]].Data.GetSize(), Result.CompactStoresStatSum.RemovedDisk); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], true, true)); } SUBCASE("expire all objects, skip cid") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = true, .IsDeleteMode = true, .SkipCidDelete = true, .Verbose = true}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_GE(Result.CompactStoresStatSum.RemovedDisk, 0); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], false, true)); } SUBCASE("expire all large objects") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = false, .IsDeleteMode = true, .SkipCidDelete = false, .Verbose = true}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); // Only one cache value is pruned/deleted as that is the only // large item in the cache (all other large items as in cas) CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats .FoundCount); // We won't remove any references since all referencers are small which retains all references CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats .DeletedCount); // We won't remove any references since all referencers are small which retains all references CHECK_EQ(CacheEntries[UnstructuredCacheValues[2]].Data.GetSize(), Result.CompactStoresStatSum.RemovedDisk); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], true, true)); } SUBCASE("expire all objects") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = true, .IsDeleteMode = true, .SkipCidDelete = false, .Verbose = true}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_GT(Result.CompactStoresStatSum.RemovedDisk, 0); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], false, false)); } SUBCASE("keep 1 cache record, skip cid") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); Zcs.SetAccessTime(TearDrinkerBucket, CacheRecords[0], GcClock::Now() + std::chrono::hours(2)); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = true, .IsDeleteMode = true, .SkipCidDelete = true, .Verbose = true, .CompactBlockUsageThresholdPercent = 100}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(6u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(6u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); uint64_t MinExpectedRemoveSize = CacheEntries[UnstructuredCacheValues[2]].Data.GetSize(); CHECK_LT(MinExpectedRemoveSize, Result.CompactStoresStatSum.RemovedDisk); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], false, false)); } SUBCASE("keep 2 cache records") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); Zcs.SetAccessTime(TearDrinkerBucket, CacheRecords[0], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, CacheRecords[1], GcClock::Now() + std::chrono::hours(2)); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = true, .IsDeleteMode = true, .SkipCidDelete = false, .Verbose = true}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(5u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(5u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_GT(Result.CompactStoresStatSum.RemovedDisk, 0); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], false, false)); } SUBCASE("keep 3 cache value") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[1], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[2], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[3], GcClock::Now() + std::chrono::hours(2)); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = true, .IsDeleteMode = true, .SkipCidDelete = false, .Verbose = true}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(4u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(4u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(5u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_GT(Result.CompactStoresStatSum.RemovedDisk, 0); CHECK_EQ(0u, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], true, true)); } SUBCASE("keep 3 cache value, skip cid") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); // Prime so we can check GC of memory layer ZenCacheValue Dummy; Zcs.Get(TearDrinkerBucket, CacheRecords[0], Dummy); Zcs.Get(TearDrinkerBucket, CacheRecords[1], Dummy); Zcs.Get(TearDrinkerBucket, CacheRecords[2], Dummy); Zcs.Get(TearDrinkerBucket, UnstructuredCacheValues[0], Dummy); Zcs.Get(TearDrinkerBucket, UnstructuredCacheValues[1], Dummy); Zcs.Get(TearDrinkerBucket, UnstructuredCacheValues[2], Dummy); Zcs.Get(TearDrinkerBucket, UnstructuredCacheValues[3], Dummy); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[1], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[2], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[3], GcClock::Now() + std::chrono::hours(2)); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = true, .IsDeleteMode = true, .SkipCidDelete = true, .Verbose = true, .CompactBlockUsageThresholdPercent = 100}); CHECK_EQ(7u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(4u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(4u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(0u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_GT(Result.CompactStoresStatSum.RemovedDisk, 0); uint64_t MemoryClean = CacheEntries[CacheRecords[0]].Data.GetSize() + CacheEntries[CacheRecords[1]].Data.GetSize() + CacheEntries[CacheRecords[2]].Data.GetSize() + CacheEntries[UnstructuredCacheValues[0]].Data.GetSize(); CHECK_EQ(MemoryClean, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[0], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[1], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, CacheRecords[2], false, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[0], false, false)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[1], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[2], true, true)); CHECK(ValidateCacheEntry(Zcs, CidStore, TearDrinkerBucket, UnstructuredCacheValues[3], true, true)); } SUBCASE("leave write block") { GcManager Gc; CidStore CidStore(Gc); CidStore.Initialize({.RootDirectory = TempDir.Path() / "cas"}); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path() / "cache", {}); CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); auto Attachments = CreateCompressedAttachment(CidStore, std::vector{177, 1024 * 1024 * 2 + 31, 8999, 1024 * 1024 * 2 + 187}); IoHash CacheRecord = CreateCacheRecord(Zcs, CidStore, TearDrinkerBucket, Attachments); { // Do get so it ends up in memcache ZenCacheValue _; Zcs.Get(TearDrinkerBucket, CacheRecord, _); } Zcs.SetAccessTime(TearDrinkerBucket, CacheRecord, GcClock::Now() - std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, CacheRecords[0], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, CacheRecords[1], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, CacheRecords[2], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[0], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[1], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[2], GcClock::Now() + std::chrono::hours(2)); Zcs.SetAccessTime(TearDrinkerBucket, UnstructuredCacheValues[3], GcClock::Now() + std::chrono::hours(2)); GcResult Result = Gc.CollectGarbage(GcSettings{.CacheExpireTime = GcClock::Now() + std::chrono::hours(1), .ProjectStoreExpireTime = GcClock::Now() + std::chrono::hours(1), .CollectSmallObjects = true, .IsDeleteMode = true, .SkipCidDelete = false, .Verbose = true}); // Write block can't be compacted so Compacted will be less than Deleted CHECK_EQ(8u, Result.ReferencerStatSum.RemoveExpiredDataStats.CheckedCount); CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.FoundCount); CHECK_EQ(1u, Result.ReferencerStatSum.RemoveExpiredDataStats.DeletedCount); CHECK_EQ(9u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.CheckedCount); CHECK_EQ(4u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.FoundCount); CHECK_EQ(4u, Result.ReferenceStoreStatSum.RemoveUnreferencedDataStats.DeletedCount); CHECK_EQ(Attachments[1].second.GetCompressed().GetSize() + Attachments[3].second.GetCompressed().GetSize(), Result.CompactStoresStatSum.RemovedDisk); uint64_t MemoryClean = CacheEntries[CacheRecord].Data.GetSize(); CHECK_EQ(MemoryClean, Result.ReferencerStatSum.RemoveExpiredDataStats.FreedMemory); } } #endif void structured_cachestore_forcelink() { } } // namespace zen