diff options
| author | Stefan Boberg <[email protected]> | 2022-01-25 15:16:04 +0100 |
|---|---|---|
| committer | Stefan Boberg <[email protected]> | 2022-01-25 15:16:04 +0100 |
| commit | 080b73be664064d13eb88e53cd627ef859aa7da8 (patch) | |
| tree | 1614a7637a33551e46dcb53ebb0e92d02a26b4f7 /zenutil | |
| parent | Implemented support for storing compressed buffers as values in structured ca... (diff) | |
| parent | Cachepolicy (#36) (diff) | |
| download | zen-080b73be664064d13eb88e53cd627ef859aa7da8.tar.xz zen-080b73be664064d13eb88e53cd627ef859aa7da8.zip | |
Merge branch 'main' of https://github.com/EpicGames/zen
Diffstat (limited to 'zenutil')
| -rw-r--r-- | zenutil/cache/cachepolicy.cpp | 284 | ||||
| -rw-r--r-- | zenutil/include/zenutil/cache/cachepolicy.h | 137 |
2 files changed, 278 insertions, 143 deletions
diff --git a/zenutil/cache/cachepolicy.cpp b/zenutil/cache/cachepolicy.cpp index e1c31d885..142ba682a 100644 --- a/zenutil/cache/cachepolicy.cpp +++ b/zenutil/cache/cachepolicy.cpp @@ -6,62 +6,113 @@ #include <zencore/compactbinarybuilder.h> #include <zencore/string.h> +#include <algorithm> +#include <unordered_map> + namespace zen { using namespace std::literals; -namespace detail { namespace cacheopt { - constexpr std::string_view Local = "local"sv; - constexpr std::string_view Remote = "remote"sv; - constexpr std::string_view Data = "data"sv; - constexpr std::string_view Meta = "meta"sv; - constexpr std::string_view Value = "value"sv; - constexpr std::string_view Attachments = "attachments"sv; -}} // namespace detail::cacheopt - -CachePolicy -ParseQueryCachePolicy(std::string_view QueryPolicy, CachePolicy Default) +namespace detail::CachePolicyImpl { + constexpr char DelimiterChar = ','; + constexpr std::string_view Delimiter = ","sv; + constexpr std::string_view None = "None"sv; + constexpr std::string_view QueryLocal = "QueryLocal"sv; + constexpr std::string_view QueryRemote = "QueryRemote"sv; + constexpr std::string_view Query = "Query"sv; + constexpr std::string_view StoreLocal = "StoreLocal"sv; + constexpr std::string_view StoreRemote = "StoreRemote"sv; + constexpr std::string_view Store = "Store"sv; + constexpr std::string_view SkipMeta = "SkipMeta"sv; + constexpr std::string_view SkipData = "SkipData"sv; + constexpr std::string_view PartialRecord = "PartialRecord"sv; + constexpr std::string_view KeepAlive = "KeepAlive"sv; + constexpr std::string_view Local = "Local"sv; + constexpr std::string_view Remote = "Remote"sv; + constexpr std::string_view Default = "Default"sv; + constexpr std::string_view Disable = "Disable"sv; + + using TextToPolicyMap = std::unordered_map<std::string_view, CachePolicy>; + const TextToPolicyMap TextToPolicy = {{None, CachePolicy::None}, + {QueryLocal, CachePolicy::QueryLocal}, + {QueryRemote, CachePolicy::QueryRemote}, + {Query, CachePolicy::Query}, + {StoreLocal, CachePolicy::StoreLocal}, + {StoreRemote, CachePolicy::StoreRemote}, + {Store, CachePolicy::Store}, + {SkipMeta, CachePolicy::SkipMeta}, + {SkipData, CachePolicy::SkipData}, + {PartialRecord, CachePolicy::PartialRecord}, + {KeepAlive, CachePolicy::KeepAlive}, + {Local, CachePolicy::Local}, + {Remote, CachePolicy::Remote}, + {Default, CachePolicy::Default}, + {Disable, CachePolicy::Disable}}; + + using PolicyTextPair = std::pair<CachePolicy, std::string_view>; + const PolicyTextPair FlagsToString[]{ + // Order of these Flags is important: we want the aliases before the atomic values, + // and the bigger aliases first, to reduce the number of tokens we add + {CachePolicy::Default, Default}, + {CachePolicy::Remote, Remote}, + {CachePolicy::Local, Local}, + {CachePolicy::Store, Store}, + {CachePolicy::Query, Query}, + + // Order of Atomics doesn't matter, so arbitrarily we list them in enum order + {CachePolicy::QueryLocal, QueryLocal}, + {CachePolicy::QueryRemote, QueryRemote}, + {CachePolicy::StoreLocal, StoreLocal}, + {CachePolicy::StoreRemote, StoreRemote}, + {CachePolicy::SkipMeta, SkipMeta}, + {CachePolicy::SkipData, SkipData}, + {CachePolicy::PartialRecord, PartialRecord}, + {CachePolicy::KeepAlive, KeepAlive}, + + // None must come at the end of the array, to write out only if no others exist + {CachePolicy::None, None}, + }; + constexpr CachePolicy KnownFlags = + CachePolicy::Default | CachePolicy::SkipMeta | CachePolicy::SkipData | CachePolicy::KeepAlive | CachePolicy::PartialRecord; +} // namespace detail::CachePolicyImpl + +StringBuilderBase& +AppendToBuilderImpl(StringBuilderBase& Builder, CachePolicy Policy) { - if (QueryPolicy.empty()) + // Remove any bits we don't recognize; write None if there are not any bits we recognize + Policy = Policy & detail::CachePolicyImpl::KnownFlags; + for (const detail::CachePolicyImpl::PolicyTextPair& Pair : detail::CachePolicyImpl::FlagsToString) { - return Default; - } - - CachePolicy Result = CachePolicy::None; - - ForEachStrTok(QueryPolicy, ',', [&Result](const std::string_view& Token) { - if (Token == detail::cacheopt::Local) + if (EnumHasAllFlags(Policy, Pair.first)) { - Result |= CachePolicy::QueryLocal; + EnumRemoveFlags(Policy, Pair.first); + Builder << Pair.second << detail::CachePolicyImpl::DelimiterChar; + if (Policy == CachePolicy::None) + { + break; + } } - if (Token == detail::cacheopt::Remote) - { - Result |= CachePolicy::QueryRemote; - } - return true; - }); - - return Result; + } + Builder.RemoveSuffix(1); // Text will have been added by CachePolicy::None if not by anything else + return Builder; +} +StringBuilderBase& +operator<<(StringBuilderBase& Builder, CachePolicy Policy) +{ + return AppendToBuilderImpl(Builder, Policy); } CachePolicy -ParseStoreCachePolicy(std::string_view StorePolicy, CachePolicy Default) +ParseCachePolicy(std::string_view Text) { - if (StorePolicy.empty()) - { - return Default; - } + ZEN_ASSERT(!Text.empty()); // Empty string is not valid input to ParseCachePolicy CachePolicy Result = CachePolicy::None; - - ForEachStrTok(StorePolicy, ',', [&Result](const std::string_view& Token) { - if (Token == detail::cacheopt::Local) - { - Result |= CachePolicy::StoreLocal; - } - if (Token == detail::cacheopt::Remote) + ForEachStrTok(Text, detail::CachePolicyImpl::DelimiterChar, [&Result](const std::string_view& Token) { + auto it = detail::CachePolicyImpl::TextToPolicy.find(Token); + if (it != detail::CachePolicyImpl::TextToPolicy.end()) { - Result |= CachePolicy::StoreRemote; + Result |= it->second; } return true; }); @@ -69,101 +120,120 @@ ParseStoreCachePolicy(std::string_view StorePolicy, CachePolicy Default) return Result; } -CachePolicy -ParseSkipCachePolicy(std::string_view SkipPolicy, CachePolicy Default) -{ - if (SkipPolicy.empty()) +namespace Private { + + class CacheRecordPolicyShared final : public ICacheRecordPolicyShared { - return Default; - } + public: + inline std::span<const CacheValuePolicy> GetValuePolicies() const final { return Values; } - CachePolicy Result = CachePolicy::None; + inline void AddValuePolicy(const CacheValuePolicy& Policy) final { Values.push_back(Policy); } - ForEachStrTok(SkipPolicy, ',', [&Result](const std::string_view& Token) { - if (Token == detail::cacheopt::Meta) - { - Result |= CachePolicy::SkipMeta; - } - if (Token == detail::cacheopt::Value) + inline void Build() final { - Result |= CachePolicy::SkipValue; + std::sort(Values.begin(), Values.end(), [](const CacheValuePolicy& A, const CacheValuePolicy& B) { return A.Id < B.Id; }); } - if (Token == detail::cacheopt::Attachments) - { - Result |= CachePolicy::SkipAttachments; - } - if (Token == detail::cacheopt::Data) - { - Result |= CachePolicy::SkipData; - } - return true; - }); - return Result; -} + private: + std::vector<CacheValuePolicy> Values; + }; -CacheRecordPolicy::CacheRecordPolicy(const CachePolicy RecordPolicy, const CachePolicy PayloadPolicy) -: m_RecordPolicy(RecordPolicy) -, m_DefaultPayloadPolicy(PayloadPolicy) -{ -} +} // namespace Private CachePolicy -CacheRecordPolicy::GetPayloadPolicy(const Oid& PayloadId) const +CacheRecordPolicy::GetValuePolicy(const Oid& Id) const { - if (const auto It = m_PayloadPolicies.find(PayloadId); It != m_PayloadPolicies.end()) + if (Shared) { - return It->second; + if (std::span<const CacheValuePolicy> Values = Shared->GetValuePolicies(); !Values.empty()) + { + 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 m_DefaultPayloadPolicy; + return DefaultValuePolicy; } -bool -CacheRecordPolicy::Load(CbObjectView RecordPolicyObject, CacheRecordPolicy& OutRecordPolicy) +void +CacheRecordPolicy::Save(CbWriter& Writer) const { - using namespace std::literals; - - const uint32_t RecordPolicy = RecordPolicyObject["RecordPolicy"sv].AsUInt32(static_cast<uint32_t>(CachePolicy::Default)); - const uint32_t DefaultPayloadPolicy = - RecordPolicyObject["DefaultPayloadPolicy"sv].AsUInt32(static_cast<uint32_t>(CachePolicy::Default)); - - OutRecordPolicy.m_RecordPolicy = static_cast<CachePolicy>(RecordPolicy); - OutRecordPolicy.m_DefaultPayloadPolicy = static_cast<CachePolicy>(DefaultPayloadPolicy); - - for (CbFieldView PayloadPolicyView : RecordPolicyObject["PayloadPolicies"sv]) + Writer.BeginObject(); { - CbObjectView PayloadPolicyObject = PayloadPolicyView.AsObjectView(); - const Oid PayloadId = PayloadPolicyObject["Id"sv].AsObjectId(); - const uint32_t PayloadPolicy = PayloadPolicyObject["Policy"sv].AsUInt32(); - - if (PayloadId != Oid::Zero && PayloadPolicy != 0) + // The RecordPolicy is calculated from the ValuePolicies and does not need to be saved separately. + Writer << "DefaultValuePolicy"sv << WriteToString<128>(GetDefaultValuePolicy()); + if (!IsUniform()) { - OutRecordPolicy.m_PayloadPolicies.emplace(PayloadId, static_cast<CachePolicy>(PayloadPolicy)); + // FCacheRecordPolicyBuilder guarantees IsUniform -> non-empty GetValuePolicies. Small size penalty here if not. + Writer.BeginArray("ValuePolicies"sv); + { + for (const CacheValuePolicy& ValuePolicy : GetValuePolicies()) + { + // FCacheRecordPolicyBuilder is responsible for ensuring that each ValuePolicy != DefaultValuePolicy + // If it lets any duplicates through we will incur a small serialization size penalty here + Writer.BeginObject(); + Writer << "Id"sv << ValuePolicy.Id; + Writer << "Policy"sv << WriteToString<128>(ValuePolicy.Policy); + Writer.EndObject(); + } + } + Writer.EndArray(); } } + Writer.EndObject(); +} + +CacheRecordPolicy +CacheRecordPolicy::Load(CbObjectView Object, CachePolicy DefaultPolicy) +{ + std::string_view PolicyText = Object["DefaultValuePolicy"sv].AsString(); + CachePolicy DefaultValuePolicy = !PolicyText.empty() ? ParseCachePolicy(PolicyText) : DefaultPolicy; + + CacheRecordPolicyBuilder Builder(DefaultValuePolicy); + for (CbFieldView ValueObjectField : Object["ValuePolicies"sv]) + { + CbObjectView ValueObject = ValueObjectField.AsObjectView(); + const Oid ValueId = ValueObject["Id"sv].AsObjectId(); + PolicyText = ValueObject["Policy"sv].AsString(); + CachePolicy ValuePolicy = !PolicyText.empty() ? ParseCachePolicy(PolicyText) : DefaultValuePolicy; + // FCacheRecordPolicyBuilder should guarantee that FValueId(ValueId).IsValid and ValuePolicy != DefaultValuePolicy + // If it lets any through we will have unused data in the record we create. + Builder.AddValuePolicy(ValueId, ValuePolicy); + } - return true; + return Builder.Build(); } void -CacheRecordPolicy::Save(const CacheRecordPolicy& Policy, CbWriter& Writer) +CacheRecordPolicyBuilder::AddValuePolicy(const CacheValuePolicy& Policy) { - Writer << "RecordPolicy"sv << static_cast<uint32_t>(Policy.GetRecordPolicy()); - Writer << "DefaultPayloadPolicy"sv << static_cast<uint32_t>(Policy.GetDefaultPayloadPolicy()); + if (!Shared) + { + Shared = new Private::CacheRecordPolicyShared; + } + Shared->AddValuePolicy(Policy); +} - if (!Policy.m_PayloadPolicies.empty()) +CacheRecordPolicy +CacheRecordPolicyBuilder::Build() +{ + CacheRecordPolicy Policy(BasePolicy); + if (Shared) { - Writer.BeginArray("PayloadPolicies"sv); - for (const auto& Kv : Policy.m_PayloadPolicies) + Shared->Build(); + const auto PolicyOr = [](CachePolicy A, CachePolicy B) { return A | (B & ~CachePolicy::SkipData); }; + const std::span<const CacheValuePolicy> Values = Shared->GetValuePolicies(); + Policy.RecordPolicy = BasePolicy; + for (const CacheValuePolicy& ValuePolicy : Values) { - Writer.BeginObject(); - Writer.AddObjectId("Id"sv, Kv.first); - Writer << "Policy"sv << static_cast<uint32_t>(Kv.second); - Writer.EndObject(); + Policy.RecordPolicy = PolicyOr(Policy.RecordPolicy, ValuePolicy.Policy); } - Writer.EndArray(); + Policy.Shared = std::move(Shared); } + return Policy; } } // namespace zen diff --git a/zenutil/include/zenutil/cache/cachepolicy.h b/zenutil/include/zenutil/cache/cachepolicy.h index 5675ccf4d..f967f707b 100644 --- a/zenutil/include/zenutil/cache/cachepolicy.h +++ b/zenutil/include/zenutil/cache/cachepolicy.h @@ -2,10 +2,14 @@ #pragma once +#include <zencore/compactbinary.h> +#include <zencore/enumflags.h> +#include <zencore/refcount.h> #include <zencore/string.h> #include <zencore/uid.h> #include <gsl/gsl-lite.hpp> +#include <span> #include <unordered_map> namespace zen { @@ -34,16 +38,21 @@ enum class CachePolicy : uint32_t /** Skip fetching the metadata for record requests. */ SkipMeta = 1 << 4, - /** Skip fetching the value for record, chunk, or value requests. */ - SkipValue = 1 << 5, - /** Skip fetching the attachments for record requests. */ - SkipAttachments = 1 << 6, + /** Skip fetching the data for values. */ + SkipData = 1 << 5, + /** - * Skip fetching the data for any requests. + * 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 or chunks, but with only the hash and size. * - * Put requests with skip flags may assume that record existence implies payload existence. + * Applying this flag for a put of a record allows a partial record to be stored. */ - SkipData = SkipMeta | SkipValue | SkipAttachments, + PartialRecord = 1 << 6, /** * Keep records in the cache for at least the duration of the session. @@ -53,18 +62,6 @@ enum class CachePolicy : uint32_t */ KeepAlive = 1 << 7, - /** - * Partial output will be provided with the error status when a required payload is missing. - * - * This is meant for cases when the missing payloads 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 payloads from. - * - * Requests for records would return records where the missing payloads have a hash and size, - * but no data. Requests for chunks or values would return the hash and size, but no data. - */ - PartialOnError = 1 << 8, - /** 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. */ @@ -78,35 +75,103 @@ enum class CachePolicy : uint32_t }; gsl_DEFINE_ENUM_BITMASK_OPERATORS(CachePolicy); +/** Serialize Policy to text and append to Builder. Appended text will not be empty. */ +StringBuilderBase& operator<<(StringBuilderBase& Builder, CachePolicy Policy); +/** Parse text written by operator<< back into an ECachePolicy. Text must not be empty. */ +CachePolicy ParseCachePolicy(std::string_view Text); -CachePolicy ParseQueryCachePolicy(std::string_view QueryPolicy, CachePolicy Default = CachePolicy::Query); - -CachePolicy ParseStoreCachePolicy(std::string_view StorePolicy, CachePolicy Default = CachePolicy::Store); - -CachePolicy ParseSkipCachePolicy(std::string_view SkipPolicy, CachePolicy Default = CachePolicy::None); +/** A value ID and the cache policy to use for that value. */ +struct CacheValuePolicy +{ + Oid Id; + CachePolicy Policy = CachePolicy::Default; +}; +namespace Private { + /** Interface for the private implementation of the cache record policy. */ + class ICacheRecordPolicyShared : public RefCounted + { + public: + virtual ~ICacheRecordPolicyShared() = default; + virtual std::span<const CacheValuePolicy> GetValuePolicies() const = 0; + virtual void AddValuePolicy(const CacheValuePolicy& Policy) = 0; + virtual void Build() = 0; + }; +} // namespace Private + +/** + * Flags to control the behavior of cache record requests, with optional overrides by value. + * + * Examples: + * - A base policy of Disable, 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; - CacheRecordPolicy(const CachePolicy RecordPolicy, const CachePolicy DefaultPayloadPolicy = CachePolicy::Default); - CachePolicy GetRecordPolicy() const { return m_RecordPolicy; } - CachePolicy GetPayloadPolicy(const Oid& PayloadId) const; - CachePolicy GetDefaultPayloadPolicy() const { return m_DefaultPayloadPolicy; } + /** Construct a cache record policy with a uniform policy for the record and every value. */ + inline CacheRecordPolicy(CachePolicy Policy) : RecordPolicy(Policy), DefaultValuePolicy(Policy) {} + + /** Returns true if the record and every value use the same cache policy. */ + inline bool IsUniform() const { return !Shared && RecordPolicy == DefaultValuePolicy; } - bool HasRecordPolicy(const CachePolicy Policy) const { return (m_RecordPolicy & Policy) == Policy; } - bool HasPayloadPolicy(const Oid& PayloadId, const CachePolicy Policy) const { return (GetPayloadPolicy(PayloadId) & Policy) == Policy; } + /** Returns the cache policy to use for the record. */ + inline CachePolicy GetRecordPolicy() const { return RecordPolicy; } - static bool Load(CbObjectView RecordPolicyObject, CacheRecordPolicy& OutRecordPolicy); - static void Save(const CacheRecordPolicy& Policy, CbWriter& Writer); + /** Returns the cache policy to use for the value. */ + CachePolicy GetValuePolicy(const Oid& Id) const; + + /** Returns the cache policy to use for values with no override. */ + inline CachePolicy GetDefaultValuePolicy() const { return DefaultValuePolicy; } + + /** 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>(); + } + + /** Save the values from *this into the given writer. */ + void Save(CbWriter& Writer) const; + + /** + * Returns a policy loaded from values on Object. + * Invalid data will result in a uniform CacheRecordPolicy with defaultValuePolicy == DefaultPolicy. + */ + static CacheRecordPolicy Load(CbObjectView Object, CachePolicy DefaultPolicy = CachePolicy::Default); private: - using PayloadPolicyMap = std::unordered_map<Oid, CachePolicy, Oid::Hasher>; + friend class CacheRecordPolicyBuilder; + + CachePolicy RecordPolicy = CachePolicy::Default; + CachePolicy DefaultValuePolicy = CachePolicy::Default; + RefPtr<const Private::ICacheRecordPolicyShared> Shared; +}; - CachePolicy m_RecordPolicy = CachePolicy::Default; - CachePolicy m_DefaultPayloadPolicy = CachePolicy::Default; - PayloadPolicyMap m_PayloadPolicies; +/** 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& Policy); + 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; }; } // namespace zen |