aboutsummaryrefslogtreecommitdiff
path: root/src/zenstore
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2025-10-03 15:57:42 +0200
committerGitHub Enterprise <[email protected]>2025-10-03 15:57:42 +0200
commit42a2c2582b10a598ce5ef50f7feb4bab394b8fc1 (patch)
tree267816281dcdbeda9900a38e6863265ecc257f15 /src/zenstore
parent5.7.5-pre0 (diff)
downloadzen-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.cpp235
-rw-r--r--src/zenstore/cache/cachekey.cpp9
-rw-r--r--src/zenstore/cache/cachepolicy.cpp426
-rw-r--r--src/zenstore/cache/cacherpc.cpp5
-rw-r--r--src/zenstore/cache/structuredcachestore.cpp3
-rw-r--r--src/zenstore/include/zenstore/cache/cache.h52
-rw-r--r--src/zenstore/include/zenstore/cache/cachekey.h92
-rw-r--r--src/zenstore/include/zenstore/cache/cachepolicy.h229
-rw-r--r--src/zenstore/include/zenstore/cache/cacherpc.h2
-rw-r--r--src/zenstore/include/zenstore/cache/structuredcachestore.h2
-rw-r--r--src/zenstore/include/zenstore/cache/upstreamcacheclient.h2
-rw-r--r--src/zenstore/zenstore.cpp2
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();