diff options
| author | Dan Engelbrecht <[email protected]> | 2025-10-03 15:57:42 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2025-10-03 15:57:42 +0200 |
| commit | 42a2c2582b10a598ce5ef50f7feb4bab394b8fc1 (patch) | |
| tree | 267816281dcdbeda9900a38e6863265ecc257f15 /src/zenstore | |
| parent | 5.7.5-pre0 (diff) | |
| download | zen-42a2c2582b10a598ce5ef50f7feb4bab394b8fc1.tar.xz zen-42a2c2582b10a598ce5ef50f7feb4bab394b8fc1.zip | |
cacherequests helpers test only (#551)
* don't use cacherequests utils in cache_cmd.cpp
* make zenutil/cacherequests code into test code helpers only
Diffstat (limited to 'src/zenstore')
| -rw-r--r-- | src/zenstore/cache/cache.cpp | 235 | ||||
| -rw-r--r-- | src/zenstore/cache/cachekey.cpp | 9 | ||||
| -rw-r--r-- | src/zenstore/cache/cachepolicy.cpp | 426 | ||||
| -rw-r--r-- | src/zenstore/cache/cacherpc.cpp | 5 | ||||
| -rw-r--r-- | src/zenstore/cache/structuredcachestore.cpp | 3 | ||||
| -rw-r--r-- | src/zenstore/include/zenstore/cache/cache.h | 52 | ||||
| -rw-r--r-- | src/zenstore/include/zenstore/cache/cachekey.h | 92 | ||||
| -rw-r--r-- | src/zenstore/include/zenstore/cache/cachepolicy.h | 229 | ||||
| -rw-r--r-- | src/zenstore/include/zenstore/cache/cacherpc.h | 2 | ||||
| -rw-r--r-- | src/zenstore/include/zenstore/cache/structuredcachestore.h | 2 | ||||
| -rw-r--r-- | src/zenstore/include/zenstore/cache/upstreamcacheclient.h | 2 | ||||
| -rw-r--r-- | src/zenstore/zenstore.cpp | 2 |
12 files changed, 1051 insertions, 8 deletions
diff --git a/src/zenstore/cache/cache.cpp b/src/zenstore/cache/cache.cpp new file mode 100644 index 000000000..0436bfd7b --- /dev/null +++ b/src/zenstore/cache/cache.cpp @@ -0,0 +1,235 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenstore/cache/cache.h> + +#include <zencore/logging.h> + +namespace zen { + +namespace { + constinit AsciiSet ValidNamespaceNameCharactersSet{"abcdefghijklmnopqrstuvwxyz0123456789-_.ABCDEFGHIJKLMNOPQRSTUVWXYZ"}; + constinit AsciiSet ValidBucketNameCharactersSet{"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"}; +} // namespace + +std::optional<std::string> +GetValidNamespaceName(std::string_view Name) +{ + if (Name.empty()) + { + ZEN_WARN("Namespace is invalid, empty namespace is not allowed"); + return {}; + } + + if (Name.length() > 64) + { + ZEN_WARN("Namespace '{}' is invalid, length exceeds 64 characters", Name); + return {}; + } + + if (!AsciiSet::HasOnly(Name, ValidNamespaceNameCharactersSet)) + { + ZEN_WARN("Namespace '{}' is invalid, invalid characters detected", Name); + return {}; + } + + return ToLower(Name); +} + +std::optional<std::string> +GetValidBucketName(std::string_view Name) +{ + if (Name.empty()) + { + ZEN_WARN("Bucket name is invalid, empty bucket name is not allowed"); + return {}; + } + + if (!AsciiSet::HasOnly(Name, ValidBucketNameCharactersSet)) + { + ZEN_WARN("Bucket name '{}' is invalid, invalid characters detected", Name); + return {}; + } + + return ToLower(Name); +} + +std::optional<IoHash> +GetValidIoHash(std::string_view Hash) +{ + if (Hash.length() != IoHash::StringLength) + { + return {}; + } + + IoHash KeyHash; + if (!ParseHexBytes(Hash.data(), Hash.size(), KeyHash.Hash)) + { + return {}; + } + return KeyHash; +} + +bool +HttpCacheRequestParseRelativeUri(std::string_view Key, std::string_view DefaultNamespace, HttpCacheRequestData& Data) +{ + std::vector<std::string_view> Tokens; + uint32_t TokenCount = ForEachStrTok(Key, '/', [&](const std::string_view& Token) { + Tokens.push_back(Token); + return true; + }); + + switch (TokenCount) + { + case 0: + return true; + case 1: + Data.Namespace = GetValidNamespaceName(Tokens[0]); + return Data.Namespace.has_value(); + case 2: + { + std::optional<IoHash> PossibleHashKey = GetValidIoHash(Tokens[1]); + if (PossibleHashKey.has_value()) + { + // Legacy bucket/key request + Data.Bucket = GetValidBucketName(Tokens[0]); + if (!Data.Bucket.has_value()) + { + return false; + } + Data.HashKey = PossibleHashKey; + Data.Namespace = DefaultNamespace; + return true; + } + Data.Namespace = GetValidNamespaceName(Tokens[0]); + if (!Data.Namespace.has_value()) + { + return false; + } + Data.Bucket = GetValidBucketName(Tokens[1]); + if (!Data.Bucket.has_value()) + { + return false; + } + return true; + } + case 3: + { + std::optional<IoHash> PossibleHashKey = GetValidIoHash(Tokens[1]); + if (PossibleHashKey.has_value()) + { + // Legacy bucket/key/valueid request + Data.Bucket = GetValidBucketName(Tokens[0]); + if (!Data.Bucket.has_value()) + { + return false; + } + Data.HashKey = PossibleHashKey; + Data.ValueContentId = GetValidIoHash(Tokens[2]); + if (!Data.ValueContentId.has_value()) + { + return false; + } + Data.Namespace = DefaultNamespace; + return true; + } + Data.Namespace = GetValidNamespaceName(Tokens[0]); + if (!Data.Namespace.has_value()) + { + return false; + } + Data.Bucket = GetValidBucketName(Tokens[1]); + if (!Data.Bucket.has_value()) + { + return false; + } + Data.HashKey = GetValidIoHash(Tokens[2]); + if (!Data.HashKey) + { + return false; + } + return true; + } + case 4: + { + Data.Namespace = GetValidNamespaceName(Tokens[0]); + if (!Data.Namespace.has_value()) + { + return false; + } + + Data.Bucket = GetValidBucketName(Tokens[1]); + if (!Data.Bucket.has_value()) + { + return false; + } + + Data.HashKey = GetValidIoHash(Tokens[2]); + if (!Data.HashKey.has_value()) + { + return false; + } + + Data.ValueContentId = GetValidIoHash(Tokens[3]); + if (!Data.ValueContentId.has_value()) + { + return false; + } + return true; + } + default: + return false; + } +} + +std::optional<std::string> +GetCacheRequestNamespace(const CbObjectView& Params) +{ + CbFieldView NamespaceField = Params["Namespace"]; + if (!NamespaceField) + { + return std::string("!default!"); // ZenCacheStore::DefaultNamespace); + } + + if (NamespaceField.HasError()) + { + return {}; + } + if (!NamespaceField.IsString()) + { + return {}; + } + return GetValidNamespaceName(NamespaceField.AsString()); +} + +bool +GetCacheRequestCacheKey(const CbObjectView& KeyView, CacheKey& Key) +{ + CbFieldView BucketField = KeyView["Bucket"]; + if (BucketField.HasError()) + { + return false; + } + if (!BucketField.IsString()) + { + return false; + } + std::optional<std::string> Bucket = GetValidBucketName(BucketField.AsString()); + if (!Bucket.has_value()) + { + return false; + } + CbFieldView HashField = KeyView["Hash"]; + if (HashField.HasError()) + { + return false; + } + if (!HashField.IsHash()) + { + return false; + } + Key.Bucket = *Bucket; + Key.Hash = HashField.AsHash(); + return true; +} + +} // namespace zen diff --git a/src/zenstore/cache/cachekey.cpp b/src/zenstore/cache/cachekey.cpp new file mode 100644 index 000000000..e5a5d0334 --- /dev/null +++ b/src/zenstore/cache/cachekey.cpp @@ -0,0 +1,9 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenstore/cache/cachekey.h> + +namespace zen { + +const CacheKey CacheKey::Empty = CacheKey{.Bucket = std::string(), .Hash = IoHash()}; + +} // namespace zen diff --git a/src/zenstore/cache/cachepolicy.cpp b/src/zenstore/cache/cachepolicy.cpp new file mode 100644 index 000000000..ca8a95ca1 --- /dev/null +++ b/src/zenstore/cache/cachepolicy.cpp @@ -0,0 +1,426 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenstore/cache/cachepolicy.h> + +#include <zencore/compactbinary.h> +#include <zencore/compactbinarybuilder.h> +#include <zencore/enumflags.h> +#include <zencore/string.h> + +#include <algorithm> +#include <unordered_map> + +#if ZEN_WITH_TESTS +# include <zencore/testing.h> +#endif + +namespace zen::Private { +class CacheRecordPolicyShared; +} + +namespace zen { + +using namespace std::literals; + +namespace DerivedData::Private { + + constinit char CachePolicyDelimiter = ','; + + struct CachePolicyToTextData + { + CachePolicy Policy; + std::string_view Text; + }; + + constinit CachePolicyToTextData CachePolicyToText[]{ + // Flags with multiple bits are ordered by bit count to minimize token count in the text format. + {CachePolicy::Default, "Default"sv}, + {CachePolicy::Remote, "Remote"sv}, + {CachePolicy::Local, "Local"sv}, + {CachePolicy::Store, "Store"sv}, + {CachePolicy::Query, "Query"sv}, + // Flags with only one bit can be in any order. Match the order in CachePolicy. + {CachePolicy::QueryLocal, "QueryLocal"sv}, + {CachePolicy::QueryRemote, "QueryRemote"sv}, + {CachePolicy::StoreLocal, "StoreLocal"sv}, + {CachePolicy::StoreRemote, "StoreRemote"sv}, + {CachePolicy::SkipMeta, "SkipMeta"sv}, + {CachePolicy::SkipData, "SkipData"sv}, + {CachePolicy::PartialRecord, "PartialRecord"sv}, + {CachePolicy::KeepAlive, "KeepAlive"sv}, + // None must be last because it matches every policy. + {CachePolicy::None, "None"sv}, + }; + + constinit CachePolicy CachePolicyKnownFlags = + CachePolicy::Default | CachePolicy::SkipMeta | CachePolicy::SkipData | CachePolicy::PartialRecord | CachePolicy::KeepAlive; + + StringBuilderBase& CachePolicyToString(StringBuilderBase& Builder, CachePolicy Policy) + { + // Mask out unknown flags. None will be written if no flags are known. + Policy &= CachePolicyKnownFlags; + for (const CachePolicyToTextData& Pair : CachePolicyToText) + { + if (EnumHasAllFlags(Policy, Pair.Policy)) + { + EnumRemoveFlags(Policy, Pair.Policy); + Builder << Pair.Text << CachePolicyDelimiter; + if (Policy == CachePolicy::None) + { + break; + } + } + } + Builder.RemoveSuffix(1); + return Builder; + } + + CachePolicy ParseCachePolicy(const std::string_view Text) + { + ZEN_ASSERT(!Text.empty()); // ParseCachePolicy requires a non-empty string + CachePolicy Policy = CachePolicy::None; + ForEachStrTok(Text, CachePolicyDelimiter, [&Policy, Index = int32_t(0)](const std::string_view& Token) mutable { + const int32_t EndIndex = Index; + for (; size_t(Index) < sizeof(CachePolicyToText) / sizeof(CachePolicyToText[0]); ++Index) + { + if (CachePolicyToText[Index].Text == Token) + { + Policy |= CachePolicyToText[Index].Policy; + ++Index; + return true; + } + } + for (Index = 0; Index < EndIndex; ++Index) + { + if (CachePolicyToText[Index].Text == Token) + { + Policy |= CachePolicyToText[Index].Policy; + ++Index; + return true; + } + } + return true; + }); + return Policy; + } + +} // namespace DerivedData::Private + +StringBuilderBase& +operator<<(StringBuilderBase& Builder, CachePolicy Policy) +{ + return DerivedData::Private::CachePolicyToString(Builder, Policy); +} + +CachePolicy +ParseCachePolicy(std::string_view Text) +{ + return DerivedData::Private::ParseCachePolicy(Text); +} + +CachePolicy +ConvertToUpstream(CachePolicy Policy) +{ + // Set Local flags equal to downstream's Remote flags. + // Delete Skip flags if StoreLocal is true, otherwise use the downstream value. + // Use the downstream value for all other flags. + + CachePolicy UpstreamPolicy = CachePolicy::None; + + if (EnumHasAllFlags(Policy, CachePolicy::QueryRemote)) + { + UpstreamPolicy |= CachePolicy::QueryLocal; + } + + if (EnumHasAllFlags(Policy, CachePolicy::StoreRemote)) + { + UpstreamPolicy |= CachePolicy::StoreLocal; + } + + if (!EnumHasAllFlags(Policy, CachePolicy::StoreLocal)) + { + UpstreamPolicy |= (Policy & (CachePolicy::SkipData | CachePolicy::SkipMeta)); + } + + UpstreamPolicy |= Policy & ~(CachePolicy::Local | CachePolicy::SkipData | CachePolicy::SkipMeta); + + return UpstreamPolicy; +} + +class Private::CacheRecordPolicyShared final : public Private::ICacheRecordPolicyShared +{ +public: + inline void AddValuePolicy(const CacheValuePolicy& Value) final + { + ZEN_ASSERT(Value.Id); // Failed to add value policy because the ID is null. + const auto Insert = + std::lower_bound(Values.begin(), Values.end(), Value, [](const CacheValuePolicy& Existing, const CacheValuePolicy& New) { + return Existing.Id < New.Id; + }); + ZEN_ASSERT( + !(Insert < Values.end() && + Insert->Id == Value.Id)); // Failed to add value policy with ID %s because it has an existing value policy with that ID. ") + Values.insert(Insert, Value); + } + + inline std::span<const CacheValuePolicy> GetValuePolicies() const final { return Values; } + +private: + std::vector<CacheValuePolicy> Values; +}; + +CachePolicy +CacheRecordPolicy::GetValuePolicy(const Oid& Id) const +{ + if (Shared) + { + const std::span<const CacheValuePolicy> Values = Shared->GetValuePolicies(); + const auto Iter = + std::lower_bound(Values.begin(), Values.end(), Id, [](const CacheValuePolicy& A, const Oid& B) { return A.Id < B; }); + if (Iter != Values.end() && Iter->Id == Id) + { + return Iter->Policy; + } + } + return DefaultValuePolicy; +} + +void +CacheRecordPolicy::Save(CbWriter& Writer) const +{ + Writer.BeginObject(); + // The RecordPolicy is calculated from the ValuePolicies and does not need to be saved separately. + Writer.AddString("BasePolicy"sv, WriteToString<128>(GetBasePolicy())); + if (!IsUniform()) + { + Writer.BeginArray("ValuePolicies"sv); + for (const CacheValuePolicy& Value : GetValuePolicies()) + { + Writer.BeginObject(); + Writer.AddObjectId("Id"sv, Value.Id); + Writer.AddString("Policy"sv, WriteToString<128>(Value.Policy)); + Writer.EndObject(); + } + Writer.EndArray(); + } + Writer.EndObject(); +} + +OptionalCacheRecordPolicy +CacheRecordPolicy::Load(const CbObjectView Object) +{ + std::string_view BasePolicyText = Object["BasePolicy"sv].AsString(); + if (BasePolicyText.empty()) + { + return {}; + } + + CacheRecordPolicyBuilder Builder(ParseCachePolicy(BasePolicyText)); + for (CbFieldView ValueField : Object["ValuePolicies"sv]) + { + const CbObjectView Value = ValueField.AsObjectView(); + const Oid Id = Value["Id"sv].AsObjectId(); + const std::string_view PolicyText = Value["Policy"sv].AsString(); + if (!Id || PolicyText.empty()) + { + return {}; + } + CachePolicy Policy = ParseCachePolicy(PolicyText); + if (EnumHasAnyFlags(Policy, ~CacheValuePolicy::PolicyMask)) + { + return {}; + } + Builder.AddValuePolicy(Id, Policy); + } + + return Builder.Build(); +} + +CacheRecordPolicy +CacheRecordPolicy::ConvertToUpstream() const +{ + CacheRecordPolicyBuilder Builder(zen::ConvertToUpstream(GetBasePolicy())); + for (const CacheValuePolicy& ValuePolicy : GetValuePolicies()) + { + Builder.AddValuePolicy(ValuePolicy.Id, zen::ConvertToUpstream(ValuePolicy.Policy)); + } + return Builder.Build(); +} + +void +CacheRecordPolicyBuilder::AddValuePolicy(const CacheValuePolicy& Value) +{ + ZEN_ASSERT(!EnumHasAnyFlags(Value.Policy, + ~Value.PolicyMask)); // Value policy contains flags that only make sense on the record policy. Policy: %s + if (Value.Policy == (BasePolicy & Value.PolicyMask)) + { + return; + } + if (!Shared) + { + Shared = new Private::CacheRecordPolicyShared; + } + Shared->AddValuePolicy(Value); +} + +CacheRecordPolicy +CacheRecordPolicyBuilder::Build() +{ + CacheRecordPolicy Policy(BasePolicy); + if (Shared) + { + const auto Add = [](const CachePolicy A, const CachePolicy B) { + return ((A | B) & ~CachePolicy::SkipData) | ((A & B) & CachePolicy::SkipData); + }; + const std::span<const CacheValuePolicy> Values = Shared->GetValuePolicies(); + Policy.RecordPolicy = BasePolicy; + for (const CacheValuePolicy& ValuePolicy : Values) + { + Policy.RecordPolicy = Add(Policy.RecordPolicy, ValuePolicy.Policy); + } + Policy.Shared = std::move(Shared); + } + return Policy; +} + +#if ZEN_WITH_TESTS +TEST_CASE("cachepolicy") +{ + SUBCASE("atomics serialization") + { + CachePolicy SomeAtomics[] = {CachePolicy::None, + CachePolicy::QueryLocal, + CachePolicy::StoreRemote, + CachePolicy::SkipData, + CachePolicy::KeepAlive}; + for (CachePolicy Atomic : SomeAtomics) + { + CHECK(ParseCachePolicy(WriteToString<128>(Atomic)) == Atomic); + } + // Also verify that we ignore unrecognized bits + for (CachePolicy Atomic : SomeAtomics) + { + CHECK(ParseCachePolicy(WriteToString<128>(Atomic | (CachePolicy)0x10000000)) == Atomic); + } + } + SUBCASE("aliases serialization") + { + CachePolicy SomeAliases[] = {CachePolicy::Query, CachePolicy::Local}; + for (CachePolicy Alias : SomeAliases) + { + CHECK(ParseCachePolicy(WriteToString<128>(Alias)) == Alias); + } + // Also verify that we ignore unrecognized bits + for (CachePolicy Alias : SomeAliases) + { + CHECK(ParseCachePolicy(WriteToString<128>(Alias | (CachePolicy)0x10000000)) == Alias); + } + } + SUBCASE("aliases take priority over atomics") + { + CHECK(WriteToString<128>(CachePolicy::Default).ToView() == "Default"sv); + CHECK(WriteToString<128>(CachePolicy::Query).ToView() == "Query"sv); + CHECK(WriteToString<128>(CachePolicy::Local).ToView() == "Local"sv); + } + SUBCASE("policies requiring multiple strings work") + { + char Delimiter = ','; + CachePolicy Combination = CachePolicy::SkipData | CachePolicy::QueryLocal; + CHECK(WriteToString<128>(Combination).ToView().find(Delimiter) != std::string_view::npos); + CHECK(ParseCachePolicy(WriteToString<128>(Combination)) == Combination); + } + SUBCASE("parsing invalid text") + { + CHECK(ParseCachePolicy(",,,") == CachePolicy::None); + CHECK(ParseCachePolicy("fee,fie,foo,fum") == CachePolicy::None); + CHECK(ParseCachePolicy("fee,KeepAlive,foo,fum") == CachePolicy::KeepAlive); + } +} + +TEST_CASE("cacherecordpolicy") +{ + SUBCASE("policy with no values") + { + CachePolicy Policy = CachePolicy::SkipData | CachePolicy::QueryLocal | CachePolicy::PartialRecord; + CachePolicy ValuePolicy = Policy & CacheValuePolicy::PolicyMask; + CacheRecordPolicy RecordPolicy; + CacheRecordPolicyBuilder Builder(Policy); + RecordPolicy = Builder.Build(); + SUBCASE("construct") + { + CHECK(RecordPolicy.IsUniform()); + CHECK(RecordPolicy.GetRecordPolicy() == Policy); + CHECK(RecordPolicy.GetBasePolicy() == Policy); + CHECK(RecordPolicy.GetValuePolicy(Oid::NewOid()) == ValuePolicy); + CHECK(RecordPolicy.GetValuePolicies().size() == 0); + } + SUBCASE("saveload") + { + CbWriter Writer; + RecordPolicy.Save(Writer); + CbObject Saved = Writer.Save()->AsObject(); + CacheRecordPolicy Loaded = CacheRecordPolicy::Load(Saved).Get(); + CHECK(Loaded.IsUniform()); + CHECK(Loaded.GetRecordPolicy() == Policy); + CHECK(Loaded.GetBasePolicy() == Policy); + CHECK(Loaded.GetValuePolicy(Oid::NewOid()) == ValuePolicy); + CHECK(Loaded.GetValuePolicies().size() == 0); + } + } + + SUBCASE("policy with values") + { + CachePolicy DefaultPolicy = CachePolicy::StoreRemote | CachePolicy::QueryLocal | CachePolicy::PartialRecord; + CachePolicy DefaultValuePolicy = DefaultPolicy & CacheValuePolicy::PolicyMask; + CachePolicy PartialOverlap = CachePolicy::StoreRemote; + CachePolicy NoOverlap = CachePolicy::QueryRemote; + CachePolicy UnionPolicy = DefaultPolicy | PartialOverlap | NoOverlap | CachePolicy::PartialRecord; + + CacheRecordPolicy RecordPolicy; + CacheRecordPolicyBuilder Builder(DefaultPolicy); + Oid PartialOid = Oid::NewOid(); + Oid NoOverlapOid = Oid::NewOid(); + Oid OtherOid = Oid::NewOid(); + Builder.AddValuePolicy(PartialOid, PartialOverlap); + Builder.AddValuePolicy(NoOverlapOid, NoOverlap); + RecordPolicy = Builder.Build(); + SUBCASE("construct") + { + CHECK(!RecordPolicy.IsUniform()); + CHECK(RecordPolicy.GetRecordPolicy() == UnionPolicy); + CHECK(RecordPolicy.GetBasePolicy() == DefaultPolicy); + CHECK(RecordPolicy.GetValuePolicy(PartialOid) == PartialOverlap); + CHECK(RecordPolicy.GetValuePolicy(NoOverlapOid) == NoOverlap); + CHECK(RecordPolicy.GetValuePolicy(OtherOid) == DefaultValuePolicy); + CHECK(RecordPolicy.GetValuePolicies().size() == 2); + } + SUBCASE("saveload") + { + CbWriter Writer; + RecordPolicy.Save(Writer); + CbObject Saved = Writer.Save()->AsObject(); + CacheRecordPolicy Loaded = CacheRecordPolicy::Load(Saved).Get(); + CHECK(!RecordPolicy.IsUniform()); + CHECK(RecordPolicy.GetRecordPolicy() == UnionPolicy); + CHECK(RecordPolicy.GetBasePolicy() == DefaultPolicy); + CHECK(RecordPolicy.GetValuePolicy(PartialOid) == PartialOverlap); + CHECK(RecordPolicy.GetValuePolicy(NoOverlapOid) == NoOverlap); + CHECK(RecordPolicy.GetValuePolicy(OtherOid) == DefaultValuePolicy); + CHECK(RecordPolicy.GetValuePolicies().size() == 2); + } + } + + SUBCASE("parsing invalid text") + { + OptionalCacheRecordPolicy Loaded = CacheRecordPolicy::Load(CbObject()); + CHECK(Loaded.IsNull()); + } +} +#endif + +void +cachepolicy_forcelink() +{ +} + +} // namespace zen diff --git a/src/zenstore/cache/cacherpc.cpp b/src/zenstore/cache/cacherpc.cpp index fdee017e0..4c44f43ca 100644 --- a/src/zenstore/cache/cacherpc.cpp +++ b/src/zenstore/cache/cacherpc.cpp @@ -15,7 +15,6 @@ #include <zenstore/cache/structuredcachestore.h> #include <zenstore/cache/upstreamcacheclient.h> #include <zenstore/cidstore.h> -#include <zenutil/cache/cacherequests.h> #include <zenutil/workerpools.h> #include <zencore/memory/llm.h> @@ -61,7 +60,7 @@ GetRpcRequestNamespace(const CbObjectView Params) { return {}; } - return cacherequests::GetValidNamespaceName(NamespaceField.AsString()); + return GetValidNamespaceName(NamespaceField.AsString()); } bool @@ -76,7 +75,7 @@ GetRpcRequestCacheKey(const CbObjectView& KeyView, CacheKey& Key) { return false; } - std::optional<std::string> Bucket = cacherequests::GetValidBucketName(BucketField.AsString()); + std::optional<std::string> Bucket = GetValidBucketName(BucketField.AsString()); if (!Bucket.has_value()) { return false; diff --git a/src/zenstore/cache/structuredcachestore.cpp b/src/zenstore/cache/structuredcachestore.cpp index b58f70ea7..fd54e6765 100644 --- a/src/zenstore/cache/structuredcachestore.cpp +++ b/src/zenstore/cache/structuredcachestore.cpp @@ -15,9 +15,8 @@ #include <zencore/thread.h> #include <zencore/timer.h> #include <zencore/trace.h> -#include <zencore/workthreadpool.h> +#include <zenstore/cache/cache.h> #include <zenstore/scrubcontext.h> -#include <zenutil/cache/cache.h> #include <future> #include <limits> diff --git a/src/zenstore/include/zenstore/cache/cache.h b/src/zenstore/include/zenstore/cache/cache.h new file mode 100644 index 000000000..4e72d1b05 --- /dev/null +++ b/src/zenstore/include/zenstore/cache/cache.h @@ -0,0 +1,52 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zenstore/cache/cachekey.h> +#include <zenstore/cache/cachepolicy.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <fmt/format.h> +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen { + +struct CacheRequestContext +{ + Oid SessionId{Oid::Zero}; + uint32_t RequestId{0}; +}; + +std::optional<std::string> GetValidNamespaceName(std::string_view Name); +std::optional<std::string> GetValidBucketName(std::string_view Name); +std::optional<IoHash> GetValidIoHash(std::string_view Hash); + +struct HttpCacheRequestData +{ + std::optional<std::string> Namespace; + std::optional<std::string> Bucket; + std::optional<IoHash> HashKey; + std::optional<IoHash> ValueContentId; +}; + +bool HttpCacheRequestParseRelativeUri(std::string_view Key, std::string_view DefaultNamespace, HttpCacheRequestData& Data); + +// Temporarily public +std::optional<std::string> GetCacheRequestNamespace(const CbObjectView& Params); +bool GetCacheRequestCacheKey(const CbObjectView& KeyView, CacheKey& Key); + +} // namespace zen + +template<> +struct fmt::formatter<zen::CacheRequestContext> : formatter<string_view> +{ + template<typename FormatContext> + auto format(const zen::CacheRequestContext& Context, FormatContext& ctx) const + { + zen::ExtendableStringBuilder<64> String; + Context.SessionId.ToString(String); + String << "."; + String << Context.RequestId; + return formatter<string_view>::format(String.ToView(), ctx); + } +}; diff --git a/src/zenstore/include/zenstore/cache/cachekey.h b/src/zenstore/include/zenstore/cache/cachekey.h new file mode 100644 index 000000000..ae79f2a3d --- /dev/null +++ b/src/zenstore/include/zenstore/cache/cachekey.h @@ -0,0 +1,92 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zencore/iohash.h> +#include <zencore/string.h> +#include <zencore/uid.h> + +#include <zenstore/cache/cachepolicy.h> + +namespace zen { + +struct CacheKey +{ + std::string Bucket; + IoHash Hash; + + static CacheKey Create(std::string_view Bucket, const IoHash& Hash) { return {.Bucket = ToLower(Bucket), .Hash = Hash}; } + + // This should be used whenever the bucket name has already been validated to avoid redundant ToLower calls + static CacheKey CreateValidated(std::string&& BucketValidated, const IoHash& Hash) + { + return {.Bucket = std::move(BucketValidated), .Hash = Hash}; + } + + auto operator<=>(const CacheKey& that) const + { + if (auto b = caseSensitiveCompareStrings(Bucket, that.Bucket); b != std::strong_ordering::equal) + { + return b; + } + return Hash <=> that.Hash; + } + + auto operator==(const CacheKey& that) const { return (*this <=> that) == std::strong_ordering::equal; } + + static const CacheKey Empty; +}; + +struct CacheChunkRequest +{ + CacheKey Key; + IoHash ChunkId; + Oid ValueId; + uint64_t RawOffset = 0ull; + uint64_t RawSize = ~uint64_t(0); + CachePolicy Policy = CachePolicy::Default; +}; + +struct CacheKeyRequest +{ + CacheKey Key; + CacheRecordPolicy Policy; +}; + +struct CacheValueRequest +{ + CacheKey Key; + CachePolicy Policy = CachePolicy::Default; +}; + +inline bool +operator<(const CacheChunkRequest& A, const CacheChunkRequest& B) +{ + if (A.Key < B.Key) + { + return true; + } + if (B.Key < A.Key) + { + return false; + } + if (A.ChunkId < B.ChunkId) + { + return true; + } + if (B.ChunkId < A.ChunkId) + { + return false; + } + if (A.ValueId < B.ValueId) + { + return true; + } + if (B.ValueId < A.ValueId) + { + return false; + } + return A.RawOffset < B.RawOffset; +} + +} // namespace zen diff --git a/src/zenstore/include/zenstore/cache/cachepolicy.h b/src/zenstore/include/zenstore/cache/cachepolicy.h new file mode 100644 index 000000000..7773cd3d1 --- /dev/null +++ b/src/zenstore/include/zenstore/cache/cachepolicy.h @@ -0,0 +1,229 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zenbase/refcount.h> +#include <zencore/compactbinary.h> +#include <zencore/enumflags.h> +#include <zencore/string.h> +#include <zencore/uid.h> + +#include <gsl/gsl-lite.hpp> +#include <span> +namespace zen::Private { +class ICacheRecordPolicyShared; +} +namespace zen { + +class CbObjectView; +class CbWriter; + +class OptionalCacheRecordPolicy; + +enum class CachePolicy : uint32_t +{ + /** A value with no flags. Disables access to the cache unless combined with other flags. */ + None = 0, + + /** Allow a cache request to query local caches. */ + QueryLocal = 1 << 0, + /** Allow a cache request to query remote caches. */ + QueryRemote = 1 << 1, + /** Allow a cache request to query any caches. */ + Query = QueryLocal | QueryRemote, + + /** Allow cache requests to query and store records and values in local caches. */ + StoreLocal = 1 << 2, + /** Allow cache records and values to be stored in remote caches. */ + StoreRemote = 1 << 3, + /** Allow cache records and values to be stored in any caches. */ + Store = StoreLocal | StoreRemote, + + /** Allow cache requests to query and store records and values in local caches. */ + Local = QueryLocal | StoreLocal, + /** Allow cache requests to query and store records and values in remote caches. */ + Remote = QueryRemote | StoreRemote, + + /** Allow cache requests to query and store records and values in any caches. */ + Default = Query | Store, + + /** Skip fetching the data for values. */ + SkipData = 1 << 4, + + /** Skip fetching the metadata for record requests. */ + SkipMeta = 1 << 5, + + /** + * Partial output will be provided with the error status when a required value is missing. + * + * This is meant for cases when the missing values can be individually recovered, or rebuilt, + * without rebuilding the whole record. The cache automatically adds this flag when there are + * other cache stores that it may be able to recover missing values from. + * + * Missing values will be returned in the records, but with only the hash and size. + * + * Applying this flag for a put of a record allows a partial record to be stored. + */ + PartialRecord = 1 << 6, + + /** + * Keep records in the cache for at least the duration of the session. + * + * This is a hint that the record may be accessed again in this session. This is mainly meant + * to be used when subsequent accesses will not tolerate a cache miss. + */ + KeepAlive = 1 << 7, +}; + +gsl_DEFINE_ENUM_BITMASK_OPERATORS(CachePolicy); +/** Append a non-empty text version of the policy to the builder. */ +StringBuilderBase& operator<<(StringBuilderBase& Builder, CachePolicy Policy); +/** Parse non-empty text written by operator<< into a policy. */ +CachePolicy ParseCachePolicy(std::string_view Text); +/** Return input converted into the equivalent policy that the upstream should use when forwarding a put or get to an upstream server. */ +CachePolicy ConvertToUpstream(CachePolicy Policy); + +inline CachePolicy +Union(CachePolicy A, CachePolicy B) +{ + constexpr CachePolicy InvertedFlags = CachePolicy::SkipData | CachePolicy::SkipMeta; + return (A & ~(InvertedFlags)) | (B & ~(InvertedFlags)) | (A & B & InvertedFlags); +} + +/** A value ID and the cache policy to use for that value. */ +struct CacheValuePolicy +{ + Oid Id; + CachePolicy Policy = CachePolicy::Default; + + /** Flags that are valid on a value policy. */ + static constexpr CachePolicy PolicyMask = CachePolicy::Default | CachePolicy::SkipData; +}; + +/** Interface for the private implementation of the cache record policy. */ +class Private::ICacheRecordPolicyShared : public RefCounted +{ +public: + virtual ~ICacheRecordPolicyShared() = default; + virtual void AddValuePolicy(const CacheValuePolicy& Policy) = 0; + virtual std::span<const CacheValuePolicy> GetValuePolicies() const = 0; +}; + +/** + * Flags to control the behavior of cache record requests, with optional overrides by value. + * + * Examples: + * - A base policy of None with value policy overrides of Default will fetch those values if they + * exist in the record, and skip data for any other values. + * - A base policy of Default, with value policy overrides of (Query | SkipData), will skip those + * values, but still check if they exist, and will load any other values. + */ +class CacheRecordPolicy +{ +public: + /** Construct a cache record policy that uses the default policy. */ + CacheRecordPolicy() = default; + + /** Construct a cache record policy with a uniform policy for the record and every value. */ + inline CacheRecordPolicy(CachePolicy BasePolicy) + : RecordPolicy(BasePolicy) + , DefaultValuePolicy(BasePolicy & CacheValuePolicy::PolicyMask) + { + } + + /** Returns true if the record and every value use the same cache policy. */ + inline bool IsUniform() const { return !Shared; } + + /** Returns the cache policy to use for the record. */ + inline CachePolicy GetRecordPolicy() const { return RecordPolicy; } + + /** Returns the base cache policy that this was constructed from. */ + inline CachePolicy GetBasePolicy() const { return DefaultValuePolicy | (RecordPolicy & ~CacheValuePolicy::PolicyMask); } + + /** Returns the cache policy to use for the value. */ + CachePolicy GetValuePolicy(const Oid& Id) const; + + /** Returns the array of cache policy overrides for values, sorted by ID. */ + inline std::span<const CacheValuePolicy> GetValuePolicies() const + { + return Shared ? Shared->GetValuePolicies() : std::span<const CacheValuePolicy>(); + } + + /** Saves the cache record policy to a compact binary object. */ + void Save(CbWriter& Writer) const; + + /** Loads a cache record policy from an object. */ + static OptionalCacheRecordPolicy Load(CbObjectView Object); + + /** Return *this converted into the equivalent policy that the upstream should use when forwarding a put or get to an upstream server. + */ + CacheRecordPolicy ConvertToUpstream() const; + +private: + friend class CacheRecordPolicyBuilder; + friend class OptionalCacheRecordPolicy; + + CachePolicy RecordPolicy = CachePolicy::Default; + CachePolicy DefaultValuePolicy = CachePolicy::Default; + RefPtr<const Private::ICacheRecordPolicyShared> Shared; +}; + +/** A cache record policy builder is used to construct a cache record policy. */ +class CacheRecordPolicyBuilder +{ +public: + /** Construct a policy builder that uses the default policy as its base policy. */ + CacheRecordPolicyBuilder() = default; + + /** Construct a policy builder that uses the provided policy for the record and values with no override. */ + inline explicit CacheRecordPolicyBuilder(CachePolicy Policy) : BasePolicy(Policy) {} + + /** Adds a cache policy override for a value. */ + void AddValuePolicy(const CacheValuePolicy& Value); + inline void AddValuePolicy(const Oid& Id, CachePolicy Policy) { AddValuePolicy({Id, Policy}); } + + /** Build a cache record policy, which makes this builder subsequently unusable. */ + CacheRecordPolicy Build(); + +private: + CachePolicy BasePolicy = CachePolicy::Default; + RefPtr<Private::ICacheRecordPolicyShared> Shared; +}; + +/** + * A cache record policy that can be null. + * + * @see CacheRecordPolicy + */ +class OptionalCacheRecordPolicy : private CacheRecordPolicy +{ +public: + inline OptionalCacheRecordPolicy() : CacheRecordPolicy(~CachePolicy::None) {} + + inline OptionalCacheRecordPolicy(CacheRecordPolicy&& InOutput) : CacheRecordPolicy(std::move(InOutput)) {} + inline OptionalCacheRecordPolicy(const CacheRecordPolicy& InOutput) : CacheRecordPolicy(InOutput) {} + inline OptionalCacheRecordPolicy& operator=(CacheRecordPolicy&& InOutput) + { + CacheRecordPolicy::operator=(std::move(InOutput)); + return *this; + } + inline OptionalCacheRecordPolicy& operator=(const CacheRecordPolicy& InOutput) + { + CacheRecordPolicy::operator=(InOutput); + return *this; + } + + /** Returns the cache record policy. The caller must check for null before using this accessor. */ + inline const CacheRecordPolicy& Get() const& { return *this; } + inline CacheRecordPolicy Get() && { return std::move(*this); } + + inline bool IsNull() const { return RecordPolicy == ~CachePolicy::None; } + inline bool IsValid() const { return !IsNull(); } + inline explicit operator bool() const { return !IsNull(); } + + inline void Reset() { *this = OptionalCacheRecordPolicy(); } +}; + +void cachepolicy_forcelink(); + +} // namespace zen diff --git a/src/zenstore/include/zenstore/cache/cacherpc.h b/src/zenstore/include/zenstore/cache/cacherpc.h index e0c8738ca..eb40befa0 100644 --- a/src/zenstore/include/zenstore/cache/cacherpc.h +++ b/src/zenstore/include/zenstore/cache/cacherpc.h @@ -4,9 +4,9 @@ #include <zencore/iobuffer.h> #include <zencore/logging.h> +#include <zenstore/cache/cache.h> #include <zenstore/cache/cacheshared.h> #include <zenstore/cache/structuredcachestore.h> -#include <zenutil/cache/cache.h> #include <atomic> #include <string_view> diff --git a/src/zenstore/include/zenstore/cache/structuredcachestore.h b/src/zenstore/include/zenstore/cache/structuredcachestore.h index c51d7312c..1ba469431 100644 --- a/src/zenstore/include/zenstore/cache/structuredcachestore.h +++ b/src/zenstore/include/zenstore/cache/structuredcachestore.h @@ -5,9 +5,9 @@ #include <zencore/compactbinary.h> #include <zencore/iohash.h> #include <zencore/stats.h> +#include <zenstore/cache/cache.h> #include <zenstore/cache/cachedisklayer.h> #include <zenstore/gc.h> -#include <zenutil/cache/cache.h> #include <zenutil/statsreporter.h> #include <atomic> diff --git a/src/zenstore/include/zenstore/cache/upstreamcacheclient.h b/src/zenstore/include/zenstore/cache/upstreamcacheclient.h index 152031c3a..2f3b6b0d7 100644 --- a/src/zenstore/include/zenstore/cache/upstreamcacheclient.h +++ b/src/zenstore/include/zenstore/cache/upstreamcacheclient.h @@ -8,7 +8,7 @@ #include <zencore/iohash.h> #include <zencore/stats.h> #include <zencore/zencore.h> -#include <zenutil/cache/cache.h> +#include <zenstore/cache/cache.h> #include <functional> #include <memory> diff --git a/src/zenstore/zenstore.cpp b/src/zenstore/zenstore.cpp index 85b50f03d..c563cc202 100644 --- a/src/zenstore/zenstore.cpp +++ b/src/zenstore/zenstore.cpp @@ -6,6 +6,7 @@ # include <zenstore/blockstore.h> # include <zenstore/buildstore/buildstore.h> +# include <zenstore/cache/cachepolicy.h> # include <zenstore/cache/structuredcachestore.h> # include <zenstore/projectstore.h> # include <zenstore/workspaces.h> @@ -22,6 +23,7 @@ void zenstore_forcelinktests() { buildstore_forcelink(); + cachepolicy_forcelink(); CAS_forcelink(); filecas_forcelink(); blockstore_forcelink(); |