diff options
| author | Stefan Boberg <[email protected]> | 2025-10-09 18:28:41 +0200 |
|---|---|---|
| committer | Stefan Boberg <[email protected]> | 2025-10-09 18:28:41 +0200 |
| commit | e2701e73646b382c9e6c6690505ae7bccea3ad34 (patch) | |
| tree | e37f8d54c7d0a721f8ccdeaa04b338cd8351805c /src | |
| parent | minor additions (diff) | |
| parent | shorten thread pool names for Linux which has a limit of 15 characters (#563) (diff) | |
| download | zen-e2701e73646b382c9e6c6690505ae7bccea3ad34.tar.xz zen-e2701e73646b382c9e6c6690505ae7bccea3ad34.zip | |
Merge remote-tracking branch 'origin/main' into sb/zen-master
Diffstat (limited to 'src')
210 files changed, 17455 insertions, 14412 deletions
diff --git a/src/zen/authutils.cpp b/src/zen/authutils.cpp new file mode 100644 index 000000000..bc185535b --- /dev/null +++ b/src/zen/authutils.cpp @@ -0,0 +1,247 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "authutils.h" + +#include <zencore/filesystem.h> +#include <zencore/fmtutils.h> +#include <zencore/logging.h> + +#include <zenhttp/auth/authmgr.h> +#include <zenhttp/httpclient.h> +#include <zenhttp/httpclientauth.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <json11.hpp> +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen { +using namespace std::literals; + +std::string_view +GetDefaultAccessTokenEnvVariableName() +{ +#if ZEN_PLATFORM_WINDOWS + return "UE-CloudDataCacheAccessToken"sv; +#endif +#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + return "UE_CloudDataCacheAccessToken"sv; +#endif +} + +std::string +ReadAccessTokenFromJsonFile(const std::filesystem::path& Path) +{ + if (!IsFile(Path)) + { + throw std::runtime_error(fmt::format("the file '{}' does not exist", Path)); + } + IoBuffer Body = IoBufferBuilder::MakeFromFile(Path); + std::string JsonText(reinterpret_cast<const char*>(Body.GetData()), Body.GetSize()); + std::string JsonError; + json11::Json TokenInfo = json11::Json::parse(JsonText, JsonError); + if (!JsonError.empty()) + { + throw std::runtime_error(fmt::format("failed parsing json file '{}'. Reason: '{}'", Path, JsonError)); + } + const std::string AuthToken = TokenInfo["Token"].string_value(); + if (AuthToken.empty()) + { + throw std::runtime_error(fmt::format("the json file '{}' does not contain a value for \"Token\"", Path)); + } + return AuthToken; +} + +std::filesystem::path +FindOidcTokenExePath(std::string_view OidcTokenAuthExecutablePath) +{ + if (OidcTokenAuthExecutablePath.empty()) + { + const std::string OidcExecutableName = "OidcToken" ZEN_EXE_SUFFIX_LITERAL; + std::filesystem::path OidcTokenPath = (GetRunningExecutablePath().parent_path() / OidcExecutableName).make_preferred(); + if (IsFile(OidcTokenPath)) + { + return OidcTokenPath; + } + OidcTokenPath = (std::filesystem::current_path() / OidcExecutableName).make_preferred(); + if (IsFile(OidcTokenPath)) + { + return OidcTokenPath; + } + } + else + { + std::filesystem::path OidcTokenPath = std::filesystem::absolute(StringToPath(OidcTokenAuthExecutablePath)).make_preferred(); + if (IsFile(OidcTokenPath)) + { + return OidcTokenPath; + } + } + return {}; +}; + +void +AuthCommandLineOptions::AddOptions(cxxopts::Options& Ops) +{ + // Direct access token (may expire) + Ops.add_option("auth-token", "", "access-token", "Remote host access token", cxxopts::value(m_AccessToken), "<accesstoken>"); + Ops.add_option("auth-token", + "", + "access-token-env", + "Name of environment variable that holds the remote host access token", + cxxopts::value(m_AccessTokenEnv)->default_value(std::string(GetDefaultAccessTokenEnvVariableName())), + "<envvariable>"); + Ops.add_option("auth-token", + "", + "access-token-path", + "Path to json file that holds the remote host access token", + cxxopts::value(m_AccessTokenPath), + "<filepath>"); + + // Auth manager token encryption + Ops.add_option("security", "", "encryption-aes-key", "256 bit AES encryption key", cxxopts::value<std::string>(m_EncryptionKey), ""); + Ops.add_option("security", + "", + "encryption-aes-iv", + "128 bit AES encryption initialization vector", + cxxopts::value<std::string>(m_EncryptionIV), + ""); + + // OpenId acccess token + Ops.add_option("openid", + "", + "openid-provider-name", + "Open ID provider name", + cxxopts::value<std::string>(m_OpenIdProviderName), + "Default"); + Ops.add_option("openid", "", "openid-provider-url", "Open ID provider url", cxxopts::value<std::string>(m_OpenIdProviderUrl), ""); + Ops.add_option("openid", "", "openid-client-id", "Open ID client id", cxxopts::value<std::string>(m_OpenIdClientId), ""); + Ops.add_option("openid", "", "openid-refresh-token", "Open ID refresh token", cxxopts::value<std::string>(m_OpenIdRefreshToken), ""); + + // OAuth acccess token + Ops.add_option("oauth", "", "oauth-url", "OAuth provier url", cxxopts::value<std::string>(m_OAuthUrl)->default_value(""), ""); + Ops.add_option("oauth", "", "oauth-clientid", "OAuth client id", cxxopts::value<std::string>(m_OAuthClientId)->default_value(""), ""); + Ops.add_option("oauth", + "", + "oauth-clientsecret", + "OAuth client secret", + cxxopts::value<std::string>(m_OAuthClientSecret)->default_value(""), + ""); + Ops.add_option("auth", + "", + "oidctoken-exe-path", + "Path to OidcToken executable", + cxxopts::value<std::string>(m_OidcTokenAuthExecutablePath)->default_value(""), + ""); + Ops.add_option("auth", + "", + "oidctoken-exe-unattended", + "Set mode to unattended when launcing OidcToken executable", + cxxopts::value<bool>(m_OidcTokenUnattended), + ""); +}; + +void +AuthCommandLineOptions::ParseOptions(cxxopts::Options& Ops, + const std::filesystem::path& SystemRootDir, + HttpClientSettings& ClientSettings, + std::string_view HostUrl, + std::unique_ptr<AuthMgr>& Auth, + bool Quiet, + bool Hidden) +{ + auto CreateAuthMgr = [&]() { + ZEN_ASSERT(!SystemRootDir.empty()); + if (!Auth) + { + if (m_EncryptionKey.empty()) + { + m_EncryptionKey = "abcdefghijklmnopqrstuvxyz0123456"; + if (!Quiet) + { + ZEN_CONSOLE_WARN("Using default encryption key"); + } + } + + if (m_EncryptionIV.empty()) + { + m_EncryptionIV = "0123456789abcdef"; + if (!Quiet) + { + ZEN_CONSOLE_WARN("Using default encryption initialization vector"); + } + } + + AuthConfig AuthMgrConfig = {.RootDirectory = SystemRootDir / "auth", + .EncryptionKey = AesKey256Bit::FromString(m_EncryptionKey), + .EncryptionIV = AesIV128Bit::FromString(m_EncryptionIV)}; + if (!AuthMgrConfig.EncryptionKey.IsValid()) + { + throw OptionParseException(fmt::format("'--encryption-aes-key' ('{}') is malformed", m_EncryptionKey), Ops.help()); + } + if (!AuthMgrConfig.EncryptionIV.IsValid()) + { + throw OptionParseException(fmt::format("'--encryption-aes-iv' ('{}') is malformed", m_EncryptionIV), Ops.help()); + } + Auth = AuthMgr::Create(AuthMgrConfig); + } + }; + + if (!m_OpenIdProviderUrl.empty() && !m_OpenIdClientId.empty()) + { + CreateAuthMgr(); + std::string ProviderName = m_OpenIdProviderName.empty() ? "Default" : m_OpenIdProviderName; + Auth->AddOpenIdProvider({.Name = ProviderName, .Url = m_OpenIdProviderUrl, .ClientId = m_OpenIdClientId}); + if (!m_OpenIdRefreshToken.empty()) + { + Auth->AddOpenIdToken({.ProviderName = ProviderName, .RefreshToken = m_OpenIdRefreshToken}); + } + } + + auto GetEnvAccessToken = [](const std::string& AccessTokenEnv) -> std::string { + if (!AccessTokenEnv.empty()) + { + return GetEnvVariable(AccessTokenEnv); + } + return {}; + }; + + if (!m_AccessToken.empty()) + { + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(m_AccessToken); + } + else if (!m_AccessTokenPath.empty()) + { + MakeSafeAbsolutePathÍnPlace(m_AccessTokenPath); + std::string ResolvedAccessToken = ReadAccessTokenFromJsonFile(m_AccessTokenPath); + if (!ResolvedAccessToken.empty()) + { + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(ResolvedAccessToken); + } + } + else if (!m_OAuthUrl.empty()) + { + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOAuthClientCredentials( + {.Url = m_OAuthUrl, .ClientId = m_OAuthClientId, .ClientSecret = m_OAuthClientSecret}); + } + else if (!m_OpenIdProviderName.empty()) + { + CreateAuthMgr(); + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOpenIdProvider(*Auth, m_OpenIdProviderName); + } + else if (std::string ResolvedAccessToken = GetEnvAccessToken(m_AccessTokenEnv); !ResolvedAccessToken.empty()) + { + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(ResolvedAccessToken); + } + else if (std::filesystem::path OidcTokenExePath = FindOidcTokenExePath(m_OidcTokenAuthExecutablePath); !OidcTokenExePath.empty()) + { + ClientSettings.AccessTokenProvider = + httpclientauth::CreateFromOidcTokenExecutable(OidcTokenExePath, HostUrl, Quiet, m_OidcTokenUnattended, Hidden); + } + + if (!ClientSettings.AccessTokenProvider) + { + CreateAuthMgr(); + ClientSettings.AccessTokenProvider = httpclientauth::CreateFromDefaultOpenIdProvider(*Auth); + } +} +} // namespace zen diff --git a/src/zen/authutils.h b/src/zen/authutils.h new file mode 100644 index 000000000..8f041c8fc --- /dev/null +++ b/src/zen/authutils.h @@ -0,0 +1,52 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "zen.h" + +namespace zen { + +struct HttpClientSettings; +class AuthMgr; + +struct AuthCommandLineOptions +{ + // Direct access token (may expire) + std::string m_AccessToken; + std::string m_AccessTokenEnv; + std::filesystem::path m_AccessTokenPath; + + // Auth manager token encryption + std::string m_EncryptionKey; // 256 bit AES encryption key + std::string m_EncryptionIV; // 128 bit AES initialization vector + + // OpenId acccess token + std::string m_OpenIdProviderName; + std::string m_OpenIdProviderUrl; + std::string m_OpenIdClientId; + std::string m_OpenIdRefreshToken; + + // OAuth acccess token + std::string m_OAuthUrl; + std::string m_OAuthClientId; + std::string m_OAuthClientSecret; + + std::string m_OidcTokenAuthExecutablePath; + bool m_OidcTokenUnattended = false; + + void AddOptions(cxxopts::Options& Ops); + + void ParseOptions(cxxopts::Options& Ops, + const std::filesystem::path& SystemRootDir, + HttpClientSettings& InOutClientSettings, + std::string_view HostUrl, + std::unique_ptr<AuthMgr>& OutAuthMgr, + bool Quiet, + bool Hidden); +}; + +std::string ReadAccessTokenFromJsonFile(const std::filesystem::path& Path); +std::string_view GetDefaultAccessTokenEnvVariableName(); +std::filesystem::path FindOidcTokenExePath(std::string_view OidcTokenAuthExecutablePath); + +} // namespace zen diff --git a/src/zen/cmds/admin_cmd.cpp b/src/zen/cmds/admin_cmd.cpp index dca6da6c4..502d1e799 100644 --- a/src/zen/cmds/admin_cmd.cpp +++ b/src/zen/cmds/admin_cmd.cpp @@ -4,15 +4,12 @@ #include <zencore/basicfile.h> #include <zencore/filesystem.h> +#include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zenhttp/formatters.h> #include <zenhttp/httpclient.h> #include <zenhttp/httpcommon.h> -ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> -ZEN_THIRD_PARTY_INCLUDES_END - using namespace std::literals; namespace zen { @@ -28,21 +25,21 @@ ScrubCommand::ScrubCommand() ScrubCommand::~ScrubCommand() = default; -int +void ScrubCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); @@ -52,19 +49,11 @@ ScrubCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (HttpClient::Response Response = Http.Post("/admin/scrub"sv, /* headers */ HttpClient::KeyValueMap{}, Params)) { ZEN_CONSOLE("Scrub started OK: {}", Response.ToText()); - - return 0; - } - else if (int StatusCode = (int)Response.StatusCode) - { - ZEN_CONSOLE_ERROR("Scrub start failed: {}: {} ({})", StatusCode, ReasonStringForHttpResultCode(StatusCode), Response.ToText()); } else { - ZEN_CONSOLE_ERROR("Scrub start failed: {}", Response.ToText()); + Response.ThrowError("Scrub start failed"); } - - return 1; } ////////////////////////////////////////////////////////////////////////// @@ -160,113 +149,115 @@ GcCommand::~GcCommand() { } -int +void GcCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } - cpr::Parameters Params; - Params.Add({"smallobjects", m_SmallObjects ? "true" : "false"}); + HttpClient::KeyValueMap Params; + Params.Entries.insert({"smallobjects", m_SmallObjects ? "true" : "false"}); if (m_MaxCacheDuration != 0) { - Params.Add({"maxcacheduration", fmt::format("{}", m_MaxCacheDuration)}); + Params.Entries.insert({"maxcacheduration", fmt::format("{}", m_MaxCacheDuration)}); } if (m_DiskSizeSoftLimit != 0) { - Params.Add({"disksizesoftlimit", fmt::format("{}", m_DiskSizeSoftLimit)}); + Params.Entries.insert({"disksizesoftlimit", fmt::format("{}", m_DiskSizeSoftLimit)}); } - Params.Add({"skipcid", m_SkipCid ? "true" : "false"}); - Params.Add({"skipdelete", m_SkipDelete ? "true" : "false"}); + Params.Entries.insert({"skipcid", m_SkipCid ? "true" : "false"}); + Params.Entries.insert({"skipdelete", m_SkipDelete ? "true" : "false"}); if (m_ForceUseGCV1) { - throw OptionParseException("usegcv1 is deprecated and can no longer be used"); + throw OptionParseException("'--usegcv1' is deprecated and can no longer be used", m_Options.help()); } if (m_ForceUseGCV2) { - Params.Add({"forceusegcv2", "true"}); + Params.Entries.insert({"forceusegcv2", "true"}); } if (m_CompactBlockThreshold) { - Params.Add({"compactblockthreshold", fmt::format("{}", m_CompactBlockThreshold)}); + Params.Entries.insert({"compactblockthreshold", fmt::format("{}", m_CompactBlockThreshold)}); } IoHash LowRef = IoHash::Zero; if (!m_ReferenceHashLow.empty()) { if (m_ReferenceHashLow.length() != IoHash::StringLength) { - throw OptionParseException(fmt::format("reference-low must be a {} character hex string", IoHash::StringLength)); + throw OptionParseException(fmt::format("'--reference-low' ('{}') is malformed, it must be a {} character hex string", + m_ReferenceHashLow, + IoHash::StringLength), + m_Options.help()); + } + if (!IoHash::TryParse(m_ReferenceHashLow, LowRef)) + { + throw OptionParseException(fmt::format("'--reference-low' ('{}') is malformed", m_ReferenceHashLow), m_Options.help()); } - LowRef = IoHash::FromHexString(m_ReferenceHashLow); } IoHash HighRef = IoHash::Max; if (!m_ReferenceHashHigh.empty()) { if (m_ReferenceHashHigh.length() != IoHash::StringLength) { - throw OptionParseException(fmt::format("reference-high must be a {} character hex string", IoHash::StringLength)); + throw OptionParseException(fmt::format("''--reference-high' ('{}') is malformed, it must be a {} character hex string", + m_ReferenceHashHigh, + IoHash::StringLength), + m_Options.help()); + } + if (!IoHash::TryParse(m_ReferenceHashHigh, HighRef)) + { + throw OptionParseException(fmt::format("'--reference-high' ('{}') is malformed", m_ReferenceHashHigh), m_Options.help()); } - HighRef = IoHash::FromHexString(m_ReferenceHashHigh); } if (HighRef < LowRef) { - throw OptionParseException(fmt::format("invalid reference range, reference-high must be higher value than reference-low")); + throw OptionParseException( + fmt::format("'--reference-high' ('{}') is invalid, it must be a higher value than '--reference-low' ('{}')", + m_ReferenceHashHigh, + m_ReferenceHashLow), + m_Options.help()); } if (!m_ReferenceHashLow.empty() || !m_ReferenceHashHigh.empty()) { - Params.Add({"referencehashlow", LowRef.ToHexString()}); - Params.Add({"referencehashhigh", HighRef.ToHexString()}); + Params.Entries.insert({"referencehashlow", LowRef.ToHexString()}); + Params.Entries.insert({"referencehashhigh", HighRef.ToHexString()}); } - Params.Add({"verbose", m_Verbose ? "true" : "false"}); - Params.Add({"singlethreaded", m_SingleThreaded ? "true" : "false"}); + Params.Entries.insert({"verbose", m_Verbose ? "true" : "false"}); + Params.Entries.insert({"singlethreaded", m_SingleThreaded ? "true" : "false"}); if (m_StoreCacheAttachmentMetaData) { - Params.Add({"storecacheattachmentmetadata", m_StoreCacheAttachmentMetaData ? "true" : "false"}); + Params.Entries.insert({"storecacheattachmentmetadata", m_StoreCacheAttachmentMetaData ? "true" : "false"}); } if (m_StoreProjectAttachmentMetaData) { - Params.Add({"storeprojectattachmentmetadata", m_StoreProjectAttachmentMetaData ? "true" : "false"}); - } - Params.Add({"enablevalidation", m_EnableValidation ? "true" : "false"}); - - cpr::Session Session; - Session.SetHeader(cpr::Header{{"Accept", "application/json"}}); - Session.SetUrl({fmt::format("{}/admin/gc", m_HostName)}); - Session.SetParameters(Params); - - cpr::Response Result = Session.Post(); - - if (zen::IsHttpSuccessCode(Result.status_code)) - { - ZEN_CONSOLE("OK: {}", Result.text); - return 0; + Params.Entries.insert({"storeprojectattachmentmetadata", m_StoreProjectAttachmentMetaData ? "true" : "false"}); } + Params.Entries.insert({"enablevalidation", m_EnableValidation ? "true" : "false"}); - if (Result.status_code) + HttpClient Http(m_HostName); + if (HttpClient::Response Response = Http.Post("/admin/gc"sv, HttpClient::Accept(HttpContentType::kJSON), Params)) { - ZEN_CONSOLE_ERROR("GC start failed: {}: {} ({})", Result.status_code, Result.reason, Result.text); + ZEN_CONSOLE("OK: {}", Response.ToText()); } else { - ZEN_CONSOLE_ERROR("GC start failed: {}", Result.error.message); + Response.ThrowError("GC start failed"); } - - return 1; } GcStatusCommand::GcStatusCommand() @@ -280,49 +271,32 @@ GcStatusCommand::~GcStatusCommand() { } -int +void GcStatusCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); - } - - cpr::Session Session; - Session.SetHeader(cpr::Header{{"Accept", "application/json"}}); - Session.SetUrl({fmt::format("{}/admin/gc", m_HostName)}); - if (m_Details) - { - Session.SetParameters({{"details", "true"}}); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } - cpr::Response Result = Session.Get(); - - if (zen::IsHttpSuccessCode(Result.status_code)) - { - ZEN_CONSOLE("OK: {}", Result.text); - return 0; - } - - if (Result.status_code) + HttpClient Http(m_HostName); + if (HttpClient::Response Response = Http.Get("/admin/gc"sv, HttpClient::Accept(HttpContentType::kJSON))) { - ZEN_CONSOLE_ERROR("GC status failed: {}: {} ({})", Result.status_code, Result.reason, Result.text); + ZEN_CONSOLE("OK: {}", Response.ToText()); } else { - ZEN_CONSOLE_ERROR("GC status failed: {}", Result.error.message); + Response.ThrowError("Gc status failed"); } - - return 1; } GcStopCommand::GcStopCommand() @@ -335,48 +309,39 @@ GcStopCommand::~GcStopCommand() { } -int +void GcStopCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); - } - - cpr::Session Session; - Session.SetUrl({fmt::format("{}/admin/gc-stop", m_HostName)}); - cpr::Response Result = Session.Post(); - - if (static_cast<HttpResponseCode>(Result.status_code) == HttpResponseCode::Accepted) - { - ZEN_CONSOLE("OK: {}", "Cancel request accepted"); - return 0; - } - else if (zen::IsHttpSuccessCode(Result.status_code)) - { - ZEN_CONSOLE("OK: {}", "No GC running"); - return 0; + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } - if (Result.status_code) + HttpClient Http(m_HostName); + if (HttpClient::Response Response = Http.Post("/admin/gc-stop"sv, HttpClient::Accept(HttpContentType::kJSON))) { - ZEN_CONSOLE_ERROR("GC status failed: {}: {} ({})", Result.status_code, Result.reason, Result.text); + if (Response.StatusCode == HttpResponseCode::Accepted) + { + ZEN_CONSOLE("OK: {}", "Cancel request accepted"); + } + else + { + ZEN_CONSOLE("OK: {}", "No GC running"); + } } else { - ZEN_CONSOLE_ERROR("GC status failed: {}", Result.error.message); + Response.ThrowError("Gc stop failed"); } - - return 1; } //////////////////////////////////////////// @@ -391,7 +356,7 @@ JobCommand::JobCommand() JobCommand::~JobCommand() = default; -int +void JobCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); @@ -400,14 +365,14 @@ JobCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); @@ -416,8 +381,7 @@ JobCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (m_JobId == 0) { - throw OptionParseException("Job id must be given"); - return 1; + throw OptionParseException("'--jobid' is required", m_Options.help()); } } std::string Url = m_JobId != 0 ? fmt::format("/admin/jobs/{}", m_JobId) : "/admin/jobs"; @@ -431,7 +395,6 @@ JobCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) else { Result.ThrowError("failed cancelling job"sv); - return 1; } } else if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) @@ -441,10 +404,7 @@ JobCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) else { Result.ThrowError("failed fetching job info"sv); - return 1; } - - return 0; } //////////////////////////////////////////// @@ -479,7 +439,7 @@ LoggingCommand::LoggingCommand() LoggingCommand::~LoggingCommand() = default; -int +void LoggingCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); @@ -488,14 +448,14 @@ LoggingCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); @@ -514,8 +474,8 @@ LoggingCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } else { - ZEN_CONSOLE_ERROR("Invalid value for parameter 'cache-write-log'. Use 'enable' or 'disable'"); - return 1; + throw OptionParseException(fmt::format("'--cache-write-log' ('{}') is invalid, use 'enable' or 'disable'", m_CacheWriteLog), + m_Options.help()); } } @@ -531,8 +491,8 @@ LoggingCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } else { - ZEN_CONSOLE_ERROR("Invalid value for parameter 'cache-access-log'. Use 'enable' or 'disable'"); - return 1; + throw OptionParseException(fmt::format("'--cache-access-log' ('{}') is invalid, use 'enable' or 'disable'", m_CacheAccessLog), + m_Options.help()); } } @@ -548,49 +508,37 @@ LoggingCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ZEN_CONSOLE("{}", Result.ToText()); const CbObject LogsResponse = Result.AsObject(); - auto CopyLog = [](std::string_view SourceName, std::string_view SourcePath, std::string_view TargetPath) -> bool { + auto CopyLog = [](std::string_view SourceName, std::string_view SourcePath, std::string_view TargetPath) { if (SourcePath.empty()) { - ZEN_CONSOLE_ERROR("Failed to retrieve {} log path", SourceName); - return false; + throw std::runtime_error(fmt::format("Failed to retrieve {} log path", SourceName)); } if (!CopyFile(SourcePath, TargetPath, {})) { - ZEN_CONSOLE_ERROR("Failed to copy {} log file {} to output file '{}'", SourceName, SourcePath, TargetPath); - return false; + throw std::runtime_error( + fmt::format("Failed to copy {} log file {} to output file '{}'", SourceName, SourcePath, TargetPath)); } - return true; }; if (!m_ServerLogTarget.empty()) { - if (!CopyLog("server", LogsResponse["Logfile"].AsString(), m_ServerLogTarget)) - { - return 1; - } + CopyLog("server", LogsResponse["Logfile"].AsString(), m_ServerLogTarget); } if (!m_CacheLogTarget.empty()) { - if (!CopyLog("cache", LogsResponse["cache"].AsObjectView()["Logfile"].AsString(), m_CacheLogTarget)) - { - return 1; - } + CopyLog("cache", LogsResponse["cache"].AsObjectView()["Logfile"].AsString(), m_CacheLogTarget); } if (!m_HttpLogTarget.empty()) { - if (!CopyLog("http", LogsResponse["http"].AsObjectView()["Logfile"].AsString(), m_HttpLogTarget)) - { - return 1; - } + CopyLog("http", LogsResponse["http"].AsObjectView()["Logfile"].AsString(), m_HttpLogTarget); } } else { Result.ThrowError("failed fetching log info"sv); - return 1; } - return 0; + return; } if (HttpClient::Response Result = Http.Post("/admin/logs", HttpClient::KeyValueMap{}, Parameters)) { @@ -599,10 +547,7 @@ LoggingCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) else { Result.ThrowError("failed setting log info"sv); - return 1; } - - return 0; } ////////////////////////////////////////////////////////////////////////// @@ -615,21 +560,21 @@ FlushCommand::FlushCommand() FlushCommand::~FlushCommand() = default; -int +void FlushCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } zen::HttpClient Http(m_HostName); @@ -638,18 +583,12 @@ FlushCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_CONSOLE("OK: {}", Response.ToText()); - return 0; - } - else if (int StatusCode = (int)Response.StatusCode) - { - ZEN_CONSOLE_ERROR("Flush failed: {}: {} ({})", StatusCode, ReasonStringForHttpResultCode(StatusCode), Response.ToText()); + return; } else { - ZEN_CONSOLE_ERROR("Flush failed: {}", Response.ToText()); + Response.ThrowError("Flush failed"); } - - return 1; } ////////////////////////////////////////////////////////////////////////// @@ -693,29 +632,29 @@ TryCopy(const std::filesystem::path& Source, const std::filesystem::path& Target return CopyFile(Source, Target, Options); } -int +void CopyStateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } if (m_DataPath.empty()) { - throw OptionParseException("data path must be given"); + throw OptionParseException("'--data-path' is required", m_Options.help()); } if (!IsDir(m_DataPath)) { - throw OptionParseException("data path must exist"); + throw std::runtime_error(fmt::format("'--data-path' '{}' must exist", m_DataPath)); } if (m_TargetPath.empty()) { - throw OptionParseException("target path must be given"); + throw OptionParseException("'--target-path' is required", m_Options.help()); } std::filesystem::path RootManifestPath = m_DataPath / "root_manifest"; @@ -723,7 +662,8 @@ CopyStateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!TryCopy(RootManifestPath, TargetRootManifestPath)) { - throw OptionParseException("data path is invalid, missing root_manifest"); + throw std::runtime_error( + fmt::format("'--data-path' ('{}') is invalid, missing root_manifest at '{}'", m_DataPath, RootManifestPath)); } std::filesystem::path CachePath = m_DataPath / "cache"; @@ -828,8 +768,6 @@ CopyStateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } } } - - return 0; } } // namespace zen diff --git a/src/zen/cmds/admin_cmd.h b/src/zen/cmds/admin_cmd.h index c593b2cac..4f97b7ad4 100644 --- a/src/zen/cmds/admin_cmd.h +++ b/src/zen/cmds/admin_cmd.h @@ -16,7 +16,7 @@ public: ScrubCommand(); ~ScrubCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -35,7 +35,7 @@ public: GcCommand(); ~GcCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -64,7 +64,7 @@ public: GcStatusCommand(); ~GcStatusCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -79,7 +79,7 @@ public: GcStopCommand(); ~GcStopCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -95,7 +95,7 @@ public: JobCommand(); ~JobCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -113,7 +113,7 @@ public: LoggingCommand(); ~LoggingCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -135,7 +135,7 @@ public: FlushCommand(); ~FlushCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -151,7 +151,7 @@ public: CopyStateCommand(); ~CopyStateCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/bench_cmd.cpp b/src/zen/cmds/bench_cmd.cpp index d904a51f6..b9c45a328 100644 --- a/src/zen/cmds/bench_cmd.cpp +++ b/src/zen/cmds/bench_cmd.cpp @@ -25,14 +25,14 @@ BenchCommand::BenchCommand() BenchCommand::~BenchCommand() = default; -int +void BenchCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } #if ZEN_PLATFORM_WINDOWS @@ -97,7 +97,7 @@ BenchCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } #endif - return 0; + return; } } // namespace zen diff --git a/src/zen/cmds/bench_cmd.h b/src/zen/cmds/bench_cmd.h index 29d7fcc08..ed123be75 100644 --- a/src/zen/cmds/bench_cmd.h +++ b/src/zen/cmds/bench_cmd.h @@ -12,7 +12,7 @@ public: BenchCommand(); ~BenchCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } diff --git a/src/zen/cmds/builds_cmd.cpp b/src/zen/cmds/builds_cmd.cpp index 0c77db2a0..80ff385b0 100644 --- a/src/zen/cmds/builds_cmd.cpp +++ b/src/zen/cmds/builds_cmd.cpp @@ -8,11 +8,11 @@ #include <zencore/compactbinaryfmt.h> #include <zencore/compactbinaryvalue.h> #include <zencore/compress.h> -#include <zencore/config.h> #include <zencore/except.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> +#include <zencore/parallelwork.h> #include <zencore/scopeguard.h> #include <zencore/session.h> #include <zencore/stream.h> @@ -23,23 +23,22 @@ #include <zenhttp/httpclient.h> #include <zenhttp/httpclientauth.h> #include <zenhttp/httpcommon.h> +#include <zenremotestore/builds/buildstoragecache.h> +#include <zenremotestore/builds/filebuildstorage.h> +#include <zenremotestore/builds/jupiterbuildstorage.h> +#include <zenremotestore/chunking/chunkblock.h> +#include <zenremotestore/chunking/chunkedcontent.h> +#include <zenremotestore/chunking/chunkedfile.h> +#include <zenremotestore/chunking/chunkingcontroller.h> +#include <zenremotestore/jupiter/jupiterhost.h> #include <zenutil/bufferedwritefilecache.h> -#include <zenutil/buildstoragecache.h> -#include <zenutil/chunkblock.h> -#include <zenutil/chunkedcontent.h> -#include <zenutil/chunkedfile.h> -#include <zenutil/chunkingcontroller.h> -#include <zenutil/filebuildstorage.h> -#include <zenutil/jupiter/jupiterbuildstorage.h> -#include <zenutil/jupiter/jupitersession.h> -#include <zenutil/parallelwork.h> #include <zenutil/wildcard.h> #include <zenutil/workerpools.h> #include <zenutil/zenserverprocess.h> #include <signal.h> #include <memory> -#include <regex> +#include <numeric> ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_map.h> @@ -224,16 +223,19 @@ namespace { } static const size_t DefaultMaxBlockSize = 64u * 1024u * 1024u; + static const size_t DefaultMaxChunksPerBlock = 4u * 1000u; static const size_t DefaultMaxChunkEmbedSize = 3u * 512u * 1024u; struct ChunksBlockParameters { size_t MaxBlockSize = DefaultMaxBlockSize; + size_t MaxChunksPerBlock = DefaultMaxChunksPerBlock; size_t MaxChunkEmbedSize = DefaultMaxChunkEmbedSize; }; const ChunksBlockParameters DefaultChunksBlockParams{ .MaxBlockSize = 32u * 1024u * 1024u, + .MaxChunksPerBlock = DefaultMaxChunksPerBlock, .MaxChunkEmbedSize = 2u * 1024u * 1024u // DefaultChunkedParams.MaxSize }; const uint64_t DefaultPreferredMultipartChunkSize = 32u * 1024u * 1024u; @@ -366,16 +368,34 @@ namespace { ); - bool IncludePath(const std::string_view IncludeWildcard, const std::string_view ExcludeWildcard, const std::filesystem::path& Path) + bool IncludePath(std::span<const std::string> IncludeWildcards, + std::span<const std::string> ExcludeWildcards, + const std::filesystem::path& Path) { - const std::string PathString = Path.generic_string(); - if (!IncludeWildcard.empty() && !MatchWildcard(IncludeWildcard, PathString, /*CaseSensitive*/ false)) + const std::string PathString = Path.generic_string(); + bool IncludePath = true; + if (!IncludeWildcards.empty()) { - return false; + IncludePath = false; + for (const std::string& IncludeWildcard : IncludeWildcards) + { + if (MatchWildcard(IncludeWildcard, PathString, /*CaseSensitive*/ false)) + { + IncludePath = true; + break; + } + } + if (!IncludePath) + { + return false; + } } - if (!ExcludeWildcard.empty() && MatchWildcard(ExcludeWildcard, PathString, /*CaseSensitive*/ false)) + for (const std::string& ExcludeWildcard : ExcludeWildcards) { - return false; + if (MatchWildcard(ExcludeWildcard, PathString, /*CaseSensitive*/ false)) + { + return false; + } } return true; } @@ -596,7 +616,7 @@ namespace { std::atomic<uint64_t> DiscoveredItemCount = 0; std::atomic<uint64_t> DeletedItemCount = 0; std::atomic<uint64_t> DeletedByteCount = 0; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); struct AsyncVisitor : public GetDirectoryContentVisitor { @@ -784,72 +804,6 @@ namespace { return CleanWipe; } - bool ParseCloudUrl(std::string_view InUrl, - std::string& OutHost, - std::string& OutNamespace, - std::string& OutBucket, - std::string& OutBuildId) - { - std::string Url(RemoveQuotes(InUrl)); - const std::string_view ExtendedApiString = "api/v2/builds/"; - if (auto ApiString = ToLower(Url).find(ExtendedApiString); ApiString != std::string::npos) - { - Url.erase(ApiString, ExtendedApiString.length()); - } - - const std::string ArtifactURLRegExString = R"((http[s]?:\/\/.*?)\/(.*?)\/(.*?)\/(.*))"; - const std::regex ArtifactURLRegEx(ArtifactURLRegExString, std::regex::ECMAScript | std::regex::icase); - std::match_results<std::string_view::const_iterator> MatchResults; - std::string_view UrlToParse(Url); - if (regex_match(begin(UrlToParse), end(UrlToParse), MatchResults, ArtifactURLRegEx) && MatchResults.size() == 5) - { - auto GetMatch = [&MatchResults](uint32_t Index) -> std::string_view { - ZEN_ASSERT(Index < MatchResults.size()); - - const auto& Match = MatchResults[Index]; - - return std::string_view(&*Match.first, Match.second - Match.first); - }; - - const std::string_view Host = GetMatch(1); - const std::string_view Namespace = GetMatch(2); - const std::string_view Bucket = GetMatch(3); - const std::string_view BuildId = GetMatch(4); - - OutHost = Host; - OutNamespace = Namespace; - OutBucket = Bucket; - OutBuildId = BuildId; - return true; - } - else - { - return false; - } - } - - std::string ReadAccessTokenFromFile(const std::filesystem::path& Path) - { - if (!IsFile(Path)) - { - throw std::runtime_error(fmt::format("the file '{}' does not exist", Path)); - } - IoBuffer Body = IoBufferBuilder::MakeFromFile(Path); - std::string JsonText(reinterpret_cast<const char*>(Body.GetData()), Body.GetSize()); - std::string JsonError; - json11::Json TokenInfo = json11::Json::parse(JsonText, JsonError); - if (!JsonError.empty()) - { - throw std::runtime_error(fmt::format("failed parsing json file '{}'. Reason: '{}'", Path, JsonError)); - } - const std::string AuthToken = TokenInfo["Token"].string_value(); - if (AuthToken.empty()) - { - throw std::runtime_error(fmt::format("the json file '{}' does not contain a value for \"Token\"", Path)); - } - return AuthToken; - } - IoBuffer WriteToTempFile(CompositeBuffer&& Buffer, const std::filesystem::path& TempFolderPath, const IoHash& Hash, @@ -1682,6 +1636,275 @@ namespace { return Result; } + struct BlockRangeDescriptor + { + uint32_t BlockIndex = (uint32_t)-1; + uint64_t RangeStart = 0; + uint64_t RangeLength = 0; + uint32_t ChunkBlockIndexStart = 0; + uint32_t ChunkBlockIndexCount = 0; + }; + + BlockRangeDescriptor MergeBlockRanges(std::span<const BlockRangeDescriptor> Ranges) + { + ZEN_ASSERT(Ranges.size() > 1); + const BlockRangeDescriptor& First = Ranges.front(); + const BlockRangeDescriptor& Last = Ranges.back(); + + return BlockRangeDescriptor{ + .BlockIndex = First.BlockIndex, + .RangeStart = First.RangeStart, + .RangeLength = Last.RangeStart + Last.RangeLength - First.RangeStart, + .ChunkBlockIndexStart = First.ChunkBlockIndexStart, + .ChunkBlockIndexCount = Last.ChunkBlockIndexStart + Last.ChunkBlockIndexCount - First.ChunkBlockIndexStart}; + } + + std::optional<std::vector<BlockRangeDescriptor>> MakeOptionalBlockRangeVector(uint64_t TotalBlockSize, + const BlockRangeDescriptor& Range) + { + if (Range.RangeLength == TotalBlockSize) + { + return {}; + } + else + { + return std::vector<BlockRangeDescriptor>{Range}; + } + }; + + struct BlockRangeLimit + { + uint16_t SizePercent; + uint16_t MaxRangeCount; + }; + + static const uint16_t FullBlockRangePercentLimit = 95; + + static const std::vector<BlockRangeLimit> ForceMergeLimits = {{.SizePercent = FullBlockRangePercentLimit, .MaxRangeCount = 1}, + {.SizePercent = 90, .MaxRangeCount = 2}, + {.SizePercent = 85, .MaxRangeCount = 8}, + {.SizePercent = 80, .MaxRangeCount = 16}, + {.SizePercent = 70, .MaxRangeCount = 32}, + {.SizePercent = 60, .MaxRangeCount = 48}, + {.SizePercent = 2, .MaxRangeCount = 56}, + {.SizePercent = 0, .MaxRangeCount = 64}}; + + const BlockRangeLimit* GetBlockRangeLimitForRange(std::span<const BlockRangeLimit> Limits, + uint64_t TotalBlockSize, + std::span<const BlockRangeDescriptor> Ranges) + { + if (Ranges.size() > 1) + { + const std::uint64_t WantedSize = + std::accumulate(Ranges.begin(), Ranges.end(), uint64_t(0), [](uint64_t Current, const BlockRangeDescriptor& Range) { + return Current + Range.RangeLength; + }); + + const double RangeRequestedPercent = (WantedSize * 100.0) / TotalBlockSize; + + for (const BlockRangeLimit& Limit : Limits) + { + if (RangeRequestedPercent >= Limit.SizePercent && Ranges.size() > Limit.MaxRangeCount) + { + return &Limit; + } + } + } + return nullptr; + }; + + std::vector<BlockRangeDescriptor> CollapseBlockRanges(const uint64_t AlwaysAcceptableGap, + std::span<const BlockRangeDescriptor> BlockRanges) + { + ZEN_ASSERT(BlockRanges.size() > 1); + std::vector<BlockRangeDescriptor> CollapsedBlockRanges; + + auto BlockRangesIt = BlockRanges.begin(); + CollapsedBlockRanges.push_back(*BlockRangesIt++); + for (; BlockRangesIt != BlockRanges.end(); BlockRangesIt++) + { + BlockRangeDescriptor& LastRange = CollapsedBlockRanges.back(); + + const uint64_t BothRangeSize = BlockRangesIt->RangeLength + LastRange.RangeLength; + + const uint64_t Gap = BlockRangesIt->RangeStart - (LastRange.RangeStart + LastRange.RangeLength); + if (Gap <= Max(BothRangeSize / 16, AlwaysAcceptableGap)) + { + LastRange.ChunkBlockIndexCount = + (BlockRangesIt->ChunkBlockIndexStart + BlockRangesIt->ChunkBlockIndexCount) - LastRange.ChunkBlockIndexStart; + LastRange.RangeLength = (BlockRangesIt->RangeStart + BlockRangesIt->RangeLength) - LastRange.RangeStart; + } + else + { + CollapsedBlockRanges.push_back(*BlockRangesIt); + } + } + + return CollapsedBlockRanges; + }; + + uint64_t CalculateNextGap(std::span<const BlockRangeDescriptor> BlockRanges) + { + ZEN_ASSERT(BlockRanges.size() > 1); + uint64_t AcceptableGap = (uint64_t)-1; + for (size_t RangeIndex = 0; RangeIndex < BlockRanges.size() - 1; RangeIndex++) + { + const BlockRangeDescriptor& Range = BlockRanges[RangeIndex]; + const BlockRangeDescriptor& NextRange = BlockRanges[RangeIndex + 1]; + + const uint64_t Gap = NextRange.RangeStart - (Range.RangeStart + Range.RangeLength); + AcceptableGap = Min(Gap, AcceptableGap); + } + AcceptableGap = RoundUp(AcceptableGap, 16u * 1024u); + return AcceptableGap; + }; + + std::optional<std::vector<BlockRangeDescriptor>> CalculateBlockRanges(uint32_t BlockIndex, + const ChunkBlockDescription& BlockDescription, + std::span<const uint32_t> BlockChunkIndexNeeded, + bool LimitToSingleRange, + const uint64_t ChunkStartOffsetInBlock, + const uint64_t TotalBlockSize, + uint64_t& OutTotalWantedChunksSize) + { + ZEN_TRACE_CPU("UpdateFolder_HandleBlocks_PartialAnalysis"); + + std::vector<BlockRangeDescriptor> BlockRanges; + { + uint64_t CurrentOffset = ChunkStartOffsetInBlock; + uint32_t ChunkBlockIndex = 0; + uint32_t NeedBlockChunkIndexOffset = 0; + BlockRangeDescriptor NextRange{.BlockIndex = BlockIndex}; + while (NeedBlockChunkIndexOffset < BlockChunkIndexNeeded.size() && ChunkBlockIndex < BlockDescription.ChunkRawHashes.size()) + { + const uint32_t ChunkCompressedLength = BlockDescription.ChunkCompressedLengths[ChunkBlockIndex]; + if (ChunkBlockIndex < BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) + { + if (NextRange.RangeLength > 0) + { + BlockRanges.push_back(NextRange); + NextRange = {.BlockIndex = BlockIndex}; + } + ChunkBlockIndex++; + CurrentOffset += ChunkCompressedLength; + } + else if (ChunkBlockIndex == BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) + { + if (NextRange.RangeLength == 0) + { + NextRange.RangeStart = CurrentOffset; + NextRange.ChunkBlockIndexStart = ChunkBlockIndex; + } + NextRange.RangeLength += ChunkCompressedLength; + NextRange.ChunkBlockIndexCount++; + ChunkBlockIndex++; + CurrentOffset += ChunkCompressedLength; + NeedBlockChunkIndexOffset++; + } + else + { + ZEN_ASSERT(false); + } + } + if (NextRange.RangeLength > 0) + { + BlockRanges.push_back(NextRange); + } + } + ZEN_ASSERT(!BlockRanges.empty()); + + OutTotalWantedChunksSize = + std::accumulate(BlockRanges.begin(), BlockRanges.end(), uint64_t(0), [](uint64_t Current, const BlockRangeDescriptor& Range) { + return Current + Range.RangeLength; + }); + + double RangeWantedPercent = (OutTotalWantedChunksSize * 100.0) / TotalBlockSize; + + if (BlockRanges.size() == 1) + { + ZEN_CONSOLE_VERBOSE("Range request of {} ({:.2f}%) using single range from block {} ({}) as is", + NiceBytes(OutTotalWantedChunksSize), + RangeWantedPercent, + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize)); + return BlockRanges; + } + + if (LimitToSingleRange) + { + const BlockRangeDescriptor MergedRange = MergeBlockRanges(BlockRanges); + const double RangeRequestedPercent = (MergedRange.RangeLength * 100.0) / TotalBlockSize; + const double WastedPercent = ((MergedRange.RangeLength - OutTotalWantedChunksSize) * 100.0) / MergedRange.RangeLength; + ZEN_CONSOLE_VERBOSE( + "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) limited to single block range {} ({:.2f}%) wasting " + "{:.2f}% ({})", + NiceBytes(OutTotalWantedChunksSize), + RangeWantedPercent, + BlockRanges.size(), + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize), + NiceBytes(MergedRange.RangeLength), + RangeRequestedPercent, + WastedPercent, + NiceBytes(MergedRange.RangeLength - OutTotalWantedChunksSize)); + return MakeOptionalBlockRangeVector(TotalBlockSize, MergedRange); + } + + if (RangeWantedPercent > FullBlockRangePercentLimit) + { + const BlockRangeDescriptor MergedRange = MergeBlockRanges(BlockRanges); + const double RangeRequestedPercent = (MergedRange.RangeLength * 100.0) / TotalBlockSize; + const double WastedPercent = ((MergedRange.RangeLength - OutTotalWantedChunksSize) * 100.0) / MergedRange.RangeLength; + ZEN_CONSOLE_VERBOSE( + "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) exceeds {}%. Merged to single block range {} " + "({:.2f}%) wasting {:.2f}% ({})", + NiceBytes(OutTotalWantedChunksSize), + RangeWantedPercent, + BlockRanges.size(), + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize), + FullBlockRangePercentLimit, + NiceBytes(MergedRange.RangeLength), + RangeRequestedPercent, + WastedPercent, + NiceBytes(MergedRange.RangeLength - OutTotalWantedChunksSize)); + return MakeOptionalBlockRangeVector(TotalBlockSize, MergedRange); + } + + std::vector<BlockRangeDescriptor> CollapsedBlockRanges = CollapseBlockRanges(16u * 1024u, BlockRanges); + while (GetBlockRangeLimitForRange(ForceMergeLimits, TotalBlockSize, CollapsedBlockRanges)) + { + CollapsedBlockRanges = CollapseBlockRanges(CalculateNextGap(CollapsedBlockRanges), CollapsedBlockRanges); + } + + const std::uint64_t WantedCollapsedSize = + std::accumulate(CollapsedBlockRanges.begin(), + CollapsedBlockRanges.end(), + uint64_t(0), + [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; }); + + const double CollapsedRangeRequestedPercent = (WantedCollapsedSize * 100.0) / TotalBlockSize; + + { + const double WastedPercent = ((WantedCollapsedSize - OutTotalWantedChunksSize) * 100.0) / WantedCollapsedSize; + + ZEN_CONSOLE_VERBOSE( + "Range request of {} ({:.2f}%) using {} ranges from block {} ({}) collapsed to {} {:.2f}% using {} ranges wasting {:.2f}% " + "({})", + NiceBytes(OutTotalWantedChunksSize), + RangeWantedPercent, + BlockRanges.size(), + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize), + NiceBytes(WantedCollapsedSize), + CollapsedRangeRequestedPercent, + CollapsedBlockRanges.size(), + WastedPercent, + NiceBytes(WantedCollapsedSize - OutTotalWantedChunksSize)); + return CollapsedBlockRanges; + } + }; + class BufferedOpenFile { public: @@ -2171,7 +2394,7 @@ namespace { WorkerThreadPool& NetworkPool = GetNetworkPool(); WorkerThreadPool& VerifyPool = GetIOWorkerPool(); - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); const std::filesystem::path TempFolder = ".zen-tmp"; @@ -2372,6 +2595,7 @@ namespace { void ArrangeChunksIntoBlocks(const ChunkedFolderContent& Content, const ChunkedContentLookup& Lookup, uint64_t MaxBlockSize, + uint64_t MaxChunksPerBlock, std::vector<uint32_t>& ChunkIndexes, std::vector<std::vector<uint32_t>>& OutBlocks) { @@ -2400,7 +2624,7 @@ namespace { const uint32_t ChunkIndex = ChunkIndexes[ChunkIndexOffset]; const uint64_t ChunkSize = Content.ChunkedContent.ChunkRawSizes[ChunkIndex]; - if ((BlockSize + ChunkSize) > MaxBlockSize) + if (((BlockSize + ChunkSize) > MaxBlockSize) || (ChunkIndexOffset - ChunkIndexStart) > MaxChunksPerBlock) { // Within the span of MaxBlockSizeLowThreshold and MaxBlockSize, see if there is a break // between source paths for chunks. Break the block at the last such break if any. @@ -2575,11 +2799,12 @@ namespace { struct GeneratedBlocks { - std::vector<ChunkBlockDescription> BlockDescriptions; - std::vector<uint64_t> BlockSizes; - std::vector<CompositeBuffer> BlockHeaders; - std::vector<CbObject> BlockMetaDatas; - std::vector<bool> MetaDataHasBeenUploaded; + std::vector<ChunkBlockDescription> BlockDescriptions; + std::vector<uint64_t> BlockSizes; + std::vector<CompositeBuffer> BlockHeaders; + std::vector<CbObject> BlockMetaDatas; + std::vector<uint8_t> + MetaDataHasBeenUploaded; // NOTE: Do not use std::vector<bool> here as this vector is modified by multiple threads tsl::robin_map<IoHash, size_t, IoHash::Hasher> BlockHashToBlockIndex; }; @@ -2604,7 +2829,7 @@ namespace { OutBlocks.BlockSizes.resize(NewBlockCount); OutBlocks.BlockMetaDatas.resize(NewBlockCount); OutBlocks.BlockHeaders.resize(NewBlockCount); - OutBlocks.MetaDataHasBeenUploaded.resize(NewBlockCount, false); + OutBlocks.MetaDataHasBeenUploaded.resize(NewBlockCount, 0); OutBlocks.BlockHashToBlockIndex.reserve(NewBlockCount); RwLock Lock; @@ -2615,7 +2840,7 @@ namespace { FilteredRate FilteredGeneratedBytesPerSecond; FilteredRate FilteredUploadedBytesPerSecond; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); std::atomic<uint64_t> QueuedPendingBlocksForUpload = 0; @@ -2849,7 +3074,7 @@ namespace { FilteredRate FilteredCompressedBytesPerSecond; FilteredRate FilteredUploadedBytesPerSecond; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); std::atomic<size_t> UploadedBlockSize = 0; std::atomic<size_t> UploadedBlockCount = 0; @@ -3502,56 +3727,58 @@ namespace { FindBlocksStatistics FindBlocksStats; - std::future<PrepareBuildResult> PrepBuildResultFuture = GetNetworkPool().EnqueueTask(std::packaged_task<PrepareBuildResult()>{ - [&Storage, BuildId, FindBlockMaxCount, &MetaData, CreateBuild, AllowMultiparts, IgnoreExistingBlocks, &FindBlocksStats] { - ZEN_TRACE_CPU("PrepareBuild"); + std::future<PrepareBuildResult> PrepBuildResultFuture = GetNetworkPool().EnqueueTask( + std::packaged_task<PrepareBuildResult()>{ + [&Storage, BuildId, FindBlockMaxCount, &MetaData, CreateBuild, AllowMultiparts, IgnoreExistingBlocks, &FindBlocksStats] { + ZEN_TRACE_CPU("PrepareBuild"); - PrepareBuildResult Result; - Stopwatch Timer; - if (CreateBuild) - { - ZEN_TRACE_CPU("CreateBuild"); - - Stopwatch PutBuildTimer; - CbObject PutBuildResult = Storage.BuildStorage->PutBuild(BuildId, MetaData); - Result.PrepareBuildTimeMs = PutBuildTimer.GetElapsedTimeMs(); - Result.PreferredMultipartChunkSize = PutBuildResult["chunkSize"sv].AsUInt64(Result.PreferredMultipartChunkSize); - Result.PayloadSize = MetaData.GetSize(); - } - else - { - ZEN_TRACE_CPU("PutBuild"); - Stopwatch GetBuildTimer; - CbObject Build = Storage.BuildStorage->GetBuild(BuildId); - Result.PrepareBuildTimeMs = GetBuildTimer.GetElapsedTimeMs(); - Result.PayloadSize = Build.GetSize(); - if (auto ChunkSize = Build["chunkSize"sv].AsUInt64(); ChunkSize != 0) + PrepareBuildResult Result; + Stopwatch Timer; + if (CreateBuild) { - Result.PreferredMultipartChunkSize = ChunkSize; + ZEN_TRACE_CPU("CreateBuild"); + + Stopwatch PutBuildTimer; + CbObject PutBuildResult = Storage.BuildStorage->PutBuild(BuildId, MetaData); + Result.PrepareBuildTimeMs = PutBuildTimer.GetElapsedTimeMs(); + Result.PreferredMultipartChunkSize = PutBuildResult["chunkSize"sv].AsUInt64(Result.PreferredMultipartChunkSize); + Result.PayloadSize = MetaData.GetSize(); } - else if (AllowMultiparts) + else { - ZEN_CONSOLE_WARN("PreferredMultipartChunkSize is unknown. Defaulting to '{}'", - NiceBytes(Result.PreferredMultipartChunkSize)); + ZEN_TRACE_CPU("PutBuild"); + Stopwatch GetBuildTimer; + CbObject Build = Storage.BuildStorage->GetBuild(BuildId); + Result.PrepareBuildTimeMs = GetBuildTimer.GetElapsedTimeMs(); + Result.PayloadSize = Build.GetSize(); + if (auto ChunkSize = Build["chunkSize"sv].AsUInt64(); ChunkSize != 0) + { + Result.PreferredMultipartChunkSize = ChunkSize; + } + else if (AllowMultiparts) + { + ZEN_CONSOLE_WARN("PreferredMultipartChunkSize is unknown. Defaulting to '{}'", + NiceBytes(Result.PreferredMultipartChunkSize)); + } } - } - if (!IgnoreExistingBlocks) - { - ZEN_TRACE_CPU("FindBlocks"); - Stopwatch KnownBlocksTimer; - CbObject BlockDescriptionList = Storage.BuildStorage->FindBlocks(BuildId, FindBlockMaxCount); - if (BlockDescriptionList) + if (!IgnoreExistingBlocks) { - Result.KnownBlocks = ParseChunkBlockDescriptionList(BlockDescriptionList); + ZEN_TRACE_CPU("FindBlocks"); + Stopwatch KnownBlocksTimer; + CbObject BlockDescriptionList = Storage.BuildStorage->FindBlocks(BuildId, FindBlockMaxCount); + if (BlockDescriptionList) + { + Result.KnownBlocks = ParseChunkBlockDescriptionList(BlockDescriptionList); + } + FindBlocksStats.FindBlockTimeMS = KnownBlocksTimer.GetElapsedTimeMs(); + FindBlocksStats.FoundBlockCount = Result.KnownBlocks.size(); + Result.FindBlocksTimeMs = KnownBlocksTimer.GetElapsedTimeMs(); } - FindBlocksStats.FindBlockTimeMS = KnownBlocksTimer.GetElapsedTimeMs(); - FindBlocksStats.FoundBlockCount = Result.KnownBlocks.size(); - Result.FindBlocksTimeMs = KnownBlocksTimer.GetElapsedTimeMs(); - } - Result.ElapsedTimeMs = Timer.GetElapsedTimeMs(); - return Result; - }}); + Result.ElapsedTimeMs = Timer.GetElapsedTimeMs(); + return Result; + }}, + WorkerThreadPool::EMode::EnableBacklog); ChunkedFolderContent LocalContent; @@ -3847,7 +4074,12 @@ namespace { } std::vector<std::vector<uint32_t>> NewBlockChunks; - ArrangeChunksIntoBlocks(LocalContent, LocalLookup, DefaultChunksBlockParams.MaxBlockSize, NewBlockChunkIndexes, NewBlockChunks); + ArrangeChunksIntoBlocks(LocalContent, + LocalLookup, + DefaultChunksBlockParams.MaxBlockSize, + DefaultChunksBlockParams.MaxChunksPerBlock, + NewBlockChunkIndexes, + NewBlockChunks); FindBlocksStats.NewBlocksCount = NewBlockChunks.size(); for (uint32_t ChunkIndex : NewBlockChunkIndexes) @@ -4554,7 +4786,7 @@ namespace { WorkerThreadPool& VerifyPool = GetIOWorkerPool(); - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); const uint32_t PathCount = gsl::narrow<uint32_t>(Content.Paths.size()); @@ -5655,7 +5887,7 @@ namespace { Stopwatch Timer; auto _ = MakeGuard([&LocalFolderScanStats, &Timer]() { LocalFolderScanStats.ElapsedWallTimeUS = Timer.GetElapsedTimeUs(); }); - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); std::atomic<uint64_t> CompletedPathCount = 0; uint32_t PathIndex = 0; @@ -5717,21 +5949,52 @@ namespace { return Result; } - void UpdateFolder(const std::filesystem::path& SystemRootDir, - StorageInstance& Storage, + enum EPartialBlockRequestMode + { + Off, + ZenCacheOnly, + Mixed, + All, + Invalid + }; + static EPartialBlockRequestMode PartialBlockRequestModeFromString(const std::string_view ModeString) + { + switch (HashStringAsLowerDjb2(ModeString)) + { + case HashStringDjb2("false"): + return EPartialBlockRequestMode::Off; + case HashStringDjb2("zencacheonly"): + return EPartialBlockRequestMode::ZenCacheOnly; + case HashStringDjb2("mixed"): + return EPartialBlockRequestMode::Mixed; + case HashStringDjb2("true"): + return EPartialBlockRequestMode::All; + default: + return EPartialBlockRequestMode::Invalid; + } + } + + struct UpdateOptions + { + std::filesystem::path SystemRootDir; + std::filesystem::path ZenFolderPath; + std::uint64_t LargeAttachmentSize = DefaultPreferredMultipartChunkSize * 4u; + std::uint64_t PreferredMultipartChunkSize = DefaultPreferredMultipartChunkSize; + EPartialBlockRequestMode PartialBlockRequestMode = EPartialBlockRequestMode::Mixed; + bool WipeTargetFolder = false; + bool PrimeCacheOnly = false; + bool EnableOtherDownloadsScavenging = true; + bool EnableTargetFolderScavenging = true; + }; + + void UpdateFolder(StorageInstance& Storage, const Oid& BuildId, const std::filesystem::path& Path, - const std::filesystem::path& ZenFolderPath, - const std::uint64_t LargeAttachmentSize, - const std::uint64_t PreferredMultipartChunkSize, const ChunkedFolderContent& LocalContent, const ChunkedFolderContent& RemoteContent, const std::vector<ChunkBlockDescription>& BlockDescriptions, const std::vector<IoHash>& LooseChunkHashes, - bool AllowPartialBlockRequests, - bool WipeTargetFolder, - bool PrimeCacheOnly, - bool EnableScavenging, + const UpdateOptions& Options, FolderContent& OutLocalFolderState, DiskStatistics& DiskStats, CacheMappingStatistics& CacheMappingStats, @@ -5741,7 +6004,8 @@ namespace { { ZEN_TRACE_CPU("UpdateFolder"); - ZEN_ASSERT((!PrimeCacheOnly) || (PrimeCacheOnly && (!AllowPartialBlockRequests))); + ZEN_ASSERT((!Options.PrimeCacheOnly) || + (Options.PrimeCacheOnly && (Options.PartialBlockRequestMode == EPartialBlockRequestMode::Off))); Stopwatch IndexTimer; @@ -5754,7 +6018,7 @@ namespace { ZEN_CONSOLE("Indexed local and remote content in {}", NiceTimeSpanMs(IndexTimer.GetElapsedTimeMs())); } - const std::filesystem::path CacheFolderPath = ZenTempCacheFolderPath(ZenFolderPath); + const std::filesystem::path CacheFolderPath = ZenTempCacheFolderPath(Options.ZenFolderPath); Stopwatch CacheMappingTimer; @@ -5764,7 +6028,7 @@ namespace { tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> CachedChunkHashesFound; tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> CachedSequenceHashesFound; - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { ZEN_TRACE_CPU("UpdateFolder_CheckChunkCache"); @@ -5776,40 +6040,43 @@ namespace { CacheDirContent); for (size_t Index = 0; Index < CacheDirContent.Files.size(); Index++) { - IoHash FileHash; - if (IoHash::TryParse(CacheDirContent.Files[Index].filename().string(), FileHash)) + if (Options.EnableTargetFolderScavenging) { - if (auto ChunkIt = RemoteLookup.ChunkHashToChunkIndex.find(FileHash); - ChunkIt != RemoteLookup.ChunkHashToChunkIndex.end()) + IoHash FileHash; + if (IoHash::TryParse(CacheDirContent.Files[Index].filename().string(), FileHash)) { - const uint32_t ChunkIndex = ChunkIt->second; - const uint64_t ChunkSize = RemoteContent.ChunkedContent.ChunkRawSizes[ChunkIndex]; - if (ChunkSize == CacheDirContent.FileSizes[Index]) + if (auto ChunkIt = RemoteLookup.ChunkHashToChunkIndex.find(FileHash); + ChunkIt != RemoteLookup.ChunkHashToChunkIndex.end()) { - CachedChunkHashesFound.insert({FileHash, ChunkIndex}); - CacheMappingStats.CacheChunkCount++; - CacheMappingStats.CacheChunkByteCount += ChunkSize; - continue; + const uint32_t ChunkIndex = ChunkIt->second; + const uint64_t ChunkSize = RemoteContent.ChunkedContent.ChunkRawSizes[ChunkIndex]; + if (ChunkSize == CacheDirContent.FileSizes[Index]) + { + CachedChunkHashesFound.insert({FileHash, ChunkIndex}); + CacheMappingStats.CacheChunkCount++; + CacheMappingStats.CacheChunkByteCount += ChunkSize; + continue; + } } - } - else if (auto SequenceIt = RemoteLookup.RawHashToSequenceIndex.find(FileHash); - SequenceIt != RemoteLookup.RawHashToSequenceIndex.end()) - { - const uint32_t SequenceIndex = SequenceIt->second; - const uint32_t PathIndex = RemoteLookup.SequenceIndexFirstPathIndex[SequenceIndex]; - const uint64_t SequenceSize = RemoteContent.RawSizes[PathIndex]; - if (SequenceSize == CacheDirContent.FileSizes[Index]) + else if (auto SequenceIt = RemoteLookup.RawHashToSequenceIndex.find(FileHash); + SequenceIt != RemoteLookup.RawHashToSequenceIndex.end()) { - CachedSequenceHashesFound.insert({FileHash, SequenceIndex}); - CacheMappingStats.CacheSequenceHashesCount++; - CacheMappingStats.CacheSequenceHashesByteCount += SequenceSize; + const uint32_t SequenceIndex = SequenceIt->second; + const uint32_t PathIndex = RemoteLookup.SequenceIndexFirstPathIndex[SequenceIndex]; + const uint64_t SequenceSize = RemoteContent.RawSizes[PathIndex]; + if (SequenceSize == CacheDirContent.FileSizes[Index]) + { + CachedSequenceHashesFound.insert({FileHash, SequenceIndex}); + CacheMappingStats.CacheSequenceHashesCount++; + CacheMappingStats.CacheSequenceHashesByteCount += SequenceSize; - const std::filesystem::path CacheFilePath = - GetFinalChunkedSequenceFileName(CacheFolderPath, - RemoteContent.ChunkedContent.SequenceRawHashes[SequenceIndex]); - ZEN_ASSERT_SLOW(IsFile(CacheFilePath)); + const std::filesystem::path CacheFilePath = + GetFinalChunkedSequenceFileName(CacheFolderPath, + RemoteContent.ChunkedContent.SequenceRawHashes[SequenceIndex]); + ZEN_ASSERT_SLOW(IsFile(CacheFilePath)); - continue; + continue; + } } } } @@ -5819,7 +6086,7 @@ namespace { } tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> CachedBlocksFound; - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { ZEN_TRACE_CPU("UpdateFolder_CheckBlockCache"); @@ -5834,31 +6101,34 @@ namespace { } DirectoryContent BlockDirContent; - GetDirectoryContent(ZenTempBlockFolderPath(ZenFolderPath), + GetDirectoryContent(ZenTempBlockFolderPath(Options.ZenFolderPath), DirectoryContentFlags::IncludeFiles | DirectoryContentFlags::IncludeFileSizes, BlockDirContent); CachedBlocksFound.reserve(BlockDirContent.Files.size()); for (size_t Index = 0; Index < BlockDirContent.Files.size(); Index++) { - IoHash FileHash; - if (IoHash::TryParse(BlockDirContent.Files[Index].filename().string(), FileHash)) + if (Options.EnableTargetFolderScavenging) { - if (auto BlockIt = AllBlockSizes.find(FileHash); BlockIt != AllBlockSizes.end()) + IoHash FileHash; + if (IoHash::TryParse(BlockDirContent.Files[Index].filename().string(), FileHash)) { - const uint32_t BlockIndex = BlockIt->second; - const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; - uint64_t BlockSize = CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize; - for (uint64_t ChunkSize : BlockDescription.ChunkCompressedLengths) + if (auto BlockIt = AllBlockSizes.find(FileHash); BlockIt != AllBlockSizes.end()) { - BlockSize += ChunkSize; - } + const uint32_t BlockIndex = BlockIt->second; + const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; + uint64_t BlockSize = CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize; + for (uint64_t ChunkSize : BlockDescription.ChunkCompressedLengths) + { + BlockSize += ChunkSize; + } - if (BlockSize == BlockDirContent.FileSizes[Index]) - { - CachedBlocksFound.insert({FileHash, BlockIndex}); - CacheMappingStats.CacheBlockCount++; - CacheMappingStats.CacheBlocksByteCount += BlockSize; - continue; + if (BlockSize == BlockDirContent.FileSizes[Index]) + { + CachedBlocksFound.insert({FileHash, BlockIndex}); + CacheMappingStats.CacheBlockCount++; + CacheMappingStats.CacheBlocksByteCount += BlockSize; + continue; + } } } } @@ -5871,7 +6141,7 @@ namespace { std::vector<uint32_t> LocalPathIndexesMatchingSequenceIndexes; tsl::robin_map<IoHash, uint32_t, IoHash::Hasher> SequenceIndexesLeftToFindToRemoteIndex; - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly && Options.EnableTargetFolderScavenging) { // Pick up all whole files we can use from current local state ZEN_TRACE_CPU("UpdateFolder_GetLocalSequences"); @@ -5946,7 +6216,7 @@ namespace { std::vector<ScavengeCopyOperation> ScavengeCopyOperations; uint64_t ScavengedPathsCount = 0; - if (!PrimeCacheOnly && EnableScavenging) + if (!Options.PrimeCacheOnly && Options.EnableOtherDownloadsScavenging) { ZEN_TRACE_CPU("UpdateFolder_GetScavengedSequences"); @@ -5954,7 +6224,7 @@ namespace { if (!SequenceIndexesLeftToFindToRemoteIndex.empty()) { - std::vector<ScavengeSource> ScavengeSources = GetDownloadedStatePaths(SystemRootDir); + std::vector<ScavengeSource> ScavengeSources = GetDownloadedStatePaths(Options.SystemRootDir); auto EraseIt = std::remove_if(ScavengeSources.begin(), ScavengeSources.end(), [&Path](const ScavengeSource& Source) { return Source.Path == Path; }); @@ -5967,7 +6237,7 @@ namespace { ScavengedPaths.resize(ScavengePathCount); ProgressBar ScavengeProgressBar(ProgressMode, "Scavenging"); - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); std::atomic<uint64_t> PathsFound(0); std::atomic<uint64_t> ChunksFound(0); @@ -6193,7 +6463,7 @@ namespace { tsl::robin_map<IoHash, size_t, IoHash::Hasher> RawHashToCacheCopyDataIndex; std::vector<CacheCopyData> CacheCopyDatas; - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly && Options.EnableTargetFolderScavenging) { ZEN_TRACE_CPU("UpdateFolder_GetLocalChunks"); @@ -6274,7 +6544,7 @@ namespace { CacheMappingStats.LocalScanElapsedWallTimeUs += LocalTimer.GetElapsedTimeUs(); } - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly && Options.EnableOtherDownloadsScavenging) { ZEN_TRACE_CPU("UpdateFolder_GetScavengeChunks"); @@ -6434,8 +6704,8 @@ namespace { WorkerThreadPool& NetworkPool = GetNetworkPool(); WorkerThreadPool& WritePool = GetIOWorkerPool(); - ProgressBar WriteProgressBar(ProgressMode, PrimeCacheOnly ? "Downloading" : "Writing"); - ParallelWork Work(AbortFlag, PauseFlag); + ProgressBar WriteProgressBar(ProgressMode, Options.PrimeCacheOnly ? "Downloading" : "Writing"); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); struct LooseChunkHashWorkData { @@ -6505,32 +6775,20 @@ namespace { return NeededBlockChunkIndexes; }; - std::vector<uint32_t> CachedChunkBlockIndexes; - - struct BlockRangeDescriptor - { - uint32_t BlockIndex = (uint32_t)-1; - uint64_t RangeStart = 0; - uint64_t RangeLength = 0; - uint32_t ChunkBlockIndexStart = 0; - uint32_t ChunkBlockIndexCount = 0; - }; - std::vector<BlockRangeDescriptor> BlockRangeWorks; - - std::vector<uint32_t> FullBlockWorks; + std::vector<uint32_t> CachedChunkBlockIndexes; + std::vector<uint32_t> FetchBlockIndexes; + std::vector<std::vector<uint32_t>> AllBlockChunkIndexNeeded; for (uint32_t BlockIndex = 0; BlockIndex < BlockCount; BlockIndex++) { - const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; - const std::vector<uint32_t> BlockChunkIndexNeeded = GetNeededChunkBlockIndexes(BlockDescription); + const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; + + std::vector<uint32_t> BlockChunkIndexNeeded = GetNeededChunkBlockIndexes(BlockDescription); if (!BlockChunkIndexNeeded.empty()) { - if (PrimeCacheOnly) + if (Options.PrimeCacheOnly) { - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); + FetchBlockIndexes.push_back(BlockIndex); } else { @@ -6542,196 +6800,20 @@ namespace { TotalPartWriteCount++; std::filesystem::path BlockPath = - ZenTempBlockFolderPath(ZenFolderPath) / BlockDescription.BlockHash.ToHexString(); + ZenTempBlockFolderPath(Options.ZenFolderPath) / BlockDescription.BlockHash.ToHexString(); if (IsFile(BlockPath)) { CachedChunkBlockIndexes.push_back(BlockIndex); UsingCachedBlock = true; } } - if (!UsingCachedBlock) { - bool WantsToDoPartialBlockDownload = BlockChunkIndexNeeded.size() < BlockDescription.ChunkRawHashes.size(); - bool CanDoPartialBlockDownload = - (BlockDescription.HeaderSize > 0) && - (BlockDescription.ChunkCompressedLengths.size() == BlockDescription.ChunkRawHashes.size()); - if (AllowPartialBlockRequests && WantsToDoPartialBlockDownload && CanDoPartialBlockDownload) - { - std::vector<BlockRangeDescriptor> BlockRanges; - - ZEN_TRACE_CPU("UpdateFolder_HandleBlocks_PartialAnalysis"); - - uint32_t NeedBlockChunkIndexOffset = 0; - uint32_t ChunkBlockIndex = 0; - uint32_t CurrentOffset = - gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); - - const uint64_t TotalBlockSize = std::accumulate(BlockDescription.ChunkCompressedLengths.begin(), - BlockDescription.ChunkCompressedLengths.end(), - std::uint64_t(CurrentOffset)); - - BlockRangeDescriptor NextRange{.BlockIndex = BlockIndex}; - while (NeedBlockChunkIndexOffset < BlockChunkIndexNeeded.size() && - ChunkBlockIndex < BlockDescription.ChunkRawHashes.size()) - { - const uint32_t ChunkCompressedLength = BlockDescription.ChunkCompressedLengths[ChunkBlockIndex]; - if (ChunkBlockIndex < BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) - { - if (NextRange.RangeLength > 0) - { - BlockRanges.push_back(NextRange); - NextRange = {.BlockIndex = BlockIndex}; - } - ChunkBlockIndex++; - CurrentOffset += ChunkCompressedLength; - } - else if (ChunkBlockIndex == BlockChunkIndexNeeded[NeedBlockChunkIndexOffset]) - { - if (NextRange.RangeLength == 0) - { - NextRange.RangeStart = CurrentOffset; - NextRange.ChunkBlockIndexStart = ChunkBlockIndex; - } - NextRange.RangeLength += ChunkCompressedLength; - NextRange.ChunkBlockIndexCount++; - ChunkBlockIndex++; - CurrentOffset += ChunkCompressedLength; - NeedBlockChunkIndexOffset++; - } - else - { - ZEN_ASSERT(false); - } - } - if (NextRange.RangeLength > 0) - { - BlockRanges.push_back(NextRange); - } - - ZEN_ASSERT(!BlockRanges.empty()); - - std::vector<BlockRangeDescriptor> CollapsedBlockRanges; - auto It = BlockRanges.begin(); - CollapsedBlockRanges.push_back(*It++); - while (It != BlockRanges.end()) - { - BlockRangeDescriptor& LastRange = CollapsedBlockRanges.back(); - uint64_t Slack = It->RangeStart - (LastRange.RangeStart + LastRange.RangeLength); - uint64_t BothRangeSize = It->RangeLength + LastRange.RangeLength; - if (Slack <= Max(BothRangeSize / 8, 64u * 1024u)) // Made up heuristic - we'll see how it pans out - { - LastRange.ChunkBlockIndexCount = - (It->ChunkBlockIndexStart + It->ChunkBlockIndexCount) - LastRange.ChunkBlockIndexStart; - LastRange.RangeLength = (It->RangeStart + It->RangeLength) - LastRange.RangeStart; - } - else - { - CollapsedBlockRanges.push_back(*It); - } - ++It; - } - - const std::uint64_t WantedSize = std::accumulate( - CollapsedBlockRanges.begin(), - CollapsedBlockRanges.end(), - uint64_t(0), - [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; }); - ZEN_ASSERT(WantedSize <= TotalBlockSize); - if (WantedSize > ((TotalBlockSize * 95) / 100)) - { - ZEN_CONSOLE_VERBOSE("Using more than 95% ({}) of block {} ({}), requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize)); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else if ((WantedSize > ((TotalBlockSize * 9) / 10)) && CollapsedBlockRanges.size() > 1) - { - ZEN_CONSOLE_VERBOSE( - "Using more than 90% ({}) of block {} ({}) using {} requests, requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else if ((WantedSize > ((TotalBlockSize * 8) / 10)) && (CollapsedBlockRanges.size() > 16)) - { - ZEN_CONSOLE_VERBOSE( - "Using more than 80% ({}) of block {} ({}) using {} requests, requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else if ((WantedSize > ((TotalBlockSize * 7) / 10)) && (CollapsedBlockRanges.size() > 48)) - { - ZEN_CONSOLE_VERBOSE( - "Using more than 70% ({}) of block {} ({}) using {} requests, requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else if ((WantedSize > ((TotalBlockSize * 6) / 10)) && (CollapsedBlockRanges.size() > 64)) - { - ZEN_CONSOLE_VERBOSE( - "Using more than 60% ({}) of block {} ({}) using {} requests, requesting full block", - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } - else - { - if (WantedSize > ((TotalBlockSize * 5) / 10)) - { - ZEN_CONSOLE_VERBOSE("Using {}% ({}) of block {} ({}) using {} requests, requesting partial block", - (WantedSize * 100) / TotalBlockSize, - NiceBytes(WantedSize), - BlockDescription.BlockHash, - NiceBytes(TotalBlockSize), - CollapsedBlockRanges.size()); - } - TotalRequestCount += CollapsedBlockRanges.size(); - TotalPartWriteCount += CollapsedBlockRanges.size(); - - BlockRangeWorks.insert(BlockRangeWorks.end(), CollapsedBlockRanges.begin(), CollapsedBlockRanges.end()); - } - } - else - { - TotalRequestCount++; - TotalPartWriteCount++; - - FullBlockWorks.push_back(BlockIndex); - } + FetchBlockIndexes.push_back(BlockIndex); } } } - else - { - ZEN_CONSOLE_VERBOSE("Skipping block {} due to cache reuse", BlockDescriptions[BlockIndex].BlockHash); - } + AllBlockChunkIndexNeeded.emplace_back(std::move(BlockChunkIndexNeeded)); } struct BlobsExistsResult @@ -6749,18 +6831,12 @@ namespace { tsl::robin_set<IoHash> BlobHashesSet; - BlobHashesSet.reserve(LooseChunkHashWorks.size() + FullBlockWorks.size()); + BlobHashesSet.reserve(LooseChunkHashWorks.size() + FetchBlockIndexes.size()); for (LooseChunkHashWorkData& LooseChunkHashWork : LooseChunkHashWorks) { BlobHashesSet.insert(RemoteContent.ChunkedContent.ChunkHashes[LooseChunkHashWork.RemoteChunkIndex]); } - for (const BlockRangeDescriptor& BlockRange : BlockRangeWorks) - { - const uint32_t BlockIndex = BlockRange.BlockIndex; - const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; - BlobHashesSet.insert(BlockDescription.BlockHash); - } - for (uint32_t BlockIndex : FullBlockWorks) + for (uint32_t BlockIndex : FetchBlockIndexes) { const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; BlobHashesSet.insert(BlockDescription.BlockHash); @@ -6794,6 +6870,162 @@ namespace { } } + std::vector<BlockRangeDescriptor> BlockRangeWorks; + std::vector<uint32_t> FullBlockWorks; + { + Stopwatch Timer; + + std::vector<uint32_t> PartialBlockIndexes; + + for (uint32_t BlockIndex : FetchBlockIndexes) + { + const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; + + const std::vector<uint32_t> BlockChunkIndexNeeded = std::move(AllBlockChunkIndexNeeded[BlockIndex]); + if (!BlockChunkIndexNeeded.empty()) + { + bool WantsToDoPartialBlockDownload = BlockChunkIndexNeeded.size() < BlockDescription.ChunkRawHashes.size(); + bool CanDoPartialBlockDownload = + (BlockDescription.HeaderSize > 0) && + (BlockDescription.ChunkCompressedLengths.size() == BlockDescription.ChunkRawHashes.size()); + + bool AllowedToDoPartialRequest = false; + bool BlockExistInCache = ExistsResult.ExistingBlobs.contains(BlockDescription.BlockHash); + switch (Options.PartialBlockRequestMode) + { + case EPartialBlockRequestMode::Off: + break; + case EPartialBlockRequestMode::ZenCacheOnly: + AllowedToDoPartialRequest = BlockExistInCache; + break; + case EPartialBlockRequestMode::Mixed: + case EPartialBlockRequestMode::All: + AllowedToDoPartialRequest = true; + break; + default: + ZEN_ASSERT(false); + break; + } + + const uint32_t ChunkStartOffsetInBlock = + gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); + + const uint64_t TotalBlockSize = std::accumulate(BlockDescription.ChunkCompressedLengths.begin(), + BlockDescription.ChunkCompressedLengths.end(), + std::uint64_t(ChunkStartOffsetInBlock)); + + if (AllowedToDoPartialRequest && WantsToDoPartialBlockDownload && CanDoPartialBlockDownload) + { + ZEN_TRACE_CPU("UpdateFolder_HandleBlocks_PartialAnalysis"); + + bool LimitToSingleRange = + BlockExistInCache ? false : Options.PartialBlockRequestMode == EPartialBlockRequestMode::Mixed; + uint64_t TotalWantedChunksSize = 0; + std::optional<std::vector<BlockRangeDescriptor>> MaybeBlockRanges = + CalculateBlockRanges(BlockIndex, + BlockDescription, + BlockChunkIndexNeeded, + LimitToSingleRange, + ChunkStartOffsetInBlock, + TotalBlockSize, + TotalWantedChunksSize); + ZEN_ASSERT(TotalWantedChunksSize <= TotalBlockSize); + + if (MaybeBlockRanges.has_value()) + { + const std::vector<BlockRangeDescriptor>& BlockRanges = MaybeBlockRanges.value(); + ZEN_ASSERT(!BlockRanges.empty()); + BlockRangeWorks.insert(BlockRangeWorks.end(), BlockRanges.begin(), BlockRanges.end()); + TotalRequestCount += BlockRanges.size(); + TotalPartWriteCount += BlockRanges.size(); + + uint64_t RequestedSize = std::accumulate( + BlockRanges.begin(), + BlockRanges.end(), + uint64_t(0), + [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; }); + PartialBlockIndexes.push_back(BlockIndex); + + if (RequestedSize > TotalWantedChunksSize) + { + ZEN_CONSOLE_VERBOSE("Requesting {} chunks ({}) from block {} ({}) using {} requests (extra bytes {})", + BlockChunkIndexNeeded.size(), + NiceBytes(RequestedSize), + BlockDescription.BlockHash, + NiceBytes(TotalBlockSize), + BlockRanges.size(), + NiceBytes(RequestedSize - TotalWantedChunksSize)); + } + } + else + { + FullBlockWorks.push_back(BlockIndex); + TotalRequestCount++; + TotalPartWriteCount++; + } + } + else + { + FullBlockWorks.push_back(BlockIndex); + TotalRequestCount++; + TotalPartWriteCount++; + } + } + } + + if (!PartialBlockIndexes.empty()) + { + uint64_t TotalFullBlockRequestBytes = 0; + for (uint32_t BlockIndex : FullBlockWorks) + { + const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; + uint32_t CurrentOffset = + gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); + + TotalFullBlockRequestBytes += std::accumulate(BlockDescription.ChunkCompressedLengths.begin(), + BlockDescription.ChunkCompressedLengths.end(), + std::uint64_t(CurrentOffset)); + } + + uint64_t TotalPartialBlockBytes = 0; + for (uint32_t BlockIndex : PartialBlockIndexes) + { + const ChunkBlockDescription& BlockDescription = BlockDescriptions[BlockIndex]; + uint32_t CurrentOffset = + gsl::narrow<uint32_t>(CompressedBuffer::GetHeaderSizeForNoneEncoder() + BlockDescription.HeaderSize); + + TotalPartialBlockBytes += std::accumulate(BlockDescription.ChunkCompressedLengths.begin(), + BlockDescription.ChunkCompressedLengths.end(), + std::uint64_t(CurrentOffset)); + } + + uint64_t NonPartialTotalBlockBytes = TotalFullBlockRequestBytes + TotalPartialBlockBytes; + + const uint64_t TotalPartialBlockRequestBytes = + std::accumulate(BlockRangeWorks.begin(), + BlockRangeWorks.end(), + uint64_t(0), + [](uint64_t Current, const BlockRangeDescriptor& Range) { return Current + Range.RangeLength; }); + uint64_t TotalExtraPartialBlocksRequests = BlockRangeWorks.size() - PartialBlockIndexes.size(); + + uint64_t TotalSavedBlocksSize = TotalPartialBlockBytes - TotalPartialBlockRequestBytes; + double SavedSizePercent = (TotalSavedBlocksSize * 100.0) / NonPartialTotalBlockBytes; + + if (!IsQuiet) + { + ZEN_CONSOLE( + "Analisys of partial block requests saves download of {} out of {} ({:.1f}%) using {} extra " + "requests. Completed in {}", + NiceBytes(TotalSavedBlocksSize), + NiceBytes(NonPartialTotalBlockBytes), + SavedSizePercent, + TotalExtraPartialBlocksRequests, + NiceTimeSpanMs(ExistsResult.ElapsedTimeMs)); + } + // exit(0); + } + } + BufferedWriteFileCache WriteCache; for (uint32_t ScavengeOpIndex = 0; ScavengeOpIndex < ScavengeCopyOperations.size(); ScavengeOpIndex++) @@ -6802,7 +7034,7 @@ namespace { { break; } - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { Work.ScheduleWork( WritePool, @@ -6865,7 +7097,8 @@ namespace { std::move(LooseChunkHashWork.ChunkTargetPtrs); const uint32_t RemoteChunkIndex = LooseChunkHashWork.RemoteChunkIndex; - if (PrimeCacheOnly && ExistsResult.ExistingBlobs.contains(RemoteContent.ChunkedContent.ChunkHashes[RemoteChunkIndex])) + if (Options.PrimeCacheOnly && + ExistsResult.ExistingBlobs.contains(RemoteContent.ChunkedContent.ChunkHashes[RemoteChunkIndex])) { DownloadStats.RequestsCompleteCount++; continue; @@ -6875,7 +7108,7 @@ namespace { WritePool, [&Storage, &Path, - &ZenFolderPath, + &Options, &RemoteContent, &RemoteLookup, &CacheFolderPath, @@ -6883,7 +7116,6 @@ namespace { &Work, &WritePool, &NetworkPool, - PrimeCacheOnly, &ExistsResult, &DiskStats, &DownloadStats, @@ -6892,8 +7124,6 @@ namespace { RemoteChunkIndex, ChunkTargetPtrs, BuildId = Oid(BuildId), - LargeAttachmentSize, - PreferredMultipartChunkSize, TotalRequestCount, TotalPartWriteCount, &WriteCache, @@ -6903,11 +7133,11 @@ namespace { { ZEN_TRACE_CPU("UpdateFolder_ReadPreDownloaded"); std::filesystem::path ExistingCompressedChunkPath; - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { const IoHash& ChunkHash = RemoteContent.ChunkedContent.ChunkHashes[RemoteChunkIndex]; std::filesystem::path CompressedChunkPath = - ZenTempDownloadFolderPath(ZenFolderPath) / ChunkHash.ToHexString(); + ZenTempDownloadFolderPath(Options.ZenFolderPath) / ChunkHash.ToHexString(); if (IsFile(CompressedChunkPath)) { IoBuffer ExistingCompressedPart = IoBufferBuilder::MakeFromFile(ExistingCompressedChunkPath); @@ -6940,7 +7170,7 @@ namespace { Work.ScheduleWork( WritePool, [&Path, - &ZenFolderPath, + &Options, &RemoteContent, &RemoteLookup, &CacheFolderPath, @@ -6973,7 +7203,7 @@ namespace { CompressedChunkPath)); } - std::filesystem::path TargetFolder = ZenTempCacheFolderPath(ZenFolderPath); + std::filesystem::path TargetFolder = ZenTempCacheFolderPath(Options.ZenFolderPath); bool NeedHashVerify = WriteCompressedChunk(TargetFolder, RemoteContent, RemoteLookup, @@ -7018,10 +7248,9 @@ namespace { Work.ScheduleWork( NetworkPool, [&Path, - &ZenFolderPath, + &Options, &Storage, BuildId = Oid(BuildId), - PrimeCacheOnly, &RemoteContent, &RemoteLookup, &ExistsResult, @@ -7037,8 +7266,6 @@ namespace { TotalRequestCount, &FilteredDownloadedBytesPerSecond, &FilteredWrittenBytesPerSecond, - LargeAttachmentSize, - PreferredMultipartChunkSize, RemoteChunkIndex, ChunkTargetPtrs, &DownloadStats](std::atomic<bool>&) mutable { @@ -7063,7 +7290,7 @@ namespace { { FilteredDownloadedBytesPerSecond.Stop(); } - AsyncWriteDownloadedChunk(ZenFolderPath, + AsyncWriteDownloadedChunk(Options.ZenFolderPath, RemoteContent, RemoteLookup, RemoteChunkIndex, @@ -7080,23 +7307,23 @@ namespace { } else { - if (RemoteContent.ChunkedContent.ChunkRawSizes[RemoteChunkIndex] >= LargeAttachmentSize) + if (RemoteContent.ChunkedContent.ChunkRawSizes[RemoteChunkIndex] >= + Options.LargeAttachmentSize) { ZEN_TRACE_CPU("UpdateFolder_GetLargeChunk"); DownloadLargeBlob(*Storage.BuildStorage, - ZenTempDownloadFolderPath(ZenFolderPath), + ZenTempDownloadFolderPath(Options.ZenFolderPath), BuildId, ChunkHash, - PreferredMultipartChunkSize, + Options.PreferredMultipartChunkSize, Work, NetworkPool, DownloadStats, [&Storage, - &ZenFolderPath, + &Options, &RemoteContent, &RemoteLookup, BuildId, - PrimeCacheOnly, &SequenceIndexChunksLeftToWriteCounters, &WriteCache, &Work, @@ -7124,12 +7351,12 @@ namespace { ZenContentType::kCompressedBinary, CompositeBuffer(SharedBuffer(Payload))); } - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { if (!AbortFlag) { AsyncWriteDownloadedChunk( - ZenFolderPath, + Options.ZenFolderPath, RemoteContent, RemoteLookup, RemoteChunkIndex, @@ -7156,14 +7383,14 @@ namespace { Storage.BuildCacheStorage->PutBuildBlob( BuildId, ChunkHash, - BuildBlob.GetContentType(), + ZenContentType::kCompressedBinary, CompositeBuffer(SharedBuffer(BuildBlob))); } if (!BuildBlob) { throw std::runtime_error(fmt::format("Chunk {} is missing", ChunkHash)); } - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { if (!AbortFlag) { @@ -7175,7 +7402,7 @@ namespace { { FilteredDownloadedBytesPerSecond.Stop(); } - AsyncWriteDownloadedChunk(ZenFolderPath, + AsyncWriteDownloadedChunk(Options.ZenFolderPath, RemoteContent, RemoteLookup, RemoteChunkIndex, @@ -7203,7 +7430,7 @@ namespace { for (size_t CopyDataIndex = 0; CopyDataIndex < CacheCopyDatas.size(); CopyDataIndex++) { - ZEN_ASSERT(!PrimeCacheOnly); + ZEN_ASSERT(!Options.PrimeCacheOnly); if (AbortFlag) { break; @@ -7409,7 +7636,7 @@ namespace { for (uint32_t BlockIndex : CachedChunkBlockIndexes) { - ZEN_ASSERT(!PrimeCacheOnly); + ZEN_ASSERT(!Options.PrimeCacheOnly); if (AbortFlag) { break; @@ -7417,7 +7644,7 @@ namespace { Work.ScheduleWork( WritePool, - [&ZenFolderPath, + [&Options, &CacheFolderPath, &RemoteContent, &RemoteLookup, @@ -7440,7 +7667,7 @@ namespace { FilteredWrittenBytesPerSecond.Start(); std::filesystem::path BlockChunkPath = - ZenTempBlockFolderPath(ZenFolderPath) / BlockDescription.BlockHash.ToHexString(); + ZenTempBlockFolderPath(Options.ZenFolderPath) / BlockDescription.BlockHash.ToHexString(); IoBuffer BlockBuffer = IoBufferBuilder::MakeFromFile(BlockChunkPath); if (!BlockBuffer) { @@ -7482,7 +7709,7 @@ namespace { for (size_t BlockRangeIndex = 0; BlockRangeIndex < BlockRangeWorks.size(); BlockRangeIndex++) { - ZEN_ASSERT(!PrimeCacheOnly); + ZEN_ASSERT(!Options.PrimeCacheOnly); if (AbortFlag) { break; @@ -7493,7 +7720,7 @@ namespace { Work.ScheduleWork( NetworkPool, [&Storage, - &ZenFolderPath, + &Options, BuildId, &RemoteLookup, &BlockDescriptions, @@ -7569,11 +7796,12 @@ namespace { if (!Ec) { BlockBuffer.SetDeleteOnClose(false); - BlockBuffer = {}; - BlockChunkPath = ZenTempBlockFolderPath(ZenFolderPath) / fmt::format("{}_{:x}_{:x}", - BlockDescription.BlockHash, - BlockRange.RangeStart, - BlockRange.RangeLength); + BlockBuffer = {}; + BlockChunkPath = + ZenTempBlockFolderPath(Options.ZenFolderPath) / fmt::format("{}_{:x}_{:x}", + BlockDescription.BlockHash, + BlockRange.RangeStart, + BlockRange.RangeLength); RenameFile(TempBlobPath, BlockChunkPath, Ec); if (Ec) { @@ -7592,10 +7820,10 @@ namespace { { ZEN_TRACE_CPU("UpdateFolder_WriteTempBlock"); // Could not be moved and rather large, lets store it on disk - BlockChunkPath = ZenTempBlockFolderPath(ZenFolderPath) / fmt::format("{}_{:x}_{:x}", - BlockDescription.BlockHash, - BlockRange.RangeStart, - BlockRange.RangeLength); + BlockChunkPath = ZenTempBlockFolderPath(Options.ZenFolderPath) / fmt::format("{}_{:x}_{:x}", + BlockDescription.BlockHash, + BlockRange.RangeStart, + BlockRange.RangeLength); TemporaryFile::SafeWriteFile(BlockChunkPath, BlockBuffer); BlockBuffer = {}; } @@ -7691,7 +7919,7 @@ namespace { break; } - if (PrimeCacheOnly && ExistsResult.ExistingBlobs.contains(BlockDescriptions[BlockIndex].BlockHash)) + if (Options.PrimeCacheOnly && ExistsResult.ExistingBlobs.contains(BlockDescriptions[BlockIndex].BlockHash)) { DownloadStats.RequestsCompleteCount++; continue; @@ -7700,9 +7928,8 @@ namespace { Work.ScheduleWork( NetworkPool, [&Storage, - &ZenFolderPath, + &Options, BuildId, - PrimeCacheOnly, &BlockDescriptions, &WritePartsComplete, TotalPartWriteCount, @@ -7744,7 +7971,7 @@ namespace { { Storage.BuildCacheStorage->PutBuildBlob(BuildId, BlockDescription.BlockHash, - BlockBuffer.GetContentType(), + ZenContentType::kCompressedBinary, CompositeBuffer(SharedBuffer(BlockBuffer))); } } @@ -7763,7 +7990,7 @@ namespace { FilteredDownloadedBytesPerSecond.Stop(); } - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { std::filesystem::path BlockChunkPath; @@ -7779,9 +8006,9 @@ namespace { if (!Ec) { BlockBuffer.SetDeleteOnClose(false); - BlockBuffer = {}; - BlockChunkPath = - ZenTempBlockFolderPath(ZenFolderPath) / BlockDescription.BlockHash.ToHexString(); + BlockBuffer = {}; + BlockChunkPath = ZenTempBlockFolderPath(Options.ZenFolderPath) / + BlockDescription.BlockHash.ToHexString(); RenameFile(TempBlobPath, BlockChunkPath, Ec); if (Ec) { @@ -7800,7 +8027,8 @@ namespace { { ZEN_TRACE_CPU("UpdateFolder_WriteTempBlock"); // Could not be moved and rather large, lets store it on disk - BlockChunkPath = ZenTempBlockFolderPath(ZenFolderPath) / BlockDescription.BlockHash.ToHexString(); + BlockChunkPath = + ZenTempBlockFolderPath(Options.ZenFolderPath) / BlockDescription.BlockHash.ToHexString(); TemporaryFile::SafeWriteFile(BlockChunkPath, BlockBuffer); BlockBuffer = {}; } @@ -7902,11 +8130,11 @@ namespace { (DownloadStats.RequestsCompleteCount == TotalRequestCount) ? "" : fmt::format(" {}bits/s", NiceNum(FilteredDownloadedBytesPerSecond.GetCurrent() * 8)); - std::string WriteDetails = PrimeCacheOnly ? "" - : fmt::format(" {}/{} ({}B/s) written.", - NiceBytes(DiskStats.WriteByteCount.load()), - NiceBytes(BytesToWrite), - NiceNum(FilteredWrittenBytesPerSecond.GetCurrent())); + std::string WriteDetails = Options.PrimeCacheOnly ? "" + : fmt::format(" {}/{} ({}B/s) written.", + NiceBytes(DiskStats.WriteByteCount.load()), + NiceBytes(BytesToWrite), + NiceNum(FilteredWrittenBytesPerSecond.GetCurrent())); std::string Details = fmt::format("{}/{} ({}{}) downloaded.{}", DownloadStats.RequestsCompleteCount.load(), TotalRequestCount, @@ -7914,11 +8142,11 @@ namespace { DownloadRateString, WriteDetails); WriteProgressBar.UpdateState( - {.Task = PrimeCacheOnly ? "Downloading " : "Writing chunks ", + {.Task = Options.PrimeCacheOnly ? "Downloading " : "Writing chunks ", .Details = Details, - .TotalCount = PrimeCacheOnly ? TotalRequestCount : BytesToWrite, - .RemainingCount = PrimeCacheOnly ? (TotalRequestCount - DownloadStats.RequestsCompleteCount.load()) - : (BytesToWrite - DiskStats.WriteByteCount.load()), + .TotalCount = Options.PrimeCacheOnly ? TotalRequestCount : BytesToWrite, + .RemainingCount = Options.PrimeCacheOnly ? (TotalRequestCount - DownloadStats.RequestsCompleteCount.load()) + : (BytesToWrite - DiskStats.WriteByteCount.load()), .Status = ProgressBar::State::CalculateStatus(IsAborted, IsPaused)}, false); }); @@ -7933,7 +8161,7 @@ namespace { return; } - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { uint32_t RawSequencesMissingWriteCount = 0; for (uint32_t SequenceIndex = 0; SequenceIndex < SequenceIndexChunksLeftToWriteCounters.size(); SequenceIndex++) @@ -7978,7 +8206,7 @@ namespace { WriteChunkStats.WriteTimeUs = FilteredWrittenBytesPerSecond.GetElapsedTimeUS(); } - if (PrimeCacheOnly) + if (Options.PrimeCacheOnly) { return; } @@ -8025,7 +8253,7 @@ namespace { ZEN_ASSERT_SLOW(IsFile((Path / LocalContent.Paths[LocalPathIndex]).make_preferred())); - if (!WipeTargetFolder) + if (!Options.WipeTargetFolder) { if (auto RemotePathIt = RemotePathToRemoteIndex.find(LocalPath.generic_string()); RemotePathIt != RemotePathToRemoteIndex.end()) @@ -8064,7 +8292,7 @@ namespace { SkippedCount++; } } - else if (!WipeTargetFolder) + else if (!Options.WipeTargetFolder) { // We don't need it RemoveLocalPathIndexes.push_back(LocalPathIndex); @@ -8085,7 +8313,7 @@ namespace { WorkerThreadPool& WritePool = GetIOWorkerPool(); ProgressBar CacheLocalProgressBar(ProgressMode, "Cache Local Data"); - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); for (uint32_t LocalPathIndex : FilesToCache) { @@ -8147,7 +8375,7 @@ namespace { } } - if (WipeTargetFolder) + if (Options.WipeTargetFolder) { ZEN_TRACE_CPU("UpdateFolder_WipeTarget"); Stopwatch Timer; @@ -8172,7 +8400,7 @@ namespace { WorkerThreadPool& WritePool = GetIOWorkerPool(); ProgressBar RebuildProgressBar(ProgressMode, "Rebuild State"); - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); OutLocalFolderState.Paths.resize(RemoteContent.Paths.size()); OutLocalFolderState.RawSizes.resize(RemoteContent.Paths.size()); @@ -8234,7 +8462,8 @@ namespace { } size_t TargetCount = 1; - while (Targets[TargetOffset + TargetCount].RawHash == Targets[TargetOffset].RawHash) + while ((TargetOffset + TargetCount) < Targets.size() && + (Targets[TargetOffset + TargetCount].RawHash == Targets[TargetOffset].RawHash)) { TargetCount++; } @@ -8637,8 +8866,8 @@ namespace { ChunkedFolderContent GetRemoteContent(StorageInstance& Storage, const Oid& BuildId, const std::vector<std::pair<Oid, std::string>>& BuildParts, - std::string_view IncludeWildcard, - std::string_view ExcludeWildcard, + std::span<const std::string> IncludeWildcards, + std::span<const std::string> ExcludeWildcards, std::unique_ptr<ChunkingController>& OutChunkController, std::vector<ChunkedFolderContent>& OutPartContents, std::vector<ChunkBlockDescription>& OutBlockDescriptions, @@ -8671,8 +8900,8 @@ namespace { const Oid& BuildId, const Oid& BuildPartId, CbObject BuildPartManifest, - std::string_view IncludeWildcard, - std::string_view ExcludeWildcard, + std::span<const std::string> IncludeWildcards, + std::span<const std::string> ExcludeWildcards, ChunkedFolderContent& OutRemoteContent, std::vector<ChunkBlockDescription>& OutBlockDescriptions, std::vector<IoHash>& OutLooseChunkHashes) { @@ -8802,6 +9031,20 @@ namespace { std::string ErrorDescription = fmt::format("All required blocks could not be found, {} blocks does not have metadata in this context.", BlockRawHashes.size() - OutBlockDescriptions.size()); + if (IsVerbose) + { + for (const IoHash& BlockHash : BlockRawHashes) + { + if (auto It = std::find_if( + OutBlockDescriptions.begin(), + OutBlockDescriptions.end(), + [BlockHash](const ChunkBlockDescription& Description) { return Description.BlockHash == BlockHash; }); + It == OutBlockDescriptions.end()) + { + ErrorDescription += fmt::format("\n {}", BlockHash); + } + } + } if (AttemptFallback) { ZEN_CONSOLE_WARN("{} Attemping fallback options.", ErrorDescription); @@ -8878,11 +9121,12 @@ namespace { OutRemoteContent.ChunkedContent.ChunkRawSizes, OutRemoteContent.ChunkedContent.ChunkOrders); + if (!IncludeWildcards.empty() || !ExcludeWildcards.empty()) { std::vector<std::filesystem::path> DeletedPaths; for (const std::filesystem::path& RemotePath : OutRemoteContent.Paths) { - if (!IncludePath(IncludeWildcard, ExcludeWildcard, RemotePath)) + if (!IncludePath(IncludeWildcards, ExcludeWildcards, RemotePath)) { DeletedPaths.push_back(RemotePath); } @@ -8914,8 +9158,8 @@ namespace { BuildId, BuildPartId, BuildPartManifest, - IncludeWildcard, - ExcludeWildcard, + IncludeWildcards, + ExcludeWildcards, OutPartContents[0], OutBlockDescriptions, OutLooseChunkHashes); @@ -8947,8 +9191,8 @@ namespace { BuildId, OverlayBuildPartId, OverlayBuildPartManifest, - IncludeWildcard, - ExcludeWildcard, + IncludeWildcards, + ExcludeWildcards, OverlayPartContent, OverlayPartBlockDescriptions, OverlayPartLooseChunkHashes); @@ -9003,8 +9247,8 @@ namespace { const std::filesystem::path& StateFilePath, ChunkingController& ChunkController, std::span<const std::filesystem::path> ReferencePaths, - std::string_view IncludeWildcard, - std::string_view ExcludeWildcard, + std::span<const std::string> IncludeWildcards, + std::span<const std::string> ExcludeWildcards, FolderContent& OutLocalFolderContent) { FolderContent LocalFolderState; @@ -9028,7 +9272,7 @@ namespace { for (const std::filesystem::path& LocalPath : LocalFolderState.Paths) { - if (IncludePath(IncludeWildcard, ExcludeWildcard, LocalPath)) + if (IncludePath(IncludeWildcards, ExcludeWildcards, LocalPath)) { FileSet.insert(LocalPath.generic_string()); PathsToCheck.push_back(LocalPath); @@ -9037,7 +9281,7 @@ namespace { for (const std::filesystem::path& RemotePath : ReferencePaths) { - if (IncludePath(IncludeWildcard, ExcludeWildcard, RemotePath)) + if (IncludePath(IncludeWildcards, ExcludeWildcards, RemotePath)) { if (FileSet.insert(RemotePath.generic_string()).second) { @@ -9345,21 +9589,27 @@ namespace { return Result; }; + struct DownloadOptions + { + std::filesystem::path SystemRootDir; + std::filesystem::path ZenFolderPath; + bool AllowMultiparts = true; + EPartialBlockRequestMode PartialBlockRequestMode = EPartialBlockRequestMode::Mixed; + bool CleanTargetFolder = false; + bool PostDownloadVerify = false; + bool PrimeCacheOnly = false; + bool EnableOtherDownloadsScavenging = true; + bool EnableTargetFolderScavenging = true; + std::vector<std::string> IncludeWildcards; + std::vector<std::string> ExcludeWildcards; + }; + void DownloadFolder(StorageInstance& Storage, const Oid& BuildId, const std::vector<Oid>& BuildPartIds, std::span<const std::string> BuildPartNames, const std::filesystem::path& Path, - const std::filesystem::path& ZenFolderPath, - const std::filesystem::path SystemRootDir, - bool AllowMultiparts, - bool AllowPartialBlockRequests, - bool WipeTargetFolder, - bool PostDownloadVerify, - bool PrimeCacheOnly, - bool EnableScavenging, - std::string_view IncludeWildcard, - std::string_view ExcludeWildcard) + const DownloadOptions& Options) { ZEN_TRACE_CPU("DownloadFolder"); @@ -9378,18 +9628,19 @@ namespace { auto EndProgress = MakeGuard([&]() { ProgressBar::SetLogOperationProgress(ProgressMode, TaskSteps::StepCount, TaskSteps::StepCount); }); - ZEN_ASSERT((!PrimeCacheOnly) || (PrimeCacheOnly && (!AllowPartialBlockRequests))); + ZEN_ASSERT((!Options.PrimeCacheOnly) || + (Options.PrimeCacheOnly && (Options.PartialBlockRequestMode == EPartialBlockRequestMode::Off))); Stopwatch DownloadTimer; ProgressBar::SetLogOperationProgress(ProgressMode, TaskSteps::CheckState, TaskSteps::StepCount); - const std::filesystem::path ZenTempFolder = ZenTempFolderPath(ZenFolderPath); + const std::filesystem::path ZenTempFolder = ZenTempFolderPath(Options.ZenFolderPath); CreateDirectories(ZenTempFolder); - CreateDirectories(ZenTempBlockFolderPath(ZenFolderPath)); - CreateDirectories(ZenTempCacheFolderPath(ZenFolderPath)); - CreateDirectories(ZenTempDownloadFolderPath(ZenFolderPath)); + CreateDirectories(ZenTempBlockFolderPath(Options.ZenFolderPath)); + CreateDirectories(ZenTempCacheFolderPath(Options.ZenFolderPath)); + CreateDirectories(ZenTempDownloadFolderPath(Options.ZenFolderPath)); std::uint64_t PreferredMultipartChunkSize = 32u * 1024u * 1024u; @@ -9408,19 +9659,19 @@ namespace { ChunkedFolderContent RemoteContent = GetRemoteContent(Storage, BuildId, AllBuildParts, - IncludeWildcard, - ExcludeWildcard, + Options.IncludeWildcards, + Options.ExcludeWildcards, ChunkController, PartContents, BlockDescriptions, LooseChunkHashes); - const std::uint64_t LargeAttachmentSize = AllowMultiparts ? PreferredMultipartChunkSize * 4u : (std::uint64_t)-1; + const std::uint64_t LargeAttachmentSize = Options.AllowMultiparts ? PreferredMultipartChunkSize * 4u : (std::uint64_t)-1; GetFolderContentStatistics LocalFolderScanStats; ChunkingStatistics ChunkingStats; ChunkedFolderContent LocalContent; FolderContent LocalFolderContent; - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { if (IsDir(Path)) { @@ -9433,11 +9684,11 @@ namespace { LocalContent = GetLocalContent(LocalFolderScanStats, ChunkingStats, Path, - ZenStateFilePath(ZenFolderPath), + ZenStateFilePath(Options.ZenFolderPath), *ChunkController, RemoteContent.Paths, - IncludeWildcard, - ExcludeWildcard, + Options.IncludeWildcards, + Options.ExcludeWildcards, LocalFolderContent); } else @@ -9491,7 +9742,7 @@ namespace { return true; }; - if (CompareContent(RemoteContent, LocalContent) && !WipeTargetFolder) + if (Options.EnableTargetFolderScavenging && !Options.CleanTargetFolder && CompareContent(RemoteContent, LocalContent)) { if (!IsQuiet) { @@ -9501,14 +9752,14 @@ namespace { Stopwatch WriteStateTimer; CbObject StateObject = CreateStateObject(BuildId, AllBuildParts, PartContents, LocalFolderContent, Path); - CreateDirectories(ZenStateFilePath(ZenFolderPath).parent_path()); - TemporaryFile::SafeWriteFile(ZenStateFilePath(ZenFolderPath), StateObject.GetView()); + CreateDirectories(ZenStateFilePath(Options.ZenFolderPath).parent_path()); + TemporaryFile::SafeWriteFile(ZenStateFilePath(Options.ZenFolderPath), StateObject.GetView()); if (!IsQuiet) { ZEN_CONSOLE("Wrote local state in {}", NiceTimeSpanMs(WriteStateTimer.GetElapsedTimeMs())); } - AddDownloadedPath(SystemRootDir, BuildId, AllBuildParts, ZenStateFilePath(ZenFolderPath), Path); + AddDownloadedPath(Options.SystemRootDir, BuildId, AllBuildParts, ZenStateFilePath(Options.ZenFolderPath), Path); } else { @@ -9535,21 +9786,22 @@ namespace { ProgressBar::SetLogOperationProgress(ProgressMode, TaskSteps::Download, TaskSteps::StepCount); - UpdateFolder(SystemRootDir, - Storage, + UpdateFolder(Storage, BuildId, Path, - ZenFolderPath, - LargeAttachmentSize, - PreferredMultipartChunkSize, LocalContent, RemoteContent, BlockDescriptions, LooseChunkHashes, - AllowPartialBlockRequests, - WipeTargetFolder, - PrimeCacheOnly, - EnableScavenging, + UpdateOptions{.SystemRootDir = Options.SystemRootDir, + .ZenFolderPath = Options.ZenFolderPath, + .LargeAttachmentSize = LargeAttachmentSize, + .PreferredMultipartChunkSize = PreferredMultipartChunkSize, + .PartialBlockRequestMode = Options.PartialBlockRequestMode, + .WipeTargetFolder = Options.CleanTargetFolder, + .PrimeCacheOnly = Options.PrimeCacheOnly, + .EnableOtherDownloadsScavenging = Options.EnableOtherDownloadsScavenging, + .EnableTargetFolderScavenging = Options.EnableTargetFolderScavenging}, LocalFolderState, DiskStats, CacheMappingStats, @@ -9559,28 +9811,28 @@ namespace { if (!AbortFlag) { - if (!PrimeCacheOnly) + if (!Options.PrimeCacheOnly) { ProgressBar::SetLogOperationProgress(ProgressMode, TaskSteps::Verify, TaskSteps::StepCount); - VerifyFolder(RemoteContent, Path, PostDownloadVerify, VerifyFolderStats); + VerifyFolder(RemoteContent, Path, Options.PostDownloadVerify, VerifyFolderStats); Stopwatch WriteStateTimer; CbObject StateObject = CreateStateObject(BuildId, AllBuildParts, PartContents, LocalFolderState, Path); - CreateDirectories(ZenStateFilePath(ZenFolderPath).parent_path()); - TemporaryFile::SafeWriteFile(ZenStateFilePath(ZenFolderPath), StateObject.GetView()); + CreateDirectories(ZenStateFilePath(Options.ZenFolderPath).parent_path()); + TemporaryFile::SafeWriteFile(ZenStateFilePath(Options.ZenFolderPath), StateObject.GetView()); if (!IsQuiet) { ZEN_CONSOLE("Wrote local state in {}", NiceTimeSpanMs(WriteStateTimer.GetElapsedTimeMs())); } - AddDownloadedPath(SystemRootDir, BuildId, AllBuildParts, ZenStateFilePath(ZenFolderPath), Path); + AddDownloadedPath(Options.SystemRootDir, BuildId, AllBuildParts, ZenStateFilePath(Options.ZenFolderPath), Path); #if 0 ExtendableStringBuilder<1024> SB; CompactBinaryToJson(StateObject, SB); - WriteFile(ZenStateFileJsonPath(ZenFolderPath), IoBuffer(IoBuffer::Wrap, SB.Data(), SB.Size())); + WriteFile(ZenStateFileJsonPath(Options.ZenFolderPath), IoBuffer(IoBuffer::Wrap, SB.Data(), SB.Size())); #endif // 0 } const uint64_t DownloadCount = DownloadStats.DownloadedChunkCount.load() + DownloadStats.DownloadedBlockCount.load() + @@ -9619,7 +9871,7 @@ namespace { } } } - if (PrimeCacheOnly) + if (Options.PrimeCacheOnly) { if (Storage.BuildCacheStorage) { @@ -9652,8 +9904,8 @@ namespace { const Oid& BuildId, const std::vector<Oid>& BuildPartIds, std::span<const std::string> BuildPartNames, - std::string_view IncludeWildcard, - std::string_view ExcludeWildcard) + std::span<const std::string> IncludeWildcards, + std::span<const std::string> ExcludeWildcards) { std::uint64_t PreferredMultipartChunkSize = 32u * 1024u * 1024u; @@ -9716,7 +9968,7 @@ namespace { for (size_t Index : Order) { const std::filesystem::path& Path = Paths[Index]; - if (IncludePath(IncludeWildcard, ExcludeWildcard, Path)) + if (IncludePath(IncludeWildcards, ExcludeWildcards, Path)) { const IoHash& RawHash = RawHashes[Index]; const uint64_t RawSize = RawSizes[Index]; @@ -9932,81 +10184,9 @@ BuildsCommand::BuildsCommand() "<usesparsefiles>"); }; - auto AddAuthOptions = [this](cxxopts::Options& Ops) { - // Direct access token (may expire) - Ops.add_option("auth-token", - "", - "access-token", - "Cloud/Builds Storage access token", - cxxopts::value(m_AccessToken), - "<accesstoken>"); - Ops.add_option("auth-token", - "", - "access-token-env", - "Name of environment variable that holds the cloud/builds Storage access token", - cxxopts::value(m_AccessTokenEnv)->default_value(DefaultAccessTokenEnvVariableName), - "<envvariable>"); - Ops.add_option("auth-token", - "", - "access-token-path", - "Path to json file that holds the cloud/builds Storage access token", - cxxopts::value(m_AccessTokenPath), - "<filepath>"); - - // Auth manager token encryption - Ops.add_option("security", - "", - "encryption-aes-key", - "256 bit AES encryption key", - cxxopts::value<std::string>(m_EncryptionKey), - ""); - Ops.add_option("security", - "", - "encryption-aes-iv", - "128 bit AES encryption initialization vector", - cxxopts::value<std::string>(m_EncryptionIV), - ""); + auto AddCloudOptions = [this](cxxopts::Options& Ops) { + m_AuthOptions.AddOptions(Ops); - // OpenId acccess token - Ops.add_option("openid", - "", - "openid-provider-name", - "Open ID provider name", - cxxopts::value<std::string>(m_OpenIdProviderName), - "Default"); - Ops.add_option("openid", "", "openid-provider-url", "Open ID provider url", cxxopts::value<std::string>(m_OpenIdProviderUrl), ""); - Ops.add_option("openid", "", "openid-client-id", "Open ID client id", cxxopts::value<std::string>(m_OpenIdClientId), ""); - Ops.add_option("openid", - "", - "openid-refresh-token", - "Open ID refresh token", - cxxopts::value<std::string>(m_OpenIdRefreshToken), - ""); - - // OAuth acccess token - Ops.add_option("oauth", "", "oauth-url", "OAuth provier url", cxxopts::value<std::string>(m_OAuthUrl)->default_value(""), ""); - Ops.add_option("oauth", - "", - "oauth-clientid", - "OAuth client id", - cxxopts::value<std::string>(m_OAuthClientId)->default_value(""), - ""); - Ops.add_option("oauth", - "", - "oauth-clientsecret", - "OAuth client secret", - cxxopts::value<std::string>(m_OAuthClientSecret)->default_value(""), - ""); - Ops.add_option("auth", - "", - "oidctoken-exe-path", - "Path to OidcToken executable", - cxxopts::value<std::string>(m_OidcTokenAuthExecutablePath)->default_value(""), - ""); - }; - - auto AddCloudOptions = [this, &AddAuthOptions](cxxopts::Options& Ops) { - AddAuthOptions(Ops); Ops.add_option("cloud build", "", "override-host", "Cloud Builds URL", cxxopts::value(m_OverrideHost), "<override-host>"); Ops.add_option("cloud build", "", @@ -10086,18 +10266,44 @@ BuildsCommand::BuildsCommand() Ops.add_option("", "", "wildcard", - "Windows style wildcard (using * and ?) to match file paths to include", + "Windows style wildcard(s) (using * and ?) to match file paths to include, separated by ;", cxxopts::value(m_IncludeWildcard), "<wildcard>"); Ops.add_option("", "", "exclude-wildcard", - "Windows style wildcard (using * and ?) to match file paths to exclude. Applied after --wildcard include filter", + "Windows style wildcard(s) (using * and ?) to match file paths to exclude, separated by ;. Applied after --wildcard " + "include filter", cxxopts::value(m_ExcludeWildcard), "<excludewildcard>"); }; + auto AddMultipartOptions = [this](cxxopts::Options& Ops) { + Ops.add_option("", + "", + "allow-multipart", + "Allow large attachments to be transfered using multipart protocol. Defaults to true.", + cxxopts::value(m_AllowMultiparts), + "<allowmultipart>"); + }; + + auto AddPartialBlockRequestOptions = [this](cxxopts::Options& Ops) { + Ops.add_option("", + "", + "allow-partial-block-requests", + "Allow request for partial chunk blocks.\n" + " false = only full block requests allowed\n" + " mixed = multiple partial block ranges requests per block allowed to zen cache, single partial block range " + "request per block to host\n" + " zencacheonly = multiple partial block ranges requests per block allowed to zen cache, only full block requests " + "allowed to host\n" + " true = multiple partial block ranges requests per block allowed to zen cache and host\n" + "Defaults to 'mixed'.", + cxxopts::value(m_AllowPartialBlockRequests), + "<allowpartialblockrequests>"); + }; + m_Options.add_option("", "v", "verb", @@ -10151,6 +10357,25 @@ BuildsCommand::BuildsCommand() m_ListOptions.parse_positional({"query-path", "result-path"}); m_ListOptions.positional_help("query-path result-path"); + // list-blocks + AddSystemOptions(m_ListBlocksOptions); + AddCloudOptions(m_ListBlocksOptions); + AddZenFolderOptions(m_ListBlocksOptions); + m_ListBlocksOptions.add_options()("h,help", "Print help"); + m_ListBlocksOptions.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), "<id>"); + m_ListBlocksOptions.add_option("", + "", + "result-path", + "Path to json or compactbinary to write query result to", + cxxopts::value(m_ListResultPath), + "<result-path>"); + + m_ListBlocksOptions + .add_option("", "", "max-count", "Maximum number of blocks to list", cxxopts::value(m_ListBlocksMaxCount), "<maxcount>"); + + m_ListBlocksOptions.parse_positional({"build-id"}); + m_ListBlocksOptions.positional_help("build-id"); + // upload AddSystemOptions(m_UploadOptions); AddCloudOptions(m_UploadOptions); @@ -10200,12 +10425,9 @@ BuildsCommand::BuildsCommand() "Percent of an existing block that must be relevant for it to be resused. Defaults to 85.", cxxopts::value(m_BlockReuseMinPercentLimit), "<minreuse>"); - m_UploadOptions.add_option("", - "", - "allow-multipart", - "Allow large attachments to be transfered using multipart protocol. Defaults to true.", - cxxopts::value(m_AllowMultiparts), - "<allowmultipart>"); + + AddMultipartOptions(m_UploadOptions); + m_UploadOptions.add_option("", "", "manifest-path", @@ -10258,20 +10480,21 @@ BuildsCommand::BuildsCommand() "all parts will be downloaded", cxxopts::value(m_BuildPartNames), "<name>"); - m_DownloadOptions - .add_option("", "", "clean", "Delete all data in target folder before downloading", cxxopts::value(m_Clean), "<clean>"); m_DownloadOptions.add_option("", "", - "allow-multipart", - "Allow large attachments to be transfered using multipart protocol. Defaults to true.", - cxxopts::value(m_AllowMultiparts), - "<allowmultipart>"); + "clean", + "Delete all data in target folder that is not part of the downloaded content", + cxxopts::value(m_Clean), + "<clean>"); m_DownloadOptions.add_option("", "", - "allow-partial-block-requests", - "Allow request for partial chunk blocks. Defaults to true.", - cxxopts::value(m_AllowPartialBlockRequests), - "<allowpartialblockrequests>"); + "force", + "Force download of all content by ignoring any existing local content", + cxxopts::value(m_Force), + "<force>"); + AddMultipartOptions(m_DownloadOptions); + + AddPartialBlockRequestOptions(m_DownloadOptions); m_DownloadOptions .add_option("", "", "verify", "Enable post download verify of all tracked files", cxxopts::value(m_PostDownloadVerify), "<verify>"); @@ -10338,18 +10561,8 @@ BuildsCommand::BuildsCommand() AddWorkerOptions(m_TestOptions); m_TestOptions.add_options()("h,help", "Print help"); m_TestOptions.add_option("", "l", "local-path", "Root file system folder used as base", cxxopts::value(m_Path), "<local-path>"); - m_TestOptions.add_option("", - "", - "allow-multipart", - "Allow large attachments to be transfered using multipart protocol. Defaults to true.", - cxxopts::value(m_AllowMultiparts), - "<allowmultipart>"); - m_TestOptions.add_option("", - "", - "allow-partial-block-requests", - "Allow request for partial chunk blocks. Defaults to true.", - cxxopts::value(m_AllowPartialBlockRequests), - "<allowpartialblockrequests>"); + AddMultipartOptions(m_TestOptions); + AddPartialBlockRequestOptions(m_TestOptions); m_TestOptions.add_option("", "", "enable-scavenge", @@ -10433,7 +10646,7 @@ BuildsCommand::BuildsCommand() BuildsCommand::~BuildsCommand() = default; -int +void BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); @@ -10450,52 +10663,49 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) int ParentCommandArgCount = GetSubCommand(m_Options, argc, argv, m_SubCommands, SubOption, SubCommandArguments); if (!ParseOptions(ParentCommandArgCount, argv)) { - return 0; + return; } if (SubOption == nullptr) { - throw zen::OptionParseException("command verb is missing"); + throw OptionParseException("'verb' option is required", m_Options.help()); } if (!ParseOptions(*SubOption, gsl::narrow<int>(SubCommandArguments.size()), SubCommandArguments.data())) { - return 0; + return; } - std::filesystem::path SystemRootDir; - auto ParseSystemOptions = [&]() { - if (m_SystemRootDir.empty()) - { - m_SystemRootDir = PickDefaultSystemRootDirectory(); - } - MakeSafeAbsolutePathÍnPlace(m_SystemRootDir); + auto ParseSystemOptions = [&]() { + if (m_SystemRootDir.empty()) + { + m_SystemRootDir = PickDefaultSystemRootDirectory(); + } + MakeSafeAbsolutePathÍnPlace(m_SystemRootDir); }; ParseSystemOptions(); auto ParseStorageOptions = [&](bool RequireNamespace, bool RequireBucket) { - m_Host = RemoveQuotes(m_Host); - m_OverrideHost = RemoveQuotes(m_OverrideHost); - m_Url = RemoveQuotes(m_Url); - m_Namespace = RemoveQuotes(m_Namespace); - m_Bucket = RemoveQuotes(m_Bucket); if (!m_Url.empty()) { if (!m_Host.empty()) { - throw zen::OptionParseException(fmt::format("host is not compatible with the url option\n{}", SubOption->help())); + throw OptionParseException(fmt::format("'--host' ('{}') conflicts with '--url' ('{}')", m_Host, m_Url), SubOption->help()); } if (!m_Bucket.empty()) { - throw zen::OptionParseException(fmt::format("bucket is not compatible with the url option\n{}", SubOption->help())); + throw OptionParseException(fmt::format("'--bucket' ('{}') conflicts with '--url' ('{}')", m_Bucket, m_Url), + SubOption->help()); } if (!m_BuildId.empty()) { - throw zen::OptionParseException(fmt::format("buildid is not compatible with the url option\n{}", SubOption->help())); + throw OptionParseException(fmt::format("'--buildid' ('{}') conflicts with '--url' ('{}')", m_BuildId, m_Url), + SubOption->help()); } - if (!ParseCloudUrl(m_Url, m_Host, m_Namespace, m_Bucket, m_BuildId)) + if (!ParseBuildStorageUrl(m_Url, m_Host, m_Namespace, m_Bucket, m_BuildId)) { - throw zen::OptionParseException(fmt::format("url does not match the Cloud Artifact URL format\n{}", SubOption->help())); + throw OptionParseException("'--url' ('{}') is malformed, it does not match the Cloud Artifact URL format", + SubOption->help()); } } @@ -10503,183 +10713,51 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (!m_StoragePath.empty()) { - throw zen::OptionParseException( - fmt::format("host/url/override-host is not compatible with the storage-path option\n{}", SubOption->help())); + throw OptionParseException( + fmt::format("'--storage-path' ('{}') conflicts with '--host'/'--url'/'--override-host' options", m_StoragePath), + SubOption->help()); } if (RequireNamespace && m_Namespace.empty()) { - throw zen::OptionParseException(fmt::format("namespace option is required for this storage option\n{}", SubOption->help())); + throw OptionParseException("'--namespace' is required", SubOption->help()); } if (RequireBucket && m_Bucket.empty()) { - throw zen::OptionParseException(fmt::format("bucket option is required for this storage option\n{}", SubOption->help())); + throw OptionParseException("'--bucket' is required", SubOption->help()); } } else if (m_StoragePath.empty()) { - throw zen::OptionParseException(fmt::format("At least one storage option is required\n{}", SubOption->help())); + throw OptionParseException("'--host', '--url', '--override-host' or '--storage-path' is required", SubOption->help()); } MakeSafeAbsolutePathÍnPlace(m_StoragePath); }; - std::unique_ptr<AuthMgr> Auth; - HttpClientSettings ClientSettings{.LogCategory = "httpbuildsclient", - .AssumeHttp2 = m_AssumeHttp2, - .AllowResume = true, - .RetryCount = 2}; - - auto CreateAuthMgr = [&]() { - ZEN_ASSERT(!m_SystemRootDir.empty()); - if (!Auth) - { - if (m_EncryptionKey.empty()) - { - m_EncryptionKey = "abcdefghijklmnopqrstuvxyz0123456"; - if (!IsQuiet) - { - ZEN_CONSOLE_WARN("Using default encryption key"); - } - } - - if (m_EncryptionIV.empty()) - { - m_EncryptionIV = "0123456789abcdef"; - if (!IsQuiet) - { - ZEN_CONSOLE_WARN("Using default encryption initialization vector"); - } - } - - AuthConfig AuthMgrConfig = {.RootDirectory = m_SystemRootDir / "auth", - .EncryptionKey = AesKey256Bit::FromString(m_EncryptionKey), - .EncryptionIV = AesIV128Bit::FromString(m_EncryptionIV)}; - if (!AuthMgrConfig.EncryptionKey.IsValid()) - { - throw zen::OptionParseException("Invalid AES encryption key"); - } - if (!AuthMgrConfig.EncryptionIV.IsValid()) - { - throw zen::OptionParseException("Invalid AES initialization vector"); - } - Auth = AuthMgr::Create(AuthMgrConfig); - } - }; - - auto ParseAuthOptions = [&]() { - m_OpenIdProviderUrl = RemoveQuotes(m_OpenIdProviderUrl); - m_OpenIdClientId = RemoveQuotes(m_OpenIdClientId); - - m_AccessToken = RemoveQuotes(m_AccessToken); - m_EncryptionKey = RemoveQuotes(m_EncryptionKey); - m_EncryptionIV = RemoveQuotes(m_EncryptionIV); - - m_OAuthUrl = RemoveQuotes(m_OAuthUrl); - m_OAuthClientId = RemoveQuotes(m_OAuthClientId); - m_OAuthClientSecret = RemoveQuotes(m_OAuthClientSecret); - - m_OidcTokenAuthExecutablePath = RemoveQuotes(m_OidcTokenAuthExecutablePath); - - if (!m_OpenIdProviderUrl.empty() && !m_OpenIdClientId.empty()) - { - CreateAuthMgr(); - std::string ProviderName = m_OpenIdProviderName.empty() ? "Default" : m_OpenIdProviderName; - Auth->AddOpenIdProvider({.Name = ProviderName, .Url = m_OpenIdProviderUrl, .ClientId = m_OpenIdClientId}); - if (!m_OpenIdRefreshToken.empty()) - { - Auth->AddOpenIdToken({.ProviderName = ProviderName, .RefreshToken = m_OpenIdRefreshToken}); - } - } - - auto GetEnvAccessToken = [](const std::string& AccessTokenEnv) -> std::string { - if (!AccessTokenEnv.empty()) - { - return GetEnvVariable(AccessTokenEnv); - } - return {}; - }; - - auto FindOidcTokenExePath = [](const std::string& OidcTokenAuthExecutablePath) -> std::filesystem::path { - if (OidcTokenAuthExecutablePath.empty()) - { - const std::string OidcExecutableName = "OidcToken" ZEN_EXE_SUFFIX_LITERAL; - std::filesystem::path OidcTokenPath = (GetRunningExecutablePath().parent_path() / OidcExecutableName).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - } - else - { - std::filesystem::path OidcTokenPath = std::filesystem::absolute(StringToPath(OidcTokenAuthExecutablePath)).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - } - return {}; - }; - - if (!m_AccessToken.empty()) - { - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(m_AccessToken); - } - else if (!m_AccessTokenPath.empty()) - { - MakeSafeAbsolutePathÍnPlace(m_AccessTokenPath); - std::string ResolvedAccessToken = ReadAccessTokenFromFile(m_AccessTokenPath); - if (!ResolvedAccessToken.empty()) - { - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(ResolvedAccessToken); - } - } - else if (!m_OAuthUrl.empty()) - { - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOAuthClientCredentials( - {.Url = m_OAuthUrl, .ClientId = m_OAuthClientId, .ClientSecret = m_OAuthClientSecret}); - } - else if (!m_OpenIdProviderName.empty()) - { - CreateAuthMgr(); - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOpenIdProvider(*Auth, m_OpenIdProviderName); - } - else if (std::string ResolvedAccessToken = GetEnvAccessToken(m_AccessTokenEnv); !ResolvedAccessToken.empty()) + auto ParseOutputOptions = [&]() { + if (m_Verbose && m_Quiet) { - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromStaticToken(ResolvedAccessToken); + throw OptionParseException("'--verbose' conflicts with '--quiet'", SubOption->help()); } - else if (std::filesystem::path OidcTokenExePath = FindOidcTokenExePath(m_OidcTokenAuthExecutablePath); !OidcTokenExePath.empty()) + if (m_LogProgress && m_PlainProgress) { - const std::string& CloudHost = m_OverrideHost.empty() ? m_Host : m_OverrideHost; - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromOidcTokenExecutable(OidcTokenExePath, CloudHost, IsQuiet); + throw OptionParseException("'--plain-progress' conflicts with '--log-progress'", SubOption->help()); } - - if (!ClientSettings.AccessTokenProvider) + if (m_LogProgress && m_Quiet) { - CreateAuthMgr(); - ClientSettings.AccessTokenProvider = httpclientauth::CreateFromDefaultOpenIdProvider(*Auth); + throw OptionParseException("'--quiet' conflicts with '--log-progress'", SubOption->help()); } - }; - - auto ParseOutputOptions = [&]() { - if (m_Verbose && m_Quiet) + if (m_PlainProgress && m_Quiet) { - throw std::runtime_error("--verbose option is not compatible with --quiet option"); + throw OptionParseException("'--quiet' conflicts with '--plain-progress'", SubOption->help()); } IsVerbose = m_Verbose; IsQuiet = m_Quiet; if (m_LogProgress) { - if (IsQuiet) - { - throw std::runtime_error("--quiet option is not compatible with --log-progress option"); - } ProgressMode = ProgressBar::Mode::Log; } - if (m_PlainProgress) + else if (m_PlainProgress) { - if (IsQuiet) - { - throw std::runtime_error("--quiet option is not compatible with --plain-progress option"); - } ProgressMode = ProgressBar::Mode::Plain; } else if (m_Verbose) @@ -10697,6 +10775,12 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) }; ParseOutputOptions(); + std::unique_ptr<AuthMgr> Auth; + HttpClientSettings ClientSettings{.LogCategory = "httpbuildsclient", + .AssumeHttp2 = m_AssumeHttp2, + .AllowResume = true, + .RetryCount = 2}; + auto CreateBuildStorage = [&](BuildStorage::Statistics& StorageStats, BuildStorageCache::Statistics& StorageCacheStats, const std::filesystem::path& TempPath, @@ -10704,8 +10788,6 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) bool RequireBucket) -> StorageInstance { ParseStorageOptions(RequireNamespace, RequireBucket); - m_ZenCacheHost = RemoveQuotes(m_ZenCacheHost); - StorageInstance Result; std::string BuildStorageName = ZEN_CLOUD_STORAGE; @@ -10716,123 +10798,78 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!m_Host.empty() || !m_OverrideHost.empty()) { - ParseAuthOptions(); + m_AuthOptions.ParseOptions(*SubOption, + m_SystemRootDir, + ClientSettings, + m_OverrideHost.empty() ? m_Host : m_OverrideHost, + Auth, + IsQuiet, + /*Hidden*/ false); } std::string CloudHost; - auto TestCacheEndpoint = [](std::string_view BaseUrl, const bool AssumeHttp2) -> std::pair<bool, std::string> { - HttpClientSettings TestClientSettings{.LogCategory = "httpcacheclient", - .ConnectTimeout = std::chrono::milliseconds{1000}, - .Timeout = std::chrono::milliseconds{2000}, - .AssumeHttp2 = AssumeHttp2, - .AllowResume = true, - .RetryCount = 0}; - HttpClient TestHttpClient(BaseUrl, TestClientSettings); - HttpClient::Response TestResponse = TestHttpClient.Get("/status/builds"); - if (TestResponse.IsSuccess()) - { - return {true, ""}; - } - return {false, TestResponse.ErrorMessage("")}; - }; - if (!m_Host.empty()) { if (m_OverrideHost.empty() || m_ZenCacheHost.empty()) { - HttpClient DiscoveryHttpClient(m_Host, ClientSettings); - HttpClient::Response ServerInfoResponse = - DiscoveryHttpClient.Get("/api/v1/status/servers", HttpClient::Accept(HttpContentType::kJSON)); - if (!ServerInfoResponse.IsSuccess()) - { - ServerInfoResponse.ThrowError(fmt::format("Failed to get list of servers from discovery url '{}'", m_Host)); - } - - std::string_view JsonResponse = ServerInfoResponse.AsText(); - CbObject ResponseObjectView = LoadCompactBinaryFromJson(JsonResponse).AsObject(); - - auto TestHostEndpoint = [](std::string_view BaseUrl, const bool AssumeHttp2) -> std::pair<bool, std::string> { - HttpClientSettings TestClientSettings{.LogCategory = "httpbuildsclient", - .ConnectTimeout = std::chrono::milliseconds{1000}, - .Timeout = std::chrono::milliseconds{2000}, - .AssumeHttp2 = AssumeHttp2, - .AllowResume = true, - .RetryCount = 0}; - - HttpClient TestHttpClient(BaseUrl, TestClientSettings); - HttpClient::Response TestResponse = TestHttpClient.Get("/health/live"); - if (TestResponse.IsSuccess()) - { - return {true, ""}; - } - return {false, TestResponse.ErrorMessage("")}; - }; + JupiterServerDiscovery Response = DiscoverJupiterEndpoints(m_Host, ClientSettings); if (m_OverrideHost.empty()) { - CbArrayView ServerEndpointsArray = ResponseObjectView["serverEndpoints"sv].AsArrayView(); - std::uint64_t ServerCount = ServerEndpointsArray.Num(); - if (ServerCount == 0) + if (Response.ServerEndPoints.empty()) { throw std::runtime_error(fmt::format("Failed to find any builds hosts at {}", m_Host)); } - for (CbFieldView ServerEndpointView : ServerEndpointsArray) + for (const JupiterServerDiscovery::EndPoint& ServerEndpoint : Response.ServerEndPoints) { - CbObjectView ServerEndpointObject = ServerEndpointView.AsObjectView(); - - std::string_view BaseUrl = ServerEndpointObject["baseUrl"sv].AsString(); - if (!BaseUrl.empty()) + if (!ServerEndpoint.BaseUrl.empty()) { - const bool AssumeHttp2 = ServerEndpointObject["assumeHttp2"sv].AsBool(false); - std::string_view Name = ServerEndpointObject["name"sv].AsString(); - if (auto TestResult = TestHostEndpoint(BaseUrl, AssumeHttp2); TestResult.first) + if (JupiterEndpointTestResult TestResult = + TestJupiterEndpoint(ServerEndpoint.BaseUrl, ServerEndpoint.AssumeHttp2); + TestResult.Success) { - CloudHost = BaseUrl; - m_AssumeHttp2 = AssumeHttp2; - BuildStorageName = Name; + CloudHost = ServerEndpoint.BaseUrl; + m_AssumeHttp2 = ServerEndpoint.AssumeHttp2; + BuildStorageName = ServerEndpoint.Name; break; } else { - ZEN_DEBUG("Unable to reach host {}. Reason: {}", BaseUrl, TestResult.second); + ZEN_DEBUG("Unable to reach host {}. Reason: {}", ServerEndpoint.BaseUrl, TestResult.FailureReason); } } } if (CloudHost.empty()) { - throw std::runtime_error( - fmt::format("Failed to find any usable builds hosts out of {} using {}", ServerCount, m_Host)); + throw std::runtime_error(fmt::format("Failed to find any usable builds hosts out of {} using {}", + Response.ServerEndPoints.size(), + m_Host)); } } - else if (auto TestResult = TestHostEndpoint(m_OverrideHost, m_AssumeHttp2); TestResult.first) + else if (JupiterEndpointTestResult TestResult = TestJupiterEndpoint(m_OverrideHost, m_AssumeHttp2); TestResult.Success) { CloudHost = m_OverrideHost; } else { - throw std::runtime_error(fmt::format("Host {} could not be reached. Reason: {}", m_OverrideHost, TestResult.second)); + throw std::runtime_error( + fmt::format("Host {} could not be reached. Reason: {}", m_OverrideHost, TestResult.FailureReason)); } if (m_ZenCacheHost.empty()) { - CbArrayView CacheEndpointsArray = ResponseObjectView["cacheEndpoints"sv].AsArrayView(); - std::uint64_t CacheCount = CacheEndpointsArray.Num(); - for (CbFieldView CacheEndpointView : CacheEndpointsArray) + for (const JupiterServerDiscovery::EndPoint& CacheEndpoint : Response.CacheEndPoints) { - CbObjectView CacheEndpointObject = CacheEndpointView.AsObjectView(); - - std::string_view BaseUrl = CacheEndpointObject["baseUrl"sv].AsString(); - if (!BaseUrl.empty()) + if (!CacheEndpoint.BaseUrl.empty()) { - const bool AssumeHttp2 = CacheEndpointObject["assumeHttp2"sv].AsBool(false); - std::string_view Name = CacheEndpointObject["name"sv].AsString(); - - if (auto TestResult = TestCacheEndpoint(BaseUrl, AssumeHttp2); TestResult.first) + if (ZenCacheEndpointTestResult TestResult = + TestZenCacheEndpoint(CacheEndpoint.BaseUrl, CacheEndpoint.AssumeHttp2); + TestResult.Success) { - m_ZenCacheHost = BaseUrl; - CacheAssumeHttp2 = AssumeHttp2; - BuildCacheName = Name; + m_ZenCacheHost = CacheEndpoint.BaseUrl; + CacheAssumeHttp2 = CacheEndpoint.AssumeHttp2; + BuildCacheName = CacheEndpoint.Name; break; } } @@ -10847,7 +10884,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { std::string ZenServerLocalHostUrl = fmt::format("http://127.0.0.1:{}", Entry.EffectiveListenPort.load()); - if (auto TestResult = TestCacheEndpoint(ZenServerLocalHostUrl, false); TestResult.first) + if (ZenCacheEndpointTestResult TestResult = TestZenCacheEndpoint(ZenServerLocalHostUrl, false); + TestResult.Success) { m_ZenCacheHost = ZenServerLocalHostUrl; CacheAssumeHttp2 = false; @@ -10858,11 +10896,13 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } if (m_ZenCacheHost.empty() && !IsQuiet) { - ZEN_CONSOLE_WARN("Failed to find any usable cache hosts out of {} using {}", CacheCount, m_Host); + ZEN_CONSOLE_WARN("Failed to find any usable cache hosts out of {} using {}", + Response.CacheEndPoints.size(), + m_Host); } } } - else if (auto TestResult = TestCacheEndpoint(m_ZenCacheHost, false); TestResult.first) + else if (ZenCacheEndpointTestResult TestResult = TestZenCacheEndpoint(m_ZenCacheHost, false); TestResult.Success) { std::string::size_type HostnameStart = 0; std::string::size_type HostnameLength = std::string::npos; @@ -10878,9 +10918,14 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } else { - ZEN_CONSOLE_WARN("Unable to reach cache host {}. Reason: {}", m_ZenCacheHost, TestResult.second); + ZEN_CONSOLE_WARN("Unable to reach cache host {}. Reason: {}", m_ZenCacheHost, TestResult.FailureReason); + m_ZenCacheHost = ""; } } + else if (!m_OverrideHost.empty()) + { + CloudHost = m_OverrideHost; + } } else { @@ -10913,37 +10958,46 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } else { - throw zen::OptionParseException(fmt::format("Storage option is missing\n{}", SubOption->help())); + throw OptionParseException("'--host', '--url', '--override-host' or '--storage-path' is required", SubOption->help()); } if (!m_ZenCacheHost.empty()) { - Result.CacheHttp = std::make_unique<HttpClient>(m_ZenCacheHost, - HttpClientSettings{.LogCategory = "httpcacheclient", - .ConnectTimeout = std::chrono::milliseconds{3000}, - .Timeout = std::chrono::milliseconds{30000}, - .AssumeHttp2 = CacheAssumeHttp2, - .AllowResume = true, - .RetryCount = 0}); - Result.BuildCacheStorage = CreateZenBuildStorageCache(*Result.CacheHttp, - StorageCacheStats, - m_Namespace, - m_Bucket, - TempPath / "zencache", - m_PrimeCacheOnly); - CacheDescription = fmt::format("Zen {}{}. SessionId: '{}'", - BuildCacheName.empty() ? "" : fmt::format("{}, ", BuildCacheName), - m_ZenCacheHost, - Result.CacheHttp->GetSessionId()); - - if (!m_Namespace.empty()) - { - CacheDescription += fmt::format(". Namespace '{}'", m_Namespace); + if (ZenCacheEndpointTestResult TestResult = TestZenCacheEndpoint(m_ZenCacheHost, false); TestResult.Success) + { + Result.CacheHttp = std::make_unique<HttpClient>(m_ZenCacheHost, + HttpClientSettings{.LogCategory = "httpcacheclient", + .ConnectTimeout = std::chrono::milliseconds{3000}, + .Timeout = std::chrono::milliseconds{30000}, + .AssumeHttp2 = CacheAssumeHttp2, + .AllowResume = true, + .RetryCount = 0}); + Result.BuildCacheStorage = CreateZenBuildStorageCache( + *Result.CacheHttp, + StorageCacheStats, + m_Namespace, + m_Bucket, + TempPath / "zencache", + m_PrimeCacheOnly ? GetSmallWorkerPool(EWorkloadType::Background) : GetTinyWorkerPool(EWorkloadType::Background)); + CacheDescription = fmt::format("Zen {}{}. SessionId: '{}'", + BuildCacheName.empty() ? "" : fmt::format("{}, ", BuildCacheName), + m_ZenCacheHost, + Result.CacheHttp->GetSessionId()); + + if (!m_Namespace.empty()) + { + CacheDescription += fmt::format(". Namespace '{}'", m_Namespace); + } + if (!m_Bucket.empty()) + { + CacheDescription += fmt::format(" Bucket '{}'", m_Bucket); + } + Result.CacheName = BuildCacheName.empty() ? m_ZenCacheHost : BuildCacheName; } - if (!m_Bucket.empty()) + else { - CacheDescription += fmt::format(" Bucket '{}'", m_Bucket); + ZEN_CONSOLE_WARN("Unable to reach cache host {}. Reason: {}", m_ZenCacheHost, TestResult.FailureReason); + m_ZenCacheHost = ""; } - Result.CacheName = BuildCacheName.empty() ? m_ZenCacheHost : BuildCacheName; } if (!IsQuiet) { @@ -10959,62 +11013,77 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) auto ParsePath = [&]() { if (m_Path.empty()) { - throw zen::OptionParseException(fmt::format("local-path is required\n{}", SubOption->help())); + throw OptionParseException("'--local-path' is required", SubOption->help()); } MakeSafeAbsolutePathÍnPlace(m_Path); }; - auto ParseFileFilters = [&]() { - for (auto It = begin(m_IncludeWildcard); It != end(m_IncludeWildcard); It++) - { - if (*It == '\\') - { - *It = '/'; - } - } + auto ParseFileFilters = [&](std::vector<std::string>& OutIncludeWildcards, std::vector<std::string>& OutExcludeWildcards) { + auto SplitWildcard = [](const std::string_view Wildcard) -> std::vector<std::string> { + std::vector<std::string> Wildcards; + ForEachStrTok(Wildcard, ';', [&Wildcards](std::string_view Wildcard) { + if (!Wildcard.empty()) + { + std::string CleanWildcard(Wildcard); + for (auto It = begin(CleanWildcard); It != end(CleanWildcard); It++) + { + if (*It == '\\') + { + *It = '/'; + } + } - for (auto It = begin(m_ExcludeWildcard); It != end(m_ExcludeWildcard); It++) - { - if (*It == '\\') - { - *It = '/'; - } - } + Wildcards.emplace_back(std::move(CleanWildcard)); + } + return true; + }); + return Wildcards; + }; + + OutIncludeWildcards = SplitWildcard(m_IncludeWildcard); + OutExcludeWildcards = SplitWildcard(m_ExcludeWildcard); }; auto ParseDiffPath = [&]() { if (m_DiffPath.empty()) { - throw zen::OptionParseException(fmt::format("compare-path is required\n{}", SubOption->help())); + throw OptionParseException("'--compare-path' is required", SubOption->help()); } MakeSafeAbsolutePathÍnPlace(m_DiffPath); }; auto ParseBlobHash = [&]() -> IoHash { - m_BlobHash = RemoveQuotes(m_BlobHash); if (m_BlobHash.empty()) { - throw zen::OptionParseException(fmt::format("Blob hash string is missing\n{}", SubOption->help())); + throw OptionParseException("'--blob-hash' is required", SubOption->help()); + } + + if (m_BlobHash.length() != IoHash::StringLength) + { + throw OptionParseException( + fmt::format("'--blob-hash' ('{}') is malfomed, it must be {} characters long", m_BlobHash, IoHash::StringLength), + SubOption->help()); } IoHash BlobHash; if (!IoHash::TryParse(m_BlobHash, BlobHash)) { - throw zen::OptionParseException(fmt::format("Blob hash string is invalid\n{}", SubOption->help())); + throw OptionParseException(fmt::format("'--blob-hash' ('{}') is malformed", m_BlobHash), SubOption->help()); } return BlobHash; }; auto ParseBuildId = [&]() -> Oid { - m_BuildId = RemoveQuotes(m_BuildId); if (m_BuildId.length() != Oid::StringLength) { - throw zen::OptionParseException(fmt::format("Invalid build id\n{}", SubOption->help())); + throw OptionParseException( + fmt::format("'--build-id' ('{}') is malfomed, it must be {} characters long", m_BuildId, Oid::StringLength), + SubOption->help()); } else if (Oid BuildId = Oid::FromHexString(m_BuildId); BuildId == Oid::Zero) { - throw zen::OptionParseException(fmt::format("Invalid build id\n{}", SubOption->help())); + throw OptionParseException(fmt::format("'--build-id' ('{}') is invalid", m_BuildId), SubOption->help()); } else { @@ -11023,14 +11092,15 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) }; auto ParseBuildPartId = [&]() -> Oid { - m_BuildPartId = RemoveQuotes(m_BuildPartId); if (m_BuildPartId.length() != Oid::StringLength) { - throw zen::OptionParseException(fmt::format("Invalid build part id\n{}", SubOption->help())); + throw OptionParseException( + fmt::format("'--build-id' ('{}') is malformed, it must be {} characters long", m_BuildPartId, Oid::StringLength), + SubOption->help()); } else if (Oid BuildPartId = Oid::FromHexString(m_BuildPartId); BuildPartId == Oid::Zero) { - throw zen::OptionParseException(fmt::format("Invalid build part id\n{}", SubOption->help())); + throw OptionParseException(fmt::format("'--build-id' ('{}') is malformed", m_BuildPartId), SubOption->help()); } else { @@ -11045,7 +11115,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) BuildPartIds.push_back(Oid::TryFromHexString(RemoveQuotes(BuildPartId))); if (BuildPartIds.back() == Oid::Zero) { - throw zen::OptionParseException(fmt::format("build-part-id '{}' is invalid\n{}", BuildPartId, SubOption->help())); + throw OptionParseException(fmt::format("'--build-part-id' ('{}') is malformed", BuildPartId), SubOption->help()); } } return BuildPartIds; @@ -11058,7 +11128,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) BuildPartNames.push_back(std::string(RemoveQuotes(BuildPartName))); if (BuildPartNames.back().empty()) { - throw zen::OptionParseException(fmt::format("build-part-names '{}' is invalid\n{}", BuildPartName, SubOption->help())); + throw OptionParseException(fmt::format("'--build-part-names' ('{}') is invalid", BuildPartName), SubOption->help()); } } return BuildPartNames; @@ -11069,11 +11139,13 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (m_BuildMetadataPath.empty() && m_BuildMetadata.empty()) { - throw zen::OptionParseException(fmt::format("Options for builds target are missing\n{}", SubOption->help())); + throw OptionParseException("'--metadata-path' or '--metadata' is required", SubOption->help()); } if (!m_BuildMetadataPath.empty() && !m_BuildMetadata.empty()) { - throw zen::OptionParseException(fmt::format("Conflicting options for builds target\n{}", SubOption->help())); + throw OptionParseException( + fmt::format("'--metadata-path' ('{}') conflicts with '--metadata' ('{}')", m_BuildMetadataPath, m_BuildMetadata), + SubOption->help()); } if (!m_BuildMetadataPath.empty()) @@ -11090,7 +11162,6 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } return MetaData; } - m_BuildMetadata = RemoveQuotes(m_BuildMetadata); if (!m_BuildMetadata.empty()) { CbObjectWriter MetaDataWriter(1024); @@ -11110,12 +11181,11 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (!m_BuildMetadataPath.empty()) { - throw zen::OptionParseException( - fmt::format("metadata-path option is only valid if creating a build\n{}", SubOption->help())); + throw OptionParseException("'--metadata-path' requires '--create-build'", SubOption->help()); } if (!m_BuildMetadata.empty()) { - throw zen::OptionParseException(fmt::format("metadata option is only valid if creating a build\n{}", SubOption->help())); + throw OptionParseException("'--metadata' requires '--create-build'", SubOption->help()); } } return {}; @@ -11130,10 +11200,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (!IsQuiet) { - ZEN_CONSOLE("Running {}: {} (pid {})", - GetRunningExecutablePath(), - ZEN_CFG_VERSION_BUILD_STRING_FULL, - GetCurrentProcessId()); + LogExecutableVersionAndPid(); } } @@ -11181,8 +11248,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) WriteFile(ListResultPath, IoBuffer(IoBuffer::Wrap, SB.Data(), SB.Size())); } } - - return 0; + return; } if (SubOption == &m_ListOptions) @@ -11194,10 +11260,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (!IsQuiet) { - ZEN_CONSOLE("Running {}: {} (pid {})", - GetRunningExecutablePath(), - ZEN_CFG_VERSION_BUILD_STRING_FULL, - GetCurrentProcessId()); + LogExecutableVersionAndPid(); } } CbObject QueryObject; @@ -11274,15 +11337,90 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) WriteFile(m_ListResultPath, IoBuffer(IoBuffer::Wrap, SB.Data(), SB.Size())); } } - - return 0; + return; } + if (SubOption == &m_ListBlocksOptions) + { + MakeSafeAbsolutePathÍnPlace(m_ListResultPath); + + if (!m_ListResultPath.empty()) + { + if (!IsQuiet) + { + LogExecutableVersionAndPid(); + } + } + + if (m_ListBlocksMaxCount == 0) + { + throw OptionParseException(fmt::format("'--max-count' ('{}') is invalid", m_ListBlocksMaxCount), SubOption->help()); + } + + BuildStorage::Statistics StorageStats; + BuildStorageCache::Statistics StorageCacheStats; + + if (m_ZenFolderPath.empty()) + { + m_ZenFolderPath = std::filesystem::current_path() / ZenFolderName; + } + MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath); + + CreateDirectories(m_ZenFolderPath); + auto _ = MakeGuard([this]() { + if (CleanDirectory(m_ZenFolderPath, {})) + { + std::error_code DummyEc; + RemoveDir(m_ZenFolderPath, DummyEc); + } + }); + + StorageInstance Storage = CreateBuildStorage(StorageStats, + StorageCacheStats, + ZenTempFolderPath(m_ZenFolderPath), + /*RequriesNamespace*/ true, + /*RequireBucket*/ true); + + const Oid BuildId = ParseBuildId(); + + CbObject Response = Storage.BuildStorage->FindBlocks(BuildId, m_ListBlocksMaxCount); + ZEN_ASSERT(ValidateCompactBinary(Response.GetView(), CbValidateMode::Default) == CbValidateError::None); + + std::vector<ChunkBlockDescription> BlockDescriptions = ParseChunkBlockDescriptionList(Response); + if (!IsQuiet) + { + ZEN_CONSOLE("Response contains {} block", BlockDescriptions.size()); + } + if (m_ListResultPath.empty()) + { + ExtendableStringBuilder<1024> SB; + CompactBinaryToJson(Response.GetView(), SB); + ForEachStrTok(SB.ToView(), '\n', [](std::string_view Row) { + ZEN_CONSOLE("{}", Row); + return true; + }); + } + else + { + if (ToLower(m_ListResultPath.extension().string()) == ".cbo") + { + MemoryView ResponseView = Response.GetView(); + WriteFile(m_ListResultPath, IoBuffer(IoBuffer::Wrap, ResponseView.GetData(), ResponseView.GetSize())); + } + else + { + ExtendableStringBuilder<1024> SB; + CompactBinaryToJson(Response.GetView(), SB); + WriteFile(m_ListResultPath, IoBuffer(IoBuffer::Wrap, SB.Data(), SB.Size())); + } + } + return; + } if (SubOption == &m_UploadOptions) { if (!IsQuiet) { - ZEN_CONSOLE("Running {}: {} (pid {})", GetRunningExecutablePath(), ZEN_CFG_VERSION_BUILD_STRING_FULL, GetCurrentProcessId()); + LogExecutableVersionAndPid(); } ZenState InstanceState; @@ -11333,8 +11471,6 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) const std::filesystem::path TempDir = ZenTempFolderPath(m_ZenFolderPath); - m_ManifestPath = RemoveQuotes(m_ManifestPath); - UploadFolder(Storage, BuildId, BuildPartId, @@ -11350,7 +11486,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) m_Clean, m_PostUploadVerify); - if (false) + if (true) { if (!IsQuiet) { @@ -11373,20 +11509,40 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) : 0); } } - return AbortFlag ? 11 : 0; + if (AbortFlag) + { + throw std::runtime_error("Upload aborted"); + } } + auto ParseAllowPartialBlockRequests = [&]() -> EPartialBlockRequestMode { + if (m_PrimeCacheOnly) + { + return EPartialBlockRequestMode::Off; + } + EPartialBlockRequestMode Mode = PartialBlockRequestModeFromString(m_AllowPartialBlockRequests); + if (Mode == EPartialBlockRequestMode::Invalid) + { + throw OptionParseException(fmt::format("'--allow-partial-block-requests' ('{}') is invalid", m_AllowPartialBlockRequests), + SubOption->help()); + } + return Mode; + }; + if (SubOption == &m_DownloadOptions) { if (!IsQuiet) { - ZEN_CONSOLE("Running {}: {} (pid {})", GetRunningExecutablePath(), ZEN_CFG_VERSION_BUILD_STRING_FULL, GetCurrentProcessId()); + LogExecutableVersionAndPid(); } ZenState InstanceState; ParsePath(); - ParseFileFilters(); + + std::vector<std::string> IncludeWildcards; + std::vector<std::string> ExcludeWildcards; + ParseFileFilters(IncludeWildcards, ExcludeWildcards); if (m_ZenFolderPath.empty()) { @@ -11407,53 +11563,64 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (m_PostDownloadVerify && m_PrimeCacheOnly) { - throw zen::OptionParseException( - fmt::format("'cache-prime-only' option is not compatible with 'verify' option\n{}", SubOption->help())); + throw OptionParseException("'--cache-prime-only' conflicts with '--verify'", SubOption->help()); } if (m_Clean && m_PrimeCacheOnly) { - ZEN_CONSOLE_WARN("Ignoring 'clean' option when 'cache-prime-only' is enabled"); + ZEN_CONSOLE_WARN("Ignoring '--clean' option when '--cache-prime-only' is enabled"); + } + + if (m_Force && m_PrimeCacheOnly) + { + ZEN_CONSOLE_WARN("Ignoring '--force' option when '--cache-prime-only' is enabled"); } - if (m_AllowPartialBlockRequests && m_PrimeCacheOnly) + if (m_AllowPartialBlockRequests != "false" && m_PrimeCacheOnly) { - ZEN_CONSOLE_WARN("Ignoring 'allow-partial-block-requests' option when 'cache-prime-only' is enabled"); + ZEN_CONSOLE_WARN("Ignoring '--allow-partial-block-requests' option when '--cache-prime-only' is enabled"); } std::vector<Oid> BuildPartIds = ParseBuildPartIds(); std::vector<std::string> BuildPartNames = ParseBuildPartNames(); - // TODO: Add file filters + EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(); + DownloadFolder(Storage, BuildId, BuildPartIds, BuildPartNames, m_Path, - m_ZenFolderPath, - m_SystemRootDir, - m_AllowMultiparts, - m_AllowPartialBlockRequests && !m_PrimeCacheOnly, - m_Clean, - m_PostDownloadVerify, - m_PrimeCacheOnly, - m_EnableScavenging, - m_IncludeWildcard, - m_ExcludeWildcard); - - return AbortFlag ? 11 : 0; + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = m_ZenFolderPath, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = m_Clean, + .PostDownloadVerify = m_PostDownloadVerify, + .PrimeCacheOnly = m_PrimeCacheOnly, + .EnableOtherDownloadsScavenging = m_EnableScavenging && !m_Force, + .EnableTargetFolderScavenging = !m_Force, + .IncludeWildcards = IncludeWildcards, + .ExcludeWildcards = ExcludeWildcards}); + + if (AbortFlag) + { + throw std::runtime_error("Download aborted"); + } } if (SubOption == &m_LsOptions) { if (!IsQuiet) { - ZEN_CONSOLE("Running {}: {} (pid {})", GetRunningExecutablePath(), ZEN_CFG_VERSION_BUILD_STRING_FULL, GetCurrentProcessId()); + LogExecutableVersionAndPid(); } ZenState InstanceState; - ParseFileFilters(); + std::vector<std::string> IncludeWildcards; + std::vector<std::string> ExcludeWildcards; + ParseFileFilters(IncludeWildcards, ExcludeWildcards); if (m_ZenFolderPath.empty()) { @@ -11475,9 +11642,12 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) std::vector<Oid> BuildPartIds = ParseBuildPartIds(); std::vector<std::string> BuildPartNames = ParseBuildPartNames(); - ListBuild(Storage, BuildId, BuildPartIds, BuildPartNames, m_IncludeWildcard, m_ExcludeWildcard); + ListBuild(Storage, BuildId, BuildPartIds, BuildPartNames, IncludeWildcards, ExcludeWildcards); - return AbortFlag ? 11 : 0; + if (AbortFlag) + { + throw std::runtime_error("List build aborted"); + } } if (SubOption == &m_DiffOptions) @@ -11486,14 +11656,17 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ParseDiffPath(); DiffFolders(m_Path, m_DiffPath, m_OnlyChunked); - return AbortFlag ? 11 : 0; + if (AbortFlag) + { + throw std::runtime_error("Diff folders aborted"); + } } if (SubOption == &m_FetchBlobOptions) { if (!IsQuiet) { - ZEN_CONSOLE("Running {}: {} (pid {})", GetRunningExecutablePath(), ZEN_CFG_VERSION_BUILD_STRING_FULL, GetCurrentProcessId()); + LogExecutableVersionAndPid(); } BuildStorage::Statistics StorageStats; @@ -11529,7 +11702,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ValidateBlob(*Storage.BuildStorage, BuildId, BlobHash, CompressedSize, DecompressedSize); if (AbortFlag) { - return 11; + throw std::runtime_error("Fetch blob aborted"); } if (!IsQuiet) { @@ -11538,14 +11711,14 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) CompressedSize, DecompressedSize); } - return 0; + return; } if (SubOption == &m_ValidateBuildPartOptions) { if (!IsQuiet) { - ZEN_CONSOLE("Running {}: {} (pid {})", GetRunningExecutablePath(), ZEN_CFG_VERSION_BUILD_STRING_FULL, GetCurrentProcessId()); + LogExecutableVersionAndPid(); } ZenState InstanceState; @@ -11578,7 +11751,9 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!m_BuildPartName.empty() && !m_BuildPartId.empty()) { - throw zen::OptionParseException(fmt::format("build-part-id conflicts with build-part-name\n{}", SubOption->help())); + throw OptionParseException( + fmt::format("'--build-part-id' ('{}') conflicts with '--build-part-name' ('{}')", m_BuildPartId, m_BuildPartName), + SubOption->help()); } const Oid BuildPartId = m_BuildPartName.empty() ? Oid::Zero : ParseBuildPartId(); @@ -11587,7 +11762,10 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) DownloadStatistics DownloadStats; ValidateBuildPart(*Storage.BuildStorage, BuildId, BuildPartId, m_BuildPartName, ValidateStats, DownloadStats); - return AbortFlag ? 13 : 0; + if (AbortFlag) + { + throw std::runtime_error("Validate build part failed"); + } } if (SubOption == &m_MultiTestDownloadOptions) @@ -11605,6 +11783,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } MakeSafeAbsolutePathÍnPlace(m_ZenFolderPath); + EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(); + BuildStorage::Statistics StorageStats; BuildStorageCache::Statistics StorageCacheStats; @@ -11620,27 +11800,25 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) Oid BuildId = Oid::FromHexString(RemoveQuotes(BuildIdString)); if (BuildId == Oid::Zero) { - throw zen::OptionParseException(fmt::format("invalid build id {}\n{}", BuildIdString, SubOption->help())); + throw OptionParseException(fmt::format("'--build-id' ('{}') is malformed", BuildIdString), SubOption->help()); } DownloadFolder(Storage, BuildId, {}, {}, m_Path, - m_ZenFolderPath, - m_SystemRootDir, - m_AllowMultiparts, - m_AllowPartialBlockRequests, - BuildIdString == m_BuildIds.front(), - true, - false, - m_EnableScavenging, - ""sv, - ""sv); + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = m_ZenFolderPath, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = BuildIdString == m_BuildIds.front(), + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = false}); if (AbortFlag) { - ZEN_CONSOLE("Download cancelled"); - return 11; + throw std::runtime_error("Multitest aborted"); } if (!IsQuiet) { @@ -11651,7 +11829,6 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_CONSOLE("Completed in {}", NiceTimeSpanMs(Timer.GetElapsedTimeMs())); } - return 0; } auto ParseZenProcessId = [&]() { @@ -11667,7 +11844,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } if (!RunningProcess.IsValid()) { - throw zen::OptionParseException( + throw std::runtime_error( fmt::format("Unable to find a running instance of the zen executable '{}'", RunningExecutablePath)); } m_ZenProcessId = RunningProcess.Pid(); @@ -11679,7 +11856,6 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ParseZenProcessId(); ZenState RunningState(m_ZenProcessId); RunningState.StateData().Pause.store(true); - return 0; } if (SubOption == &m_ResumeOptions) @@ -11687,7 +11863,6 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ParseZenProcessId(); ZenState RunningState(m_ZenProcessId); RunningState.StateData().Pause.store(false); - return 0; } if (SubOption == &m_AbortOptions) @@ -11695,7 +11870,6 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ParseZenProcessId(); ZenState RunningState(m_ZenProcessId); RunningState.StateData().Abort.store(true); - return 0; } if (SubOption == &m_TestOptions) @@ -11722,6 +11896,8 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } }); + EPartialBlockRequestMode PartialBlockRequestMode = ParseAllowPartialBlockRequests(); + BuildStorage::Statistics StorageStats; BuildStorageCache::Statistics StorageCacheStats; @@ -11795,8 +11971,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) true); if (AbortFlag) { - ZEN_CONSOLE_ERROR("Upload failed."); - return 11; + throw std::runtime_error("Test aborted. (Upload build)"); } ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}'", BuildId, BuildPartId, m_BuildPartName, DownloadPath); @@ -11805,20 +11980,19 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) {BuildPartId}, {}, DownloadPath, - DownloadPath / ZenFolderName, - m_SystemRootDir, - m_AllowMultiparts, - m_AllowPartialBlockRequests, - true, - true, - false, - m_EnableScavenging, - ""sv, - ""sv); + + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = true, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = false}); if (AbortFlag) { - ZEN_CONSOLE_ERROR("Download failed."); - return 11; + throw std::runtime_error("Test aborted. (Download build)"); } ZEN_CONSOLE("\nRe-download Build {}, Part {} ({}) to '{}' (identical target)", BuildId, BuildPartId, m_BuildPartName, DownloadPath); @@ -11827,20 +12001,19 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) {BuildPartId}, {}, DownloadPath, - DownloadPath / ZenFolderName, - m_SystemRootDir, - m_AllowMultiparts, - m_AllowPartialBlockRequests, - false, - true, - false, - m_EnableScavenging, - ""sv, - ""sv); + + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = false, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = true}); if (AbortFlag) { - ZEN_CONSOLE_ERROR("Re-download failed. (identical target)"); - return 11; + throw std::runtime_error("Test aborted. (Re-download identical target)"); } auto ScrambleDir = [](const std::filesystem::path& Path) { @@ -11870,7 +12043,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) return true; }; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); uint32_t Randomizer = 0; auto FileSizeIt = DownloadContent.FileSizes.begin(); @@ -11941,20 +12114,19 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) {BuildPartId}, {}, DownloadPath, - DownloadPath / ZenFolderName, - m_SystemRootDir, - m_AllowMultiparts, - m_AllowPartialBlockRequests, - false, - true, - false, - m_EnableScavenging, - ""sv, - ""sv); + + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = false, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = true}); if (AbortFlag) { - ZEN_CONSOLE_ERROR("Re-download failed. (scrambled target)"); - return 11; + throw std::runtime_error("Test aborted. (Re-download scrambled target)"); } ScrambleDir(DownloadPath); @@ -11985,8 +12157,7 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) true); if (AbortFlag) { - ZEN_CONSOLE_ERROR("Upload of scrambled failed."); - return 11; + throw std::runtime_error("Test aborted. (Upload scrambled)"); } ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}' (original)", BuildId, BuildPartId, m_BuildPartName, DownloadPath); @@ -11995,20 +12166,19 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) {BuildPartId}, {}, DownloadPath, - DownloadPath / ZenFolderName, - m_SystemRootDir, - m_AllowMultiparts, - m_AllowPartialBlockRequests, - false, - true, - false, - m_EnableScavenging, - ""sv, - ""sv); + + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = false, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = true}); if (AbortFlag) { - ZEN_CONSOLE_ERROR("Re-download failed."); - return 11; + throw std::runtime_error("Test aborted. (Download original)"); } ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}' (scrambled)", BuildId2, BuildPartId2, m_BuildPartName, DownloadPath); @@ -12017,20 +12187,18 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) {BuildPartId2}, {}, DownloadPath, - DownloadPath / ZenFolderName, - m_SystemRootDir, - m_AllowMultiparts, - m_AllowPartialBlockRequests, - false, - true, - false, - m_EnableScavenging, - ""sv, - ""sv); + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = false, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = true}); if (AbortFlag) { - ZEN_CONSOLE_ERROR("Re-download failed."); - return 11; + throw std::runtime_error("Test aborted. (Download scrambled)"); } ZEN_CONSOLE("\nRe-download Build {}, Part {} ({}) to '{}' (scrambled)", BuildId2, BuildPartId2, m_BuildPartName, DownloadPath); @@ -12039,20 +12207,18 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) {BuildPartId2}, {}, DownloadPath, - DownloadPath / ZenFolderName, - m_SystemRootDir, - m_AllowMultiparts, - m_AllowPartialBlockRequests, - false, - true, - false, - m_EnableScavenging, - ""sv, - ""sv); + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = false, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = true}); if (AbortFlag) { - ZEN_CONSOLE_ERROR("Re-download failed."); - return 11; + throw std::runtime_error("Test aborted. (Re-download scrambled)"); } ZEN_CONSOLE("\nDownload Build {}, Part {} ({}) to '{}' (original)", BuildId, BuildPartId, m_BuildPartName, DownloadPath2); @@ -12061,25 +12227,20 @@ BuildsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) {BuildPartId}, {}, DownloadPath2, - DownloadPath2 / ZenFolderName, - m_SystemRootDir, - m_AllowMultiparts, - m_AllowPartialBlockRequests, - false, - true, - false, - m_EnableScavenging, - ""sv, - ""sv); + DownloadOptions{.SystemRootDir = m_SystemRootDir, + .ZenFolderPath = DownloadPath2 / ZenFolderName, + .AllowMultiparts = m_AllowMultiparts, + .PartialBlockRequestMode = PartialBlockRequestMode, + .CleanTargetFolder = false, + .PostDownloadVerify = true, + .PrimeCacheOnly = false, + .EnableOtherDownloadsScavenging = m_EnableScavenging, + .EnableTargetFolderScavenging = true}); if (AbortFlag) { - ZEN_CONSOLE_ERROR("Re-download failed."); - return 11; + throw std::runtime_error("Test aborted. (Download original)"); } - - return 0; } - ZEN_ASSERT(false); } } // namespace zen diff --git a/src/zen/cmds/builds_cmd.h b/src/zen/cmds/builds_cmd.h index d057d24ac..ddec1f791 100644 --- a/src/zen/cmds/builds_cmd.h +++ b/src/zen/cmds/builds_cmd.h @@ -2,6 +2,7 @@ #pragma once +#include "../authutils.h" #include "../zen.h" #include <zenhttp/auth/authmgr.h> @@ -13,13 +14,14 @@ namespace zen { class BuildsCommand : public CacheStoreCommand { public: - static constexpr char Name[] = "builds"; - static constexpr char Description[] = "Manage builds - list, upload, download, diff"; + static constexpr char Name[] = "builds"; + static constexpr char Description[] = + "Manage builds - list, list-namespaces, ls, upload, download, diff, fetch-blob, validate-part, pause, resume, abort"; BuildsCommand(); ~BuildsCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -60,32 +62,13 @@ private: std::string m_BuildPartName; // Defaults to name of leaf folder in m_Path std::string m_BuildPartId; // Defaults to a generated id when creating part, looked up when downloading using m_BuildPartName bool m_Clean = false; + bool m_Force = false; uint8_t m_BlockReuseMinPercentLimit = 85; bool m_AllowMultiparts = true; - bool m_AllowPartialBlockRequests = true; + std::string m_AllowPartialBlockRequests = "mixed"; std::string m_ManifestPath; // Not a std::filesystem::path since it can be relative to m_Path - // Direct access token (may expire) - std::string m_AccessToken; - std::string m_AccessTokenEnv; - std::filesystem::path m_AccessTokenPath; - - // Auth manager token encryption - std::string m_EncryptionKey; // 256 bit AES encryption key - std::string m_EncryptionIV; // 128 bit AES initialization vector - - // OpenId acccess token - std::string m_OpenIdProviderName; - std::string m_OpenIdProviderUrl; - std::string m_OpenIdClientId; - std::string m_OpenIdRefreshToken; - - // OAuth acccess token - std::string m_OAuthUrl; - std::string m_OAuthClientId; - std::string m_OAuthClientSecret; - - std::string m_OidcTokenAuthExecutablePath; + AuthCommandLineOptions m_AuthOptions; std::string m_Verb; // list, upload, download @@ -96,6 +79,9 @@ private: std::filesystem::path m_ListQueryPath; std::filesystem::path m_ListResultPath; + cxxopts::Options m_ListBlocksOptions{"list-blocks", "List recent blocks"}; + uint32_t m_ListBlocksMaxCount = 16; + std::filesystem::path m_Path; cxxopts::Options m_UploadOptions{"upload", "Upload a folder"}; @@ -132,8 +118,9 @@ private: cxxopts::Options m_MultiTestDownloadOptions{"multi-test-download", "Test multiple sequenced downloads with verify"}; std::vector<std::string> m_BuildIds; - cxxopts::Options* m_SubCommands[13] = {&m_ListNamespacesOptions, + cxxopts::Options* m_SubCommands[14] = {&m_ListNamespacesOptions, &m_ListOptions, + &m_ListBlocksOptions, &m_UploadOptions, &m_DownloadOptions, &m_PauseOptions, diff --git a/src/zen/cmds/cache_cmd.cpp b/src/zen/cmds/cache_cmd.cpp index 90f428b29..85dcd7648 100644 --- a/src/zen/cmds/cache_cmd.cpp +++ b/src/zen/cmds/cache_cmd.cpp @@ -2,6 +2,7 @@ #include "cache_cmd.h" +#include <zencore/compactbinarybuilder.h> #include <zencore/compress.h> #include <zencore/except.h> #include <zencore/filesystem.h> @@ -13,15 +14,11 @@ #include <zenhttp/httpclient.h> #include <zenhttp/httpcommon.h> #include <zenhttp/packageformat.h> -#include <zenutil/cache/cacherequests.h> +#include <zenstore/cache/cachepolicy.h> #include <memory> #include <random> -ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> -ZEN_THIRD_PARTY_INCLUDES_END - namespace zen { namespace { @@ -70,59 +67,53 @@ DropCommand::DropCommand() DropCommand::~DropCommand() = default; -int +void DropCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (m_NamespaceName.empty()) { - throw zen::OptionParseException("Drop command requires a namespace"); + throw OptionParseException("'--namespace' is required", m_Options.help()); } - cpr::Session Session; + std::string Url; + std::string DropDescription; + if (m_BucketName.empty()) { - ZEN_CONSOLE("Dropping cache namespace '{}' from '{}'", m_NamespaceName, m_HostName); - Session.SetUrl({fmt::format("{}/z$/{}", m_HostName, m_NamespaceName)}); + DropDescription = fmt::format("cache namespace '{}' from '{}'", m_NamespaceName, m_HostName); + Url = fmt::format("/z$/{}", m_NamespaceName); } else { - ZEN_CONSOLE("Dropping cache bucket '{}/{}' from '{}'", m_NamespaceName, m_BucketName, m_HostName); - Session.SetUrl({fmt::format("{}/z$/{}/{}", m_HostName, m_NamespaceName, m_BucketName)}); + DropDescription = fmt::format("cache bucket '{}/{}' from '{}'", m_NamespaceName, m_BucketName, m_HostName); + Url = fmt::format("/z$/{}/{}", m_NamespaceName, m_BucketName); } - cpr::Response Result = Session.Delete(); - - if (zen::IsHttpSuccessCode(Result.status_code)) - { - ZEN_CONSOLE("OK: drop succeeded"); - - return 0; - } + ZEN_CONSOLE("Dropping {}", DropDescription); - if (Result.status_code) + HttpClient Http(m_HostName); + if (HttpClient::Response Response = Http.Delete(Url)) { - ZEN_CONSOLE_ERROR("Drop failed: {}: {} ({})", Result.status_code, Result.reason, Result.text); + ZEN_CONSOLE("{}", Response.ToText()); } else { - ZEN_CONSOLE_ERROR("Drop failed: {}", Result.error.message); + Response.ThrowError(fmt::format("Failed to drop {}", DropDescription)); } - - return 1; } CacheInfoCommand::CacheInfoCommand() @@ -144,87 +135,76 @@ CacheInfoCommand::CacheInfoCommand() CacheInfoCommand::~CacheInfoCommand() = default; -int +void CacheInfoCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } - cpr::Session Session; - Session.SetHeader(cpr::Header{{"Accept", "application/json"}}); + std::string Url; if (m_HostName.empty()) { if (!m_SizeInfoBucketNames.empty()) { - throw zen::OptionParseException("--bucketsizes option needs a --namespace"); + throw OptionParseException("'--bucketsizes' requires '--namespace'", m_Options.help()); } if (m_BucketSizeInfo) { - throw zen::OptionParseException("--bucketsizes option needs a --namespace and a --bucket"); + throw OptionParseException("'--bucketsize' requires '--namespace' and '--bucket'", m_Options.help()); } ZEN_CONSOLE("Info on cache from '{}'", m_HostName); - Session.SetUrl({fmt::format("{}/z$", m_HostName)}); + Url = "/z$"; } else if (m_BucketName.empty()) { if (m_BucketSizeInfo) { - throw zen::OptionParseException("--bucketsizes option needs a --bucket"); + throw OptionParseException(fmt::format("'--bucketsize' requires '--namespace' and '--bucket' ('{}')", m_BucketName), + m_Options.help()); } ZEN_CONSOLE("Info on cache namespace '{}' from '{}'", m_NamespaceName, m_HostName); - Session.SetUrl({fmt::format("{}/z$/{}", m_HostName, m_NamespaceName)}); + Url = fmt::format("/z$/{}", m_NamespaceName); } else { if (!m_SizeInfoBucketNames.empty()) { - throw zen::OptionParseException("--bucketsizes option can not be used together with --bucket option"); + throw OptionParseException("'--bucketsizes' conflicts with '--bucket'", m_Options.help()); } ZEN_CONSOLE("Info on cache bucket '{}/{}' from '{}'", m_NamespaceName, m_BucketName, m_HostName); - Session.SetUrl({fmt::format("{}/z$/{}/{}", m_HostName, m_NamespaceName, m_BucketName)}); + Url = fmt::format("/z$/{}/{}", m_NamespaceName, m_BucketName); } - cpr::Parameters Parameters; + HttpClient::KeyValueMap Parameters; if (!m_SizeInfoBucketNames.empty()) { - Parameters.Add({"bucketsizes", m_SizeInfoBucketNames}); + Parameters.Entries.insert({"bucketsizes", m_SizeInfoBucketNames}); } if (m_BucketSizeInfo) { - Parameters.Add({"bucketsize", "true"}); + Parameters.Entries.insert({"bucketsize", "true"}); } - Session.SetParameters(Parameters); - cpr::Response Result = Session.Get(); - - if (zen::IsHttpSuccessCode(Result.status_code)) - { - ZEN_CONSOLE("{}", Result.text); - - return 0; - } - - if (Result.status_code) + HttpClient Http(m_HostName); + if (HttpClient::Response Response = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON), Parameters)) { - ZEN_CONSOLE_ERROR("Info failed: {}: {} ({})", Result.status_code, Result.reason, Result.text); + ZEN_CONSOLE("{}", Response.ToText()); } else { - ZEN_CONSOLE_ERROR("Info failed: {}", Result.error.message); + Response.ThrowError("Info failed"); } - - return 1; } CacheStatsCommand::CacheStatsCommand() @@ -235,46 +215,32 @@ CacheStatsCommand::CacheStatsCommand() CacheStatsCommand::~CacheStatsCommand() = default; -int +void CacheStatsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } - cpr::Session Session; - Session.SetUrl({fmt::format("{}/stats/z$", m_HostName)}); - Session.SetHeader(cpr::Header{{"Accept", "application/json"}}); - - cpr::Response Result = Session.Get(); - - if (zen::IsHttpSuccessCode(Result.status_code)) - { - ZEN_CONSOLE("{}", Result.text); - - return 0; - } - - if (Result.status_code) + HttpClient Http(m_HostName); + if (HttpClient::Response Response = Http.Get("/stats/z$", HttpClient::Accept(ZenContentType::kJSON))) { - ZEN_CONSOLE_ERROR("Info failed: {}: {} ({})", Result.status_code, Result.reason, Result.text); + ZEN_CONSOLE("{}", Response.ToText()); } else { - ZEN_CONSOLE_ERROR("Info failed: {}", Result.error.message); + Response.ThrowError("Info failed"); } - - return 1; } CacheDetailsCommand::CacheDetailsCommand() @@ -296,87 +262,82 @@ CacheDetailsCommand::CacheDetailsCommand() CacheDetailsCommand::~CacheDetailsCommand() = default; -int +void CacheDetailsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } - cpr::Session Session; - cpr::Parameters Parameters; + HttpClient::KeyValueMap Parameters; if (m_Details) { - Parameters.Add({"details", "true"}); + Parameters.Entries.insert({"details", "true"}); } if (m_AttachmentDetails) { - Parameters.Add({"attachmentdetails", "true"}); + Parameters.Entries.insert({"attachmentdetails", "true"}); } + + HttpClient::KeyValueMap Headers; if (m_CSV) { - Parameters.Add({"csv", "true"}); + Parameters.Entries.insert({"csv", "true"}); } else { - Session.SetHeader(cpr::Header{{"Accept", "application/json"}}); + Headers = HttpClient::Accept(ZenContentType::kJSON); } + std::string Url; if (!m_ValueKey.empty()) { - if (m_Namespace.empty() || m_Bucket.empty()) + if (m_Namespace.empty()) + { + throw OptionParseException("'--namespace' is required", m_Options.help()); + } + if (m_Bucket.empty()) { - throw OptionParseException("Provide namespace and bucket name"); + throw OptionParseException("'--bucket' is required", m_Options.help()); } - Session.SetUrl({fmt::format("{}/z$/details$/{}/{}/{}", m_HostName, m_Namespace, m_Bucket, m_ValueKey)}); + Url = fmt::format("/z$/details$/{}/{}/{}", m_Namespace, m_Bucket, m_ValueKey); } else if (!m_Bucket.empty()) { if (m_Namespace.empty()) { - throw OptionParseException("Provide namespace name"); + throw OptionParseException("'--namespace' is required", m_Options.help()); } - Session.SetUrl({fmt::format("{}/z$/details$/{}/{}", m_HostName, m_Namespace, m_Bucket)}); + Url = fmt::format("/z$/details$/{}/{}", m_Namespace, m_Bucket); } else if (!m_Namespace.empty()) { - Session.SetUrl({fmt::format("{}/z$/details$/{}", m_HostName, m_Namespace)}); + Url = fmt::format("/z$/details$/{}", m_Namespace); } else { - Session.SetUrl({fmt::format("{}/z$/details$", m_HostName)}); - } - Session.SetParameters(Parameters); - - cpr::Response Result = Session.Get(); - - if (zen::IsHttpSuccessCode(Result.status_code)) - { - ZEN_CONSOLE("{}", Result.text); - - return 0; + Url = "/z$/details$"; } - if (Result.status_code) + HttpClient Http(m_HostName); + if (HttpClient::Response Response = Http.Get(Url, Headers, Parameters)) { - ZEN_CONSOLE_ERROR("Info failed: {}: {} ({})", Result.status_code, Result.reason, Result.text); + ZEN_CONSOLE("{}", Response.ToText()); } else { - ZEN_CONSOLE_ERROR("Info failed: {}", Result.error.message); + Response.ThrowError("Info failed"); } - - return 1; } CacheGenerateCommand::CacheGenerateCommand() @@ -407,21 +368,21 @@ CacheGenerateCommand::CacheGenerateCommand() CacheGenerateCommand::~CacheGenerateCommand() = default; -int +void CacheGenerateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (m_MaxSize == 0 && m_MinSize == 0) @@ -472,32 +433,109 @@ CacheGenerateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a HttpClient Http(m_HostName); - auto GeneratePutCacheValueRequest([this, &KeyDistribution, &Generator](std::span<std::uint64_t> BatchSizes, uint64_t RequestIndex) { - cacherequests::PutCacheValuesRequest Request({.AcceptMagic = kCbPkgMagic, .Namespace = m_Namespace}); - for (std::uint64_t ValueSize : BatchSizes) - { - uint64_t KeyBase = KeyDistribution(Generator); - std::string KeyString = fmt::format("{}-{}-{}", RequestIndex, KeyBase, ValueSize); - IoHash ValueKey = IoHash::HashBuffer(KeyString.c_str(), KeyString.length()); + auto GeneratePutCacheValueRequest( + [this, &KeyDistribution, &Generator](std::span<std::uint64_t> BatchSizes, uint64_t RequestIndex) -> CbPackage { + CbPackage Package; - Request.Requests.emplace_back(cacherequests::PutCacheValueRequest{.Key = {.Bucket = m_Bucket, .Hash = ValueKey}, - .Body = CompressBlob(CreateRandomBlob(ValueSize))}); - } - return Request; - }); + CbObjectWriter Writer; + Writer << "Method" + << "PutCacheValues"; + Writer << "Accept" << kCbPkgMagic; + + Writer.BeginObject("Params"); + { + Writer << "DefaultPolicy" << WriteToString<128>(CachePolicy::Default); + Writer << "Namespace" << m_Namespace; + + Writer.BeginArray("Requests"); + + for (std::uint64_t ValueSize : BatchSizes) + { + Writer.BeginObject(); + { + uint64_t KeyBase = KeyDistribution(Generator); + std::string KeyString = fmt::format("{}-{}-{}", RequestIndex, KeyBase, ValueSize); + IoHash ValueKey = IoHash::HashBuffer(KeyString.c_str(), KeyString.length()); + + Writer.BeginObject("Key"); + { + Writer << "Bucket" << m_Bucket; + Writer << "Hash" << ValueKey; + } + Writer.EndObject(); // Key + + CompressedBuffer Payload = CompressBlob(CreateRandomBlob(ValueSize)); + Writer.AddBinaryAttachment("RawHash", Payload.DecodeRawHash()); + Package.AddAttachment(CbAttachment(Payload, Payload.DecodeRawHash())); + } + Writer.EndObject(); + } + Writer.EndArray(); // Requests + } + Writer.EndObject(); // Params + + Package.SetObject(Writer.Save()); + + return Package; + }); auto GeneratePutCacheRecordRequest([this, &KeyDistribution, &Generator](std::span<std::uint64_t> BatchSizes, uint64_t RequestIndex) { - cacherequests::PutCacheRecordsRequest Request({.AcceptMagic = kCbPkgMagic, .Namespace = m_Namespace}); - uint64_t KeyBase = KeyDistribution(Generator); - std::string RecordKeyString = fmt::format("{}-{}-{}", RequestIndex, KeyBase, BatchSizes.size()); - IoHash RecordKey = IoHash::HashBuffer(RecordKeyString.c_str(), RecordKeyString.length()); + CbPackage Package; + + CbObjectWriter Writer; + Writer << "Method" + << "PutCacheRecords"; + Writer << "Accept" << kCbPkgMagic; - Request.Requests.emplace_back(cacherequests::PutCacheRecordRequest{.Key = {.Bucket = m_Bucket, .Hash = RecordKey}}); - for (std::uint64_t ValueSize : BatchSizes) + Writer.BeginObject("Params"); { - Request.Requests.back().Values.push_back({.Id = Oid::NewOid(), .Body = CompressBlob(CreateRandomBlob(ValueSize))}); + Writer << "DefaultPolicy" << WriteToString<128>(CachePolicy::Default); + Writer << "Namespace" << m_Namespace; + + Writer.BeginArray("Requests"); + { + Writer.BeginObject(); + { + Writer.BeginObject("Record"); + { + uint64_t KeyBase = KeyDistribution(Generator); + std::string RecordKeyString = fmt::format("{}-{}-{}", RequestIndex, KeyBase, BatchSizes.size()); + IoHash RecordKey = IoHash::HashBuffer(RecordKeyString.c_str(), RecordKeyString.length()); + + Writer.BeginObject("Key"); + { + Writer << "Bucket" << m_Bucket; + Writer << "Hash" << RecordKey; + } + Writer.EndObject(); // Key + + Writer.BeginArray("Values"); + for (std::uint64_t ValueSize : BatchSizes) + { + Writer.BeginObject(); + { + Writer.AddObjectId("Id", Oid::NewOid()); + + CompressedBuffer Payload = CompressBlob(CreateRandomBlob(ValueSize)); + Writer.AddBinaryAttachment("RawHash", Payload.DecodeRawHash()); + Package.AddAttachment(CbAttachment(Payload, Payload.DecodeRawHash())); + Writer.AddInteger("RawSize", Payload.DecodeRawSize()); + } + Writer.EndObject(); + } + Writer.EndArray(); // Values + } + Writer.EndObject(); // Record + } + Writer.EndObject(); + } + Writer.EndArray(); // Requests } - return Request; + Writer.EndObject(); // Params + + Package.SetObject(Writer.Save()); + + return Package; }); WorkerThreadPool WorkerPool(gsl::narrow<int>(Max((std::thread::hardware_concurrency() / 2u), 2u))); @@ -514,25 +552,26 @@ CacheGenerateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a std::span<uint64_t> BatchSizes = std::span<uint64_t>(Sizes).subspan(Offset, Min(Max(SizeCount, 1u), Sizes.size() - Offset)); WorkLatch.AddCount(1); - WorkerPool.ScheduleWork([&, BatchSizes, RequestIndex]() { - auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); }); - CbPackage Package; - if (m_MaxAttachmentCount > 0 && SizeCount > 0) - { - auto Request = GeneratePutCacheRecordRequest(BatchSizes, RequestIndex); - ZEN_ASSERT(Request.Format(Package)); - } - else - { - auto Request = GeneratePutCacheValueRequest(BatchSizes, RequestIndex); - ZEN_ASSERT(Request.Format(Package)); - } - - if (HttpClient::Response Response = Http.Post("/z$/$rpc", Package, HttpClient::Accept(ZenContentType::kCbPackage)); !Response) - { - ZEN_CONSOLE("{}", Response.ErrorMessage(fmt::format("{}: ", RequestIndex))); - } - }); + WorkerPool.ScheduleWork( + [&, BatchSizes, RequestIndex]() { + auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); }); + CbPackage Package; + if (m_MaxAttachmentCount > 0 && SizeCount > 0) + { + Package = GeneratePutCacheRecordRequest(BatchSizes, RequestIndex); + } + else + { + Package = GeneratePutCacheValueRequest(BatchSizes, RequestIndex); + } + + if (HttpClient::Response Response = Http.Post("/z$/$rpc", Package, HttpClient::Accept(ZenContentType::kCbPackage)); + !Response) + { + ZEN_CONSOLE("{}", Response.ErrorMessage(fmt::format("{}: ", RequestIndex))); + } + }, + WorkerThreadPool::EMode::EnableBacklog); Offset += BatchSizes.size(); RequestIndex++; } @@ -542,8 +581,6 @@ CacheGenerateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a { ZEN_INFO("Creating data, {} requests remaining", WorkLatch.Remaining()); } - - return 0; } CacheGetCommand::CacheGetCommand() @@ -570,7 +607,7 @@ CacheGetCommand::CacheGetCommand() CacheGetCommand::~CacheGetCommand() = default; -int +void CacheGetCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); @@ -579,35 +616,35 @@ CacheGetCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (m_Namespace.empty()) { - throw zen::OptionParseException("cache-get command requires a namespace"); + throw OptionParseException("'--namespace' is required", m_Options.help()); } if (m_Bucket.empty()) { - throw zen::OptionParseException("cache-get command requires a bucket"); + throw OptionParseException("'--bucket' is required", m_Options.help()); } if (m_ValueKey.empty()) { - throw zen::OptionParseException("cache-get command requires a value key"); + throw OptionParseException("'--valuekey' is required", m_Options.help()); } IoHash ValueId; if (!IoHash::TryParse(m_ValueKey, ValueId)) { - throw zen::OptionParseException("cache-get --valuekey option requires a valid IoHash string"); + throw OptionParseException(fmt::format("'--value-key' ('{}') is malformed", m_ValueKey), m_Options.help()); } IoHash AttachmentHash; @@ -615,7 +652,7 @@ CacheGetCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (!IoHash::TryParse(m_AttachmentHash, AttachmentHash)) { - throw zen::OptionParseException("cache-get --attachmenthash option requires a valid IoHash string"); + throw OptionParseException(fmt::format("'--attachmenthash' ('{}') is malformed", m_AttachmentHash), m_Options.help()); } } @@ -681,8 +718,6 @@ CacheGetCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { Result.ThrowError("Failed to fetch data"sv); } - - return 0; } } // namespace zen diff --git a/src/zen/cmds/cache_cmd.h b/src/zen/cmds/cache_cmd.h index b8a319359..4dc05bbdc 100644 --- a/src/zen/cmds/cache_cmd.h +++ b/src/zen/cmds/cache_cmd.h @@ -12,7 +12,7 @@ public: DropCommand(); ~DropCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -27,7 +27,7 @@ class CacheInfoCommand : public CacheStoreCommand public: CacheInfoCommand(); ~CacheInfoCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -44,7 +44,7 @@ class CacheStatsCommand : public CacheStoreCommand public: CacheStatsCommand(); ~CacheStatsCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -57,7 +57,7 @@ class CacheDetailsCommand : public CacheStoreCommand public: CacheDetailsCommand(); ~CacheDetailsCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -79,7 +79,7 @@ public: CacheGenerateCommand(); ~CacheGenerateCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -104,7 +104,7 @@ public: CacheGetCommand(); ~CacheGetCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/copy_cmd.cpp b/src/zen/cmds/copy_cmd.cpp index 4e54f27bb..530661607 100644 --- a/src/zen/cmds/copy_cmd.cpp +++ b/src/zen/cmds/copy_cmd.cpp @@ -24,23 +24,23 @@ CopyCommand::CopyCommand() CopyCommand::~CopyCommand() = default; -int +void CopyCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); - if (!ZenCmdBase::ParseOptions(argc, argv)) + if (!ParseOptions(argc, argv)) { - return 0; + return; } // Validate arguments if (m_CopySource.empty()) - throw std::runtime_error("No source specified"); + throw OptionParseException("'--source' is required", m_Options.help()); if (m_CopyTarget.empty()) - throw std::runtime_error("No target specified"); + throw OptionParseException("'--target' is required", m_Options.help()); std::filesystem::path FromPath = m_CopySource; std::filesystem::path ToPath = m_CopyTarget; @@ -180,9 +180,7 @@ CopyCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (Visitor.FailedFileCount) { - ZEN_CONSOLE_ERROR("{} file copy operations FAILED", Visitor.FailedFileCount); - - return 1; + throw std::runtime_error(fmt::format("{} file copy operations FAILED", Visitor.FailedFileCount)); } } else @@ -194,27 +192,16 @@ CopyCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) zen::CopyFileOptions CopyOptions; CopyOptions.EnableClone = !m_NoClone; - try + zen::CreateDirectories(ToPath.parent_path()); + if (zen::CopyFile(FromPath, ToPath, CopyOptions)) { - zen::CreateDirectories(ToPath.parent_path()); - if (zen::CopyFile(FromPath, ToPath, CopyOptions)) - { - ZEN_CONSOLE("Copy completed in {}", zen::NiceTimeSpanMs(Timer.GetElapsedTimeMs())); - } - else - { - throw std::logic_error("CopyFile failed in an unexpected way"); - } + ZEN_CONSOLE("Copy completed in {}", zen::NiceTimeSpanMs(Timer.GetElapsedTimeMs())); } - catch (const std::exception& Ex) + else { - ZEN_CONSOLE_ERROR("Failed to copy '{}' to '{}': '{}'", FromPath, ToPath, Ex.what()); - - return 1; + throw std::runtime_error(fmt::format("Failed to copy '{}' to '{}'", FromPath, ToPath)); } } - - return 0; } } // namespace zen diff --git a/src/zen/cmds/copy_cmd.h b/src/zen/cmds/copy_cmd.h index e9735c159..e1a5dcb82 100644 --- a/src/zen/cmds/copy_cmd.h +++ b/src/zen/cmds/copy_cmd.h @@ -15,7 +15,7 @@ public: ~CopyCommand(); virtual cxxopts::Options& Options() override { return m_Options; } - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } private: diff --git a/src/zen/cmds/dedup_cmd.cpp b/src/zen/cmds/dedup_cmd.cpp index 033ac87c3..9ef50a97d 100644 --- a/src/zen/cmds/dedup_cmd.cpp +++ b/src/zen/cmds/dedup_cmd.cpp @@ -51,14 +51,14 @@ DedupCommand::DedupCommand() DedupCommand::~DedupCommand() = default; -int +void DedupCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } // Validate arguments @@ -68,14 +68,12 @@ DedupCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!SourceGood) { - ZEN_CONSOLE_ERROR("Source directory '{}' does not support deduplication", m_DedupSource); - return 0; + throw std::runtime_error(fmt::format("Source directory '{}' does not support deduplication", m_DedupSource)); } if (!TargetGood) { - ZEN_CONSOLE_ERROR("Target directory '{}' does not support deduplication", m_DedupTarget); - return 0; + throw std::runtime_error(fmt::format("Target directory '{}' does not support deduplication", m_DedupTarget)); } ZEN_CONSOLE("Performing dedup operation between {} and {}, size threshold {}", @@ -295,8 +293,6 @@ DedupCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } ZEN_CONSOLE("Elapsed: {} Deduped: {}", zen::NiceTimeSpanMs(Timer.GetElapsedTimeMs()), zen::NiceBytes(DupeBytes)); - - return 0; } } // namespace zen diff --git a/src/zen/cmds/dedup_cmd.h b/src/zen/cmds/dedup_cmd.h index 2721be2b9..5b8387dd2 100644 --- a/src/zen/cmds/dedup_cmd.h +++ b/src/zen/cmds/dedup_cmd.h @@ -15,7 +15,7 @@ public: ~DedupCommand(); virtual cxxopts::Options& Options() override { return m_Options; } - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } private: diff --git a/src/zen/cmds/info_cmd.cpp b/src/zen/cmds/info_cmd.cpp index 8e0b3d663..49ad022cf 100644 --- a/src/zen/cmds/info_cmd.cpp +++ b/src/zen/cmds/info_cmd.cpp @@ -21,21 +21,21 @@ InfoCommand::~InfoCommand() { } -int +void InfoCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); @@ -48,8 +48,6 @@ InfoCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { Result.ThrowError(fmt::format("Failed getting info from {}", m_HostName)); } - - return 0; } } // namespace zen diff --git a/src/zen/cmds/info_cmd.h b/src/zen/cmds/info_cmd.h index 9723a075b..231565bfd 100644 --- a/src/zen/cmds/info_cmd.h +++ b/src/zen/cmds/info_cmd.h @@ -12,7 +12,7 @@ public: InfoCommand(); ~InfoCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } // virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } diff --git a/src/zen/cmds/print_cmd.cpp b/src/zen/cmds/print_cmd.cpp index fd6d92f28..557808ba7 100644 --- a/src/zen/cmds/print_cmd.cpp +++ b/src/zen/cmds/print_cmd.cpp @@ -19,7 +19,10 @@ PrintCbObject(CbObject Object, bool AddTypeComment) { ExtendableStringBuilder<1024> ObjStr; CompactBinaryToJson(Object, ObjStr, AddTypeComment); - ZEN_CONSOLE("{}", ObjStr); + ForEachStrTok(ObjStr.ToView(), '\n', [](std::string_view Row) { + ZEN_CONSOLE("{}", Row); + return true; + }); } static void @@ -27,7 +30,10 @@ PrintCompactBinary(IoBuffer Data, bool AddTypeComment) { ExtendableStringBuilder<1024> StreamString; CompactBinaryToJson(Data.GetView(), StreamString, AddTypeComment); - ZEN_CONSOLE("{}", StreamString); + ForEachStrTok(StreamString.ToView(), '\n', [](std::string_view Row) { + ZEN_CONSOLE("{}", Row); + return true; + }); } static CbValidateError @@ -55,20 +61,20 @@ PrintCommand::PrintCommand() PrintCommand::~PrintCommand() = default; -int +void PrintCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } // Validate arguments if (m_Filename.empty()) - throw std::runtime_error("No file specified"); + throw OptionParseException("'--source' is required", m_Options.help()); FileContents Fc; @@ -84,9 +90,7 @@ PrintCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (Fc.ErrorCode) { - ZEN_CONSOLE_ERROR("Failed to read file '{}': {}", m_Filename, Fc.ErrorCode.message()); - - return 1; + throw std::runtime_error(fmt::format("Failed to read file '{}': {}", m_Filename, Fc.ErrorCode.message())); } IoBuffer Data = Fc.Flatten(); @@ -158,11 +162,11 @@ PrintCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (FilteredResult != CbValidateError::None) { - ZEN_CONSOLE_ERROR("Object in package message file '{}' does not appear to be compact binary (validation error {:#x}: '{}')", - m_Filename, - uint32_t(FilteredResult), - ToString(FilteredResult)); - return 1; + throw std::runtime_error( + fmt::format("Object in package message file '{}' does not appear to be compact binary (validation error {:#x}: '{}')", + m_Filename, + uint32_t(FilteredResult), + ToString(FilteredResult))); } else { @@ -185,18 +189,16 @@ PrintCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (FilteredResult != CbValidateError::None) { - ZEN_CONSOLE_ERROR("Data in file '{}' does not appear to be compact binary (validation error {:#x}: '{}')", - m_Filename, - uint32_t(FilteredResult), - ToString(FilteredResult)); - return 1; + throw std::runtime_error(fmt::format("Data in file '{}' does not appear to be compact binary (validation error {:#x}: '{}')", + m_Filename, + uint32_t(FilteredResult), + ToString(FilteredResult))); } else { PrintCompactBinary(Data, m_ShowCbObjectTypeInfo); } } - return 0; } ////////////////////////////////////////////////////////////////////////// @@ -218,20 +220,20 @@ PrintPackageCommand::~PrintPackageCommand() { } -int +void PrintPackageCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } // Validate arguments if (m_Filename.empty()) - throw std::runtime_error("No file specified"); + throw OptionParseException("'--source' is required", m_Options.help()); MakeSafeAbsolutePathÍnPlace(m_Filename); FileContents Fc = ReadFile(m_Filename); @@ -250,8 +252,6 @@ PrintPackageCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** ar { ZEN_ERROR("error: malformed package?"); } - - return 0; } } // namespace zen diff --git a/src/zen/cmds/print_cmd.h b/src/zen/cmds/print_cmd.h index 80729901e..6c1529b7c 100644 --- a/src/zen/cmds/print_cmd.h +++ b/src/zen/cmds/print_cmd.h @@ -14,7 +14,7 @@ public: PrintCommand(); ~PrintCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } @@ -32,7 +32,7 @@ public: PrintPackageCommand(); ~PrintPackageCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } diff --git a/src/zen/cmds/projectstore_cmd.cpp b/src/zen/cmds/projectstore_cmd.cpp index f99e93cd1..6f9719909 100644 --- a/src/zen/cmds/projectstore_cmd.cpp +++ b/src/zen/cmds/projectstore_cmd.cpp @@ -4,23 +4,30 @@ #include <zencore/basicfile.h> #include <zencore/compactbinarybuilder.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compress.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> +#include <zencore/process.h> #include <zencore/scopeguard.h> #include <zencore/stream.h> #include <zencore/timer.h> #include <zencore/workthreadpool.h> +#include <zenhttp/auth/authmgr.h> #include <zenhttp/formatters.h> #include <zenhttp/httpclient.h> +#include <zenhttp/httpclientauth.h> #include <zenhttp/httpcommon.h> +#include <zenremotestore/builds/jupiterbuildstorage.h> +#include <zenremotestore/jupiter/jupiterhost.h> ZEN_THIRD_PARTY_INCLUDES_START #include <json11.hpp> ZEN_THIRD_PARTY_INCLUDES_END #include <signal.h> +#include <iostream> namespace zen { @@ -28,64 +35,7 @@ namespace { using namespace std::literals; - const std::string DefaultJupiterAccessTokenEnvVariableName( -#if ZEN_PLATFORM_WINDOWS - "UE-CloudDataCacheAccessToken"sv -#endif -#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC - "UE_CloudDataCacheAccessToken"sv -#endif - - ); - - std::string ReadJupiterAccessTokenFromFile(const std::filesystem::path& Path) - { - if (!IsFile(Path)) - { - throw std::runtime_error(fmt::format("the file '{}' does not exist", Path)); - } - IoBuffer Body = IoBufferBuilder::MakeFromFile(Path); - std::string JsonText(reinterpret_cast<const char*>(Body.GetData()), Body.GetSize()); - std::string JsonError; - json11::Json TokenInfo = json11::Json::parse(JsonText, JsonError); - if (!JsonError.empty()) - { - throw std::runtime_error(fmt::format("failed parsing json file '{}'. Reason: '{}'", Path, JsonError)); - } - const std::string AuthToken = TokenInfo["Token"].string_value(); - if (AuthToken.empty()) - { - throw std::runtime_error(fmt::format("the json file '{}' does not contain a value for \"Token\"", Path)); - } - return AuthToken; - } - - std::filesystem::path FindOidcTokenExePath(std::string_view OidcTokenAuthExecutablePath) - { - if (OidcTokenAuthExecutablePath.empty()) - { - const std::string OidcExecutableName = "OidcToken" ZEN_EXE_SUFFIX_LITERAL; - std::filesystem::path OidcTokenPath = (GetRunningExecutablePath().parent_path() / OidcExecutableName).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - OidcTokenPath = (std::filesystem::current_path() / OidcExecutableName).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - } - else - { - std::filesystem::path OidcTokenPath = std::filesystem::absolute(StringToPath(OidcTokenAuthExecutablePath)).make_preferred(); - if (IsFile(OidcTokenPath)) - { - return OidcTokenPath; - } - } - return {}; - }; +#define ZEN_CLOUD_STORAGE "Cloud Storage" void WriteAuthOptions(CbObjectWriter& Writer, std::string_view JupiterOpenIdProvider, @@ -104,7 +54,7 @@ namespace { } if (!JupiterAccessTokenPath.empty()) { - std::string ResolvedCloudAccessToken = ReadJupiterAccessTokenFromFile(JupiterAccessTokenPath); + std::string ResolvedCloudAccessToken = ReadAccessTokenFromJsonFile(JupiterAccessTokenPath); if (!ResolvedCloudAccessToken.empty()) { Writer.AddString("access-token"sv, ResolvedCloudAccessToken); @@ -148,16 +98,6 @@ namespace { } } - class AsyncJobError : public std::runtime_error - { - public: - using _Mybase = runtime_error; - - AsyncJobError(const std::string& Message, int ReturnCode) : _Mybase(Message), m_ReturnCode(ReturnCode) {} - - const int m_ReturnCode = 0; - }; - void ExecuteAsyncOperation(HttpClient& Http, std::string_view Url, IoBuffer&& Payload, bool PlainProgress) { signal(SIGINT, SignalCallbackHandler); @@ -264,11 +204,11 @@ namespace { int ReturnCode = StatusObject["ReturnCode"].AsInt32(-1); if (!AbortReason.empty()) { - throw AsyncJobError(std::string(AbortReason), ReturnCode); + throw ErrorWithReturnCode(std::string(AbortReason), ReturnCode); } else { - throw AsyncJobError("Aborted", ReturnCode); + throw ErrorWithReturnCode("Aborted", ReturnCode); } break; } @@ -563,21 +503,21 @@ DropProjectCommand::~DropProjectCommand() { } -int +void DropProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); @@ -585,7 +525,7 @@ DropProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { - return 1; + throw std::runtime_error(fmt::format("Can't find project '{}'", m_ProjectName)); } if (m_OplogName.empty()) @@ -604,7 +544,6 @@ DropProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg else { Result.ThrowError("delete project failed"sv); - return 1; } } } @@ -613,7 +552,7 @@ DropProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { - return 1; + throw std::runtime_error(fmt::format("Can't find oplog in project '{}'", m_OplogName, m_ProjectName)); } if (m_DryRun) { @@ -632,12 +571,9 @@ DropProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg else { Result.ThrowError("delete oplog failed"sv); - return 1; } } } - - return 0; } /////////////////////////////////////// @@ -656,26 +592,26 @@ ProjectInfoCommand::~ProjectInfoCommand() { } -int +void ProjectInfoCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (!m_OplogName.empty() && m_ProjectName.empty()) { - throw OptionParseException("an oplog can't be specified without also specifying a project"); + throw OptionParseException("'--project' is required", m_Options.help()); } HttpClient Http(m_HostName); @@ -691,7 +627,7 @@ ProjectInfoCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { - return 1; + throw std::runtime_error("Unable to resolve project"); } Url = fmt::format("/prj/{}", m_ProjectName); @@ -702,13 +638,13 @@ ProjectInfoCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { - return 1; + throw std::runtime_error("Unable to resolve project"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { - return 1; + throw std::runtime_error("Unable to resolve oplog"); } Url = fmt::format("/prj/{}/oplog/{}", m_ProjectName, m_OplogName); @@ -718,12 +654,10 @@ ProjectInfoCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); - return 0; } else { Result.ThrowError("failed to fetch info"sv); - return 1; } } @@ -744,7 +678,7 @@ CreateProjectCommand::CreateProjectCommand() CreateProjectCommand::~CreateProjectCommand() = default; -int +void CreateProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); @@ -753,19 +687,19 @@ CreateProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (m_ProjectId.empty()) { - throw OptionParseException("Project name must be given"); + throw OptionParseException("'--project' is required", m_Options.help()); } HttpClient Http(m_HostName); @@ -776,8 +710,7 @@ CreateProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a { if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { - ZEN_CONSOLE_ERROR("Project already exists.\n{}", Result.ToText()); - return 1; + throw std::runtime_error(fmt::format("Project already exists.\n{}", Result.ToText())); } } @@ -792,12 +725,10 @@ CreateProjectCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a : Http.Post(Url, Payload, HttpClient::Accept(ZenContentType::kText))) { ZEN_CONSOLE("{}", Result); - return 0; } else { Result.ThrowError("failed to create project"sv); - return 1; } } @@ -816,7 +747,7 @@ CreateOplogCommand::CreateOplogCommand() CreateOplogCommand::~CreateOplogCommand() = default; -int +void CreateOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); @@ -825,31 +756,31 @@ CreateOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (m_ProjectId.empty()) { - throw OptionParseException("project name must be specified"); + throw OptionParseException("'--project' is required", m_Options.help()); } HttpClient Http(m_HostName); m_ProjectId = ResolveProject(Http, m_ProjectId); if (m_ProjectId.empty()) { - return 1; + throw std::runtime_error("Project can not be found"); } if (m_OplogId.empty()) { - throw OptionParseException("oplog name must be specified"); + throw OptionParseException("'--oplog' is required", m_Options.help()); } std::string Url = fmt::format("/prj/{}/oplog/{}", m_ProjectId, m_OplogId); @@ -857,8 +788,7 @@ CreateOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg { if (HttpClient::Response Result = Http.Get(Url, HttpClient::Accept(ZenContentType::kJSON))) { - ZEN_CONSOLE("Oplog already exists.\n{}", Result.ToText()); - return 1; + throw std::runtime_error(fmt::format("Oplog already exists.\n{}", Result.ToText())); } } @@ -872,12 +802,10 @@ CreateOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg : Http.Post(Url, OplogPayload, HttpClient::Accept(ZenContentType::kText))) { ZEN_CONSOLE("{}", Result); - return 0; } else { Result.ThrowError("failed to create oplog"sv); - return 1; } } @@ -892,6 +820,12 @@ ExportOplogCommand::ExportOplogCommand() m_Options.add_option("", "", "maxblocksize", "Max size for bundled attachments", cxxopts::value(m_MaxBlockSize), "<blocksize>"); m_Options.add_option("", "", + "maxchunksperblock", + "Max number of chunks in one block", + cxxopts::value(m_MaxChunksPerBlock), + "<maxchunksperblock>"); + m_Options.add_option("", + "", "maxchunkembedsize", "Max size for attachment to be bundled", cxxopts::value(m_MaxChunkEmbedSize), @@ -931,7 +865,7 @@ ExportOplogCommand::ExportOplogCommand() "", "access-token-env", "Name of environment variable that holds the cloud/builds Storage access token", - cxxopts::value(m_JupiterAccessTokenEnv)->default_value(DefaultJupiterAccessTokenEnvVariableName), + cxxopts::value(m_JupiterAccessTokenEnv)->default_value(std::string(GetDefaultAccessTokenEnvVariableName())), "<envvariable>"); m_Options.add_option("", "", @@ -1013,7 +947,7 @@ ExportOplogCommand::~ExportOplogCommand() { } -int +void ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { using namespace std::literals; @@ -1022,32 +956,32 @@ ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (m_ProjectName.empty()) { - throw OptionParseException("project name must be specified"); + throw OptionParseException("'--project' is required", m_Options.help()); } HttpClient Http(m_HostName); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { - return 1; + throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { - return 1; + throw std::runtime_error("Oplog can not be found"); } size_t TargetCount = 0; @@ -1059,19 +993,23 @@ ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg { if (TargetCount == 0) { - throw OptionParseException("an export target must be specified"); + throw OptionParseException("'--cloud', '--builds', '--zen' or '--file' is required", m_Options.help()); } else { - throw OptionParseException("a single export target must be specified"); + throw OptionParseException("'--cloud', '--builds', '--zen' or '--file' are conflicting", m_Options.help()); } } if (!m_CloudUrl.empty()) { - if (m_JupiterNamespace.empty() || m_JupiterBucket.empty()) + if (m_JupiterNamespace.empty()) + { + throw OptionParseException("'--namespace' is required", m_Options.help()); + } + if (m_JupiterBucket.empty()) { - throw OptionParseException("Options for cloud target are missing"); + throw OptionParseException("'--bucket' is required", m_Options.help()); } if (m_CloudKey.empty()) { @@ -1086,19 +1024,21 @@ ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg { if (m_JupiterNamespace.empty()) { - throw OptionParseException("Namespace for builds target it missing"); + throw OptionParseException("'--namespace' is required", m_Options.help()); } if (m_JupiterNamespace.empty() || m_JupiterBucket.empty()) { - throw OptionParseException("Bucket for builds target it missing"); + throw OptionParseException("'--bucket' is required", m_Options.help()); } if (m_BuildsMetadataPath.empty() && m_BuildsMetadata.empty()) { - throw OptionParseException("Options for builds target metadata are missing"); + throw OptionParseException("'--metadata' or --'metadata-path' is required", m_Options.help()); } if (!m_BuildsMetadataPath.empty() && !m_BuildsMetadata.empty()) { - throw OptionParseException("Conflicting options for builds target metadata"); + throw OptionParseException( + fmt::format("'--metadata' ('{}') conflicts with --'metadata-path' ('{}')", m_BuildsMetadata, m_BuildsMetadataPath), + m_Options.help()); } if (m_BuildsId.empty()) { @@ -1140,7 +1080,6 @@ ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg if (!Result) { Result.ThrowError("failed deleting existing zen remote oplog"sv); - return 1; } CreateOplog = true; } @@ -1152,7 +1091,6 @@ ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg else { Result.ThrowError("failed checking zen remote oplog"sv); - return 1; } if (CreateOplog) @@ -1161,7 +1099,6 @@ ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg if (HttpClient::Response Result = TargetHttp.Post(Url); !Result) { Result.ThrowError("failed creating zen remote oplog"sv); - return 1; } } } @@ -1185,6 +1122,10 @@ ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg { Writer.AddInteger("maxblocksize"sv, m_MaxBlockSize); } + if (m_MaxChunksPerBlock != 0) + { + Writer.AddInteger("maxchunksperblock"sv, m_MaxChunksPerBlock); + } if (m_MaxChunkEmbedSize != 0) { Writer.AddInteger("maxchunkembedsize"sv, m_MaxChunkEmbedSize); @@ -1340,36 +1281,24 @@ ExportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg ZEN_CONSOLE("Saving oplog '{}/{}' from '{}' to {}", m_ProjectName, m_OplogName, m_HostName, TargetDescription); - try + if (m_Async) { - if (m_Async) + if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), + std::move(Payload), + HttpClient::Accept(ZenContentType::kJSON)); + Result) { - if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), - std::move(Payload), - HttpClient::Accept(ZenContentType::kJSON)); - Result) - { - ZEN_CONSOLE("{}", Result.ToText()); - } - else - { - Result.ThrowError("failed requesting loading oplog export"sv); - } + ZEN_CONSOLE("{}", Result.ToText()); } else { - ExecuteAsyncOperation(Http, - fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), - std::move(Payload), - m_PlainProgress); + Result.ThrowError("failed requesting loading oplog export"sv); } } - catch (const AsyncJobError& Ex) + else { - ZEN_CONSOLE_ERROR("Oplog export failed: '{}'", Ex.what()); - return Ex.m_ReturnCode; + ExecuteAsyncOperation(Http, fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), m_PlainProgress); } - return 0; } //////////////////////////// @@ -1386,13 +1315,6 @@ ImportOplogCommand::ImportOplogCommand() "Absolute path to oplog lifetime marker file if we create the oplog", cxxopts::value(m_GcPath), "<path>"); - m_Options.add_option("", "", "maxblocksize", "Max size for bundled attachments", cxxopts::value(m_MaxBlockSize), "<blocksize>"); - m_Options.add_option("", - "", - "maxchunkembedsize", - "Max size for attachment to be bundled", - cxxopts::value(m_MaxChunkEmbedSize), - "<chunksize>"); m_Options.add_option("", "f", "force", "Force import of all attachments", cxxopts::value(m_Force), "<force>"); m_Options.add_option("", "a", "async", "Trigger import but don't wait for completion", cxxopts::value(m_Async), "<async>"); m_Options.add_option("", "", "clean", "Delete existing target oplog", cxxopts::value(m_Clean), "<clean>"); @@ -1417,7 +1339,7 @@ ImportOplogCommand::ImportOplogCommand() "", "access-token-env", "Name of environment variable that holds the cloud/builds Storage access token", - cxxopts::value(m_JupiterAccessTokenEnv)->default_value(DefaultJupiterAccessTokenEnvVariableName), + cxxopts::value(m_JupiterAccessTokenEnv)->default_value(std::string(GetDefaultAccessTokenEnvVariableName())), "<envvariable>"); m_Options.add_option("", "", @@ -1461,7 +1383,7 @@ ImportOplogCommand::~ImportOplogCommand() { } -int +void ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { using namespace std::literals; @@ -1470,31 +1392,31 @@ ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (m_ProjectName.empty()) { - throw OptionParseException("Project name must be given"); + throw OptionParseException("'--project' is required", m_Options.help()); } if (m_OplogName.empty()) { - throw OptionParseException("Oplog name must be given"); + throw OptionParseException("'--oplog' is required", m_Options.help()); } HttpClient Http(m_HostName); m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { - return 1; + throw std::runtime_error("Project can not be found"); } size_t TargetCount = 0; @@ -1502,20 +1424,24 @@ ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg TargetCount += m_BuildsUrl.empty() ? 0 : 1; TargetCount += m_ZenUrl.empty() ? 0 : 1; TargetCount += m_FileDirectoryPath.empty() ? 0 : 1; - if (TargetCount != 1) + if (TargetCount == 0) + { + throw OptionParseException("'--cloud', '--builds', '--zen' or '--file' is required", m_Options.help()); + } + else if (TargetCount > 1) { - throw OptionParseException("Provide one source only"); + throw OptionParseException("'--cloud', '--builds', '--zen' or '--file' are conflicting", m_Options.help()); } if (!m_CloudUrl.empty()) { if (m_JupiterNamespace.empty()) { - throw OptionParseException("Namespace option for cloud source is missing"); + throw OptionParseException("--'namespace' is required", m_Options.help()); } if (m_JupiterBucket.empty()) { - throw OptionParseException("Bucket option for cloud source is missing"); + throw OptionParseException("--'bucket' is required", m_Options.help()); } if (m_CloudKey.empty()) { @@ -1530,15 +1456,15 @@ ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg { if (m_JupiterNamespace.empty()) { - throw OptionParseException("Namespace option for builds source is missing"); + throw OptionParseException("'--namespace' is required", m_Options.help()); } if (m_JupiterBucket.empty()) { - throw OptionParseException("Bucket option for builds source is missing"); + throw OptionParseException("'--bucket' is required", m_Options.help()); } if (m_BuildsId.empty()) { - throw OptionParseException("Build id option for builds source is missing"); + throw OptionParseException("'--build-id' is required", m_Options.help()); } } @@ -1576,7 +1502,6 @@ ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg else if (!IsHttpSuccessCode(Result.StatusCode)) { Result.ThrowError("failed checking oplog"sv); - return 1; } if (CreateOplog) @@ -1590,7 +1515,6 @@ ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg if (HttpClient::Response Result = Http.Post(Url, OplogPayload); !Result) { Result.ThrowError("failed creating oplog"sv); - return 1; } } @@ -1687,36 +1611,24 @@ ImportOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg ZEN_CONSOLE("Loading oplog '{}/{}' from '{}' to {}", m_ProjectName, m_OplogName, SourceDescription, m_HostName); - try + if (m_Async) { - if (m_Async) + if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), + std::move(Payload), + HttpClient::Accept(ZenContentType::kJSON)); + Result) { - if (HttpClient::Response Result = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), - std::move(Payload), - HttpClient::Accept(ZenContentType::kJSON)); - Result) - { - ZEN_CONSOLE("{}", Result.ToText()); - } - else - { - Result.ThrowError("failed requesting loading oplog import"sv); - } + ZEN_CONSOLE("{}", Result.ToText()); } else { - ExecuteAsyncOperation(Http, - fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), - std::move(Payload), - m_PlainProgress); + Result.ThrowError("failed requesting loading oplog import"sv); } } - catch (const AsyncJobError& Ex) + else { - ZEN_CONSOLE_ERROR("Oplog export failed: '{}'", Ex.what()); - return Ex.m_ReturnCode; + ExecuteAsyncOperation(Http, fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), std::move(Payload), m_PlainProgress); } - return 0; } //////////////////////////// @@ -1735,7 +1647,7 @@ SnapshotOplogCommand::~SnapshotOplogCommand() { } -int +void SnapshotOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { using namespace std::literals; @@ -1744,34 +1656,33 @@ SnapshotOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); if (m_ProjectName.empty()) { - throw OptionParseException("Project name must be given"); - return 1; + throw OptionParseException("'--project' is required", m_Options.help()); } m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { - return 1; + throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { - return 1; + throw std::runtime_error("Oplog can not be found"); } IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); @@ -1781,12 +1692,10 @@ SnapshotOplogCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", m_ProjectName, m_OplogName), Payload, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result); - return 0; } else { Result.ThrowError("failed to create project"sv); - return 1; } } @@ -1802,33 +1711,31 @@ ProjectStatsCommand::~ProjectStatsCommand() { } -int +void ProjectStatsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get("/stats/prj", HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); - return 0; } else { Result.ThrowError("failed to get project stats"sv); - return 1; } } @@ -1856,21 +1763,21 @@ ProjectOpDetailsCommand::~ProjectOpDetailsCommand() { } -int +void ProjectOpDetailsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); @@ -1878,13 +1785,13 @@ ProjectOpDetailsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char* m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { - return 1; + throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { - return 1; + throw std::runtime_error("Oplog can not be found"); } ExtendableStringBuilder<128> Url; @@ -1914,12 +1821,10 @@ ProjectOpDetailsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char* {"csv", m_CSV ? "true" : "false"}})) { ZEN_CONSOLE("{}", Result.ToText()); - return 0; } else { Result.ThrowError("failed to get project details"sv); - return 1; } } @@ -1973,21 +1878,21 @@ OplogMirrorCommand::~OplogMirrorCommand() { } -int +void OplogMirrorCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); @@ -1995,18 +1900,18 @@ OplogMirrorCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { - return 1; + throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { - return 1; + throw std::runtime_error("Oplog can not be found"); } if (m_MirrorRootPath.empty()) { - throw OptionParseException("a target path must be specified"); + throw OptionParseException("'--target' is required", m_Options.help()); } Oid ChunkIdFilter = Oid::Zero; @@ -2015,7 +1920,7 @@ OplogMirrorCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg ChunkIdFilter = Oid::TryFromHexString(m_ChunkIdFilter); if (ChunkIdFilter == Oid::Zero) { - throw OptionParseException("chunkid must be an Oid hex string"); + throw OptionParseException(fmt::format("'--chunk' ('{}') is malformed", m_ChunkIdFilter), m_Options.help()); } } @@ -2128,7 +2033,8 @@ OplogMirrorCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg ZEN_CONSOLE_ERROR("Failed writing file to '{}'. Reason: '{}'", TargetPath, Ex.what()); } } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); } } }; @@ -2137,6 +2043,7 @@ OplogMirrorCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg if (HttpClient::Response Response = Http.Get(fmt::format("/prj/{}/oplog/{}/entries"sv, m_ProjectName, m_OplogName), HttpClient::KeyValueMap(), Parameters)) { + ZEN_CONSOLE("Fetched oplog in {}", NiceTimeSpanMs(uint64_t(Response.ElapsedSeconds * 1000.0))); if (CbObject ResponseObject = Response.AsObject()) { std::unique_ptr<ProgressBar> EmitProgressBar; @@ -2205,25 +2112,20 @@ OplogMirrorCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** arg if (AbortFlag) { - // Error has already been reported by async code - return 1; + throw std::runtime_error("Failed top mirror oplog"); } } else { - ZEN_CONSOLE_ERROR("Unknown format response to oplog entries request"); + throw std::runtime_error("Unknown format response to oplog entries request"); } } else { Response.ThrowError("oplog entries fetch failed"); - - return 1; } ZEN_CONSOLE("mirrored {} files from {} oplog entries successfully", FileCount.load(), OplogEntryCount); - - return 0; } //////////////////////////// @@ -2243,21 +2145,21 @@ OplogValidateCommand::~OplogValidateCommand() { } -int +void OplogValidateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } HttpClient Http(m_HostName); @@ -2265,13 +2167,13 @@ OplogValidateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a m_ProjectName = ResolveProject(Http, m_ProjectName); if (m_ProjectName.empty()) { - return 1; + throw std::runtime_error("Project can not be found"); } m_OplogName = ResolveOplog(Http, m_ProjectName, m_OplogName); if (m_OplogName.empty()) { - return 1; + throw std::runtime_error("Oplog can not be found"); } std::string Url = fmt::format("/prj/{}/oplog/{}/validate", m_ProjectName, m_OplogName); @@ -2279,15 +2181,340 @@ OplogValidateCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** a if (HttpClient::Response Result = Http.Post(Url, HttpClient::Accept(ZenContentType::kJSON))) { ZEN_CONSOLE("{}", Result.ToText()); - return 0; } else { Result.ThrowError("failed to get validate project oplog"sv); - return 1; + } +} + +//////////////////////////// + +OplogDownloadCommand::OplogDownloadCommand() +{ + m_Options.add_options()("h,help", "Print help"); + + m_Options.add_option("", "", "system-dir", "Specify system root", cxxopts::value(m_SystemRootDir), "<systemdir>"); + + m_Options.add_option("output", "", "quiet", "Suppress non-essential output", cxxopts::value(m_Quiet), "<quiet>"); + m_Options.add_option("", "y", "yes", "Don't query for confirmation", cxxopts::value(m_Yes), "<yes>"); + + auto AddCloudOptions = [this](cxxopts::Options& Ops) { + m_AuthOptions.AddOptions(Ops); + + Ops.add_option("cloud build", "", "override-host", "Cloud Builds URL", cxxopts::value(m_OverrideHost), "<override-host>"); + Ops.add_option("cloud build", "", "cloud-url", "Cloud Artifact URL", cxxopts::value(m_Url), "<cloud-url>"); + Ops.add_option("cloud build", "", "host", "Cloud Builds host", cxxopts::value(m_Host), "<host>"); + Ops.add_option("cloud build", + "", + "assume-http2", + "Assume that the builds endpoint is a HTTP/2 endpoint skipping HTTP/1.1 upgrade handshake", + cxxopts::value(m_AssumeHttp2), + "<assumehttp2>"); + + Ops.add_option("cloud build", "", "namespace", "Builds Storage namespace", cxxopts::value(m_Namespace), "<namespace>"); + Ops.add_option("cloud build", "", "bucket", "Builds Storage bucket", cxxopts::value(m_Bucket), "<bucket>"); + }; + + AddCloudOptions(m_Options); + + m_Options.add_option("", "", "build-id", "Build Id", cxxopts::value(m_BuildId), "<id>"); + m_Options.add_option("", + "", + "output-path", + "Path to oplog output, extension .json or .cb (compact binary). Default is output to console", + cxxopts::value(m_OutputPath), + "<path>"); + + m_Options.parse_positional({"cloud-url", "output-path"}); + m_Options.positional_help("[<cloud-url> <output-path>]"); +} + +OplogDownloadCommand::~OplogDownloadCommand() +{ +} + +void +OplogDownloadCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) +{ + ZEN_UNUSED(GlobalOptions); + + if (!ParseOptions(argc, argv)) + { + return; } - return 0; + if (!m_Quiet) + { + LogExecutableVersionAndPid(); + } + + auto ParseSystemOptions = [&]() { + if (m_SystemRootDir.empty()) + { + m_SystemRootDir = PickDefaultSystemRootDirectory(); + } + MakeSafeAbsolutePathÍnPlace(m_SystemRootDir); + }; + ParseSystemOptions(); + + auto ParseStorageOptions = [&](bool RequireNamespace, bool RequireBucket) { + if (!m_Url.empty()) + { + if (!m_Host.empty()) + { + throw OptionParseException(fmt::format("'--host' ('{}') conflicts with '--url' ('{}')", m_Host, m_Url), m_Options.help()); + } + if (!m_Bucket.empty()) + { + throw OptionParseException(fmt::format("'--bucket' ('{}') conflicts with '--url' ('{}')", m_Bucket, m_Url), + m_Options.help()); + } + if (!m_BuildId.empty()) + { + throw OptionParseException(fmt::format("'--buildid' ('{}') conflicts with '--url' ('{}')", m_BuildId, m_Url), + m_Options.help()); + } + if (!ParseBuildStorageUrl(m_Url, m_Host, m_Namespace, m_Bucket, m_BuildId)) + { + throw OptionParseException("'--url' ('{}') is malformed, it does not match the Cloud Artifact URL format", + m_Options.help()); + } + } + + if (!m_OverrideHost.empty() || !m_Host.empty()) + { + if (RequireNamespace && m_Namespace.empty()) + { + throw OptionParseException("'--namespace' is required", m_Options.help()); + } + if (RequireBucket && m_Bucket.empty()) + { + throw OptionParseException("'--bucket' is required", m_Options.help()); + } + } + + if (m_OverrideHost.empty() && m_Host.empty()) + { + throw OptionParseException("'--host' or '--overridehost' is required", m_Options.help()); + } + }; + + std::unique_ptr<AuthMgr> Auth; + HttpClientSettings ClientSettings{.LogCategory = "httpbuildsclient", + .AssumeHttp2 = m_AssumeHttp2, + .AllowResume = true, + .RetryCount = 2}; + + ParseStorageOptions(/*RequireNamespace*/ true, /*RequireBucket*/ true); + + m_AuthOptions.ParseOptions(m_Options, + m_SystemRootDir, + ClientSettings, + m_OverrideHost.empty() ? m_Host : m_OverrideHost, + Auth, + m_Quiet, + /*Hidden*/ false); + + std::string BuildStorageName = ZEN_CLOUD_STORAGE; + + std::string CloudHost; + + auto TestHostEndpoint = [](std::string_view BaseUrl, const bool AssumeHttp2) -> std::pair<bool, std::string> { + HttpClientSettings TestClientSettings{.LogCategory = "httpbuildsclient", + .ConnectTimeout = std::chrono::milliseconds{1000}, + .Timeout = std::chrono::milliseconds{2000}, + .AssumeHttp2 = AssumeHttp2, + .AllowResume = true, + .RetryCount = 0}; + + HttpClient TestHttpClient(BaseUrl, TestClientSettings); + HttpClient::Response TestResponse = TestHttpClient.Get("/health/live"); + if (TestResponse.IsSuccess()) + { + return {true, ""}; + } + return {false, TestResponse.ErrorMessage("")}; + }; + + if (m_OverrideHost.empty()) + { + JupiterServerDiscovery Response = DiscoverJupiterEndpoints(m_Host, ClientSettings); + + if (Response.ServerEndPoints.empty()) + { + throw std::runtime_error(fmt::format("Failed to find any builds hosts at {}", m_Host)); + } + for (const JupiterServerDiscovery::EndPoint& ServerEndpoint : Response.ServerEndPoints) + { + if (!ServerEndpoint.BaseUrl.empty()) + { + if (JupiterEndpointTestResult TestResult = TestJupiterEndpoint(ServerEndpoint.BaseUrl, ServerEndpoint.AssumeHttp2); + TestResult.Success) + { + CloudHost = ServerEndpoint.BaseUrl; + m_AssumeHttp2 = ServerEndpoint.AssumeHttp2; + BuildStorageName = ServerEndpoint.Name; + break; + } + else + { + ZEN_DEBUG("Unable to reach host {}. Reason: {}", ServerEndpoint.BaseUrl, TestResult.FailureReason); + } + } + } + if (CloudHost.empty()) + { + throw std::runtime_error( + fmt::format("Failed to find any usable builds hosts out of {} using {}", Response.ServerEndPoints.size(), m_Host)); + } + } + else if (JupiterEndpointTestResult TestResult = TestJupiterEndpoint(m_OverrideHost, m_AssumeHttp2); TestResult.Success) + { + CloudHost = m_OverrideHost; + } + else + { + throw std::runtime_error(fmt::format("Host {} could not be reached. Reason: {}", m_OverrideHost, TestResult.FailureReason)); + } + + Oid BuildId = Oid::TryFromHexString(m_BuildId); + if (BuildId == Oid::Zero) + { + throw OptionParseException("'--build-id' is malformed", m_Options.help()); + } + + BuildStorage::Statistics StorageStats; + HttpClient BuildStorageHttp(CloudHost, ClientSettings); + + if (!m_Quiet) + { + std::string StorageDescription = fmt::format("Cloud {}{}. SessionId: '{}'. Namespace '{}', Bucket '{}'", + BuildStorageName.empty() ? "" : fmt::format("{}, ", BuildStorageName), + CloudHost, + BuildStorageHttp.GetSessionId(), + m_Namespace, + m_Bucket); + + ZEN_CONSOLE("Remote: {}", StorageDescription); + } + + std::filesystem::path StorageTempPath = std::filesystem::temp_directory_path() / ("zen_" + Oid::NewOid().ToString()); + + std::unique_ptr<BuildStorage> BuildStorage = + CreateJupiterBuildStorage(Log(), BuildStorageHttp, StorageStats, m_Namespace, m_Bucket, m_AllowRedirect, StorageTempPath); + + Stopwatch Timer; + CbObject BuildObject = BuildStorage->GetBuild(BuildId); + if (!m_Quiet) + { + ZEN_CONSOLE("Fetched {}/{}/{}/{} in {}", m_Url, m_Namespace, m_Bucket, BuildId, NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + } + + Timer.Reset(); + + CbObjectView PartsObject = BuildObject["parts"sv].AsObjectView(); + if (!PartsObject) + { + throw std::runtime_error( + fmt::format("The build {}/{}/{}/{} payload does not contain a 'parts' object"sv, m_Url, m_Namespace, m_Bucket, m_BuildId)); + } + + static const std::string_view OplogContainerPartName = "oplogcontainer"sv; + + const Oid OplogBuildPartId = PartsObject[OplogContainerPartName].AsObjectId(); + if (OplogBuildPartId == Oid::Zero) + { + throw std::runtime_error(fmt::format("The build {}/{}/{}/{} payload 'parts' object does not contain a '{}' entry"sv, + m_Url, + m_Namespace, + m_Bucket, + m_BuildId, + OplogContainerPartName)); + } + + CbObject ContainerObject = BuildStorage->GetBuildPart(BuildId, OplogBuildPartId); + + MemoryView OpsSection = ContainerObject["ops"sv].AsBinaryView(); + IoBuffer OpsBuffer(IoBuffer::Wrap, OpsSection.GetData(), OpsSection.GetSize()); + IoBuffer SectionPayload = CompressedBuffer::FromCompressedNoValidate(std::move(OpsBuffer)).Decompress().AsIoBuffer(); + + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject SectionObject = ValidateAndReadCompactBinaryObject(std::move(SectionPayload), ValidateResult); + ValidateResult == CbValidateError::None && ContainerObject) + { + if (!m_Quiet) + { + ZEN_CONSOLE("Decompressed and validated oplog payload {} -> {} in {}", + NiceBytes(OpsSection.GetSize()), + NiceBytes(SectionObject.GetSize()), + NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + } + + if (m_OutputPath.empty()) + { + if (!m_Yes) + { + if (OpsSection.GetSize() > 8u * 1024u * 1024u) + { + while (!m_Yes) + { + const std::string Prompt = fmt::format("Do you want to output an oplog of size {} to console? (yes/no) ", + NiceBytes(SectionObject.GetSize())); + printf("%s", Prompt.c_str()); + std::string Reponse; + std::getline(std::cin, Reponse); + Reponse = ToLower(Reponse); + if (Reponse == "y" || Reponse == "yes") + { + m_Yes = true; + } + else if (Reponse == "n" || Reponse == "no") + { + return; + } + } + } + } + ExtendableStringBuilder<1024> SB; + SectionObject.ToJson(SB); + ForEachStrTok(SB.ToView(), '\n', [](std::string_view Row) { + ZEN_CONSOLE("{}", Row); + return true; + }); + } + else + { + Timer.Reset(); + const std::string Extension = ToLower(m_OutputPath.extension().string()); + if (Extension == ".cb" || Extension == ".cbo") + { + WriteFile(m_OutputPath, IoBuffer(IoBuffer::Wrap, SectionObject.GetView().GetData(), SectionObject.GetSize())); + } + else if (Extension == ".json") + { + ExtendableStringBuilder<1024> SB; + SectionObject.ToJson(SB); + WriteFile(m_OutputPath, IoBuffer(IoBuffer::Wrap, SB.Data(), SB.Size())); + } + else + { + throw std::runtime_error(fmt::format("Unsupported output extension type '{}'", Extension)); + } + if (!m_Quiet) + { + ZEN_CONSOLE("Wrote {} to '{}' in {}", + NiceBytes(FileSizeFromPath(m_OutputPath)), + m_OutputPath, + NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + } + } + } + else + { + throw std::runtime_error( + fmt::format("Failed to parse oplog container: '{}' ('{}')", "Section has unexpected data type", ToString(ValidateResult))); + } } } // namespace zen diff --git a/src/zen/cmds/projectstore_cmd.h b/src/zen/cmds/projectstore_cmd.h index 0d24d8529..ae8233f51 100644 --- a/src/zen/cmds/projectstore_cmd.h +++ b/src/zen/cmds/projectstore_cmd.h @@ -4,6 +4,8 @@ #include "../zen.h" +#include "../authutils.h" + namespace zen { class ProjectStoreCommand : public ZenCmdBase @@ -17,7 +19,7 @@ public: DropProjectCommand(); ~DropProjectCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -33,7 +35,7 @@ class ProjectInfoCommand : public ProjectStoreCommand public: ProjectInfoCommand(); ~ProjectInfoCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -49,7 +51,7 @@ public: CreateProjectCommand(); ~CreateProjectCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -69,7 +71,7 @@ public: CreateOplogCommand(); ~CreateOplogCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -87,7 +89,7 @@ public: ExportOplogCommand(); ~ExportOplogCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -97,6 +99,7 @@ private: std::string m_ProjectName; std::string m_OplogName; uint64_t m_MaxBlockSize = 0; + uint64_t m_MaxChunksPerBlock = 0; uint64_t m_MaxChunkEmbedSize = 0; bool m_EmbedLooseFiles = false; bool m_Force = false; @@ -142,7 +145,7 @@ public: ImportOplogCommand(); ~ImportOplogCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -152,8 +155,6 @@ private: std::string m_ProjectName; std::string m_OplogName; std::string m_GcPath; - size_t m_MaxBlockSize = 0; - size_t m_MaxChunkEmbedSize = 0; bool m_Force = false; bool m_Async = false; bool m_IgnoreMissingAttachments = false; @@ -162,6 +163,7 @@ private: std::string m_JupiterNamespace; std::string m_JupiterBucket; + std::string m_JupiterOpenIdProvider; std::string m_JupiterAccessToken; std::string m_JupiterAccessTokenEnv; @@ -189,7 +191,7 @@ public: SnapshotOplogCommand(); ~SnapshotOplogCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -204,7 +206,7 @@ class ProjectStatsCommand : public ProjectStoreCommand public: ProjectStatsCommand(); ~ProjectStatsCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -217,7 +219,7 @@ class ProjectOpDetailsCommand : public ProjectStoreCommand public: ProjectOpDetailsCommand(); ~ProjectOpDetailsCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -237,7 +239,7 @@ class OplogMirrorCommand : public ProjectStoreCommand public: OplogMirrorCommand(); ~OplogMirrorCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -258,7 +260,7 @@ class OplogValidateCommand : public ProjectStoreCommand public: OplogValidateCommand(); ~OplogValidateCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -268,4 +270,38 @@ private: std::string m_OplogName; }; +class OplogDownloadCommand : public ProjectStoreCommand +{ +public: + static constexpr char Name[] = "oplog-download"; + static constexpr char Description[] = "Download an cloud storage oplog"; + + OplogDownloadCommand(); + ~OplogDownloadCommand(); + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual cxxopts::Options& Options() override { return m_Options; } + +private: + cxxopts::Options m_Options{Name, Description}; + + std::filesystem::path m_SystemRootDir; + + bool m_Quiet = false; + bool m_Yes = false; + + AuthCommandLineOptions m_AuthOptions; + + // cloud builds + std::string m_OverrideHost; + std::string m_Host; + std::string m_Url; + bool m_AssumeHttp2 = false; + bool m_AllowRedirect = false; + std::string m_Namespace; + std::string m_Bucket; + std::string m_BuildId; + + std::filesystem::path m_OutputPath; +}; + } // namespace zen diff --git a/src/zen/cmds/rpcreplay_cmd.cpp b/src/zen/cmds/rpcreplay_cmd.cpp index 6c26eaf23..dbf15ddd4 100644 --- a/src/zen/cmds/rpcreplay_cmd.cpp +++ b/src/zen/cmds/rpcreplay_cmd.cpp @@ -12,12 +12,13 @@ #include <zencore/stream.h> #include <zencore/timer.h> #include <zencore/workthreadpool.h> +#include <zenhttp/formatters.h> +#include <zenhttp/httpclient.h> #include <zenhttp/httpcommon.h> #include <zenhttp/packageformat.h> -#include <zenutil/cache/rpcrecording.h> +#include <zenutil/rpcrecording.h> ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> #include <fmt/format.h> #include <gsl/gsl-lite.hpp> ZEN_THIRD_PARTY_INCLUDES_END @@ -39,33 +40,37 @@ RpcStartRecordingCommand::RpcStartRecordingCommand() RpcStartRecordingCommand::~RpcStartRecordingCommand() = default; -int +void RpcStartRecordingCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions, argc, argv); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (m_RecordingPath.empty()) { - throw zen::OptionParseException("Rpc start recording command requires a path"); + throw OptionParseException("'--path' is required", m_Options.help()); } - cpr::Session Session; - Session.SetUrl(fmt::format("{}/z$/exec$/start-recording"sv, m_HostName)); - Session.SetParameters({{"path", m_RecordingPath}}); - cpr::Response Response = Session.Post(); - ZEN_CONSOLE("{}", FormatHttpResponse(Response)); - return MapHttpToCommandReturnCode(Response); + HttpClient Http(m_HostName); + if (HttpClient::Response Response = + Http.Post("/z$/exec$/start-recording"sv, HttpClient::KeyValueMap{}, HttpClient::KeyValueMap({{"path", m_RecordingPath}}))) + { + ZEN_CONSOLE("{}", Response.ToText()); + } + else + { + Response.ThrowError("Failed to start recording"); + } } //////////////////////////////////////////////////// @@ -78,28 +83,32 @@ RpcStopRecordingCommand::RpcStopRecordingCommand() RpcStopRecordingCommand::~RpcStopRecordingCommand() = default; -int +void RpcStopRecordingCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions, argc, argv); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } - cpr::Session Session; - Session.SetUrl(fmt::format("{}/z$/exec$/stop-recording"sv, m_HostName)); - cpr::Response Response = Session.Post(); - ZEN_CONSOLE("{}", FormatHttpResponse(Response)); - return MapHttpToCommandReturnCode(Response); + HttpClient Http(m_HostName); + if (HttpClient::Response Response = Http.Post("/z$/exec$/stop-recording"sv)) + { + ZEN_CONSOLE("{}", Response.ToText()); + } + else + { + Response.ThrowError("Failed to stop recording"); + } } //////////////////////////////////////////////////// @@ -174,26 +183,26 @@ RpcReplayCommand::RpcReplayCommand() RpcReplayCommand::~RpcReplayCommand() = default; -int +void RpcReplayCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions, argc, argv); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } if (m_RecordingPath.empty()) { - throw zen::OptionParseException("Rpc replay command requires a path"); + throw OptionParseException("'--path' is required", m_Options.help()); } if (!IsDir(m_RecordingPath)) @@ -203,16 +212,31 @@ RpcReplayCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) m_ThreadCount = Max(m_ThreadCount, 1); + ZEN_CONSOLE("Replay '{}' (start offset {}, stride {}) to '{}', {} threads", + m_RecordingPath, + m_Offset, + m_Stride, + m_HostName, + m_ThreadCount); + Stopwatch TotalTimer; if (m_OnHost) { - cpr::Session Session; - Session.SetUrl(fmt::format("{}/z$/exec$/replay-recording"sv, m_HostName)); - Session.SetParameters({{"path", m_RecordingPath}, {"thread-count", fmt::format("{}", m_ThreadCount)}}); - cpr::Response Response = Session.Post(); - ZEN_CONSOLE("{}", FormatHttpResponse(Response)); - return MapHttpToCommandReturnCode(Response); + HttpClient Http(m_HostName); + if (HttpClient::Response Response = + Http.Post("/z$/exec$/replay-recording"sv, + HttpClient::KeyValueMap{}, + HttpClient::KeyValueMap({{"path", m_RecordingPath}, {"thread-count", fmt::format("{}", m_ThreadCount)}}))) + { + ZEN_CONSOLE("{}", Response.ToText()); + + return; + } + else + { + Response.ThrowError("Failed to start replay"); + } } std::unique_ptr<cache::IRpcRequestReplayer> Replayer = cache::MakeDiskRequestReplayer(m_RecordingPath, true); @@ -251,7 +275,7 @@ RpcReplayCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ZEN_CONSOLE("Waiting for worker processes..."); Sleep(1000); } - return 0; + return; } else { @@ -278,8 +302,7 @@ RpcReplayCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } }); - cpr::Session Session; - Session.SetUrl(fmt::format("{}/z$/$rpc"sv, m_HostName)); + HttpClient Http{m_HostName}; uint64_t EntryIndex = EntryOffset.fetch_add(m_Stride); while (EntryIndex < EntryCount) @@ -399,46 +422,26 @@ RpcReplayCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!m_DryRun) { - StringBuilder<32> SessionIdString; + Http.SetSessionId(RequestInfo.SessionId); + Payload.SetContentType(RequestInfo.ContentType); - if (RequestInfo.SessionId != Oid::Zero) - { - RequestInfo.SessionId.ToString(SessionIdString); - } - else - { - GetSessionId().ToString(SessionIdString); - } + HttpClient::Response Response = + Http.Post("/z$/$rpc", Payload, {HttpClient::Accept(RequestInfo.AcceptType)}); - Session.SetHeader({{"Content-Type", std::string(MapContentTypeToString(RequestInfo.ContentType))}, - {"Accept", std::string(MapContentTypeToString(RequestInfo.AcceptType))}, - {"UE-Session", std::string(SessionIdString)}}); - - uint64_t Offset = 0; - auto ReadCallback = [&Payload, &Offset](char* buffer, size_t& size, intptr_t) { - size = Min<size_t>(size, Payload.GetSize() - Offset); - IoBuffer PayloadRange = IoBuffer(Payload, Offset, size); - MutableMemoryView Data(buffer, size); - Data.CopyFrom(PayloadRange.GetView()); - Offset += size; - return true; - }; - Session.SetReadCallback(cpr::ReadCallback(gsl::narrow<cpr::cpr_off_t>(Payload.GetSize()), ReadCallback)); - cpr::Response Response = Session.Post(); BytesSent.fetch_add(Payload.GetSize()); - if (Response.error || !(IsHttpSuccessCode(Response.status_code) || - Response.status_code == gsl::narrow<long>(HttpResponseCode::NotFound))) + if (!Response) { - ZEN_CONSOLE_ERROR("{}", FormatHttpResponse(Response)); + ZEN_CONSOLE_ERROR("{}", Response); break; } - BytesReceived.fetch_add(Response.downloaded_bytes); + BytesReceived.fetch_add(Response.DownloadedBytes); } } EntryIndex = EntryOffset.fetch_add(m_Stride); } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); } while (!WorkLatch.Wait(1000)) @@ -478,8 +481,6 @@ RpcReplayCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) NiceByteRate(Received, ElapsedMS), NiceTimeSpanMs(ElapsedMS), NiceTimeSpanMs(TotalTimer.GetElapsedTimeMs())); - - return 0; } } // namespace zen diff --git a/src/zen/cmds/rpcreplay_cmd.h b/src/zen/cmds/rpcreplay_cmd.h index 42cdd4ac1..a6363b614 100644 --- a/src/zen/cmds/rpcreplay_cmd.h +++ b/src/zen/cmds/rpcreplay_cmd.h @@ -12,7 +12,7 @@ public: RpcStartRecordingCommand(); ~RpcStartRecordingCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -27,7 +27,7 @@ public: RpcStopRecordingCommand(); ~RpcStopRecordingCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -41,7 +41,7 @@ public: RpcReplayCommand(); ~RpcReplayCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/run_cmd.cpp b/src/zen/cmds/run_cmd.cpp index 309b8996a..00ab16fe5 100644 --- a/src/zen/cmds/run_cmd.cpp +++ b/src/zen/cmds/run_cmd.cpp @@ -57,32 +57,32 @@ RunCommand::~RunCommand() { } -int +void RunCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { - ZEN_UNUSED(GlobalOptions); - if (!ParseOptions(argc, argv)) { - return 0; + return; } // Validate arguments if (GlobalOptions.PassthroughArgV.empty() || GlobalOptions.PassthroughArgV[0].empty()) - throw OptionParseException("No command specified. The command to run is passed in after a double dash ('--') on the command line"); + throw OptionParseException("No command specified. The command to run is passed in after a double dash ('--') on the command line", + m_Options.help()); if (m_RunCount < 0) - throw OptionParseException("Invalid count specified"); + throw OptionParseException(fmt::format("'--count' ('{}') is invalid", m_RunCount), m_Options.help()); if (m_RunTime < -1 || m_RunTime == 0) - throw OptionParseException("Invalid run time specified"); + throw OptionParseException(fmt::format("'--time' ('{}') is invalid", m_RunTime), m_Options.help()); if (m_MaxBaseDirectoryCount < 0) - throw OptionParseException("Invalid directory count specified"); + throw OptionParseException(fmt::format("'--max-dirs' ('{}') is invalid", m_MaxBaseDirectoryCount), m_Options.help()); if (m_RunTime > 0 && m_RunCount > 0) - throw OptionParseException("Specify either time or count, not both"); + throw OptionParseException(fmt::format("'--time' ('{}') conflicts with '--count' ('{}') ", m_RunTime, m_RunCount), + m_Options.help()); if (m_RunCount == 0) m_RunCount = 1; @@ -190,8 +190,6 @@ RunCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { fmt::print("run complete, no error exit code\n", m_RunCount); } - - return 0; } } // namespace zen diff --git a/src/zen/cmds/run_cmd.h b/src/zen/cmds/run_cmd.h index f6512a4e8..570a2e63a 100644 --- a/src/zen/cmds/run_cmd.h +++ b/src/zen/cmds/run_cmd.h @@ -12,7 +12,7 @@ public: RunCommand(); ~RunCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } diff --git a/src/zen/cmds/serve_cmd.cpp b/src/zen/cmds/serve_cmd.cpp index cfaa12bc0..49389bcdf 100644 --- a/src/zen/cmds/serve_cmd.cpp +++ b/src/zen/cmds/serve_cmd.cpp @@ -34,19 +34,19 @@ ServeCommand::~ServeCommand() { } -int +void ServeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } if (m_ProjectName.empty()) { - throw zen::OptionParseException("command requires a project"); + throw OptionParseException("'--project' is required", m_Options.help()); } if (m_OplogName.empty()) @@ -58,18 +58,18 @@ ServeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } else { - throw zen::OptionParseException("command requires an oplog"); + throw OptionParseException("'--oplog' is required", m_Options.help()); } } if (m_RootPath.empty()) { - throw zen::OptionParseException("command requires a root path"); + throw OptionParseException("'--path' is required", m_Options.help()); } if (!IsDir(m_RootPath)) { - throw zen::OptionParseException(fmt::format("path must exist and must be a directory: '{}'", m_RootPath)); + throw std::runtime_error(fmt::format("'--path' ('{}') must exist and must be a directory", m_RootPath)); } uint16_t ServerPort = 0; @@ -95,7 +95,7 @@ ServeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_CONSOLE_ERROR("Failed to spawn server on port {}: '{}'", ServerPort, Ex.what()); - throw zen::OptionParseException("unable to resolve server specification (even after spawning server)"); + throw std::runtime_error("Unable to resolve server specification (even after spawning server)"); } } else @@ -197,8 +197,7 @@ ServeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (auto NewProjectResponse = Client.Post(ProjectUri, Project.Save()); !NewProjectResponse) { - // TODO: include details - throw std::runtime_error("failed to create project"); + NewProjectResponse.ThrowError("Failed to create project"); } } @@ -212,8 +211,7 @@ ServeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (auto NewOplogResponse = Client.Post(ProjectOplogUri, Oplog.Save()); !NewOplogResponse) { - // TODO: include details - throw std::runtime_error("failed to create oplog"); + NewOplogResponse.ThrowError("Failed to create oplog"); } } @@ -233,8 +231,6 @@ ServeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) #if ZEN_PLATFORM_WINDOWS _getch(); // TEMPORARY HACK #endif - - return 0; } } // namespace zen diff --git a/src/zen/cmds/serve_cmd.h b/src/zen/cmds/serve_cmd.h index 007038d84..ac74981f2 100644 --- a/src/zen/cmds/serve_cmd.h +++ b/src/zen/cmds/serve_cmd.h @@ -14,7 +14,7 @@ public: ServeCommand(); ~ServeCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/service_cmd.cpp b/src/zen/cmds/service_cmd.cpp index 3847c423d..f64e6860d 100644 --- a/src/zen/cmds/service_cmd.cpp +++ b/src/zen/cmds/service_cmd.cpp @@ -19,6 +19,11 @@ # include <unistd.h> #endif +#if ZEN_PLATFORM_LINUX +# include <pwd.h> +# include <grp.h> +#endif + ZEN_THIRD_PARTY_INCLUDES_START #include <gsl/gsl-lite.hpp> ZEN_THIRD_PARTY_INCLUDES_END @@ -52,7 +57,7 @@ namespace { return fRet; } - int WinRelaunchElevated() + void WinRelaunchElevated() { TCHAR CurrentDir[4096]; GetCurrentDirectory(4096, CurrentDir); @@ -75,27 +80,26 @@ namespace { shExInfo.nShow = SW_SHOW; shExInfo.hInstApp = 0; - DWORD ReturnCode = 1; + DWORD ProcessReturnCode = 1; if (ShellExecuteEx(&shExInfo)) { WaitForSingleObject(shExInfo.hProcess, INFINITE); - GetExitCodeProcess(shExInfo.hProcess, &ReturnCode); + GetExitCodeProcess(shExInfo.hProcess, &ProcessReturnCode); CloseHandle(shExInfo.hProcess); - if (ReturnCode == 0) + if (ProcessReturnCode == 0) { ZEN_CONSOLE("Elevated execution completed successfully."); } else { - ZEN_CONSOLE("Elevated execution completed unsuccessfully, return code: '{}'.", ReturnCode); + throw ErrorWithReturnCode(fmt::format("Elevated execution completed unsuccessfully, return code: '{}'.", ProcessReturnCode), + (int)ProcessReturnCode); } } else { - ZEN_CONSOLE("Failed to run elevated, operation did not complete."); - ReturnCode = DWORD(-1); + ThrowLastError("Failed to run elevated, operation did not complete"); } - return (int)ReturnCode; } #else // ZEN_PLATFORM_WINDOWS @@ -104,25 +108,23 @@ namespace { #endif // ZEN_PLATFORM_WINDOWS - int RunElevated(bool AllowElevation) + void RunElevated(bool AllowElevation) { #if ZEN_PLATFORM_WINDOWS if (AllowElevation) { - return WinRelaunchElevated(); + WinRelaunchElevated(); } else { - ZEN_CONSOLE( + throw std::runtime_error(fmt::format( "This command requires elevated priviliges. Run command with elevated priviliges or add '--allow-elevation' command line " - "option."); - return 1; + "option.")); } #endif // ZEN_PLATFORM_WINDOWS #if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC ZEN_UNUSED(AllowElevation); - ZEN_CONSOLE("This command requires elevated priviliges. Run the command with `sudo`"); - return 1; + throw std::runtime_error(fmt::format("This command requires elevated priviliges. Run the command with `sudo`")); #endif // ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC } @@ -295,11 +297,9 @@ enum class ServiceStatusReturnCode UnknownError }; -int +void ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { - ZEN_UNUSED(GlobalOptions); - using namespace std::literals; std::vector<char*> SubCommandArguments; @@ -307,17 +307,17 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) int ParentCommandArgCount = GetSubCommand(m_Options, argc, argv, m_SubCommands, SubOption, SubCommandArguments); if (!ParseOptions(ParentCommandArgCount, argv)) { - return 0; + return; } if (SubOption == nullptr) { - throw zen::OptionParseException("command verb is missing"); + throw OptionParseException("'verb' option is required", m_Options.help()); } if (!ParseOptions(*SubOption, gsl::narrow<int>(SubCommandArguments.size()), SubCommandArguments.data())) { - return 0; + return; } if (SubOption == &m_StatusOptions) @@ -326,18 +326,15 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) std::error_code Ec = QueryInstalledService(m_ServiceName, Info); if (Ec) { - ZEN_CONSOLE("Can't get information about service '{}'. Reason: '{}'", m_ServiceName, Ec.message()); - return gsl::narrow<int>(ServiceStatusReturnCode::UnknownError); + throw std::runtime_error(fmt::format("Can't get information about service '{}'. Reason: '{}'", m_ServiceName, Ec.message())); } if (Info.Status == ServiceStatus::NotInstalled) { - ZEN_CONSOLE("Service '{}' is not installed", m_ServiceName); - return gsl::narrow<int>(ServiceStatusReturnCode::NotInstalled); + throw std::runtime_error(fmt::format("Service '{}' is not installed", m_ServiceName)); } else if (Info.Status != ServiceStatus::Running) { - ZEN_CONSOLE("Service '{}' is not running", m_ServiceName); - return gsl::narrow<int>(ServiceStatusReturnCode::NotRunning); + throw std::runtime_error(fmt::format("Service '{}' is not running", m_ServiceName)); } else { @@ -349,7 +346,8 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (!IsElevated()) { - return RunElevated(m_AllowElevation); + RunElevated(m_AllowElevation); + return; } ServiceInfo Info; @@ -363,12 +361,10 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) Ec = StopService(m_ServiceName); if (Ec) { - ZEN_CONSOLE("Failed to stop service '{}' using '{}'. Reason: '{}'", - m_ServiceName, - m_ServerExecutable, - Ec.message()); - - return 1; + throw std::runtime_error(fmt::format("Failed to stop service '{}' using '{}'. Reason: '{}'", + m_ServiceName, + m_ServerExecutable, + Ec.message())); } int Timeout = 30000; // Wait up to 30 seconds for the service to fully stop before uninstalling @@ -377,8 +373,7 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) Ec = QueryInstalledService(m_ServiceName, Info); if (Ec) { - ZEN_CONSOLE("Failed to wait for service to stop: '{}'", Ec.message()); - return 1; + throw std::runtime_error(fmt::format("Failed to wait for service to stop: '{}'", Ec.message())); } if (Info.Status == ServiceStatus::Stopped) @@ -392,26 +387,23 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (Info.Status != ServiceStatus::Stopped) { - ZEN_CONSOLE("Timed out waiting for service to stop"); - return 1; + throw std::runtime_error("Timed out waiting for service to stop"); } } Ec = UninstallService(m_ServiceName); if (Ec) { - ZEN_CONSOLE("Failed to uninstall running service '{}' using '{}'. Reason: '{}'", - m_ServiceName, - m_ServerExecutable, - Ec.message()); - - return 1; + throw std::runtime_error(fmt::format("Failed to uninstall running service '{}' using '{}'. Reason: '{}'", + m_ServiceName, + m_ServerExecutable, + Ec.message())); } } else { ZEN_CONSOLE("Service '{}' already installed:\n{}", m_ServiceName, FmtServiceInfo(Info, " ")); - return 1; + return; } } if (m_ServerExecutable.empty()) @@ -426,7 +418,7 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (m_InstallPath.empty()) { - throw zen::OptionParseException("--full requires --install-path to be specified"); + throw OptionParseException("'--full' requires '--install-path'", SubOption->help()); } std::filesystem::path ExePath = zen::GetRunningExecutablePath(); @@ -435,13 +427,14 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ExePath, m_ServerExecutable, #if ZEN_PLATFORM_WINDOWS - ExePath.replace_extension("pdb"), - m_ServerExecutable.replace_extension("pdb"), - ExePath - .replace_filename("crashpad_handler.exe") + ExePath.parent_path() / ExePath.stem().replace_extension("pdb"), + m_ServerExecutable.parent_path() / m_ServerExecutable.stem().replace_extension("pdb"), + ExePath.parent_path() / + std::filesystem::path("crashpad_handler.exe") #endif #if ZEN_PLATFORM_MAC - ExePath.replace_filename("crashpad_handler") + ExePath.parent_path() / + std::filesystem::path("crashpad_handler") #endif }; @@ -450,17 +443,44 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!std::filesystem::is_directory(m_InstallPath) && !CreateDirectories(m_InstallPath)) { - ZEN_CONSOLE("Unable to create install directory '{}'", m_InstallPath); - return 1; + throw std::runtime_error(fmt::format("Unable to create install directory '{}'", m_InstallPath)); + } + +#if ZEN_PLATFORM_LINUX + uid_t UserId = 0; + gid_t GroupId = 0; + if (!m_UserName.empty()) + { + struct passwd* Passwd = getpwnam(m_UserName.c_str()); + if (Passwd == NULL) + { + throw std::runtime_error(fmt::format("Unable to determine user ID for user '{}'", m_UserName)); + } + + UserId = Passwd->pw_uid; + + struct group* Grp = getgrnam(m_UserName.c_str()); + if (Grp == NULL) + { + throw std::runtime_error(fmt::format("Unable to determine group ID for user '{}'", m_UserName)); + } + + GroupId = Grp->gr_gid; + if (chown(m_InstallPath.c_str(), UserId, GroupId) != 0) + { + throw std::runtime_error( + fmt::format("Unable to set ownership of directory '{}' for user '{}'", m_InstallPath, m_UserName)); + } } +#endif for (const std::filesystem::path& File : FilesToCopy) { std::filesystem::path Destination = m_InstallPath / File.filename(); + if (!CopyFile(File, Destination, {.EnableClone = false})) { - ZEN_CONSOLE("Failed to copy '{}' to '{}'", File, Destination); - return 1; + throw std::runtime_error(fmt::format("Failed to copy '{}' to '{}'", File, Destination)); } ZEN_INFO("Copied '{}' to '{}'", File, Destination); @@ -469,6 +489,17 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { std::filesystem::permissions(Destination, std::filesystem::perms::owner_exec, std::filesystem::perm_options::add); } + +#if ZEN_PLATFORM_LINUX + if (UserId != 0) + { + if (chown(Destination.c_str(), UserId, GroupId) != 0) + { + throw std::runtime_error( + fmt::format("Unable to set ownership of file '{}' for user '{}'", Destination, m_UserName)); + } + } +#endif } m_ServerExecutable = m_InstallPath / m_ServerExecutable.filename(); @@ -485,8 +516,8 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) }); if (Ec) { - ZEN_CONSOLE("Failed to install service '{}' using '{}' . Reason: '{}'", m_ServiceName, m_ServerExecutable, Ec.message()); - return 1; + throw std::runtime_error( + fmt::format("Failed to install service '{}' using '{}' . Reason: '{}'", m_ServiceName, m_ServerExecutable, Ec.message())); } ZEN_CONSOLE("Installed service '{}' using '{}' successfully", m_ServiceName, m_ServerExecutable); @@ -495,8 +526,7 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) Ec = StartService(m_ServiceName); if (Ec) { - ZEN_CONSOLE("Failed to start service '{}'. Reason: '{}'", m_ServiceName, Ec.message()); - return 1; + throw std::runtime_error(fmt::format("Failed to start service '{}'. Reason: '{}'", m_ServiceName, Ec.message())); } } } @@ -507,30 +537,28 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) std::error_code Ec = QueryInstalledService(m_ServiceName, Info); if (Ec) { - ZEN_CONSOLE("Failed to inspect installed service '{}'. Reason: '{}'", m_ServiceName, Ec.message()); - return 1; + throw std::runtime_error(fmt::format("Failed to inspect installed service '{}'. Reason: '{}'", m_ServiceName, Ec.message())); } if (Info.Status == ServiceStatus::NotInstalled) { ZEN_CONSOLE("Service '{}' is not installed", m_ServiceName); - return 0; + return; } if (Info.Status != ServiceStatus::Stopped) { - ZEN_CONSOLE("Service '{}' is running, stop before uninstalling", m_ServiceName); - return 0; + throw std::runtime_error(fmt::format("Service '{}' is running, stop before uninstalling", m_ServiceName)); } if (!IsElevated()) { - return RunElevated(m_AllowElevation); + RunElevated(m_AllowElevation); + return; } Ec = UninstallService(m_ServiceName); if (Ec) { - ZEN_CONSOLE("Failed to uninstall service '{}'. Reason: '{}'", m_ServiceName, Ec.message()); - return 1; + throw std::runtime_error(fmt::format("Failed to uninstall service '{}'. Reason: '{}'", m_ServiceName, Ec.message())); } ZEN_CONSOLE("Uninstalled service {} successfully", m_ServiceName); } @@ -541,30 +569,28 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) std::error_code Ec = QueryInstalledService(m_ServiceName, Info); if (Ec) { - ZEN_CONSOLE("Failed to inspect installed service '{}'. Reason: '{}'", m_ServiceName, Ec.message()); - return 1; + throw std::runtime_error(fmt::format("Failed to inspect installed service '{}'. Reason: '{}'", m_ServiceName, Ec.message())); } if (Info.Status == ServiceStatus::NotInstalled) { - ZEN_CONSOLE("Service '{}' is not installed", m_ServiceName); - return 1; + throw std::runtime_error(fmt::format("Service '{}' is not installed", m_ServiceName)); } if (Info.Status != ServiceStatus::Stopped) { ZEN_CONSOLE("Service '{}' is already running", m_ServiceName); - return 1; + return; } if (!IsElevated()) { - return RunElevated(m_AllowElevation); + RunElevated(m_AllowElevation); + return; } Ec = StartService(m_ServiceName); if (Ec) { - ZEN_CONSOLE("Failed to start service '{}'. Reason: '{}'", m_ServiceName, Ec.message()); - return 1; + throw std::runtime_error(fmt::format("Failed to start service '{}'. Reason: '{}'", m_ServiceName, Ec.message())); } ZEN_CONSOLE("Started service '{}' successfully", m_ServiceName); } @@ -575,35 +601,31 @@ ServiceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) std::error_code Ec = QueryInstalledService(m_ServiceName, Info); if (Ec) { - ZEN_CONSOLE("Failed to inspect installed service '{}'. Reason: '{}'", m_ServiceName, Ec.message()); - return 1; + throw std::runtime_error(fmt::format("Failed to inspect installed service '{}'. Reason: '{}'", m_ServiceName, Ec.message())); } if (Info.Status == ServiceStatus::NotInstalled) { - ZEN_CONSOLE("Service '{}' is not installed", m_ServiceName); - return 1; + throw std::runtime_error(fmt::format("Service '{}' is not installed", m_ServiceName)); } if (Info.Status != ServiceStatus::Running) { ZEN_CONSOLE("Service '{}' is not running", m_ServiceName); - return 1; + return; } if (!IsElevated()) { - return RunElevated(m_AllowElevation); + RunElevated(m_AllowElevation); + return; } Ec = StopService(m_ServiceName); if (Ec) { - ZEN_CONSOLE("Failed to stop service '{}'. Reason: '{}'", m_ServiceName, Ec.message()); - return 1; + throw std::runtime_error(fmt::format("Failed to stop service '{}'. Reason: '{}'", m_ServiceName, Ec.message())); } ZEN_CONSOLE("Stopped service '{}' successfully", m_ServiceName); } - - return 0; } } // namespace zen diff --git a/src/zen/cmds/service_cmd.h b/src/zen/cmds/service_cmd.h index 359e8e854..ee98620d1 100644 --- a/src/zen/cmds/service_cmd.h +++ b/src/zen/cmds/service_cmd.h @@ -17,7 +17,7 @@ public: ServiceCommand(); ~ServiceCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/status_cmd.cpp b/src/zen/cmds/status_cmd.cpp index b5764af44..c43f85052 100644 --- a/src/zen/cmds/status_cmd.cpp +++ b/src/zen/cmds/status_cmd.cpp @@ -3,6 +3,7 @@ #include "status_cmd.h" #include <zencore/compactbinary.h> +#include <zencore/compactbinaryutil.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zencore/string.h> @@ -19,14 +20,14 @@ StatusCommand::StatusCommand() StatusCommand::~StatusCommand() = default; -int +void StatusCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } uint16_t EffectivePort = 0; @@ -34,24 +35,33 @@ StatusCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (!IsFile(m_DataDir / ".lock")) { - ZEN_CONSOLE_ERROR("Lock file does not exist in directory '{}'", m_DataDir); - return 1; + throw std::runtime_error(fmt::format("Lock file does not exist in directory '{}'", m_DataDir)); } - LockFileInfo Info = ReadLockFilePayload(LoadCompactBinaryObject(IoBufferBuilder::MakeFromFile(m_DataDir / ".lock"))); - std::string Reason; - if (!ValidateLockFileInfo(Info, Reason)) + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject LockFileObject = + ValidateAndReadCompactBinaryObject(IoBufferBuilder::MakeFromFile(m_DataDir / ".lock"), ValidateResult); + ValidateResult == CbValidateError::None) { - ZEN_CONSOLE_ERROR("Lock file in directory '{}' is not valid. Reason: '{}'", m_DataDir, Reason); - return 1; + LockFileInfo Info = ReadLockFilePayload(LockFileObject); + std::string Reason; + if (!ValidateLockFileInfo(Info, Reason)) + { + throw std::runtime_error(fmt::format("Lock file in directory '{}' is not valid. Reason: '{}'", m_DataDir, Reason)); + } + EffectivePort = Info.EffectiveListenPort; + } + else + { + throw std::runtime_error( + fmt::format("Lock file in directory '{}' is malformed. Reason: '{}'", m_DataDir, ToString(ValidateResult))); } - EffectivePort = Info.EffectiveListenPort; } ZenServerState State; if (!State.InitializeReadOnly()) { ZEN_CONSOLE("No Zen state found"); - return 0; + return; } ZEN_CONSOLE("{:>5} {:>6} {:>24}", "port", "pid", "session"); @@ -66,8 +76,6 @@ StatusCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ZEN_CONSOLE("{:>5} {:>6} {:>24}", Entry.EffectiveListenPort.load(), Entry.Pid.load(), SessionStringBuilder); } }); - - return 0; } } // namespace zen diff --git a/src/zen/cmds/status_cmd.h b/src/zen/cmds/status_cmd.h index 46bda9ee6..dc103a196 100644 --- a/src/zen/cmds/status_cmd.h +++ b/src/zen/cmds/status_cmd.h @@ -14,7 +14,7 @@ public: StatusCommand(); ~StatusCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/top_cmd.cpp b/src/zen/cmds/top_cmd.cpp index 9794dc1c0..0e44dbbec 100644 --- a/src/zen/cmds/top_cmd.cpp +++ b/src/zen/cmds/top_cmd.cpp @@ -19,7 +19,7 @@ TopCommand::TopCommand() TopCommand::~TopCommand() = default; -int +void TopCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions, argc, argv); @@ -29,7 +29,7 @@ TopCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_CONSOLE("No Zen state found"); - return 0; + return; } int n = 0; @@ -55,8 +55,6 @@ TopCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) State.Sweep(); } } - - return 0; } ////////////////////////////////////////////////////////////////////////// @@ -67,7 +65,7 @@ PsCommand::PsCommand() PsCommand::~PsCommand() = default; -int +void PsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions, argc, argv); @@ -77,14 +75,12 @@ PsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_CONSOLE("No Zen state found"); - return 0; + return; } State.Snapshot([&](const ZenServerState::ZenServerEntry& Entry) { ZEN_CONSOLE("Port {} : pid {}", Entry.EffectiveListenPort.load(), Entry.Pid.load()); }); - - return 0; } } // namespace zen diff --git a/src/zen/cmds/top_cmd.h b/src/zen/cmds/top_cmd.h index 83410587b..74167ecfd 100644 --- a/src/zen/cmds/top_cmd.h +++ b/src/zen/cmds/top_cmd.h @@ -12,7 +12,7 @@ public: TopCommand(); ~TopCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -25,7 +25,7 @@ public: PsCommand(); ~PsCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/trace_cmd.cpp b/src/zen/cmds/trace_cmd.cpp index 85caf33b8..41a30068c 100644 --- a/src/zen/cmds/trace_cmd.cpp +++ b/src/zen/cmds/trace_cmd.cpp @@ -20,21 +20,21 @@ TraceCommand::TraceCommand() TraceCommand::~TraceCommand() = default; -int +void TraceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) { - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); } zen::HttpClient Http(m_HostName); @@ -44,13 +44,12 @@ TraceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (zen::HttpClient::Response Response = Http.Post("/admin/trace/stop"sv)) { ZEN_CONSOLE("OK: {}", Response.ToText()); - return 0; } else { - ZEN_CONSOLE_ERROR("trace stop failed: {}", Response.ToText()); - return 1; + Response.ThrowError("Trace stop failed"); } + return; } std::string StartArg; @@ -68,26 +67,23 @@ TraceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (zen::HttpClient::Response Response = Http.Post(fmt::format("/admin/trace/start?{}"sv, StartArg))) { ZEN_CONSOLE("OK: {}", Response.ToText()); - return 0; } else { - ZEN_CONSOLE_ERROR("trace start failed: {}", Response.ToText()); - return 1; + Response.ThrowError("Trace start failed"); } } - - if (zen::HttpClient::Response Response = Http.Get("/admin/trace"sv)) - { - ZEN_CONSOLE("OK: {}", Response.ToText()); - return 0; - } else { - ZEN_CONSOLE_ERROR("trace status failed: {}", Response.ToText()); + if (zen::HttpClient::Response Response = Http.Get("/admin/trace"sv)) + { + ZEN_CONSOLE("OK: {}", Response.ToText()); + } + else + { + Response.ThrowError("Trace status failed"); + } } - - return 1; } } // namespace zen diff --git a/src/zen/cmds/trace_cmd.h b/src/zen/cmds/trace_cmd.h index 404fb57a4..a6c9742b7 100644 --- a/src/zen/cmds/trace_cmd.h +++ b/src/zen/cmds/trace_cmd.h @@ -14,7 +14,7 @@ public: TraceCommand(); ~TraceCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/up_cmd.cpp b/src/zen/cmds/up_cmd.cpp index fd330f616..a219677df 100644 --- a/src/zen/cmds/up_cmd.cpp +++ b/src/zen/cmds/up_cmd.cpp @@ -3,6 +3,7 @@ #include "up_cmd.h" #include <zencore/compactbinary.h> +#include <zencore/compactbinaryutil.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> @@ -30,7 +31,7 @@ UpCommand::UpCommand() UpCommand::~UpCommand() = default; -int +void UpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { using namespace std::literals; @@ -39,12 +40,12 @@ UpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!ParseOptions(argc, argv)) { - return 0; + return; } if (m_ShowConsole && m_ShowLog) { - throw OptionParseException("--show-console can not be used in combination with --show-log"); + throw OptionParseException("'--show-console' conficts with '--show-log'", m_Options.help()); } { @@ -72,7 +73,7 @@ UpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) RunningEntries[0].DesiredPort, RunningEntries[0].EffectivePort, RunningEntries[0].Pid); - return 0; + return; } } } @@ -98,20 +99,25 @@ UpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (Server.IsRunning()) { - ZEN_CONSOLE_ERROR("Zen server launch failed (timed out), terminating"); + ZEN_CONSOLE_WARN("Zen server launch failed (timed out), terminating"); Server.Terminate(); if (!m_ShowConsole) { ZEN_CONSOLE("{}", Server.GetLogOutput()); } - return 111; + throw std::runtime_error("Zen server launch failed (timed out), launched process was terminated"); } - int ReturnCode = Server.Shutdown(); + int ServerExitCode = Server.Shutdown(); if (!m_ShowConsole) { ZEN_CONSOLE("{}", Server.GetLogOutput()); } - return ReturnCode; + if (ServerExitCode != 0) + { + throw ErrorWithReturnCode( + fmt::format("Zen server failed to get to a ready state and exited with return code {}", ServerExitCode), + ServerExitCode); + } } else { @@ -124,7 +130,6 @@ UpCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ZEN_CONSOLE("Zen server up"); } } - return 0; } ////////////////////////////////////////////////////////////////////////// @@ -138,14 +143,14 @@ AttachCommand::AttachCommand() AttachCommand::~AttachCommand() = default; -int +void AttachCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } ZenServerState Instance; @@ -157,34 +162,40 @@ AttachCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (!IsFile(m_DataDir / ".lock")) { - ZEN_CONSOLE("Lock file does not exist in directory '{}'", m_DataDir); - return 1; + throw std::runtime_error(fmt::format("Lock file does not exist in directory '{}'", m_DataDir)); + } + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject LockFileObject = + ValidateAndReadCompactBinaryObject(IoBufferBuilder::MakeFromFile(m_DataDir / ".lock"), ValidateResult); + ValidateResult == CbValidateError::None && LockFileObject) + { + LockFileInfo Info = ReadLockFilePayload(LockFileObject); + std::string Reason; + if (!ValidateLockFileInfo(Info, Reason)) + { + throw std::runtime_error(fmt::format("Lock file in directory '{}' is not valid. Reason: '{}'", m_DataDir, Reason)); + } + Entry = Instance.LookupByEffectivePort(Info.EffectiveListenPort); } - LockFileInfo Info = ReadLockFilePayload(LoadCompactBinaryObject(IoBufferBuilder::MakeFromFile(m_DataDir / ".lock"))); - std::string Reason; - if (!ValidateLockFileInfo(Info, Reason)) + else { - ZEN_CONSOLE("Lock file in directory '{}' is not valid. Reason: '{}'", m_DataDir, Reason); - return 1; + throw std::runtime_error( + fmt::format("Lock file in directory '{}' is malformed. Reason: '{}'", m_DataDir, ToString(ValidateResult))); } - Entry = Instance.LookupByEffectivePort(Info.EffectiveListenPort); } if (!Entry) { - ZEN_CONSOLE_ERROR("No zen server instance to add sponsor process to"); - return 1; + throw std::runtime_error("No zen server instance to add sponsor process to"); } // Sponsor processes are checked every second, so 2 second wait time should be enough if (!Entry->AddSponsorProcess(m_OwnerPid, 2000)) { - ZEN_CONSOLE_ERROR("Unable to add sponsor process to running zen server instance"); - return 1; + throw std::runtime_error("Unable to add sponsor process to running zen server instance"); } ZEN_CONSOLE("Added sponsor process {} to running instance {} on port {}", m_OwnerPid, Entry->Pid.load(), m_Port); - return 0; } ////////////////////////////////////////////////////////////////////////// @@ -199,14 +210,14 @@ DownCommand::DownCommand() DownCommand::~DownCommand() = default; -int +void DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } // Discover executing instances @@ -224,17 +235,26 @@ DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (!IsFile(m_DataDir / ".lock")) { - ZEN_CONSOLE_ERROR("Lock file does not exist in directory '{}'", m_DataDir); - return 1; + throw std::runtime_error(fmt::format("Lock file does not exist in directory '{}'", m_DataDir)); + } + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject LockFileObject = + ValidateAndReadCompactBinaryObject(IoBufferBuilder::MakeFromFile(m_DataDir / ".lock"), ValidateResult); + ValidateResult == CbValidateError::None && LockFileObject) + { + LockFileInfo Info = ReadLockFilePayload(LockFileObject); + std::string Reason; + if (!ValidateLockFileInfo(Info, Reason)) + { + throw std::runtime_error(fmt::format("Lock file in directory '{}' is not valid. Reason: '{}'", m_DataDir, Reason)); + } + Entry = Instance.LookupByEffectivePort(Info.EffectiveListenPort); } - LockFileInfo Info = ReadLockFilePayload(LoadCompactBinaryObject(IoBufferBuilder::MakeFromFile(m_DataDir / ".lock"))); - std::string Reason; - if (!ValidateLockFileInfo(Info, Reason)) + else { - ZEN_CONSOLE_ERROR("Lock file in directory '{}' is not valid. Reason: '{}'", m_DataDir, Reason); - return 1; + throw std::runtime_error( + fmt::format("Lock file in directory '{}' is malformed. Reason: '{}'", m_DataDir, ToString(ValidateResult))); } - Entry = Instance.LookupByEffectivePort(Info.EffectiveListenPort); } if (Entry) @@ -259,7 +279,7 @@ DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (Server.WaitUntilExited(100, Ec) && !Ec) { ZEN_CONSOLE("shutdown complete"); - return 0; + return; } else if (Ec) { @@ -295,12 +315,12 @@ DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (Entry == nullptr) { ZEN_CONSOLE("Shutdown complete"); - return 0; + return; } if (Entry->Pid.load() != ServerProcessPid) { ZEN_CONSOLE("Shutdown complete"); - return 0; + return; } Sleep(100); } @@ -314,21 +334,12 @@ DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (std::error_code Ec = FindProcess(ServerExePath, RunningProcess); !Ec, /*IncludeSelf*/ false) { ZEN_CONSOLE_WARN("Attempting hard terminate of zen process with pid ({})", RunningProcess.Pid()); - try - { - if (RunningProcess.Terminate(0)) - { - ZEN_CONSOLE("Terminate complete"); - return 0; - } - ZEN_CONSOLE_ERROR("Failed to terminate server, still running"); - return 1; - } - catch (const std::exception& Ex) + if (RunningProcess.Terminate(0)) { - ZEN_CONSOLE_ERROR("Failed to terminate server: '{}'", Ex.what()); - return 1; + ZEN_CONSOLE("Terminate complete"); + return; } + throw std::runtime_error("Failed to terminate server, still running"); } else { @@ -337,13 +348,11 @@ DownCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } else if (Entry) { - ZEN_CONSOLE_ERROR("Failed to shutdown of server on port {}, use --force to hard terminate process", - Entry->DesiredListenPort.load()); - return 1; + throw std::runtime_error( + fmt::format("Failed to shutdown of server on port {}, use --force to hard terminate process", Entry->DesiredListenPort.load())); } ZEN_CONSOLE("No zen server to bring down"); - return 0; } } // namespace zen diff --git a/src/zen/cmds/up_cmd.h b/src/zen/cmds/up_cmd.h index c9af16749..2e822d5fc 100644 --- a/src/zen/cmds/up_cmd.h +++ b/src/zen/cmds/up_cmd.h @@ -14,7 +14,7 @@ public: UpCommand(); ~UpCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -31,7 +31,7 @@ public: AttachCommand(); ~AttachCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -47,7 +47,7 @@ public: DownCommand(); ~DownCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/version_cmd.cpp b/src/zen/cmds/version_cmd.cpp index eda6e6725..ed34d7011 100644 --- a/src/zen/cmds/version_cmd.cpp +++ b/src/zen/cmds/version_cmd.cpp @@ -28,13 +28,13 @@ VersionCommand::VersionCommand() VersionCommand::~VersionCommand() = default; -int +void VersionCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); if (!ParseOptions(argc, argv)) { - return 0; + return; } std::string Version; @@ -65,13 +65,14 @@ VersionCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) Parameters.Entries.insert_or_assign("detailed", "true"); } const std::string_view VersionRequest("/health/version"sv); - HttpClient::Response Response = Client.Get(VersionRequest, {}, Parameters); - if (!Response.IsSuccess()) + if (HttpClient::Response Response = Client.Get(VersionRequest, {}, Parameters)) { - ZEN_CONSOLE_ERROR("{} failed: {}", VersionRequest, Response.ErrorMessage(""sv)); - return 1; + Version = Response.AsText(); + } + else + { + Response.ThrowError(fmt::format("{} failed", VersionRequest)); } - Version = Response.AsText(); } if (m_OutputPath.empty()) @@ -86,7 +87,6 @@ VersionCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) OutputFile.Write(Version.data(), Version.length(), 0); OutputFile.Close(); } - - return 0; } + } // namespace zen diff --git a/src/zen/cmds/version_cmd.h b/src/zen/cmds/version_cmd.h index 7a910e463..934ede868 100644 --- a/src/zen/cmds/version_cmd.h +++ b/src/zen/cmds/version_cmd.h @@ -15,7 +15,7 @@ public: VersionCommand(); ~VersionCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/vfs_cmd.cpp b/src/zen/cmds/vfs_cmd.cpp index e8802d990..79ec20ce2 100644 --- a/src/zen/cmds/vfs_cmd.cpp +++ b/src/zen/cmds/vfs_cmd.cpp @@ -28,14 +28,14 @@ VfsCommand::~VfsCommand() { } -int +void VfsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions, argc, argv); - if (!ZenCmdBase::ParseOptions(argc, argv)) + if (!ParseOptions(argc, argv)) { - return 0; + return; } // Validate arguments @@ -43,14 +43,14 @@ VfsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) m_HostName = ResolveTargetHostSpec(m_HostName); if (m_HostName.empty()) - throw OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", m_Options.help()); HttpClient Http(m_HostName); if (m_Verb == "mount"sv) { if (m_MountPath.empty()) - throw OptionParseException("No source specified"); + throw OptionParseException("'--vfs-path' is required", m_Options.help()); CbObjectWriter Cbo; Cbo << "method" @@ -66,8 +66,6 @@ VfsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) else { Result.ThrowError("VFS mount request failed"sv); - - return 1; } } else if (m_Verb == "unmount"sv) @@ -82,8 +80,6 @@ VfsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) else { Result.ThrowError("VFS unmount request failed"sv); - - return 1; } } else if (m_Verb == "info"sv) @@ -97,12 +93,8 @@ VfsCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) else { Result.ThrowError("VFS info fetch failed"sv); - - return 1; } } - - return 0; } } // namespace zen diff --git a/src/zen/cmds/vfs_cmd.h b/src/zen/cmds/vfs_cmd.h index 9b2497c0e..5deaa02fa 100644 --- a/src/zen/cmds/vfs_cmd.h +++ b/src/zen/cmds/vfs_cmd.h @@ -12,7 +12,7 @@ public: VfsCommand(); ~VfsCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/cmds/wipe_cmd.cpp b/src/zen/cmds/wipe_cmd.cpp index 5a9d0174e..a3d40c142 100644 --- a/src/zen/cmds/wipe_cmd.cpp +++ b/src/zen/cmds/wipe_cmd.cpp @@ -5,10 +5,10 @@ #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> +#include <zencore/parallelwork.h> #include <zencore/string.h> #include <zencore/timer.h> #include <zencore/trace.h> -#include <zenutil/parallelwork.h> #include <zenutil/workerpools.h> #include <signal.h> @@ -251,7 +251,7 @@ namespace { return Added; }; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); struct AsyncVisitor : public GetDirectoryContentVisitor { @@ -543,7 +543,7 @@ WipeCommand::WipeCommand() WipeCommand::~WipeCommand() = default; -int +void WipeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); @@ -553,9 +553,9 @@ WipeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) signal(SIGBREAK, SignalCallbackHandler); #endif // ZEN_PLATFORM_WINDOWS - if (!ZenCmdBase::ParseOptions(argc, argv)) + if (!ParseOptions(argc, argv)) { - return 0; + return; } Quiet = m_Quiet; @@ -567,7 +567,7 @@ WipeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!IsDir(m_Directory)) { - return 0; + return; } while (!m_Yes) @@ -583,13 +583,11 @@ WipeCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) } else if (Reponse == "n" || Reponse == "no") { - return 0; + return; } } CleanDirectory(m_Directory, {}, !m_KeepReadOnlyFiles, m_Dryrun); - - return 0; } } // namespace zen diff --git a/src/zen/cmds/wipe_cmd.h b/src/zen/cmds/wipe_cmd.h index 0e910bb81..d0693a757 100644 --- a/src/zen/cmds/wipe_cmd.h +++ b/src/zen/cmds/wipe_cmd.h @@ -18,7 +18,7 @@ public: ~WipeCommand(); virtual cxxopts::Options& Options() override { return m_Options; } - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual ZenCmdCategory& CommandCategory() const override { return g_UtilitiesCategory; } private: diff --git a/src/zen/cmds/workspaces_cmd.cpp b/src/zen/cmds/workspaces_cmd.cpp index 3930e56b7..9bb118eac 100644 --- a/src/zen/cmds/workspaces_cmd.cpp +++ b/src/zen/cmds/workspaces_cmd.cpp @@ -117,7 +117,7 @@ WorkspaceCommand::WorkspaceCommand() WorkspaceCommand::~WorkspaceCommand() = default; -int +void WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); @@ -129,12 +129,12 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) int ParentCommandArgCount = GetSubCommand(m_Options, argc, argv, m_SubCommands, SubOption, SubCommandArguments); if (!ParseOptions(ParentCommandArgCount, argv)) { - return 0; + return; } if (SubOption == nullptr) { - throw zen::OptionParseException("command verb is missing"); + throw OptionParseException("'verb' option is required", m_Options.help()); } m_HostName = ResolveTargetHostSpec(m_HostName); @@ -144,7 +144,7 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) m_SystemRootDir = PickDefaultSystemRootDirectory(); if (m_SystemRootDir.empty()) { - throw zen::OptionParseException("unable to resolve system root directory"); + throw std::runtime_error("Unable to resolve default system root directory"); } } @@ -152,14 +152,14 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (!ParseOptions(*SubOption, gsl::narrow<int>(SubCommandArguments.size()), SubCommandArguments.data())) { - return 0; + return; } if (SubOption == &m_CreateOptions) { if (m_Path.empty()) { - throw zen::OptionParseException(fmt::format("path is required\n{}", m_CreateOptions.help())); + throw OptionParseException("'--root-path' is required", SubOption->help()); } std::filesystem::path Path = StringToPath(m_Path); @@ -167,12 +167,12 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) if (m_Id.empty()) { m_Id = Workspaces::PathToId(Path).ToString(); - ZEN_CONSOLE("Using generated workspace id {} from path '{}'", m_Id, Path); + ZEN_CONSOLE("Using generated workspace id '{}' from '--root-path' '{}'", m_Id, Path); } if (Oid::TryFromHexString(m_Id) == Oid::Zero) { - throw zen::OptionParseException(fmt::format("id '{}' is invalid", m_Id)); + throw OptionParseException(fmt::format("'--workspace' ('{}') is malformed", m_Id), SubOption->help()); } if (Workspaces::AddWorkspace( @@ -185,16 +185,16 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get("/ws/refresh"); !Result) { - ZEN_CONSOLE_ERROR("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); + ZEN_CONSOLE_WARN("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); } } ZEN_CONSOLE("Added/updated workspace {}", m_Id); - return 0; + return; } else { ZEN_CONSOLE_WARN("Workspace {} already exists", m_Id); - return 0; + return; } } @@ -206,7 +206,7 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) static std::vector<Workspaces::WorkspaceConfiguration> Configs = Workspaces::ReadConfig(Log(), StatePath, Error); if (!Error.empty()) { - ZEN_CONSOLE_ERROR("Failed to read workspaces state from '{}'. Reason: '{}'", StatePath, Error); + throw std::runtime_error(fmt::format("Failed to read workspaces state from '{}'. Reason: '{}'", StatePath, Error)); } else { @@ -216,20 +216,20 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) ShowWorkspace(Config, " "sv); } } - return 0; + return; } else { if (Oid::TryFromHexString(m_Id) == Oid::Zero) { - throw zen::OptionParseException(fmt::format("id '{}' is invalid", m_Id)); + throw OptionParseException(fmt::format("'--workspace' ('{}') is malformed", m_Id), SubOption->help()); } Workspaces::WorkspaceConfiguration Workspace = Workspaces::FindWorkspace(Log(), StatePath, Oid::FromHexString(m_Id)); if (Workspace.Id != Oid::Zero) { ShowWorkspace(Workspace, ""sv); - return 0; + return; } else { @@ -242,12 +242,12 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { if (m_Id.empty()) { - throw zen::OptionParseException(fmt::format("id is required", m_RemoveOptions.help())); + throw OptionParseException("'--workspace' is required", SubOption->help()); } if (Oid::TryFromHexString(m_Id) == Oid::Zero) { - throw zen::OptionParseException(fmt::format("id '{}' is invalid", m_Id)); + throw OptionParseException(fmt::format("'--workspace' ('{}') is malformed", m_Id), SubOption->help()); } if (Workspaces::RemoveWorkspace(Log(), StatePath, Oid::FromHexString(m_Id))) @@ -257,7 +257,7 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get("/ws/refresh"); !Result) { - ZEN_CONSOLE_ERROR("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); + ZEN_CONSOLE_WARN("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); } } ZEN_CONSOLE("Removed workspace {}", m_Id); @@ -266,7 +266,7 @@ WorkspaceCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_CONSOLE_WARN("Workspace {} does not exist", m_Id); } - return 0; + return; } ZEN_ASSERT(false); @@ -368,7 +368,7 @@ WorkspaceShareCommand::WorkspaceShareCommand() WorkspaceShareCommand::~WorkspaceShareCommand() = default; -int +void WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) { ZEN_UNUSED(GlobalOptions); @@ -380,12 +380,12 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** int ParentCommandArgCount = GetSubCommand(m_Options, argc, argv, m_SubCommands, SubOption, SubCommandArguments); if (!ParseOptions(ParentCommandArgCount, argv)) { - return 0; + return; } if (SubOption == nullptr) { - throw zen::OptionParseException("command verb is missing"); + throw OptionParseException("'verb' option is required", m_Options.help()); } m_HostName = ResolveTargetHostSpec(m_HostName); @@ -395,7 +395,7 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** m_SystemRootDir = PickDefaultSystemRootDirectory(); if (m_SystemRootDir.empty()) { - throw zen::OptionParseException("unable to resolve system root directory"); + throw std::runtime_error("Unable to resolve default system root directory"); } } else @@ -407,7 +407,7 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** if (!ParseOptions(*SubOption, gsl::narrow<int>(SubCommandArguments.size()), SubCommandArguments.data())) { - return 0; + return; } if (SubOption == &m_CreateOptions) @@ -416,20 +416,19 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** { if (m_WorkspaceId.empty()) { - throw zen::OptionParseException("workspace id or root path is required"); + throw OptionParseException("'--workspace' or '--root-path' is required", SubOption->help()); } Oid WorkspaceId = Oid::TryFromHexString(m_WorkspaceId); if (WorkspaceId == Oid::Zero) { - throw zen::OptionParseException(fmt::format("id '{}' is invalid", m_WorkspaceId)); + throw OptionParseException(fmt::format("'--workspace' ('{}') is malformed", m_WorkspaceId), SubOption->help()); } Workspaces::WorkspaceConfiguration WorkspaceConfig = Workspaces::FindWorkspace(Log(), StatePath, WorkspaceId); if (WorkspaceConfig.Id == Oid::Zero) { - ZEN_CONSOLE_ERROR("Workspace {} does not exist", m_WorkspaceId); - return 0; + throw std::runtime_error(fmt::format("Workspace {} does not exist", m_WorkspaceId)); } m_WorkspaceRoot = WorkspaceConfig.RootPath; } @@ -445,7 +444,7 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** { if (Oid::TryFromHexString(m_WorkspaceId) == Oid::Zero) { - throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_WorkspaceId)); + throw OptionParseException(fmt::format("'--workspace' ('{}') is malformed", m_WorkspaceId), SubOption->help()); } } if (Workspaces::AddWorkspace(Log(), StatePath, {.Id = Oid::FromHexString(m_WorkspaceId), .RootPath = m_WorkspaceRoot})) @@ -469,7 +468,7 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** if (Oid::TryFromHexString(m_ShareId) == Oid::Zero) { - throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_ShareId)); + throw OptionParseException(fmt::format("'--workspace' ('{}') is malformed", m_ShareId), SubOption->help()); } if (Workspaces::AddWorkspaceShare(Log(), @@ -481,16 +480,14 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get("/ws/refresh"); !Result) { - ZEN_CONSOLE_ERROR("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); + ZEN_CONSOLE_WARN("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); } } ZEN_CONSOLE("Created workspace share {}", m_ShareId); - return 0; } else { ZEN_CONSOLE("Workspace share {} already exists", m_ShareId); - return 0; } } @@ -503,50 +500,47 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** Workspaces::FindWorkspaceShare(Log(), StatePath, m_Alias, WorkspaceConfig); if (ShareConfig.Id == Oid::Zero) { - ZEN_CONSOLE("Workspace share with alias {} does not exist", m_Alias); - return 0; + throw std::runtime_error(fmt::format("Workspace share with alias {} does not exist", m_Alias)); } ShowShare(ShareConfig, WorkspaceConfig.Id, ""sv); - return 0; + return; } if (m_WorkspaceId.empty()) { - throw zen::OptionParseException("workspace id or root path is required"); + throw OptionParseException("'--workspace' or '--root-path' is required", SubOption->help()); } if (Oid::TryFromHexString(m_WorkspaceId) == Oid::Zero) { - throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_WorkspaceId)); + throw OptionParseException(fmt::format("'--workspace' ('{}') is malformed", m_WorkspaceId), SubOption->help()); } Workspaces::WorkspaceConfiguration WorkspaceConfig = Workspaces::FindWorkspace(Log(), StatePath, Oid::FromHexString(m_WorkspaceId)); if (WorkspaceConfig.Id == Oid::Zero) { - ZEN_CONSOLE("Workspace {} does not exist", m_WorkspaceId); - return 0; + throw std::runtime_error(fmt::format("Workspace {} does not exist", m_WorkspaceId)); } std::filesystem::path WorkspaceRoot = WorkspaceConfig.RootPath; if (m_ShareId.empty()) { - throw zen::OptionParseException("share id is required"); + throw OptionParseException("'--share' is required", SubOption->help()); } if (Oid::TryFromHexString(m_ShareId) == Oid::Zero) { - throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_ShareId)); + throw OptionParseException(fmt::format("'--share' ('{}') is malformed", m_ShareId), SubOption->help()); } Workspaces::WorkspaceShareConfiguration Share = Workspaces::FindWorkspaceShare(Log(), WorkspaceRoot, Oid::FromHexString(m_ShareId)); if (Share.Id == Oid::Zero) { - ZEN_CONSOLE("Workspace share {} does not exist", m_ShareId); - return 0; + throw std::runtime_error(fmt::format("Workspace share {} does not exist", m_ShareId)); } ShowShare(Share, Oid::Zero, ""sv); - return 0; + return; } if (SubOption == &m_RemoveOptions) @@ -560,7 +554,7 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** if (ShareConfig.Id == Oid::Zero) { ZEN_CONSOLE("Workspace share with alias {} does not exist", m_Alias); - return 0; + return; } m_ShareId = ShareConfig.Id.ToString(); m_WorkspaceId = WorkspaceConfig.Id.ToString(); @@ -569,30 +563,30 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** if (m_WorkspaceId.empty()) { - throw zen::OptionParseException("workspace id or root path is required"); + throw OptionParseException("'--workspace' or '--root-path' is required", SubOption->help()); } if (Oid::TryFromHexString(m_WorkspaceId) == Oid::Zero) { - throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_WorkspaceId)); + throw OptionParseException(fmt::format("'--workspace' ('{}') is malformed", m_WorkspaceId), SubOption->help()); } Workspaces::WorkspaceConfiguration WorkspaceConfig = Workspaces::FindWorkspace(Log(), StatePath, Oid::FromHexString(m_WorkspaceId)); if (WorkspaceConfig.Id == Oid::Zero) { ZEN_CONSOLE("Workspace {} does not exist", m_WorkspaceId); - return 0; + return; } WorkspaceRoot = WorkspaceConfig.RootPath; if (m_ShareId.empty()) { - throw zen::OptionParseException("share id is required"); + throw OptionParseException("'--share' is required", SubOption->help()); } if (Oid::TryFromHexString(m_ShareId) == Oid::Zero) { - throw zen::OptionParseException(fmt::format("workspace id '{}' is invalid", m_ShareId)); + throw OptionParseException(fmt::format("'--share' ('{}') is malformed", m_ShareId), SubOption->help()); } if (Workspaces::RemoveWorkspaceShare(Log(), WorkspaceRoot, Oid::FromHexString(m_ShareId))) @@ -602,16 +596,16 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get("/ws/refresh"); !Result) { - ZEN_CONSOLE_ERROR("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); + ZEN_CONSOLE_WARN("Failed to refresh workspaces for host {}. Reason: '{}'", m_HostName, Result.ErrorMessage(""sv)); } } ZEN_CONSOLE("Removed workspace share {}", m_ShareId); - return 0; + return; } else { ZEN_CONSOLE("Removed workspace share {} does not exist", m_ShareId); - return 0; + return; } } @@ -620,12 +614,12 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** { if (m_WorkspaceId.empty()) { - throw zen::OptionParseException("workspace id is required"); + throw OptionParseException("'--workspace' is required", Opts.help()); } if (m_ShareId.empty()) { - throw zen::OptionParseException(fmt::format("share id is required", Opts.help())); + throw OptionParseException("'--share' is required", Opts.help()); } return fmt::format("{}/{}", m_WorkspaceId, m_ShareId); } @@ -649,20 +643,19 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", SubOption->help()); } HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get(fmt::format("/ws/{}/files", GetShareIdentityUrl(m_FilesOptions)), {}, Params)) { ZEN_CONSOLE("{}: {}", Result, Result.ToText()); - return 0; } else { Result.ThrowError("failed to get workspace share files"sv); - return 1; } + return; } if (SubOption == &m_EntriesOptions) @@ -683,20 +676,19 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** if (m_HostName.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("Unable to resolve server specification", SubOption->help()); } HttpClient Http(m_HostName); if (HttpClient::Response Result = Http.Get(fmt::format("/ws/{}/entries", GetShareIdentityUrl(m_EntriesOptions)), {}, Params)) { ZEN_CONSOLE("{}: {}", Result, Result.ToText()); - return 0; } else { Result.ThrowError("failed to get workspace share entries"sv); - return 1; } + return; } auto ChunksToOidStrings = [](HttpClient& Http, @@ -751,14 +743,14 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** if (SubOption == &m_GetChunkOptions) { - if (m_ChunkId.empty()) + if (m_HostName.empty()) { - throw zen::OptionParseException("chunk id is required"); + throw OptionParseException("Unable to resolve server specification", SubOption->help()); } - if (m_HostName.empty()) + if (m_ChunkId.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("'--chunk' is required", SubOption->help()); } HttpClient Http(m_HostName); @@ -777,30 +769,29 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** if (HttpClient::Response Result = Http.Get(fmt::format("/ws/{}/{}", GetShareIdentityUrl(m_GetChunkOptions), m_ChunkId), {}, Params)) { ZEN_CONSOLE("{}: Bytes: {}", Result, NiceBytes(Result.ResponsePayload.GetSize())); - return 0; } else { Result.ThrowError("failed to get workspace share chunk"sv); - return 1; } + return; } if (SubOption == &m_GetChunkBatchOptions) { - if (m_ShareId.empty()) + if (m_HostName.empty()) { - throw zen::OptionParseException(fmt::format("share id is required", m_InfoOptions.help())); + throw OptionParseException("Unable to resolve server specification", SubOption->help()); } - if (m_ChunkIds.empty()) + if (m_ShareId.empty()) { - throw zen::OptionParseException("share is is required"); + throw OptionParseException("'--share' is required", SubOption->help()); } - if (m_HostName.empty()) + if (m_ChunkIds.empty()) { - throw zen::OptionParseException("unable to resolve server specification"); + throw OptionParseException("'--chunks' is required", SubOption->help()); } HttpClient Http(m_HostName); @@ -832,13 +823,12 @@ WorkspaceShareCommand::Run(const ZenCliOptions& GlobalOptions, int argc, char** { ZEN_CONSOLE("{}: Bytes: {}", m_ChunkIds[Index], NiceBytes(Results[Index].GetSize())); } - return 0; } else { Result.ThrowError("failed to get workspace share batch"sv); - return 1; } + return; } ZEN_ASSERT(false); diff --git a/src/zen/cmds/workspaces_cmd.h b/src/zen/cmds/workspaces_cmd.h index d85d8f7d8..f5880125b 100644 --- a/src/zen/cmds/workspaces_cmd.h +++ b/src/zen/cmds/workspaces_cmd.h @@ -17,7 +17,7 @@ public: WorkspaceCommand(); ~WorkspaceCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: @@ -49,7 +49,7 @@ public: WorkspaceShareCommand(); ~WorkspaceShareCommand(); - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) override; virtual cxxopts::Options& Options() override { return m_Options; } private: diff --git a/src/zen/xmake.lua b/src/zen/xmake.lua index dadda32dd..f36573d62 100644 --- a/src/zen/xmake.lua +++ b/src/zen/xmake.lua @@ -5,7 +5,7 @@ target("zen") add_headerfiles("**.h") add_files("**.cpp") add_files("zen.cpp", {unity_ignored = true }) - add_deps("zencore", "zenhttp", "zenstore", "zenutil") + add_deps("zencore", "zenhttp", "zenremotestore", "zenstore", "zenutil") add_includedirs(".") set_symbols("debug") diff --git a/src/zen/zen.cpp b/src/zen/zen.cpp index 6aa278947..8e56e7df9 100644 --- a/src/zen/zen.cpp +++ b/src/zen/zen.cpp @@ -29,6 +29,7 @@ #include "cmds/workspaces_cmd.h" #include <zencore/callstack.h> +#include <zencore/config.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> @@ -56,7 +57,6 @@ #endif ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> #include <spdlog/sinks/ansicolor_sink.h> #include <spdlog/spdlog.h> #include <gsl/gsl-lite.hpp> @@ -73,6 +73,117 @@ ZEN_THIRD_PARTY_INCLUDES_END namespace zen { +#if ZEN_PLATFORM_WINDOWS +static HANDLE +GetConsoleHandle() +{ + static HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); + return hStdOut; +} +#endif + +static bool +CheckStdoutTty() +{ +#if ZEN_PLATFORM_WINDOWS + HANDLE hStdOut = GetConsoleHandle(); + DWORD dwMode = 0; + static bool IsConsole = ::GetConsoleMode(hStdOut, &dwMode); + return IsConsole; +#else + return isatty(fileno(stdout)); +#endif +} + +static bool +IsStdoutTty() +{ + static bool StdoutIsTty = CheckStdoutTty(); + return StdoutIsTty; +} + +static void +OutputToConsoleRaw(const char* String, size_t Length) +{ +#if ZEN_PLATFORM_WINDOWS + HANDLE hStdOut = GetConsoleHandle(); +#endif + +#if ZEN_PLATFORM_WINDOWS + if (IsStdoutTty()) + { + WriteConsoleA(hStdOut, String, (DWORD)Length, 0, 0); + } + else + { + ::WriteFile(hStdOut, (LPCVOID)String, (DWORD)Length, 0, 0); + } +#else + fwrite(String, 1, Length, stdout); +#endif +} + +static void +OutputToConsoleRaw(const std::string& String) +{ + OutputToConsoleRaw(String.c_str(), String.length()); +} + +static void +OutputToConsoleRaw(const StringBuilderBase& SB) +{ + OutputToConsoleRaw(SB.c_str(), SB.Size()); +} + +static uint32_t +GetConsoleColumns(uint32_t Default) +{ +#if ZEN_PLATFORM_WINDOWS + HANDLE hStdOut = GetConsoleHandle(); + CONSOLE_SCREEN_BUFFER_INFO csbi; + if (GetConsoleScreenBufferInfo(hStdOut, &csbi) == TRUE) + { + return (uint32_t)(csbi.srWindow.Right - csbi.srWindow.Left + 1); + } +#else + struct winsize w; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) + { + return (uint32_t)w.ws_col; + } +#endif + return Default; +} + +enum class ReturnCode : std::int8_t +{ + kSuccess = 0, + + kOtherError = 1, + + kBadInput = 2, + kOutOfMemory = 16, + kOutOfDisk = 17, + kAssertError = 70, + + kHttpOtherClientError = 80, + kHttpCantConnectError = 81, // CONNECTION_FAILURE + kHttpNotFound = 66, // NotFound(404) + kHttpUnauthorized = 77, // Unauthorized(401), + kHttpSLLError = + 82, // SSL_CONNECT_ERROR, SSL_LOCAL_CERTIFICATE_ERROR, SSL_REMOTE_CERTIFICATE_ERROR, SSL_CACERT_ERROR, GENERIC_SSL_ERROR + kHttpForbidden = 83, // Forbidden(403) + kHttpTimeout = 84, // NETWORK_RECEIVE_ERROR, NETWORK_SEND_FAILURE, OPERATION_TIMEDOUT, RequestTimeout(408) + kHttpConflict = 85, // Conflict(409) + kHttpNoHost = 86, // HOST_RESOLUTION_FAILURE, PROXY_RESOLUTION_FAILURE + + kHttpOtherServerError = 90, + kHttpInternalServerError = 91, // InternalServerError(500) + kHttpServiceUnavailable = 69, // ServiceUnavailable(503) + kHttpBadGateway = 92, // BadGateway(502) + kHttpGatewayTimeout = 93, // GatewayTimeout(504) +}; + ZenCmdCategory DefaultCategory{.Name = "general commands"}; ZenCmdCategory g_UtilitiesCategory{.Name = "utility commands"}; ZenCmdCategory g_ProjectStoreCategory{.Name = "project store commands"}; @@ -94,6 +205,8 @@ ZenCmdBase::ParseOptions(int argc, char** argv) bool ZenCmdBase::ParseOptions(cxxopts::Options& CmdOptions, int argc, char** argv) { + CmdOptions.set_width(GetConsoleColumns(80)); + cxxopts::ParseResult Result; try @@ -102,7 +215,7 @@ ZenCmdBase::ParseOptions(cxxopts::Options& CmdOptions, int argc, char** argv) } catch (const std::exception& Ex) { - throw zen::OptionParseException(Ex.what()); + throw zen::OptionParseException(Ex.what(), CmdOptions.help()); } CmdOptions.show_positional_help(); @@ -128,7 +241,7 @@ ZenCmdBase::ParseOptions(cxxopts::Options& CmdOptions, int argc, char** argv) First = false; } - throw zen::OptionParseException(fmt::format("Invalid arguments: {}", StringBuilder.ToView())); + throw zen::OptionParseException(fmt::format("Invalid arguments: {}", StringBuilder.ToView()), {}); } return true; @@ -162,62 +275,39 @@ ZenCmdBase::GetSubCommand(cxxopts::Options&, return argc; } -std::string -ZenCmdBase::FormatHttpResponse(const cpr::Response& Response) +static ReturnCode +GetReturnCodeFromHttpResult(const HttpClientError& Ex) { - if (Response.error.code != cpr::ErrorCode::OK) - { - if (Response.error.message.empty()) - { - return fmt::format("Request '{}' failed, error code {}", Response.url.str(), static_cast<int>(Response.error.code)); - } - return fmt::format("Request '{}' failed. Reason: '{}' ({})", - Response.url.str(), - Response.error.message, - static_cast<int>(Response.error.code)); - } + HttpClientError::ResponseClass ResponseClass = Ex.GetResponseClass(); - std::string Content; - if (auto It = Response.header.find("Content-Type"); It != Response.header.end()) - { - zen::HttpContentType ContentType = zen::ParseContentType(It->second); - if (ContentType == zen::HttpContentType::kText) - { - Content = Response.text; - } - else if (ContentType == zen::HttpContentType::kJSON) - { - Content = fmt::format("\n{}", Response.text); - } - else if (!Response.text.empty()) - { - Content = fmt::format("[{}]", MapContentTypeToString(ContentType)); - } - } + if (ResponseClass == HttpClientError::ResponseClass::kSuccess) + return ReturnCode::kSuccess; - std::string_view ResponseString = zen::ReasonStringForHttpResultCode( - Response.status_code == static_cast<long>(zen::HttpResponseCode::NoContent) ? static_cast<long>(zen::HttpResponseCode::OK) - : Response.status_code); - if (Content.empty()) + switch (ResponseClass) { - return std::string(ResponseString); - } +#define HANDLE_CASE(ErrorClass) \ + case HttpClientError::ResponseClass::ErrorClass: \ + return ReturnCode::ErrorClass + + HANDLE_CASE(kHttpOtherClientError); + HANDLE_CASE(kHttpCantConnectError); + HANDLE_CASE(kHttpNotFound); + HANDLE_CASE(kHttpUnauthorized); + HANDLE_CASE(kHttpSLLError); + HANDLE_CASE(kHttpForbidden); + HANDLE_CASE(kHttpTimeout); + HANDLE_CASE(kHttpConflict); + HANDLE_CASE(kHttpNoHost); + HANDLE_CASE(kHttpOtherServerError); + HANDLE_CASE(kHttpInternalServerError); + HANDLE_CASE(kHttpServiceUnavailable); + HANDLE_CASE(kHttpBadGateway); + HANDLE_CASE(kHttpGatewayTimeout); +#undef HANDLE_CASE - return fmt::format("{}: {}", ResponseString, Content); -} - -int -ZenCmdBase::MapHttpToCommandReturnCode(const cpr::Response& Response) -{ - if (zen::IsHttpSuccessCode(Response.status_code)) - { - return 0; - } - if (Response.error.code != cpr::ErrorCode::OK) - { - return static_cast<int>(Response.error.code); + default: + return ReturnCode::kOtherError; } - return 1; } std::string @@ -274,86 +364,10 @@ ZenCmdBase::ResolveTargetHostSpec(const std::string& InHostSpec) return ResolveTargetHostSpec(InHostSpec, /* out */ Dummy); } -#if ZEN_PLATFORM_WINDOWS -static HANDLE -GetConsoleHandle() -{ - static HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); - return hStdOut; -} -#endif - -static bool -CheckStdoutTty() -{ -#if ZEN_PLATFORM_WINDOWS - HANDLE hStdOut = GetConsoleHandle(); - DWORD dwMode = 0; - static bool IsConsole = ::GetConsoleMode(hStdOut, &dwMode); - return IsConsole; -#else - return isatty(fileno(stdout)); -#endif -} - -static bool -IsStdoutTty() -{ - static bool StdoutIsTty = CheckStdoutTty(); - return StdoutIsTty; -} - -static void -OutputToConsoleRaw(const char* String, size_t Length) -{ -#if ZEN_PLATFORM_WINDOWS - HANDLE hStdOut = GetConsoleHandle(); -#endif - -#if ZEN_PLATFORM_WINDOWS - if (IsStdoutTty()) - { - WriteConsoleA(hStdOut, String, (DWORD)Length, 0, 0); - } - else - { - ::WriteFile(hStdOut, (LPCVOID)String, (DWORD)Length, 0, 0); - } -#else - fwrite(String, 1, Length, stdout); -#endif -} - -static void -OutputToConsoleRaw(const std::string& String) -{ - OutputToConsoleRaw(String.c_str(), String.length()); -} - -static void -OutputToConsoleRaw(const StringBuilderBase& SB) -{ - OutputToConsoleRaw(SB.c_str(), SB.Size()); -} - -static uint32_t -GetConsoleColumns() +void +ZenCmdBase::LogExecutableVersionAndPid() { -#if ZEN_PLATFORM_WINDOWS - HANDLE hStdOut = GetConsoleHandle(); - CONSOLE_SCREEN_BUFFER_INFO csbi; - if (GetConsoleScreenBufferInfo(hStdOut, &csbi) == TRUE) - { - return (uint32_t)(csbi.srWindow.Right - csbi.srWindow.Left + 1); - } -#else - struct winsize w; - if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) - { - return (uint32_t)w.ws_col; - } -#endif - return 1024; + ZEN_CONSOLE("Running {}: {} (pid {})", GetRunningExecutablePath(), ZEN_CFG_VERSION_BUILD_STRING_FULL, GetCurrentProcessId()); } void @@ -470,7 +484,7 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak) uint64_t ETAMS = (NewState.Status == State::EStatus::Running) && (PercentDone > 5) ? (ETAElapsedMS * NewState.RemainingCount) / Completed : 0; - uint32_t ConsoleColumns = GetConsoleColumns(); + uint32_t ConsoleColumns = GetConsoleColumns(1024); const std::string PercentString = fmt::format("{:#3}%", PercentDone); @@ -485,7 +499,7 @@ ProgressBar::UpdateState(const State& NewState, bool DoLinebreak) ExtendableStringBuilder<256> OutputBuilder; - OutputBuilder << "\r" << Task << PercentString; + OutputBuilder << "\r" << Task << " " << PercentString; if (OutputBuilder.Size() + 1 < ConsoleColumns) { size_t RemainingSpace = ConsoleColumns - (OutputBuilder.Size() + 1); @@ -675,6 +689,7 @@ main(int argc, char** argv) MasterCommand MasterCmd; OplogMirrorCommand OplogMirrorCmd; SnapshotOplogCommand SnapshotOplogCmd; + OplogDownloadCommand OplogDownload; OplogValidateCommand OplogValidateCmd; PrintCommand PrintCmd; PrintPackageCommand PrintPkgCmd; @@ -732,6 +747,7 @@ main(int argc, char** argv) {"oplog-import", &ImportOplogCmd, "Import project store oplog"}, {"oplog-mirror", &OplogMirrorCmd, "Mirror project store oplog to file system"}, {"oplog-snapshot", &SnapshotOplogCmd, "Snapshot project store oplog"}, + {OplogDownloadCommand::Name, &OplogDownload, OplogDownloadCommand::Description}, {"oplog-validate", &OplogValidateCmd, "Validate oplog for missing references"}, {"print", &PrintCmd, "Print compact binary object"}, {"printpackage", &PrintPkgCmd, "Print compact binary package"}, @@ -948,7 +964,7 @@ main(int argc, char** argv) printf("\n"); } - exit(0); + return (int)ReturnCode::kSuccess; } #if ZEN_USE_SENTRY @@ -1025,86 +1041,107 @@ main(int argc, char** argv) { if (StrCaseCompare(SubCommand.c_str(), CmdInfo.CmdName) == 0) { - cxxopts::Options& VerbOptions = CmdInfo.Cmd->Options(); - try { - return CmdInfo.Cmd->Run(GlobalOptions, (int)CommandArgVec.size(), CommandArgVec.data()); + CmdInfo.Cmd->Run(GlobalOptions, (int)CommandArgVec.size(), CommandArgVec.data()); + return (int)ReturnCode::kSuccess; } catch (const OptionParseException& Ex) { - ZEN_CONSOLE_ERROR("Invalid arguments for command '{}': {}\n\n{}", SubCommand, Ex.what(), VerbOptions.help()); - exit(11); + ZEN_CONSOLE("{}\n", Ex.m_Help); + ZEN_CONSOLE_ERROR("Invalid arguments for command '{}': {}", SubCommand, Ex.what()); + return (int)ReturnCode::kBadInput; } catch (const std::system_error& Ex) { if (IsOOD(Ex)) { ZEN_CONSOLE_ERROR("Operation failed due to out of disk space: {}", Ex.what()); - exit(3); + return (int)ReturnCode::kOutOfDisk; } else if (IsOOM(Ex)) { ZEN_CONSOLE_ERROR("Operation failed due to out of memory: {}", Ex.what()); - exit(3); + return (int)ReturnCode::kOutOfMemory; } else { ZEN_CONSOLE_ERROR("Operation failed due to system error: {} ({})\n", Ex.what(), Ex.code() ? Ex.code().value() : 0); - exit(Ex.code() ? Ex.code().value() : 10); + return (int)ReturnCode::kOtherError; } } catch (const HttpClientError& Ex) { ZEN_CONSOLE_ERROR("Operation failed due to a http error: {}", Ex.what()); - exit(Ex.m_Error != 0 ? Ex.m_Error : (int)Ex.m_ResponseCode); + ReturnCode Result = GetReturnCodeFromHttpResult(Ex); + return (int)Result; + } + catch (const AssertException& Ex) + { + ZEN_CONSOLE_ERROR("Operation failed due to an assert exception: {}", Ex.FullDescription()); + return (int)ReturnCode::kAssertError; + } + catch (const ErrorWithReturnCode& Ex) + { + ZEN_CONSOLE_ERROR("{}", Ex.what()); + return Ex.m_ReturnCode; } catch (const std::exception& Ex) { - ZEN_CONSOLE_ERROR("Operation failed due to: {}\n", Ex.what()); - exit(11); + ZEN_CONSOLE_ERROR("{}\n", Ex.what()); + return (int)ReturnCode::kOtherError; } } } printf("Unknown command specified: '%s', exiting\n", SubCommand.c_str()); + return (int)ReturnCode::kBadInput; } catch (const OptionParseException& Ex) { - std::string HelpMessage = Options.help(); - - printf("Error parsing program arguments: %s\n\n%s", Ex.what(), HelpMessage.c_str()); - - return 9; + printf("%s\n\n", Ex.m_Help.c_str()); + printf("Invalid arguments arguments: %s", Ex.what()); + return (int)ReturnCode::kBadInput; } catch (const std::system_error& Ex) { if (IsOOD(Ex)) { printf("Operation failed due to out of disk space: %s", Ex.what()); - return 3; + return (int)ReturnCode::kOutOfDisk; } else if (IsOOM(Ex)) { printf("Operation failed due to out of memory: %s", Ex.what()); - return 3; + return (int)ReturnCode::kOutOfMemory; } else { printf("Operation failed due to system error: %s (%d)\n", Ex.what(), Ex.code() ? Ex.code().value() : 0); - return Ex.code() ? Ex.code().value() : 10; + return (int)ReturnCode::kOtherError; } } catch (const HttpClientError& Ex) { - printf("Operation failed due to a http error: %s", Ex.what()); - return Ex.m_Error != 0 ? Ex.m_Error : (int)Ex.m_ResponseCode; + printf("Error: Operation failed due to a http error: %s", Ex.what()); + ReturnCode Result = GetReturnCodeFromHttpResult(Ex); + return (int)Result; + } + catch (const AssertException& Ex) + { + printf("Error: Operation failed due to an assert exception: %s", Ex.FullDescription().c_str()); + return (int)ReturnCode::kAssertError; + } + catch (const ErrorWithReturnCode& Ex) + { + printf("Error: %s", Ex.what()); + return Ex.m_ReturnCode; } catch (const std::exception& Ex) { - printf("Operation failed due to: %s\n", Ex.what()); - return 11; + printf("Error: %s\n", Ex.what()); + return (int)ReturnCode::kOtherError; } - return 0; + return (int)ReturnCode::kSuccess; } diff --git a/src/zen/zen.h b/src/zen/zen.h index 40c745bc7..ffb35e5ca 100644 --- a/src/zen/zen.h +++ b/src/zen/zen.h @@ -7,10 +7,6 @@ #include <zencore/zencore.h> #include <zenutil/commandlineoptions.h> -namespace cpr { -class Response; -} - namespace zen { struct ZenCliOptions @@ -35,13 +31,24 @@ extern ZenCmdCategory g_ProjectStoreCategory; extern ZenCmdCategory g_CacheStoreCategory; extern ZenCmdCategory g_StorageCategory; +class ErrorWithReturnCode : public std::runtime_error +{ +public: + using _Mybase = runtime_error; + ErrorWithReturnCode(const std::string& Message, int InReturnCode) : _Mybase(Message), m_ReturnCode(InReturnCode) {} + + ErrorWithReturnCode(const char* Message, int InReturnCode) : _Mybase(Message), m_ReturnCode(InReturnCode) {} + + const int m_ReturnCode = -1; +}; + /** Base class for command implementations */ class ZenCmdBase { public: - virtual int Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) = 0; + virtual void Run(const ZenCliOptions& GlobalOptions, int argc, char** argv) = 0; virtual cxxopts::Options& Options() = 0; virtual ZenCmdCategory& CommandCategory() const; @@ -53,10 +60,10 @@ public: std::span<cxxopts::Options*> SubOptions, cxxopts::Options*& OutSubOption, std::vector<char*>& OutSubCommandArguments); - static std::string FormatHttpResponse(const cpr::Response& Response); - static int MapHttpToCommandReturnCode(const cpr::Response& Response); static std::string ResolveTargetHostSpec(const std::string& InHostSpec); static std::string ResolveTargetHostSpec(const std::string& InHostSpec, uint16_t& OutEffectivePort); + + static void LogExecutableVersionAndPid(); }; class StorageCommand : public ZenCmdBase diff --git a/src/zencore-test/zencore-test.cpp b/src/zencore-test/zencore-test.cpp index 0f64f4a5f..327550b32 100644 --- a/src/zencore-test/zencore-test.cpp +++ b/src/zencore-test/zencore-test.cpp @@ -26,7 +26,14 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) zen::IgnoreChildSignals(); # endif +# if ZEN_WITH_TRACE zen::TraceInit("zencore-test"); + zen::TraceOptions TraceCommandlineOptions; + if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) + { + TraceConfigure(TraceCommandlineOptions); + } +# endif // ZEN_WITH_TRACE zen::logging::InitializeLogging(); zen::MaximizeOpenFileCount(); diff --git a/src/zencore/callstack.cpp b/src/zencore/callstack.cpp index b22f2ec1f..8aa1111bf 100644 --- a/src/zencore/callstack.cpp +++ b/src/zencore/callstack.cpp @@ -210,6 +210,54 @@ CallstackToString(const CallstackFrames* Callstack, std::string_view Prefix) return SB.ToString(); } +void +CallstackToStringRaw(const CallstackFrames* Callstack, void* CallbackUserData, CallstackRawCallback Callback) +{ + if (Callstack && Callstack->FrameCount > 0) + { +#if ZEN_PLATFORM_WINDOWS + char SymbolBuffer[sizeof(SYMBOL_INFO) + 1024]; + SYMBOL_INFO* SymbolInfo = (SYMBOL_INFO*)SymbolBuffer; + SymbolInfo->SizeOfStruct = sizeof(SYMBOL_INFO); + SymbolInfo->MaxNameLen = 1023; + DWORD64 Displacement = 0; + fmt::basic_memory_buffer<char, 2048> Message; + for (uint32_t FrameIndex = 0; FrameIndex < Callstack->FrameCount; FrameIndex++) + { + if (WinSymbols.GetSymbol(Callstack->Frames[FrameIndex], SymbolInfo, Displacement)) + { + auto Appender = fmt::appender(Message); + fmt::format_to(Appender, "{}+{:#x} [{:#x}]", SymbolInfo->Name, Displacement, (uintptr_t)Callstack->Frames[FrameIndex]); + Message.push_back('\0'); + Callback(CallbackUserData, FrameIndex, Message.data()); + Message.resize(0); + } + } +#endif +#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC + char** messages = backtrace_symbols(Callstack->Frames, (int)Callstack->FrameCount); + if (messages) + { + for (uint32_t FrameIndex = 0; FrameIndex < Callstack->FrameCount; FrameIndex++) + { + Callback(CallbackUserData, FrameIndex, messages[FrameIndex]); + } + free(messages); + } +#endif + } +} + +CallstackFrames* +GetCallstackRaw(void* CaptureBuffer, int FramesToSkip, int FramesToCapture) +{ + CallstackFrames* Callstack = (CallstackFrames*)CaptureBuffer; + + Callstack->Frames = (void**)&Callstack[1]; + Callstack->FrameCount = GetCallstack(FramesToSkip, FramesToCapture, Callstack->Frames); + return Callstack; +} + #if ZEN_WITH_TESTS TEST_CASE("Callstack.Basic") diff --git a/src/zencore/compactbinaryfile.cpp b/src/zencore/compactbinaryfile.cpp index 1526c21d5..ec2fc3cd5 100644 --- a/src/zencore/compactbinaryfile.cpp +++ b/src/zencore/compactbinaryfile.cpp @@ -1,7 +1,7 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "zencore/compactbinaryfile.h" -#include "zencore/compactbinaryvalidation.h" +#include "zencore/compactbinaryutil.h" #include <zencore/filesystem.h> @@ -19,12 +19,12 @@ LoadCompactBinaryObject(const std::filesystem::path& FilePath) IoBuffer ObjectBuffer = ObjectFile.Flatten(); - if (CbValidateError Result = ValidateCompactBinary(ObjectBuffer, CbValidateMode::Default); Result == CbValidateError::None) + CbValidateError ValidateResult; + CbObject Object = ValidateAndReadCompactBinaryObject(IoBuffer(ObjectBuffer), ValidateResult); + if (ValidateResult == CbValidateError::None) { - CbObject Object = LoadCompactBinaryObject(ObjectBuffer); const IoHash WorkerId = IoHash::HashBuffer(ObjectBuffer); - - return {.Object = Object, .Hash = WorkerId}; + return {.Object = std::move(Object), .Hash = WorkerId}; } return {.Hash = IoHash::Zero}; diff --git a/src/zencore/compactbinaryutil.cpp b/src/zencore/compactbinaryutil.cpp new file mode 100644 index 000000000..074bdaffd --- /dev/null +++ b/src/zencore/compactbinaryutil.cpp @@ -0,0 +1,39 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/compactbinaryutil.h> + +#include <zencore/compress.h> +#include <zencore/filesystem.h> + +namespace zen { + +CbObject +ValidateAndReadCompactBinaryObject(const SharedBuffer&& Payload, CbValidateError& OutError) +{ + if (Payload.GetSize() > 0) + { + if (OutError = ValidateCompactBinary(Payload.GetView(), CbValidateMode::Default); OutError == CbValidateError::None) + { + CbObject Object(std::move(Payload)); + if (Object.GetView().GetSize() != Payload.GetSize()) + { + OutError |= CbValidateError::OutOfBounds; + return {}; + } + return Object; + } + } + return CbObject(); +} + +CbObject +ValidateAndReadCompactBinaryObject(const CompressedBuffer&& Payload, CbValidateError& OutError) +{ + if (CompositeBuffer Decompressed = Payload.DecompressToComposite()) + { + return ValidateAndReadCompactBinaryObject(std::move(Decompressed).Flatten(), OutError); + } + return CbObject(); +} + +} // namespace zen diff --git a/src/zencore/filesystem.cpp b/src/zencore/filesystem.cpp index 8327838c9..d18f21dbe 100644 --- a/src/zencore/filesystem.cpp +++ b/src/zencore/filesystem.cpp @@ -1290,6 +1290,27 @@ MoveToFile(std::filesystem::path Path, IoBuffer Data) zen::CreateDirectories(Path.parent_path()); Success = SetFileInformationByHandle(ChunkFileHandle, FileRenameInfo, RenameInfo, BufferSize); } + if (!Success && (LastError == ERROR_ACCESS_DENIED)) + { + // Fallback to regular rename + std::error_code Ec; + std::filesystem::path SourcePath = PathFromHandle(FileRef.FileHandle, Ec); + if (!Ec) + { + auto NativeSourcePath = SourcePath.native().c_str(); + auto NativeTargetPath = Path.native().c_str(); + Success = ::MoveFile(NativeSourcePath, NativeTargetPath); + if (!Success) + { + LastError = GetLastError(); + if (LastError == ERROR_PATH_NOT_FOUND) + { + zen::CreateDirectories(Path.parent_path()); + Success = ::MoveFile(NativeSourcePath, NativeTargetPath); + } + } + } + } } Memory::Free(RenameInfo); if (!Success) @@ -2439,26 +2460,28 @@ GetDirectoryContent(const std::filesystem::path& RootDir, PendingWorkCount.AddCount(1); try { - WorkerPool.ScheduleWork([WorkerPool = &WorkerPool, - PendingWorkCount = &PendingWorkCount, - Visitor = Visitor, - Flags = Flags, - Path = std::move(Path), - RelativeRoot = RelativeRoot / DirectoryName]() { - ZEN_ASSERT(Visitor); - auto _ = MakeGuard([&]() { PendingWorkCount->CountDown(); }); - try - { - MultithreadedVisitor SubVisitor(*WorkerPool, *PendingWorkCount, RelativeRoot, Flags, Visitor); - FileSystemTraversal Traversal; - Traversal.TraverseFileSystem(Path, SubVisitor); - Visitor->AsyncVisitDirectory(SubVisitor.RelativeRoot, std::move(SubVisitor.Content)); - } - catch (const std::exception& Ex) - { - ZEN_ERROR("Failed scheduling work to scan subfolder '{}'. Reason: '{}'", Path / RelativeRoot, Ex.what()); - } - }); + WorkerPool.ScheduleWork( + [WorkerPool = &WorkerPool, + PendingWorkCount = &PendingWorkCount, + Visitor = Visitor, + Flags = Flags, + Path = std::move(Path), + RelativeRoot = RelativeRoot / DirectoryName]() { + ZEN_ASSERT(Visitor); + auto _ = MakeGuard([&]() { PendingWorkCount->CountDown(); }); + try + { + MultithreadedVisitor SubVisitor(*WorkerPool, *PendingWorkCount, RelativeRoot, Flags, Visitor); + FileSystemTraversal Traversal; + Traversal.TraverseFileSystem(Path, SubVisitor); + Visitor->AsyncVisitDirectory(SubVisitor.RelativeRoot, std::move(SubVisitor.Content)); + } + catch (const std::exception& Ex) + { + ZEN_ERROR("Failed scheduling work to scan subfolder '{}'. Reason: '{}'", Path / RelativeRoot, Ex.what()); + } + }, + WorkerThreadPool::EMode::DisableBacklog); } catch (const std::exception Ex) { @@ -3089,15 +3112,22 @@ TEST_CASE("filesystem") { virtual void VisitFile(const std::filesystem::path& Parent, const path_view& File, uint64_t, uint32_t, uint64_t) override { - bFoundExpected |= std::filesystem::equivalent(Parent / File, Expected); + // std::filesystem::equivalent is *very* expensive on Windows, filter out unlikely candidates + if (ExpectedFilename == ToLower(std::filesystem::path(File).string())) + { + bFoundExpected |= std::filesystem::equivalent(Parent / File, Expected); + } } virtual bool VisitDirectory(const std::filesystem::path&, const path_view&, uint32_t) override { return true; } - bool bFoundExpected = false; + bool bFoundExpected = false; + + std::string ExpectedFilename; std::filesystem::path Expected; } Visitor; - Visitor.Expected = BinPath; + Visitor.ExpectedFilename = ToLower(BinPath.filename().string()); + Visitor.Expected = BinPath; FileSystemTraversal().TraverseFileSystem(BinPath.parent_path().parent_path(), Visitor); CHECK(Visitor.bFoundExpected); diff --git a/src/zencore/include/zencore/callstack.h b/src/zencore/include/zencore/callstack.h index ef4ba0e91..ca8171435 100644 --- a/src/zencore/include/zencore/callstack.h +++ b/src/zencore/include/zencore/callstack.h @@ -32,6 +32,18 @@ GetFrameSymbols(const CallstackFrames* Callstack) void FormatCallstack(const CallstackFrames* Callstack, StringBuilderBase& SB, std::string_view Prefix); std::string CallstackToString(const CallstackFrames* Callstack, std::string_view Prefix = {}); +typedef void (*CallstackRawCallback)(void* UserData, uint32_t FrameIndex, const char* FrameText); + +constexpr size_t +CallstackRawMemorySize(int FramesToSkip, int FramesToCapture) +{ + return sizeof(CallstackFrames) + sizeof(void*) * (FramesToSkip + FramesToCapture); +} + +void CallstackToStringRaw(const CallstackFrames* Callstack, void* CallbackUserData, CallstackRawCallback Callback); + +CallstackFrames* GetCallstackRaw(void* CaptureBuffer, int FramesToSkip, int FramesToCapture); + void callstack_forcelink(); // internal } // namespace zen diff --git a/src/zencore/include/zencore/compactbinaryutil.h b/src/zencore/include/zencore/compactbinaryutil.h index 9524d1fc4..d750c6492 100644 --- a/src/zencore/include/zencore/compactbinaryutil.h +++ b/src/zencore/include/zencore/compactbinaryutil.h @@ -6,6 +6,7 @@ #include <zencore/compactbinary.h> #include <zencore/compactbinarybuilder.h> +#include <zencore/compactbinaryvalidation.h> namespace zen { @@ -43,4 +44,12 @@ RewriteCbObject(CbObjectView InObj, Invocable<CbObjectWriter&, CbFieldView&> aut return Writer.Save(); } +CbObject ValidateAndReadCompactBinaryObject(const SharedBuffer&& Payload, CbValidateError& OutError); +inline CbObject +ValidateAndReadCompactBinaryObject(const IoBuffer&& Payload, CbValidateError& OutError) +{ + return ValidateAndReadCompactBinaryObject(SharedBuffer(std::move(Payload)), OutError); +} +CbObject ValidateAndReadCompactBinaryObject(const CompressedBuffer&& Payload, CbValidateError& OutError); + } // namespace zen diff --git a/src/zencore/include/zencore/except.h b/src/zencore/include/zencore/except.h index 6810e6ea9..c933adfd8 100644 --- a/src/zencore/include/zencore/except.h +++ b/src/zencore/include/zencore/except.h @@ -63,7 +63,9 @@ MakeErrorCodeFromLastError() noexcept class OptionParseException : public std::runtime_error { public: - inline explicit OptionParseException(const std::string& Message) : std::runtime_error(Message) {} + // inline explicit OptionParseException(const std::string& Message) : std::runtime_error(Message) {} + inline OptionParseException(const std::string& Message, const std::string& Help) : std::runtime_error(Message), m_Help(Help) {} + const std::string m_Help; }; bool IsOOM(const std::system_error& SystemError); diff --git a/src/zencore/include/zencore/memory/newdelete.h b/src/zencore/include/zencore/memory/newdelete.h index 059f1d5ea..2ec92b91b 100644 --- a/src/zencore/include/zencore/memory/newdelete.h +++ b/src/zencore/include/zencore/memory/newdelete.h @@ -156,26 +156,13 @@ operator new[](std::size_t n, std::align_val_t al, const std::nothrow_t&) noexce // EASTL operator new -void* -operator new[](size_t size, const char* pName, int flags, unsigned debugFlags, const char* file, int line) -{ - ZEN_UNUSED(pName, flags, debugFlags, file, line); - return zen_new(size); -} - -void* -operator new[](size_t size, - size_t alignment, - size_t alignmentOffset, - const char* pName, - int flags, - unsigned debugFlags, - const char* file, - int line) -{ - ZEN_UNUSED(alignmentOffset, pName, flags, debugFlags, file, line); - - ZEN_ASSERT_SLOW(alignmentOffset == 0); // currently not supported - - return zen_new_aligned(size, alignment); -} +void* operator new[](size_t size, const char* pName, int flags, unsigned debugFlags, const char* file, int line); + +void* operator new[](size_t size, + size_t alignment, + size_t alignmentOffset, + const char* pName, + int flags, + unsigned debugFlags, + const char* file, + int line);
\ No newline at end of file diff --git a/src/zenutil/include/zenutil/parallelwork.h b/src/zencore/include/zencore/parallelwork.h index 639c6968c..05146d644 100644 --- a/src/zenutil/include/zenutil/parallelwork.h +++ b/src/zencore/include/zencore/parallelwork.h @@ -13,7 +13,7 @@ namespace zen { class ParallelWork { public: - ParallelWork(std::atomic<bool>& AbortFlag, std::atomic<bool>& PauseFlag); + ParallelWork(std::atomic<bool>& AbortFlag, std::atomic<bool>& PauseFlag, WorkerThreadPool::EMode Mode); ~ParallelWork(); @@ -26,21 +26,23 @@ public: m_PendingWork.AddCount(1); try { - WorkerPool.ScheduleWork([this, Work = std::move(Work), OnError = OnError ? std::move(OnError) : DefaultErrorFunction()] { - auto _ = MakeGuard([this]() { m_PendingWork.CountDown(); }); - try - { - while (m_PauseFlag && !m_AbortFlag) + WorkerPool.ScheduleWork( + [this, Work = std::move(Work), OnError = OnError ? std::move(OnError) : DefaultErrorFunction()] { + auto _ = MakeGuard([this]() { m_PendingWork.CountDown(); }); + try { - Sleep(2000); + while (m_PauseFlag && !m_AbortFlag) + { + Sleep(2000); + } + Work(m_AbortFlag); } - Work(m_AbortFlag); - } - catch (...) - { - OnError(std::current_exception(), m_AbortFlag); - } - }); + catch (...) + { + OnError(std::current_exception(), m_AbortFlag); + } + }, + m_Mode); } catch (const std::exception&) { @@ -63,10 +65,11 @@ private: ExceptionCallback DefaultErrorFunction(); void RethrowErrors(); - std::atomic<bool>& m_AbortFlag; - std::atomic<bool>& m_PauseFlag; - bool m_DispatchComplete = false; - Latch m_PendingWork; + std::atomic<bool>& m_AbortFlag; + std::atomic<bool>& m_PauseFlag; + const WorkerThreadPool::EMode m_Mode; + bool m_DispatchComplete = false; + Latch m_PendingWork; RwLock m_ErrorLock; std::vector<std::exception_ptr> m_Errors; diff --git a/src/zencore/include/zencore/process.h b/src/zencore/include/zencore/process.h index 98d352db6..04b79a1e0 100644 --- a/src/zencore/include/zencore/process.h +++ b/src/zencore/include/zencore/process.h @@ -33,6 +33,7 @@ public: ZENCORE_API bool Terminate(int ExitCode); ZENCORE_API void Reset(); [[nodiscard]] inline int Pid() const { return m_Pid; } + [[nodiscard]] inline void* Handle() const { return m_ProcessHandle; } private: void* m_ProcessHandle = nullptr; diff --git a/src/zencore/include/zencore/string.h b/src/zencore/include/zencore/string.h index 68129b691..93f8add0a 100644 --- a/src/zencore/include/zencore/string.h +++ b/src/zencore/include/zencore/string.h @@ -679,6 +679,9 @@ ParseHexNumber(const std::string_view HexString, UnsignedIntegral auto& OutValue return ParseHexNumber(HexString.data(), ExpectedCharacterCount, (uint8_t*)&OutValue); } +void UrlDecode(std::string_view InUrl, StringBuilderBase& OutUrl); +std::string UrlDecode(std::string_view InUrl); + ////////////////////////////////////////////////////////////////////////// // Format numbers for humans // diff --git a/src/zencore/include/zencore/uid.h b/src/zencore/include/zencore/uid.h index 64d3b6b9b..0c1079444 100644 --- a/src/zencore/include/zencore/uid.h +++ b/src/zencore/include/zencore/uid.h @@ -55,8 +55,8 @@ class StringBuilderBase; struct Oid { - static const int StringLength = 24; - typedef char String_t[StringLength + 1]; + static constexpr int StringLength = 24; + typedef char String_t[StringLength + 1]; static void Initialize(); [[nodiscard]] static Oid NewOid(); diff --git a/src/zencore/include/zencore/workthreadpool.h b/src/zencore/include/zencore/workthreadpool.h index 62356495c..4c38dd651 100644 --- a/src/zencore/include/zencore/workthreadpool.h +++ b/src/zencore/include/zencore/workthreadpool.h @@ -18,11 +18,7 @@ struct IWork : public RefCounted { virtual void Execute() = 0; - inline std::exception_ptr GetException() { return m_Exception; } - private: - std::exception_ptr m_Exception; - friend class WorkerThreadPool; }; @@ -35,13 +31,18 @@ public: WorkerThreadPool(int InThreadCount, std::string_view WorkerThreadBaseName); ~WorkerThreadPool(); - void ScheduleWork(Ref<IWork> Work); - void ScheduleWork(std::function<void()>&& Work); + // Decides what to do if there are no free workers in the pool when the work is submitted + enum class EMode + { + EnableBacklog, // The work will be added to a backlog of work to do + DisableBacklog // The work will be executed synchronously in the caller thread + }; + + void ScheduleWork(Ref<IWork> Work, EMode Mode); + void ScheduleWork(std::function<void()>&& Work, EMode Mode); template<typename Func> - auto EnqueueTask(std::packaged_task<Func> Task); - - [[nodiscard]] size_t PendingWorkItemCount() const; + auto EnqueueTask(std::packaged_task<Func> Task, EMode Mode); private: struct Impl; @@ -54,7 +55,7 @@ private: template<typename Func> auto -WorkerThreadPool::EnqueueTask(std::packaged_task<Func> Task) +WorkerThreadPool::EnqueueTask(std::packaged_task<Func> Task, EMode Mode) { struct FutureWork : IWork { @@ -67,7 +68,7 @@ WorkerThreadPool::EnqueueTask(std::packaged_task<Func> Task) Ref<FutureWork> Work{new FutureWork(std::move(Task))}; auto Future = Work->m_Task.get_future(); - ScheduleWork(std::move(Work)); + ScheduleWork(std::move(Work), Mode); return Future; } diff --git a/src/zencore/include/zencore/zencore.h b/src/zencore/include/zencore/zencore.h index d21c0e7e2..b5eb3e3e8 100644 --- a/src/zencore/include/zencore/zencore.h +++ b/src/zencore/include/zencore/zencore.h @@ -58,11 +58,11 @@ struct AssertImpl [[noreturn]] (const char* Filename, int LineNumber, const char* FunctionName, const char* Msg); virtual void ZEN_FORCENOINLINE ZEN_DEBUG_SECTION - OnAssert(const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, CallstackFrames* Callstack); + OnAssert(const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, const CallstackFrames* Callstack); protected: static void ZEN_FORCENOINLINE ZEN_DEBUG_SECTION ThrowAssertException - [[noreturn]] (const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, CallstackFrames* Callstack); + [[noreturn]] (const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, const CallstackFrames* Callstack); static AssertImpl* CurrentAssertImpl; static AssertImpl DefaultAssertImpl; AssertImpl* NextAssertImpl = nullptr; diff --git a/src/zencore/jobqueue.cpp b/src/zencore/jobqueue.cpp index 5d727b69c..bd391909d 100644 --- a/src/zencore/jobqueue.cpp +++ b/src/zencore/jobqueue.cpp @@ -109,10 +109,12 @@ public: WorkerCounter.AddCount(1); try { - WorkerPool.ScheduleWork([&]() { - auto _ = MakeGuard([&]() { WorkerCounter.CountDown(); }); - Worker(); - }); + WorkerPool.ScheduleWork( + [&]() { + auto _ = MakeGuard([&]() { WorkerCounter.CountDown(); }); + Worker(); + }, + WorkerThreadPool::EMode::EnableBacklog); return {.Id = NewJobId}; } catch (const std::exception& Ex) @@ -463,37 +465,39 @@ TEST_CASE("JobQueue") std::unique_ptr<JobQueue> Queue(MakeJobQueue(2, "queue")); WorkerThreadPool Pool(4); Latch JobsLatch(1); - for (uint32_t I = 0; I < 100; I++) + for (uint32_t I = 0; I < 32; I++) { JobsLatch.AddCount(1); - Pool.ScheduleWork([&Queue, &JobsLatch, I]() { - auto _ = MakeGuard([&JobsLatch]() { JobsLatch.CountDown(); }); - JobsLatch.AddCount(1); - auto Id = Queue->QueueJob(fmt::format("busy {}", I), [&JobsLatch, I](JobContext& Context) { - auto $ = MakeGuard([&JobsLatch]() { JobsLatch.CountDown(); }); - if (Context.IsCancelled()) - { - return; - } - Context.ReportProgress("going to sleep", "", 100, 100); - Sleep(10); - if (Context.IsCancelled()) - { - return; - } - Context.ReportProgress("going to sleep again", "", 100, 50); - if ((I & 0xFF) == 0x10) - { - zen::ThrowSystemError(8, fmt::format("Job {} forced to fail", I)); - } - Sleep(10); - if (Context.IsCancelled()) - { - return; - } - Context.ReportProgress("done", "", 100, 0); - }); - }); + Pool.ScheduleWork( + [&Queue, &JobsLatch, I]() { + auto _ = MakeGuard([&JobsLatch]() { JobsLatch.CountDown(); }); + JobsLatch.AddCount(1); + auto Id = Queue->QueueJob(fmt::format("busy {}", I), [&JobsLatch, I](JobContext& Context) { + auto $ = MakeGuard([&JobsLatch]() { JobsLatch.CountDown(); }); + if (Context.IsCancelled()) + { + return; + } + Context.ReportProgress("going to sleep", "", 100, 100); + Sleep(5); + if (Context.IsCancelled()) + { + return; + } + Context.ReportProgress("going to sleep again", "", 100, 50); + if ((I & 0xFF) == 0x10) + { + zen::ThrowSystemError(8, fmt::format("Job {} forced to fail", I)); + } + Sleep(5); + if (Context.IsCancelled()) + { + return; + } + Context.ReportProgress("done", "", 100, 0); + }); + }, + WorkerThreadPool::EMode::EnableBacklog); } auto Join = [](std::span<std::string> Strings, std::string_view Delimiter) -> std::string { @@ -571,7 +575,7 @@ TEST_CASE("JobQueue") RemainingJobs.size(), PendingCount, RemainingJobs.size() - PendingCount); - Sleep(100); + Sleep(5); } JobsLatch.Wait(); } diff --git a/src/zencore/logging.cpp b/src/zencore/logging.cpp index 685c79d82..a6697c443 100644 --- a/src/zencore/logging.cpp +++ b/src/zencore/logging.cpp @@ -346,6 +346,8 @@ SetErrorLog(std::string_view NewErrorLoggerId) } } +RwLock g_LoggerMutex; + LoggerRef Get(std::string_view Name) { @@ -355,9 +357,16 @@ Get(std::string_view Name) if (!Logger) { - Logger = Default().SpdLogger->clone(std::string(Name)); - spdlog::apply_logger_env_levels(Logger); - spdlog::register_logger(Logger); + g_LoggerMutex.WithExclusiveLock([&] { + Logger = spdlog::get(std::string(Name)); + + if (!Logger) + { + Logger = Default().SpdLogger->clone(std::string(Name)); + spdlog::apply_logger_env_levels(Logger); + spdlog::register_logger(Logger); + } + }); } return *Logger; diff --git a/src/zencore/memory/memory.cpp b/src/zencore/memory/memory.cpp index ae1b9abce..fced2a4d3 100644 --- a/src/zencore/memory/memory.cpp +++ b/src/zencore/memory/memory.cpp @@ -284,3 +284,29 @@ zen_free_aligned(void* Ptr, size_t Alignment) noexcept ZEN_UNUSED(Alignment); zen::Memory::Free(Ptr); } + +// EASTL operator new + +void* +operator new[](size_t size, const char* pName, int flags, unsigned debugFlags, const char* file, int line) +{ + ZEN_UNUSED(pName, flags, debugFlags, file, line); + return zen_new(size); +} + +void* +operator new[](size_t size, + size_t alignment, + size_t alignmentOffset, + const char* pName, + int flags, + unsigned debugFlags, + const char* file, + int line) +{ + ZEN_UNUSED(alignmentOffset, pName, flags, debugFlags, file, line); + + ZEN_ASSERT_SLOW(alignmentOffset == 0); // currently not supported + + return zen_new_aligned(size, alignment); +} diff --git a/src/zenutil/parallelwork.cpp b/src/zencore/parallelwork.cpp index a571d1d11..d86d5815f 100644 --- a/src/zenutil/parallelwork.cpp +++ b/src/zencore/parallelwork.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/parallelwork.h> +#include <zencore/parallelwork.h> #include <zencore/callstack.h> #include <zencore/except.h> @@ -15,9 +15,10 @@ namespace zen { -ParallelWork::ParallelWork(std::atomic<bool>& AbortFlag, std::atomic<bool>& PauseFlag) +ParallelWork::ParallelWork(std::atomic<bool>& AbortFlag, std::atomic<bool>& PauseFlag, WorkerThreadPool::EMode Mode) : m_AbortFlag(AbortFlag) , m_PauseFlag(PauseFlag) +, m_Mode(Mode) , m_PendingWork(1) { } @@ -160,7 +161,7 @@ TEST_CASE("parallellwork.nowork") { std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); Work.Wait(); } @@ -170,7 +171,7 @@ TEST_CASE("parallellwork.basic") std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); for (uint32_t I = 0; I < 5; I++) { Work.ScheduleWork(WorkerPool, [](std::atomic<bool>& AbortFlag) { CHECK(!AbortFlag); }); @@ -184,7 +185,7 @@ TEST_CASE("parallellwork.throws_in_work") std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); for (uint32_t I = 0; I < 10; I++) { Work.ScheduleWork(WorkerPool, [I](std::atomic<bool>& AbortFlag) { @@ -210,7 +211,7 @@ TEST_CASE("parallellwork.throws_in_dispatch") { std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); for (uint32_t I = 0; I < 5; I++) { Work.ScheduleWork(WorkerPool, [I, &ExecutedCount](std::atomic<bool>& AbortFlag) { @@ -234,6 +235,26 @@ TEST_CASE("parallellwork.throws_in_dispatch") } } +TEST_CASE("parallellwork.limitqueue") +{ + WorkerThreadPool WorkerPool(2); + + std::atomic<bool> AbortFlag; + std::atomic<bool> PauseFlag; + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::DisableBacklog); + for (uint32_t I = 0; I < 5; I++) + { + Work.ScheduleWork(WorkerPool, [](std::atomic<bool>& AbortFlag) { + if (AbortFlag.load()) + { + return; + } + Sleep(10); + }); + } + Work.Wait(); +} + void parallellwork_forcelink() { diff --git a/src/zencore/sentryintegration.cpp b/src/zencore/sentryintegration.cpp index 118c4158a..00e67dc85 100644 --- a/src/zencore/sentryintegration.cpp +++ b/src/zencore/sentryintegration.cpp @@ -32,13 +32,16 @@ ZEN_THIRD_PARTY_INCLUDES_END namespace sentry { namespace { - static const std::string DefaultDsn("https://[email protected]/5919284"); + static const std::string DefaultDsn("https://[email protected]/5919284"); } struct SentryAssertImpl : zen::AssertImpl { - virtual void ZEN_FORCENOINLINE ZEN_DEBUG_SECTION - OnAssert(const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, zen::CallstackFrames* Callstack) override; + virtual void ZEN_FORCENOINLINE ZEN_DEBUG_SECTION OnAssert(const char* Filename, + int LineNumber, + const char* FunctionName, + const char* Msg, + const zen::CallstackFrames* Callstack) override; }; class sentry_sink final : public spdlog::sinks::base_sink<spdlog::details::null_mutex> @@ -107,7 +110,11 @@ sentry_sink::flush_() } void -SentryAssertImpl::OnAssert(const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, zen::CallstackFrames* Callstack) +SentryAssertImpl::OnAssert(const char* Filename, + int LineNumber, + const char* FunctionName, + const char* Msg, + const zen::CallstackFrames* Callstack) { // Sentry will provide its own callstack ZEN_UNUSED(Callstack); diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp index a0d8c927f..c8c7c2cde 100644 --- a/src/zencore/string.cpp +++ b/src/zencore/string.cpp @@ -483,12 +483,67 @@ template class StringBuilderImpl<char>; template class StringBuilderImpl<wchar_t>; ////////////////////////////////////////////////////////////////////////// + +void +UrlDecode(std::string_view InUrl, StringBuilderBase& OutUrl) +{ + std::string_view::size_type i = 0; + + for (; i != InUrl.size();) + { + char c = InUrl[i]; + + if ((c == '%') && ((i + 2) < InUrl.size())) + { + char hex[2] = {InUrl[i + 1], InUrl[i + 2]}; + uint8_t HexedChar; + if (ParseHexBytes(hex, 2, &HexedChar)) + { + OutUrl.Append(HexedChar); + i += 3; + + continue; + } + } + + OutUrl.Append(c); + ++i; + } +} + +std::string +UrlDecode(std::string_view InUrl) +{ + ExtendableStringBuilder<128> Url; + UrlDecode(InUrl, Url); + + return std::string(Url.ToView()); +} + +////////////////////////////////////////////////////////////////////////// // // Unit tests // #if ZEN_WITH_TESTS +TEST_CASE("url") +{ + using namespace std::literals; + + ExtendableStringBuilder<32> OutUrl; + UrlDecode("http://blah.com/foo?bar=hi%20ho", OutUrl); + CHECK_EQ(OutUrl.ToView(), "http://blah.com/foo?bar=hi ho"sv); + + OutUrl.Reset(); + + UrlDecode("http://blah.com/foo?bar=hi%ho", OutUrl); + CHECK_EQ(OutUrl.ToView(), "http://blah.com/foo?bar=hi%ho"sv); + + CHECK_EQ(UrlDecode("http://blah.com/foo?bar=hi%20ho"), "http://blah.com/foo?bar=hi ho"sv); + CHECK_EQ(UrlDecode("http://blah.com/foo?bar=hi%ho"), "http://blah.com/foo?bar=hi%ho"sv); +} + TEST_CASE("niceNum") { char Buffer[16]; diff --git a/src/zencore/thread.cpp b/src/zencore/thread.cpp index b8ec85a4a..abf282467 100644 --- a/src/zencore/thread.cpp +++ b/src/zencore/thread.cpp @@ -608,12 +608,12 @@ TEST_CASE("NamedEvent") ReadyEvent.Set(); NamedEvent TestEvent(Name); - TestEvent.Wait(1000); + TestEvent.Wait(100); }); ReadyEvent.Wait(); - zen::Sleep(100); + zen::Sleep(10); TestEvent.Set(); Waiter.join(); @@ -621,7 +621,7 @@ TEST_CASE("NamedEvent") // Manual reset property for (uint32_t i = 0; i < 8; ++i) { - bool bEventSet = TestEvent.Wait(100); + bool bEventSet = TestEvent.Wait(10); CHECK(bEventSet); } } diff --git a/src/zencore/workthreadpool.cpp b/src/zencore/workthreadpool.cpp index 445fe939e..e241c0de8 100644 --- a/src/zencore/workthreadpool.cpp +++ b/src/zencore/workthreadpool.cpp @@ -5,6 +5,7 @@ #include <zencore/blockingqueue.h> #include <zencore/except.h> #include <zencore/logging.h> +#include <zencore/scopeguard.h> #include <zencore/string.h> #include <zencore/testing.h> #include <zencore/thread.h> @@ -13,6 +14,10 @@ #include <thread> #include <vector> +ZEN_THIRD_PARTY_INCLUDES_START +#include <gsl/gsl-lite.hpp> +ZEN_THIRD_PARTY_INCLUDES_END + #define ZEN_USE_WINDOWS_THREADPOOL 1 #if ZEN_PLATFORM_WINDOWS && ZEN_USE_WINDOWS_THREADPOOL @@ -41,18 +46,23 @@ namespace { struct WorkerThreadPool::Impl { + const int m_ThreadCount = 0; PTP_POOL m_ThreadPool = nullptr; PTP_CLEANUP_GROUP m_CleanupGroup = nullptr; TP_CALLBACK_ENVIRON m_CallbackEnvironment; PTP_WORK m_Work = nullptr; - std::string m_WorkerThreadBaseName; - std::atomic<int> m_WorkerThreadCounter{0}; + std::string m_WorkerThreadBaseName; + std::atomic<size_t> m_WorkerThreadCounter{0}; + std::atomic<int> m_FreeWorkerCount{0}; - RwLock m_QueueLock; + mutable RwLock m_QueueLock; std::deque<Ref<IWork>> m_WorkQueue; - Impl(int InThreadCount, std::string_view WorkerThreadBaseName) : m_WorkerThreadBaseName(WorkerThreadBaseName) + Impl(int InThreadCount, std::string_view WorkerThreadBaseName) + : m_ThreadCount(InThreadCount) + , m_WorkerThreadBaseName(WorkerThreadBaseName) + , m_FreeWorkerCount(m_ThreadCount) { // Thread pool setup @@ -62,11 +72,11 @@ struct WorkerThreadPool::Impl ThrowLastError("CreateThreadpool failed"); } - if (!SetThreadpoolThreadMinimum(m_ThreadPool, InThreadCount)) + if (!SetThreadpoolThreadMinimum(m_ThreadPool, (DWORD)m_ThreadCount)) { ThrowLastError("SetThreadpoolThreadMinimum failed"); } - SetThreadpoolThreadMaximum(m_ThreadPool, InThreadCount * 2); + SetThreadpoolThreadMaximum(m_ThreadPool, (DWORD)m_ThreadCount); InitializeThreadpoolEnvironment(&m_CallbackEnvironment); @@ -93,12 +103,29 @@ struct WorkerThreadPool::Impl CloseThreadpool(m_ThreadPool); } - void ScheduleWork(Ref<IWork> Work) + [[nodiscard]] Ref<IWork> ScheduleWork(Ref<IWork> Work, WorkerThreadPool::EMode Mode) { - m_QueueLock.WithExclusiveLock([&] { m_WorkQueue.push_back(std::move(Work)); }); + if (Mode == WorkerThreadPool::EMode::DisableBacklog) + { + if (m_FreeWorkerCount <= 0) + { + return Work; + } + RwLock::ExclusiveLockScope _(m_QueueLock); + const int QueuedCount = gsl::narrow<int>(m_WorkQueue.size()); + if (QueuedCount >= m_FreeWorkerCount) + { + return Work; + } + m_WorkQueue.push_back(std::move(Work)); + } + else + { + m_QueueLock.WithExclusiveLock([&] { m_WorkQueue.push_back(std::move(Work)); }); + } SubmitThreadpoolWork(m_Work); + return {}; } - [[nodiscard]] size_t PendingWorkItemCount() const { return 0; } static VOID CALLBACK WorkCallback(_Inout_ PTP_CALLBACK_INSTANCE Instance, _Inout_opt_ PVOID Context, _Inout_ PTP_WORK Work) { @@ -109,10 +136,13 @@ struct WorkerThreadPool::Impl void DoWork() { + m_FreeWorkerCount--; + auto _ = MakeGuard([&]() { m_FreeWorkerCount++; }); + if (!t_IsThreadNamed) { t_IsThreadNamed = true; - const int ThreadIndex = ++m_WorkerThreadCounter; + const size_t ThreadIndex = ++m_WorkerThreadCounter; zen::ExtendableStringBuilder<128> ThreadName; ThreadName << m_WorkerThreadBaseName << "_" << ThreadIndex; SetCurrentThreadName(ThreadName); @@ -121,7 +151,7 @@ struct WorkerThreadPool::Impl Ref<IWork> WorkFromQueue; { - RwLock::ExclusiveLockScope _{m_QueueLock}; + RwLock::ExclusiveLockScope __{m_QueueLock}; WorkFromQueue = std::move(m_WorkQueue.front()); m_WorkQueue.pop_front(); } @@ -141,20 +171,25 @@ struct WorkerThreadPool::ThreadStartInfo struct WorkerThreadPool::Impl { + const int m_ThreadCount = 0; void WorkerThreadFunction(ThreadStartInfo Info); std::string m_WorkerThreadBaseName; std::vector<std::thread> m_WorkerThreads; BlockingQueue<Ref<IWork>> m_WorkQueue; + std::atomic<int> m_FreeWorkerCount{0}; - Impl(int InThreadCount, std::string_view WorkerThreadBaseName) : m_WorkerThreadBaseName(WorkerThreadBaseName) + Impl(int InThreadCount, std::string_view WorkerThreadBaseName) + : m_ThreadCount(InThreadCount) + , m_WorkerThreadBaseName(WorkerThreadBaseName) + , m_FreeWorkerCount(m_ThreadCount) { # if ZEN_WITH_TRACE trace::ThreadGroupBegin(m_WorkerThreadBaseName.c_str()); # endif - zen::Latch WorkerLatch{InThreadCount}; + zen::Latch WorkerLatch{m_ThreadCount}; - for (int i = 0; i < InThreadCount; ++i) + for (int i = 0; i < m_ThreadCount; ++i) { m_WorkerThreads.emplace_back(&Impl::WorkerThreadFunction, this, ThreadStartInfo{i + 1, &WorkerLatch}); } @@ -181,8 +216,23 @@ struct WorkerThreadPool::Impl m_WorkerThreads.clear(); } - void ScheduleWork(Ref<IWork> Work) { m_WorkQueue.Enqueue(std::move(Work)); } - [[nodiscard]] size_t PendingWorkItemCount() const { return m_WorkQueue.Size(); } + [[nodiscard]] Ref<IWork> ScheduleWork(Ref<IWork> Work, WorkerThreadPool::EMode Mode) + { + if (Mode == WorkerThreadPool::EMode::DisableBacklog) + { + if (m_FreeWorkerCount <= 0) + { + return Work; + } + const int QueuedCount = gsl::narrow<int>(m_WorkQueue.Size()); + if (QueuedCount >= m_FreeWorkerCount) + { + return Work; + } + } + m_WorkQueue.Enqueue(std::move(Work)); + return {}; + } }; void @@ -197,21 +247,23 @@ WorkerThreadPool::Impl::WorkerThreadFunction(ThreadStartInfo Info) Ref<IWork> Work; if (m_WorkQueue.WaitAndDequeue(Work)) { + m_FreeWorkerCount--; + auto _ = MakeGuard([&]() { m_FreeWorkerCount++; }); + try { ZEN_TRACE_CPU_FLUSH("AsyncWork"); Work->Execute(); + Work = {}; } catch (const AssertException& Ex) { - Work->m_Exception = std::current_exception(); - + Work = {}; ZEN_WARN("Assert exception in worker thread: {}", Ex.FullDescription()); } catch (const std::exception& e) { - Work->m_Exception = std::current_exception(); - + Work = {}; ZEN_WARN("Caught exception in worker thread: {}", e.what()); } } @@ -243,48 +295,38 @@ WorkerThreadPool::~WorkerThreadPool() } void -WorkerThreadPool::ScheduleWork(Ref<IWork> Work) +WorkerThreadPool::ScheduleWork(Ref<IWork> Work, EMode Mode) { if (m_Impl) { - m_Impl->ScheduleWork(std::move(Work)); - } - else - { - try + if (Work = m_Impl->ScheduleWork(std::move(Work), Mode); !Work) { - ZEN_TRACE_CPU_FLUSH("SyncWork"); - Work->Execute(); - } - catch (const AssertException& Ex) - { - Work->m_Exception = std::current_exception(); - - ZEN_WARN("Assert exception in worker thread: {}", Ex.FullDescription()); + return; } - catch (const std::exception& e) - { - Work->m_Exception = std::current_exception(); + } - ZEN_WARN("Caught exception when executing worker synchronously: {}", e.what()); - } + try + { + ZEN_TRACE_CPU_FLUSH("SyncWork"); + Work->Execute(); + Work = {}; + } + catch (const AssertException& Ex) + { + Work = {}; + ZEN_WARN("Assert exception in worker thread: {}", Ex.FullDescription()); + } + catch (const std::exception& e) + { + Work = {}; + ZEN_WARN("Caught exception when executing worker synchronously: {}", e.what()); } } void -WorkerThreadPool::ScheduleWork(std::function<void()>&& Work) -{ - ScheduleWork(Ref<IWork>(new detail::LambdaWork(std::move(Work)))); -} - -[[nodiscard]] size_t -WorkerThreadPool::PendingWorkItemCount() const +WorkerThreadPool::ScheduleWork(std::function<void()>&& Work, EMode Mode) { - if (m_Impl) - { - return m_Impl->PendingWorkItemCount(); - } - return 0; + ScheduleWork(Ref<IWork>(new detail::LambdaWork(std::move(Work))), Mode); } ////////////////////////////////////////////////////////////////////////// @@ -302,9 +344,10 @@ TEST_CASE("threadpool.basic") { WorkerThreadPool Threadpool{1}; - auto Future42 = Threadpool.EnqueueTask(std::packaged_task<int()>{[] { return 42; }}); - auto Future99 = Threadpool.EnqueueTask(std::packaged_task<int()>{[] { return 99; }}); - auto FutureThrow = Threadpool.EnqueueTask(std::packaged_task<void()>{[] { throw std::runtime_error("meep!"); }}); + auto Future42 = Threadpool.EnqueueTask(std::packaged_task<int()>{[] { return 42; }}, WorkerThreadPool::EMode::EnableBacklog); + auto Future99 = Threadpool.EnqueueTask(std::packaged_task<int()>{[] { return 99; }}, WorkerThreadPool::EMode::EnableBacklog); + auto FutureThrow = Threadpool.EnqueueTask(std::packaged_task<void()>{[] { throw std::runtime_error("meep!"); }}, + WorkerThreadPool::EMode::EnableBacklog); CHECK_EQ(Future42.get(), 42); CHECK_EQ(Future99.get(), 99); diff --git a/src/zencore/zencore.cpp b/src/zencore/zencore.cpp index 51e06ae14..b78991918 100644 --- a/src/zencore/zencore.cpp +++ b/src/zencore/zencore.cpp @@ -23,6 +23,7 @@ #include <zencore/logging.h> #include <zencore/memoryview.h> #include <zencore/mpscqueue.h> +#include <zencore/parallelwork.h> #include <zencore/process.h> #include <zencore/sha1.h> #include <zencore/stats.h> @@ -116,10 +117,10 @@ AssertImpl::~AssertImpl() void AssertImpl::ExecAssert(const char* Filename, int LineNumber, const char* FunctionName, const char* Msg) { - void* Frames[8]; - uint32_t FrameCount = GetCallstack(2, 8, Frames); - - CallstackFrames* Callstack = CreateCallstack(FrameCount, Frames); + constexpr int SkipFrameCount = 2; + constexpr int FrameCount = 8; + uint8_t CallstackBuffer[CallstackRawMemorySize(SkipFrameCount, FrameCount)]; + CallstackFrames* Callstack = GetCallstackRaw(&CallstackBuffer[0], SkipFrameCount, FrameCount); AssertImpl* AssertImpl = CurrentAssertImpl; while (AssertImpl) @@ -137,7 +138,7 @@ AssertImpl::ExecAssert(const char* Filename, int LineNumber, const char* Functio ThrowAssertException(Filename, LineNumber, FunctionName, Msg, Callstack); } void -AssertImpl::OnAssert(const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, CallstackFrames* Callstack) +AssertImpl::OnAssert(const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, const CallstackFrames* Callstack) { ZEN_UNUSED(FunctionName); @@ -152,11 +153,11 @@ AssertImpl::OnAssert(const char* Filename, int LineNumber, const char* FunctionN } void -AssertImpl::ThrowAssertException(const char* Filename, - int LineNumber, - const char* FunctionName, - const char* Msg, - CallstackFrames* Callstack) +AssertImpl::ThrowAssertException(const char* Filename, + int LineNumber, + const char* FunctionName, + const char* Msg, + const CallstackFrames* Callstack) { ZEN_UNUSED(FunctionName); fmt::basic_memory_buffer<char, 2048> Message; @@ -164,7 +165,7 @@ AssertImpl::ThrowAssertException(const char* Filename, fmt::format_to(Appender, "{}({}): {}", Filename, LineNumber, Msg); Message.push_back('\0'); - throw AssertException(Message.data(), Callstack); + throw AssertException(Message.data(), CloneCallstack(Callstack)); } void refcount_forcelink(); @@ -262,6 +263,7 @@ zencore_forcelinktests() zen::logging_forcelink(); zen::memory_forcelink(); zen::mpscqueue_forcelink(); + zen::parallellwork_forcelink(); zen::process_forcelink(); zen::refcount_forcelink(); zen::sha1_forcelink(); @@ -308,7 +310,7 @@ TEST_CASE("Assert.Custom") struct MyAssertImpl : AssertImpl { virtual void ZEN_FORCENOINLINE ZEN_DEBUG_SECTION - OnAssert(const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, CallstackFrames* Callstack) + OnAssert(const char* Filename, int LineNumber, const char* FunctionName, const char* Msg, const CallstackFrames* Callstack) { ZEN_UNUSED(Callstack); AssertFileName = Filename; @@ -350,9 +352,10 @@ TEST_CASE("Assert.Callstack") WorkerThreadPool Pool(1); auto Task = Pool.EnqueueTask(std::packaged_task<int()>{[] { - ZEN_ASSERT(false); - return 1; - }}); + ZEN_ASSERT(false); + return 1; + }}, + WorkerThreadPool::EMode::EnableBacklog); try { diff --git a/src/zenhttp-test/xmake.lua b/src/zenhttp-test/xmake.lua index a6163e506..a7ee731f9 100644 --- a/src/zenhttp-test/xmake.lua +++ b/src/zenhttp-test/xmake.lua @@ -5,7 +5,7 @@ target("zenhttp-test") set_group("tests") add_headerfiles("**.h") add_files("*.cpp") - add_deps("zenhttp", "zencore") + add_deps("zenhttp") add_packages("vcpkg::doctest") if is_plat("macosx") then diff --git a/src/zenhttp-test/zenhttp-test.cpp b/src/zenhttp-test/zenhttp-test.cpp index 116971b02..d18b2167e 100644 --- a/src/zenhttp-test/zenhttp-test.cpp +++ b/src/zenhttp-test/zenhttp-test.cpp @@ -22,7 +22,15 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) zen::IgnoreChildSignals(); # endif +# if ZEN_WITH_TRACE zen::TraceInit("zenhttp-test"); + zen::TraceOptions TraceCommandlineOptions; + if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) + { + TraceConfigure(TraceCommandlineOptions); + } +# endif // ZEN_WITH_TRACE + zen::logging::InitializeLogging(); zen::MaximizeOpenFileCount(); diff --git a/src/zenhttp/auth/authmgr.cpp b/src/zenhttp/auth/authmgr.cpp index 6c1a66a99..209276621 100644 --- a/src/zenhttp/auth/authmgr.cpp +++ b/src/zenhttp/auth/authmgr.cpp @@ -5,7 +5,7 @@ #include <zencore/basicfile.h> #include <zencore/compactbinary.h> #include <zencore/compactbinarybuilder.h> -#include <zencore/compactbinaryvalidation.h> +#include <zencore/compactbinaryutil.h> #include <zencore/crypto.h> #include <zencore/filesystem.h> #include <zencore/logging.h> @@ -297,15 +297,14 @@ private: return; } - const CbValidateError ValidationError = ValidateCompactBinary(Buffer, CbValidateMode::All); - - if (ValidationError != CbValidateError::None) + CbValidateError ValidationError; + if (CbObject AuthState = ValidateAndReadCompactBinaryObject(std::move(Buffer), ValidationError); + ValidationError != CbValidateError::None) { - ZEN_WARN("load serialized state FAILED, reason 'Invalid compact binary'"); + ZEN_WARN("load serialized state FAILED, reason '{}'", ToString(ValidationError)); return; } - - if (CbObject AuthState = LoadCompactBinaryObject(Buffer)) + else { for (CbFieldView ProviderView : AuthState["OpenIdProviders"sv]) { diff --git a/src/zenhttp/auth/oidc.cpp b/src/zenhttp/auth/oidc.cpp index 318110c7d..38e7586ad 100644 --- a/src/zenhttp/auth/oidc.cpp +++ b/src/zenhttp/auth/oidc.cpp @@ -1,9 +1,9 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "zenhttp/auth/oidc.h" +#include <zenhttp/httpclient.h> ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> #include <fmt/format.h> #include <json11.hpp> ZEN_THIRD_PARTY_INCLUDES_END @@ -41,27 +41,21 @@ OidcClient::OidcClient(const OidcClient::Options& Options) OidcClient::InitResult OidcClient::Initialize() { - ExtendableStringBuilder<256> Uri; - Uri << m_BaseUrl << "/.well-known/openid-configuration"sv; + HttpClient Http{m_BaseUrl}; + HttpClient::Response Response = Http.Get("/.well-known/openid-configuration"sv); - cpr::Session Session; - - Session.SetOption(cpr::Url{Uri.c_str()}); - - cpr::Response Response = Session.Get(); - - if (Response.error) + if (!Response) { - return {.Reason = std::move(Response.error.message)}; + return {.Reason = Response.ErrorMessage("")}; } - if (Response.status_code != 200) + if (Response.StatusCode != HttpResponseCode::OK) { - return {.Reason = std::move(Response.reason)}; + return {.Reason = std::string{ToString(Response.StatusCode)}}; } std::string JsonError; - json11::Json Json = json11::Json::parse(Response.text, JsonError); + json11::Json Json = json11::Json::parse(std::string{Response.AsText()}, JsonError); if (JsonError.empty() == false) { @@ -89,26 +83,24 @@ OidcClient::RefreshToken(std::string_view RefreshToken) { const std::string Body = fmt::format("grant_type=refresh_token&refresh_token={}&client_id={}", RefreshToken, m_ClientId); - cpr::Session Session; + HttpClient Http{m_Config.TokenEndpoint}; - Session.SetOption(cpr::Url{m_Config.TokenEndpoint.c_str()}); - Session.SetOption(cpr::Header{{"Content-Type", "application/x-www-form-urlencoded"}}); - Session.SetBody(cpr::Body{Body.data(), Body.size()}); + HttpClient::KeyValueMap Headers{{"Content-Type", "application/x-www-form-urlencoded"}}; - cpr::Response Response = Session.Post(); + HttpClient::Response Response = Http.Post("", IoBufferBuilder::MakeFromMemory(MemoryView{Body.data(), Body.size()}), Headers); - if (Response.error) + if (!Response) { - return {.Reason = std::move(Response.error.message)}; + return {.Reason = std::string{Response.ErrorMessage("")}}; } - if (Response.status_code != 200) + if (Response.StatusCode != HttpResponseCode::OK) { - return {.Reason = fmt::format("{} ({})", Response.reason, Response.text)}; + return {.Reason = fmt::format("{} ({})", ToString(Response.StatusCode), Response.AsText())}; } std::string JsonError; - json11::Json Json = json11::Json::parse(Response.text, JsonError); + json11::Json Json = json11::Json::parse(std::string{Response.AsText()}, JsonError); if (JsonError.empty() == false) { diff --git a/src/zenhttp/clients/httpclientcommon.cpp b/src/zenhttp/clients/httpclientcommon.cpp new file mode 100644 index 000000000..8e5136dff --- /dev/null +++ b/src/zenhttp/clients/httpclientcommon.cpp @@ -0,0 +1,474 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "httpclientcommon.h" + +#include <fmt/format.h> +#include <zencore/except.h> +#include <zencore/filesystem.h> +#include <zencore/iohash.h> +#include <zencore/logging.h> +#include <zencore/memory/memory.h> +#include <zencore/windows.h> +#include <gsl/gsl-lite.hpp> + +#if ZEN_WITH_TESTS +# include <zencore/basicfile.h> +# include <zencore/testing.h> +# include <zencore/testutils.h> +#endif // ZEN_WITH_TESTS + +#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC +# include <fcntl.h> +# include <sys/stat.h> +# include <unistd.h> +#endif + +namespace zen { + +using namespace std::literals; + +namespace detail { + + static std::atomic_uint32_t TempFileBaseIndex; + + TempPayloadFile::TempPayloadFile() : m_FileHandle(nullptr), m_WriteOffset(0) {} + TempPayloadFile::~TempPayloadFile() + { + ZEN_TRACE_CPU("TempPayloadFile::Close"); + try + { + if (m_FileHandle) + { +#if ZEN_PLATFORM_WINDOWS + // Mark file for deletion when final handle is closed + FILE_DISPOSITION_INFO Fdi{.DeleteFile = TRUE}; + + SetFileInformationByHandle(m_FileHandle, FileDispositionInfo, &Fdi, sizeof Fdi); + BOOL Success = CloseHandle(m_FileHandle); +#else + std::error_code Ec; + std::filesystem::path FilePath = zen::PathFromHandle(m_FileHandle, Ec); + if (Ec) + { + ZEN_WARN("Error reported on get file path from handle {} for temp payload unlink operation, reason '{}'", + m_FileHandle, + Ec.message()); + } + else + { + unlink(FilePath.c_str()); + } + int Fd = int(uintptr_t(m_FileHandle)); + bool Success = (close(Fd) == 0); +#endif + if (!Success) + { + ZEN_WARN("Error reported on file handle close, reason '{}'", GetLastErrorAsString()); + } + + m_FileHandle = nullptr; + } + } + catch (const std::exception& Ex) + { + ZEN_ERROR("Failed deleting temp file {}. Reason '{}'", m_FileHandle, Ex.what()); + } + } + + std::error_code TempPayloadFile::Open(const std::filesystem::path& TempFolderPath, uint64_t FinalSize) + { + ZEN_TRACE_CPU("TempPayloadFile::Open"); + ZEN_ASSERT(m_FileHandle == nullptr); + + std::uint64_t TmpIndex = ((std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) & 0xffffffffu) << 32) | + detail::TempFileBaseIndex.fetch_add(1); + + std::filesystem::path FileName = TempFolderPath / fmt::to_string(TmpIndex); +#if ZEN_PLATFORM_WINDOWS + LPCWSTR lpFileName = FileName.c_str(); + const DWORD dwDesiredAccess = (GENERIC_READ | GENERIC_WRITE | DELETE); + const DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + LPSECURITY_ATTRIBUTES lpSecurityAttributes = nullptr; + const DWORD dwCreationDisposition = CREATE_ALWAYS; + const DWORD dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL; + const HANDLE hTemplateFile = nullptr; + const HANDLE FileHandle = CreateFile(lpFileName, + dwDesiredAccess, + dwShareMode, + lpSecurityAttributes, + dwCreationDisposition, + dwFlagsAndAttributes, + hTemplateFile); + + if (FileHandle == INVALID_HANDLE_VALUE) + { + return MakeErrorCodeFromLastError(); + } +#else // ZEN_PLATFORM_WINDOWS + int OpenFlags = O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC; + int Fd = open(FileName.c_str(), OpenFlags, 0666); + if (Fd < 0) + { + return MakeErrorCodeFromLastError(); + } + fchmod(Fd, 0666); + + void* FileHandle = (void*)(uintptr_t(Fd)); +#endif // ZEN_PLATFORM_WINDOWS + m_FileHandle = FileHandle; + + PrepareFileForScatteredWrite(m_FileHandle, FinalSize); + + return {}; + } + + std::error_code TempPayloadFile::Write(std::string_view DataString) + { + ZEN_TRACE_CPU("TempPayloadFile::Write"); + const uint8_t* DataPtr = (const uint8_t*)DataString.data(); + size_t DataSize = DataString.size(); + if (DataSize >= CacheBufferSize) + { + std::error_code Ec = Flush(); + if (Ec) + { + return Ec; + } + return AppendData(DataPtr, DataSize); + } + size_t CopySize = Min(DataSize, CacheBufferSize - m_CacheBufferOffset); + memcpy(&m_CacheBuffer[m_CacheBufferOffset], DataPtr, CopySize); + m_CacheBufferOffset += CopySize; + DataSize -= CopySize; + if (m_CacheBufferOffset == CacheBufferSize) + { + AppendData(m_CacheBuffer, CacheBufferSize); + if (DataSize > 0) + { + ZEN_ASSERT(DataSize < CacheBufferSize); + memcpy(m_CacheBuffer, DataPtr + CopySize, DataSize); + } + m_CacheBufferOffset = DataSize; + } + else + { + ZEN_ASSERT(DataSize == 0); + } + return {}; + } + + IoBuffer TempPayloadFile::DetachToIoBuffer() + { + ZEN_TRACE_CPU("TempPayloadFile::DetachToIoBuffer"); + if (std::error_code Ec = Flush(); Ec) + { + ThrowSystemError(Ec.value(), Ec.message()); + } + ZEN_ASSERT(m_FileHandle != nullptr); + void* FileHandle = m_FileHandle; + IoBuffer Buffer(IoBuffer::File, FileHandle, 0, m_WriteOffset, /*IsWholeFile*/ true); + Buffer.SetDeleteOnClose(true); + m_FileHandle = 0; + m_WriteOffset = 0; + return Buffer; + } + + IoBuffer TempPayloadFile::BorrowIoBuffer() + { + ZEN_TRACE_CPU("TempPayloadFile::BorrowIoBuffer"); + if (std::error_code Ec = Flush(); Ec) + { + ThrowSystemError(Ec.value(), Ec.message()); + } + ZEN_ASSERT(m_FileHandle != nullptr); + void* FileHandle = m_FileHandle; + IoBuffer Buffer(IoBuffer::BorrowedFile, FileHandle, 0, m_WriteOffset); + return Buffer; + } + + void TempPayloadFile::ResetWritePos(uint64_t WriteOffset) + { + ZEN_TRACE_CPU("TempPayloadFile::ResetWritePos"); + Flush(); + m_WriteOffset = WriteOffset; + } + + std::error_code TempPayloadFile::Flush() + { + ZEN_TRACE_CPU("TempPayloadFile::Flush"); + if (m_CacheBufferOffset == 0) + { + return {}; + } + std::error_code Res = AppendData(m_CacheBuffer, m_CacheBufferOffset); + m_CacheBufferOffset = 0; + return Res; + } + + std::error_code TempPayloadFile::AppendData(const void* Data, uint64_t Size) + { + ZEN_TRACE_CPU("TempPayloadFile::AppendData"); + ZEN_ASSERT(m_FileHandle != nullptr); + const uint64_t MaxChunkSize = 2u * 1024 * 1024 * 1024; + + while (Size) + { + const uint64_t NumberOfBytesToWrite = Min(Size, MaxChunkSize); + uint64_t NumberOfBytesWritten = 0; +#if ZEN_PLATFORM_WINDOWS + OVERLAPPED Ovl{}; + + Ovl.Offset = DWORD(m_WriteOffset & 0xffff'ffffu); + Ovl.OffsetHigh = DWORD(m_WriteOffset >> 32); + + DWORD dwNumberOfBytesWritten = 0; + + BOOL Success = ::WriteFile(m_FileHandle, Data, DWORD(NumberOfBytesToWrite), &dwNumberOfBytesWritten, &Ovl); + if (Success) + { + NumberOfBytesWritten = static_cast<uint64_t>(dwNumberOfBytesWritten); + } +#else + static_assert(sizeof(off_t) >= sizeof(uint64_t), "sizeof(off_t) does not support large files"); + int Fd = int(uintptr_t(m_FileHandle)); + int BytesWritten = pwrite(Fd, Data, NumberOfBytesToWrite, m_WriteOffset); + bool Success = (BytesWritten > 0); + if (Success) + { + NumberOfBytesWritten = static_cast<uint64_t>(BytesWritten); + } +#endif + + if (!Success) + { + return MakeErrorCodeFromLastError(); + } + + Size -= NumberOfBytesWritten; + m_WriteOffset += NumberOfBytesWritten; + Data = reinterpret_cast<const uint8_t*>(Data) + NumberOfBytesWritten; + } + return {}; + } + + BufferedReadFileStream::BufferedReadFileStream(void* FileHandle, uint64_t FileOffset, uint64_t FileSize, uint64_t BufferSize) + : m_FileHandle(FileHandle) + , m_FileSize(FileSize) + , m_FileEnd(FileOffset + FileSize) + , m_BufferSize(Min(BufferSize, FileSize)) + , m_FileOffset(FileOffset) + { + } + + BufferedReadFileStream::~BufferedReadFileStream() { Memory::Free(m_Buffer); } + void BufferedReadFileStream::Read(void* Data, uint64_t Size) + { + ZEN_ASSERT(Data != nullptr); + if (Size > m_BufferSize) + { + Read(Data, Size, m_FileOffset); + m_FileOffset += Size; + return; + } + uint8_t* WritePtr = ((uint8_t*)Data); + uint64_t Begin = m_FileOffset; + uint64_t End = m_FileOffset + Size; + ZEN_ASSERT(m_FileOffset >= m_BufferStart); + if (m_FileOffset < m_BufferEnd) + { + ZEN_ASSERT(m_Buffer != nullptr); + uint64_t Count = Min(m_BufferEnd, End) - m_FileOffset; + memcpy(WritePtr + Begin - m_FileOffset, m_Buffer + Begin - m_BufferStart, Count); + Begin += Count; + if (Begin == End) + { + m_FileOffset = End; + return; + } + } + if (End == m_FileEnd) + { + Read(WritePtr + Begin - m_FileOffset, End - Begin, Begin); + } + else + { + if (!m_Buffer) + { + m_BufferSize = Min(m_FileEnd - m_FileOffset, m_BufferSize); + m_Buffer = (uint8_t*)Memory::Alloc(gsl::narrow<size_t>(m_BufferSize)); + } + m_BufferStart = Begin; + m_BufferEnd = Min(Begin + m_BufferSize, m_FileEnd); + Read(m_Buffer, m_BufferEnd - m_BufferStart, m_BufferStart); + uint64_t Count = Min(m_BufferEnd, End) - m_BufferStart; + memcpy(WritePtr + Begin - m_FileOffset, m_Buffer, Count); + ZEN_ASSERT(Begin + Count == End); + } + m_FileOffset = End; + } + + void BufferedReadFileStream::Read(void* Data, uint64_t BytesToRead, uint64_t FileOffset) + { + const uint64_t MaxChunkSize = 1u * 1024 * 1024; + std::error_code Ec; + ReadFile(m_FileHandle, Data, BytesToRead, FileOffset, MaxChunkSize, Ec); + + if (Ec) + { + std::error_code DummyEc; + throw std::system_error( + Ec, + fmt::format("HttpClient::BufferedReadFileStream ReadFile/pread failed (offset {:#x}, size {:#x}) file: '{}' (size {:#x})", + FileOffset, + BytesToRead, + PathFromHandle(m_FileHandle, DummyEc).generic_string(), + m_FileSize)); + } + } + + CompositeBufferReadStream::CompositeBufferReadStream(const CompositeBuffer& Data, uint64_t BufferSize) + : m_Data(Data) + , m_BufferSize(BufferSize) + , m_SegmentIndex(0) + , m_BytesLeftInSegment(0) + { + } + uint64_t CompositeBufferReadStream::Read(void* Data, uint64_t Size) + { + uint64_t Result = 0; + uint8_t* WritePtr = (uint8_t*)Data; + while ((Size > 0) && (m_SegmentIndex < m_Data.GetSegments().size())) + { + if (m_BytesLeftInSegment == 0) + { + const SharedBuffer& Segment = m_Data.GetSegments()[m_SegmentIndex]; + IoBufferFileReference FileRef = {nullptr, 0, 0}; + if (Segment.AsIoBuffer().GetFileReference(FileRef)) + { + m_SegmentDiskBuffer = std::make_unique<BufferedReadFileStream>(FileRef.FileHandle, + FileRef.FileChunkOffset, + FileRef.FileChunkSize, + m_BufferSize); + } + else + { + m_SegmentMemoryBuffer = Segment.GetView(); + } + m_BytesLeftInSegment = Segment.GetSize(); + } + uint64_t BytesToRead = Min(m_BytesLeftInSegment, Size); + if (m_SegmentDiskBuffer) + { + m_SegmentDiskBuffer->Read(WritePtr, BytesToRead); + } + else + { + ZEN_ASSERT_SLOW(m_SegmentMemoryBuffer.GetSize() >= BytesToRead); + memcpy(WritePtr, m_SegmentMemoryBuffer.GetData(), BytesToRead); + m_SegmentMemoryBuffer.MidInline(BytesToRead); + } + WritePtr += BytesToRead; + Size -= BytesToRead; + Result += BytesToRead; + + m_BytesLeftInSegment -= BytesToRead; + if (m_BytesLeftInSegment == 0) + { + m_SegmentDiskBuffer.reset(); + m_SegmentMemoryBuffer.Reset(); + m_SegmentIndex++; + } + } + return Result; + } + +} // namespace detail + +} // namespace zen + +#if ZEN_WITH_TESTS +namespace zen { + +namespace testutil { + IoHash HashComposite(const CompositeBuffer& Payload) + { + IoHashStream Hasher; + const uint64_t PayloadSize = Payload.GetSize(); + std::vector<uint8_t> Buffer(64u * 1024u); + detail::CompositeBufferReadStream Stream(Payload, 137u * 1024u); + for (uint64_t Offset = 0; Offset < PayloadSize;) + { + uint64_t Count = Min(64u * 1024u, PayloadSize - Offset); + Stream.Read(Buffer.data(), Count); + Hasher.Append(Buffer.data(), Count); + Offset += Count; + } + return Hasher.GetHash(); + }; + + IoHash HashFileStream(void* FileHandle, uint64_t FileOffset, uint64_t FileSize) + { + IoHashStream Hasher; + std::vector<uint8_t> Buffer(64u * 1024u); + detail::BufferedReadFileStream Stream(FileHandle, FileOffset, FileSize, 137u * 1024u); + for (uint64_t Offset = 0; Offset < FileSize;) + { + uint64_t Count = Min(64u * 1024u, FileSize - Offset); + Stream.Read(Buffer.data(), Count); + Hasher.Append(Buffer.data(), Count); + Offset += Count; + } + return Hasher.GetHash(); + } + +} // namespace testutil + +TEST_CASE("BufferedReadFileStream") +{ + ScopedTemporaryDirectory TmpDir; + + IoBuffer DiskBuffer = WriteToTempFile(CompositeBuffer(CreateRandomBlob(496 * 5 * 1024)), TmpDir.Path() / "diskbuffer1"); + + IoBufferFileReference FileRef = {nullptr, 0, 0}; + CHECK(DiskBuffer.GetFileReference(FileRef)); + CHECK_EQ(IoHash::HashBuffer(DiskBuffer), testutil::HashFileStream(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize)); + + IoBuffer Partial(DiskBuffer, 37 * 1024, 512 * 1024); + CHECK(Partial.GetFileReference(FileRef)); + CHECK_EQ(IoHash::HashBuffer(Partial), testutil::HashFileStream(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize)); + + IoBuffer SmallDiskBuffer = WriteToTempFile(CompositeBuffer(CreateRandomBlob(63 * 1024)), TmpDir.Path() / "diskbuffer2"); + CHECK(SmallDiskBuffer.GetFileReference(FileRef)); + CHECK_EQ(IoHash::HashBuffer(SmallDiskBuffer), + testutil::HashFileStream(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize)); +} + +TEST_CASE("CompositeBufferReadStream") +{ + ScopedTemporaryDirectory TmpDir; + + IoBuffer MemoryBuffer1 = CreateRandomBlob(64); + CHECK_EQ(IoHash::HashBuffer(MemoryBuffer1), testutil::HashComposite(CompositeBuffer(SharedBuffer(MemoryBuffer1)))); + + IoBuffer MemoryBuffer2 = CreateRandomBlob(561 * 1024); + CHECK_EQ(IoHash::HashBuffer(MemoryBuffer2), testutil::HashComposite(CompositeBuffer(SharedBuffer(MemoryBuffer2)))); + + IoBuffer DiskBuffer1 = WriteToTempFile(CompositeBuffer(CreateRandomBlob(267 * 3 * 1024)), TmpDir.Path() / "diskbuffer1"); + CHECK_EQ(IoHash::HashBuffer(DiskBuffer1), testutil::HashComposite(CompositeBuffer(SharedBuffer(DiskBuffer1)))); + + IoBuffer DiskBuffer2 = WriteToTempFile(CompositeBuffer(CreateRandomBlob(3 * 1024)), TmpDir.Path() / "diskbuffer2"); + CHECK_EQ(IoHash::HashBuffer(DiskBuffer2), testutil::HashComposite(CompositeBuffer(SharedBuffer(DiskBuffer2)))); + + IoBuffer DiskBuffer3 = WriteToTempFile(CompositeBuffer(CreateRandomBlob(496 * 5 * 1024)), TmpDir.Path() / "diskbuffer3"); + CHECK_EQ(IoHash::HashBuffer(DiskBuffer3), testutil::HashComposite(CompositeBuffer(SharedBuffer(DiskBuffer3)))); + + CompositeBuffer Data(SharedBuffer(std::move(MemoryBuffer1)), + SharedBuffer(std::move(DiskBuffer1)), + SharedBuffer(std::move(DiskBuffer2)), + SharedBuffer(std::move(MemoryBuffer2)), + SharedBuffer(std::move(DiskBuffer3))); + CHECK_EQ(IoHash::HashBuffer(Data), testutil::HashComposite(Data)); +} + +} // namespace zen +#endif diff --git a/src/zenhttp/clients/httpclientcommon.h b/src/zenhttp/clients/httpclientcommon.h new file mode 100644 index 000000000..9060cde48 --- /dev/null +++ b/src/zenhttp/clients/httpclientcommon.h @@ -0,0 +1,147 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zencore/compositebuffer.h> +#include <zencore/trace.h> + +#include <zenhttp/httpclient.h> + +namespace zen { + +using namespace std::literals; + +class HttpClientBase +{ +public: + HttpClientBase(std::string_view BaseUri, const HttpClientSettings& Connectionsettings = {}); + virtual ~HttpClientBase() = 0; + + using Response = HttpClient::Response; + using KeyValueMap = HttpClient::KeyValueMap; + + [[nodiscard]] virtual Response Put(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {}) = 0; + [[nodiscard]] virtual Response Put(std::string_view Url, const KeyValueMap& Parameters = {}) = 0; + [[nodiscard]] virtual Response Get(std::string_view Url, + const KeyValueMap& AdditionalHeader = {}, + const KeyValueMap& Parameters = {}) = 0; + [[nodiscard]] virtual Response Head(std::string_view Url, const KeyValueMap& AdditionalHeader = {}) = 0; + [[nodiscard]] virtual Response Delete(std::string_view Url, const KeyValueMap& AdditionalHeader = {}) = 0; + [[nodiscard]] virtual Response Post(std::string_view Url, + const KeyValueMap& AdditionalHeader = {}, + const KeyValueMap& Parameters = {}) = 0; + [[nodiscard]] virtual Response Post(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {}) = 0; + [[nodiscard]] virtual Response Post(std::string_view Url, + const IoBuffer& Payload, + ZenContentType ContentType, + const KeyValueMap& AdditionalHeader = {}) = 0; + [[nodiscard]] virtual Response Post(std::string_view Url, CbObject Payload, const KeyValueMap& AdditionalHeader = {}) = 0; + [[nodiscard]] virtual Response Post(std::string_view Url, CbPackage Payload, const KeyValueMap& AdditionalHeader = {}) = 0; + [[nodiscard]] virtual Response Post(std::string_view Url, + const CompositeBuffer& Payload, + ZenContentType ContentType, + const KeyValueMap& AdditionalHeader = {}) = 0; + [[nodiscard]] virtual Response Upload(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {}) = 0; + [[nodiscard]] virtual Response Upload(std::string_view Url, + const CompositeBuffer& Payload, + ZenContentType ContentType, + const KeyValueMap& AdditionalHeader = {}) = 0; + + [[nodiscard]] virtual Response Download(std::string_view Url, + const std::filesystem::path& TempFolderPath, + const KeyValueMap& AdditionalHeader = {}) = 0; + + [[nodiscard]] virtual Response TransactPackage(std::string_view Url, CbPackage Package, const KeyValueMap& AdditionalHeader = {}) = 0; + + LoggerRef Log() { return m_Log; } + std::string_view GetBaseUri() const { return m_BaseUri; } + std::string_view GetSessionId() const { return m_SessionId; } + + bool Authenticate(); + +protected: + LoggerRef m_Log; + std::string m_BaseUri; + std::string m_SessionId; + const HttpClientSettings m_ConnectionSettings; + + const std::optional<HttpClientAccessToken> GetAccessToken(); + + RwLock m_AccessTokenLock; + HttpClientAccessToken m_CachedAccessToken; +}; + +namespace detail { + + class TempPayloadFile + { + public: + TempPayloadFile(const TempPayloadFile&) = delete; + TempPayloadFile& operator=(const TempPayloadFile&) = delete; + + TempPayloadFile(); + ~TempPayloadFile(); + + std::error_code Open(const std::filesystem::path& TempFolderPath, uint64_t FinalSize); + std::error_code Write(std::string_view DataString); + IoBuffer DetachToIoBuffer(); + IoBuffer BorrowIoBuffer(); + inline uint64_t GetSize() const { return m_WriteOffset; } + void ResetWritePos(uint64_t WriteOffset); + + private: + std::error_code Flush(); + std::error_code AppendData(const void* Data, uint64_t Size); + + void* m_FileHandle; + std::uint64_t m_WriteOffset; + static constexpr uint64_t CacheBufferSize = 512u * 1024u; + uint8_t m_CacheBuffer[CacheBufferSize]; + std::uint64_t m_CacheBufferOffset = 0; + }; + + class BufferedReadFileStream + { + public: + BufferedReadFileStream(const BufferedReadFileStream&) = delete; + BufferedReadFileStream& operator=(const BufferedReadFileStream&) = delete; + + BufferedReadFileStream(void* FileHandle, uint64_t FileOffset, uint64_t FileSize, uint64_t BufferSize); + ~BufferedReadFileStream(); + + void Read(void* Data, uint64_t Size); + + private: + void Read(void* Data, uint64_t BytesToRead, uint64_t FileOffset); + + void* m_FileHandle = nullptr; + const uint64_t m_FileSize = 0; + const uint64_t m_FileEnd = 0; + uint64_t m_BufferSize = 0; + uint8_t* m_Buffer = nullptr; + uint64_t m_BufferStart = 0; + uint64_t m_BufferEnd = 0; + uint64_t m_FileOffset = 0; + }; + + class CompositeBufferReadStream + { + public: + CompositeBufferReadStream(const CompositeBufferReadStream&) = delete; + CompositeBufferReadStream& operator=(const CompositeBufferReadStream&) = delete; + + CompositeBufferReadStream(const CompositeBuffer& Data, uint64_t BufferSize); + uint64_t Read(void* Data, uint64_t Size); + + private: + const CompositeBuffer& m_Data; + const uint64_t m_BufferSize; + size_t m_SegmentIndex; + std::unique_ptr<BufferedReadFileStream> m_SegmentDiskBuffer; + MemoryView m_SegmentMemoryBuffer; + uint64_t m_BytesLeftInSegment; + }; + +} // namespace detail + +} // namespace zen diff --git a/src/zenhttp/clients/httpclientcpr.cpp b/src/zenhttp/clients/httpclientcpr.cpp new file mode 100644 index 000000000..568106887 --- /dev/null +++ b/src/zenhttp/clients/httpclientcpr.cpp @@ -0,0 +1,1035 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "httpclientcpr.h" + +#include <zencore/compactbinary.h> +#include <zencore/compactbinarybuilder.h> +#include <zencore/compactbinarypackage.h> +#include <zencore/compactbinaryutil.h> +#include <zencore/compress.h> +#include <zencore/iobuffer.h> +#include <zencore/iohash.h> +#include <zencore/session.h> +#include <zencore/stream.h> +#include <zenhttp/packageformat.h> + +namespace zen { + +HttpClientBase* +CreateCprHttpClient(std::string_view BaseUri, const HttpClientSettings& ConnectionSettings) +{ + return new CprHttpClient(BaseUri, ConnectionSettings); +} + +static std::atomic<uint32_t> HttpClientRequestIdCounter{0}; + +// If we want to support different HTTP client implementations then we'll need to make this more abstract + +HttpClientError::ResponseClass +HttpClientError::GetResponseClass() const +{ + if ((cpr::ErrorCode)m_Error != cpr::ErrorCode::OK) + { + switch ((cpr::ErrorCode)m_Error) + { + case cpr::ErrorCode::CONNECTION_FAILURE: + return ResponseClass::kHttpCantConnectError; + case cpr::ErrorCode::HOST_RESOLUTION_FAILURE: + case cpr::ErrorCode::PROXY_RESOLUTION_FAILURE: + return ResponseClass::kHttpNoHost; + case cpr::ErrorCode::INTERNAL_ERROR: + case cpr::ErrorCode::NETWORK_RECEIVE_ERROR: + case cpr::ErrorCode::NETWORK_SEND_FAILURE: + case cpr::ErrorCode::OPERATION_TIMEDOUT: + return ResponseClass::kHttpTimeout; + case cpr::ErrorCode::SSL_CONNECT_ERROR: + case cpr::ErrorCode::SSL_LOCAL_CERTIFICATE_ERROR: + case cpr::ErrorCode::SSL_REMOTE_CERTIFICATE_ERROR: + case cpr::ErrorCode::SSL_CACERT_ERROR: + case cpr::ErrorCode::GENERIC_SSL_ERROR: + return ResponseClass::kHttpSLLError; + default: + return ResponseClass::kHttpOtherClientError; + } + } + else if (IsHttpSuccessCode(m_ResponseCode)) + { + return ResponseClass::kSuccess; + } + else + { + switch (m_ResponseCode) + { + case HttpResponseCode::Unauthorized: + return ResponseClass::kHttpUnauthorized; + case HttpResponseCode::NotFound: + return ResponseClass::kHttpNotFound; + case HttpResponseCode::Forbidden: + return ResponseClass::kHttpForbidden; + case HttpResponseCode::Conflict: + return ResponseClass::kHttpConflict; + case HttpResponseCode::InternalServerError: + return ResponseClass::kHttpInternalServerError; + case HttpResponseCode::ServiceUnavailable: + return ResponseClass::kHttpServiceUnavailable; + case HttpResponseCode::BadGateway: + return ResponseClass::kHttpBadGateway; + case HttpResponseCode::GatewayTimeout: + return ResponseClass::kHttpGatewayTimeout; + default: + if (m_ResponseCode >= HttpResponseCode::InternalServerError) + { + return ResponseClass::kHttpOtherServerError; + } + else + { + return ResponseClass::kHttpOtherClientError; + } + } + } +} + +////////////////////////////////////////////////////////////////////////// +// +// CPR helpers + +static cpr::Body +AsCprBody(const CbObject& Obj) +{ + return cpr::Body((const char*)Obj.GetBuffer().GetData(), Obj.GetBuffer().GetSize()); +} + +static cpr::Body +AsCprBody(const IoBuffer& Obj) +{ + return cpr::Body((const char*)Obj.GetData(), Obj.GetSize()); +} + +////////////////////////////////////////////////////////////////////////// + +static HttpClient::Response +ResponseWithPayload(std::string_view SessionId, cpr::Response& HttpResponse, const HttpResponseCode WorkResponseCode, IoBuffer&& Payload) +{ + // This ends up doing a memcpy, would be good to get rid of it by streaming results + // into buffer directly + IoBuffer ResponseBuffer = Payload ? std::move(Payload) : IoBuffer(IoBuffer::Clone, HttpResponse.text.data(), HttpResponse.text.size()); + + if (auto It = HttpResponse.header.find("Content-Type"); It != HttpResponse.header.end()) + { + const HttpContentType ContentType = ParseContentType(It->second); + + ResponseBuffer.SetContentType(ContentType); + } + + if (!IsHttpSuccessCode(WorkResponseCode) && WorkResponseCode != HttpResponseCode::NotFound) + { + ZEN_WARN("HttpClient request failed (session: {}): {}", SessionId, HttpResponse); + } + + return HttpClient::Response{.StatusCode = WorkResponseCode, + .ResponsePayload = std::move(ResponseBuffer), + .Header = HttpClient::KeyValueMap(HttpResponse.header.begin(), HttpResponse.header.end()), + .UploadedBytes = gsl::narrow<int64_t>(HttpResponse.uploaded_bytes), + .DownloadedBytes = gsl::narrow<int64_t>(HttpResponse.downloaded_bytes), + .ElapsedSeconds = HttpResponse.elapsed}; +} + +static HttpClient::Response +CommonResponse(std::string_view SessionId, cpr::Response&& HttpResponse, IoBuffer&& Payload = {}) +{ + const HttpResponseCode WorkResponseCode = HttpResponseCode(HttpResponse.status_code); + if (HttpResponse.error) + { + if (HttpResponse.error.code != cpr::ErrorCode::OPERATION_TIMEDOUT && + HttpResponse.error.code != cpr::ErrorCode::CONNECTION_FAILURE && HttpResponse.error.code != cpr::ErrorCode::REQUEST_CANCELLED) + { + ZEN_WARN("HttpClient client failure (session: {}): {}", SessionId, HttpResponse); + } + + // Client side failure code + return HttpClient::Response{ + .StatusCode = WorkResponseCode, + .ResponsePayload = IoBufferBuilder::MakeCloneFromMemory(HttpResponse.text.data(), HttpResponse.text.size()), + .Header = HttpClient::KeyValueMap(HttpResponse.header.begin(), HttpResponse.header.end()), + .UploadedBytes = gsl::narrow<int64_t>(HttpResponse.uploaded_bytes), + .DownloadedBytes = gsl::narrow<int64_t>(HttpResponse.downloaded_bytes), + .ElapsedSeconds = HttpResponse.elapsed, + .Error = HttpClient::ErrorContext{.ErrorCode = gsl::narrow<int>(HttpResponse.error.code), + .ErrorMessage = HttpResponse.error.message}}; + } + + if (WorkResponseCode == HttpResponseCode::NoContent || (HttpResponse.text.empty() && !Payload)) + { + return HttpClient::Response{.StatusCode = WorkResponseCode, + .Header = HttpClient::KeyValueMap(HttpResponse.header.begin(), HttpResponse.header.end()), + .UploadedBytes = gsl::narrow<int64_t>(HttpResponse.uploaded_bytes), + .DownloadedBytes = gsl::narrow<int64_t>(HttpResponse.downloaded_bytes), + .ElapsedSeconds = HttpResponse.elapsed}; + } + else + { + return ResponseWithPayload( + SessionId, + HttpResponse, + WorkResponseCode, + Payload ? std::move(Payload) : IoBufferBuilder::MakeCloneFromMemory(HttpResponse.text.data(), HttpResponse.text.size())); + } +} + +static bool +ShouldRetry(const cpr::Response& Response) +{ + switch (Response.error.code) + { + case cpr::ErrorCode::OK: + break; + case cpr::ErrorCode::INTERNAL_ERROR: + case cpr::ErrorCode::NETWORK_RECEIVE_ERROR: + case cpr::ErrorCode::NETWORK_SEND_FAILURE: + case cpr::ErrorCode::OPERATION_TIMEDOUT: + return true; + default: + return false; + } + switch ((HttpResponseCode)Response.status_code) + { + case HttpResponseCode::RequestTimeout: + case HttpResponseCode::TooManyRequests: + case HttpResponseCode::InternalServerError: + case HttpResponseCode::BadGateway: + case HttpResponseCode::ServiceUnavailable: + case HttpResponseCode::GatewayTimeout: + return true; + default: + return false; + } +}; + +static bool +ValidatePayload(cpr::Response& Response, std::unique_ptr<detail::TempPayloadFile>& PayloadFile) +{ + ZEN_TRACE_CPU("ValidatePayload"); + IoBuffer ResponseBuffer = (Response.text.empty() && PayloadFile) ? PayloadFile->BorrowIoBuffer() + : IoBuffer(IoBuffer::Wrap, Response.text.data(), Response.text.size()); + + if (auto ContentLength = Response.header.find("Content-Length"); ContentLength != Response.header.end()) + { + std::optional<uint64_t> ExpectedContentSize = ParseInt<uint64_t>(ContentLength->second); + if (!ExpectedContentSize.has_value()) + { + Response.error = + cpr::Error(/*CURLE_READ_ERROR*/ 26, fmt::format("Can not parse Content-Length header. Value: '{}'", ContentLength->second)); + return false; + } + if (ExpectedContentSize.value() != ResponseBuffer.GetSize()) + { + Response.error = cpr::Error( + /*CURLE_READ_ERROR*/ 26, + fmt::format("Payload size {} does not match Content-Length {}", ResponseBuffer.GetSize(), ContentLength->second)); + return false; + } + } + + if (Response.status_code == (long)HttpResponseCode::PartialContent) + { + return true; + } + + if (auto JupiterHash = Response.header.find("X-Jupiter-IoHash"); JupiterHash != Response.header.end()) + { + IoHash ExpectedPayloadHash; + if (IoHash::TryParse(JupiterHash->second, ExpectedPayloadHash)) + { + IoHash PayloadHash = IoHash::HashBuffer(ResponseBuffer); + if (PayloadHash != ExpectedPayloadHash) + { + Response.error = cpr::Error(/*CURLE_READ_ERROR*/ 26, + fmt::format("Payload hash {} does not match X-Jupiter-IoHash {}", + PayloadHash.ToHexString(), + ExpectedPayloadHash.ToHexString())); + return false; + } + } + } + + if (auto ContentType = Response.header.find("Content-Type"); ContentType != Response.header.end()) + { + if (ContentType->second == "application/x-ue-comp") + { + IoHash RawHash; + uint64_t RawSize; + if (CompressedBuffer::ValidateCompressedHeader(ResponseBuffer, RawHash, RawSize)) + { + return true; + } + else + { + Response.error = cpr::Error(/*CURLE_READ_ERROR*/ 26, "Compressed binary failed validation"); + return false; + } + } + if (ContentType->second == "application/x-ue-cb") + { + if (CbValidateError Error = ValidateCompactBinary(ResponseBuffer.GetView(), CbValidateMode::Default); + Error == CbValidateError::None) + { + return true; + } + else + { + Response.error = cpr::Error(/*CURLE_READ_ERROR*/ 26, fmt::format("Compact binary failed validation: {}", ToString(Error))); + return false; + } + } + } + + return true; +} + +static cpr::Response +DoWithRetry( + std::string_view SessionId, + std::function<cpr::Response()>&& Func, + uint8_t RetryCount, + std::function<bool(cpr::Response& Result)>&& Validate = [](cpr::Response&) { return true; }) +{ + uint8_t Attempt = 0; + cpr::Response Result = Func(); + while (Attempt < RetryCount) + { + if (!ShouldRetry(Result)) + { + if (Result.error || !IsHttpSuccessCode(Result.status_code)) + { + break; + } + if (Validate(Result)) + { + break; + } + } + Sleep(100 * (Attempt + 1)); + Attempt++; + ZEN_INFO("{} Attempt {}/{}", CommonResponse(SessionId, std::move(Result)).ErrorMessage("Retry"), Attempt, RetryCount + 1); + Result = Func(); + } + return Result; +} + +static cpr::Response +DoWithRetry(std::string_view SessionId, + std::function<cpr::Response()>&& Func, + std::unique_ptr<detail::TempPayloadFile>& PayloadFile, + uint8_t RetryCount) +{ + uint8_t Attempt = 0; + cpr::Response Result = Func(); + while (Attempt < RetryCount) + { + if (!ShouldRetry(Result)) + { + if (Result.error || !IsHttpSuccessCode(Result.status_code)) + { + break; + } + if (ValidatePayload(Result, PayloadFile)) + { + break; + } + } + Sleep(100 * (Attempt + 1)); + Attempt++; + ZEN_INFO("{} Attempt {}/{}", CommonResponse(SessionId, std::move(Result)).ErrorMessage("Retry"), Attempt, RetryCount + 1); + Result = Func(); + } + return Result; +} + +static std::pair<std::string, std::string> +HeaderContentType(ZenContentType ContentType) +{ + return std::make_pair("Content-Type", std::string(MapContentTypeToString(ContentType))); +} + +////////////////////////////////////////////////////////////////////////// + +CprHttpClient::CprHttpClient(std::string_view BaseUri, const HttpClientSettings& Connectionsettings) +: HttpClientBase(BaseUri, Connectionsettings) +{ +} + +CprHttpClient::~CprHttpClient() +{ + ZEN_TRACE_CPU("CprHttpClient::~CprHttpClient"); + m_SessionLock.WithExclusiveLock([&] { + for (auto CprSession : m_Sessions) + { + delete CprSession; + } + m_Sessions.clear(); + }); +} + +////////////////////////////////////////////////////////////////////////// + +CprHttpClient::Session +CprHttpClient::AllocSession(const std::string_view BaseUrl, + const std::string_view ResourcePath, + const HttpClientSettings& ConnectionSettings, + const KeyValueMap& AdditionalHeader, + const KeyValueMap& Parameters, + const std::string_view SessionId, + std::optional<HttpClientAccessToken> AccessToken) +{ + ZEN_TRACE_CPU("CprHttpClient::AllocSession"); + cpr::Session* CprSession = nullptr; + m_SessionLock.WithExclusiveLock([&] { + if (!m_Sessions.empty()) + { + CprSession = m_Sessions.back(); + m_Sessions.pop_back(); + } + }); + + if (CprSession == nullptr) + { + CprSession = new cpr::Session(); + CprSession->SetConnectTimeout(ConnectionSettings.ConnectTimeout); + CprSession->SetTimeout(ConnectionSettings.Timeout); + if (ConnectionSettings.AssumeHttp2) + { + CprSession->SetHttpVersion(cpr::HttpVersion{cpr::HttpVersionCode::VERSION_2_0_PRIOR_KNOWLEDGE}); + } + } + + if (!AdditionalHeader->empty()) + { + CprSession->SetHeader(cpr::Header(AdditionalHeader->begin(), AdditionalHeader->end())); + } + if (!SessionId.empty()) + { + CprSession->UpdateHeader({{"UE-Session", std::string(SessionId)}}); + } + if (AccessToken) + { + CprSession->UpdateHeader({{"Authorization", AccessToken->Value}}); + } + if (!Parameters->empty()) + { + cpr::Parameters Tmp; + for (auto It = Parameters->begin(); It != Parameters->end(); It++) + { + Tmp.Add({It->first, It->second}); + } + CprSession->SetParameters(Tmp); + } + else + { + CprSession->SetParameters({}); + } + + ExtendableStringBuilder<128> UrlBuffer; + UrlBuffer << BaseUrl << ResourcePath; + CprSession->SetUrl(UrlBuffer.c_str()); + + return Session(this, CprSession); +} + +void +CprHttpClient::ReleaseSession(cpr::Session* CprSession) +{ + ZEN_TRACE_CPU("CprHttpClient::ReleaseSession"); + CprSession->SetUrl({}); + CprSession->SetHeader({}); + CprSession->SetBody({}); + m_SessionLock.WithExclusiveLock([&] { m_Sessions.push_back(CprSession); }); +} + +CprHttpClient::Response +CprHttpClient::TransactPackage(std::string_view Url, CbPackage Package, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::TransactPackage"); + + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + + // First, list of offered chunks for filtering on the server end + + std::vector<IoHash> AttachmentsToSend; + std::span<const CbAttachment> Attachments = Package.GetAttachments(); + + const uint32_t RequestId = ++HttpClientRequestIdCounter; + auto RequestIdString = fmt::to_string(RequestId); + + if (Attachments.empty() == false) + { + CbObjectWriter Writer; + Writer.BeginArray("offer"); + + for (const CbAttachment& Attachment : Attachments) + { + Writer.AddHash(Attachment.GetHash()); + } + + Writer.EndArray(); + + BinaryWriter MemWriter; + Writer.Save(MemWriter); + + Sess->UpdateHeader({HeaderContentType(HttpContentType::kCbPackageOffer), {"UE-Request", RequestIdString}}); + Sess->SetBody(cpr::Body{(const char*)MemWriter.Data(), MemWriter.Size()}); + + cpr::Response FilterResponse = Sess.Post(); + + if (FilterResponse.status_code == 200) + { + IoBuffer ResponseBuffer(IoBuffer::Wrap, FilterResponse.text.data(), FilterResponse.text.size()); + CbValidateError ValidationError = CbValidateError::None; + if (CbObject ResponseObject = ValidateAndReadCompactBinaryObject(std::move(ResponseBuffer), ValidationError); + ValidationError == CbValidateError::None) + { + for (CbFieldView& Entry : ResponseObject["need"]) + { + ZEN_ASSERT(Entry.IsHash()); + AttachmentsToSend.push_back(Entry.AsHash()); + } + } + } + } + + // Prepare package for send + + CbPackage SendPackage; + SendPackage.SetObject(Package.GetObject(), Package.GetObjectHash()); + + for (const IoHash& AttachmentCid : AttachmentsToSend) + { + const CbAttachment* Attachment = Package.FindAttachment(AttachmentCid); + + if (Attachment) + { + SendPackage.AddAttachment(*Attachment); + } + else + { + // This should be an error -- server asked to have something we can't find + } + } + + // Transmit package payload + + CompositeBuffer Message = FormatPackageMessageBuffer(SendPackage); + SharedBuffer FlatMessage = Message.Flatten(); + + Sess->UpdateHeader({HeaderContentType(HttpContentType::kCbPackage), {"UE-Request", RequestIdString}}); + Sess->SetBody(cpr::Body{(const char*)FlatMessage.GetData(), FlatMessage.GetSize()}); + + cpr::Response FilterResponse = Sess.Post(); + + if (!IsHttpSuccessCode(FilterResponse.status_code)) + { + return {.StatusCode = HttpResponseCode(FilterResponse.status_code)}; + } + + IoBuffer ResponseBuffer(IoBuffer::Clone, FilterResponse.text.data(), FilterResponse.text.size()); + + if (auto It = FilterResponse.header.find("Content-Type"); It != FilterResponse.header.end()) + { + HttpContentType ContentType = ParseContentType(It->second); + + ResponseBuffer.SetContentType(ContentType); + } + + return {.StatusCode = HttpResponseCode(FilterResponse.status_code), .ResponsePayload = ResponseBuffer}; +} + +////////////////////////////////////////////////////////////////////////// +// +// Standard HTTP verbs +// + +CprHttpClient::Response +CprHttpClient::Put(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::Put"); + + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Sess->SetBody(AsCprBody(Payload)); + Sess->UpdateHeader({HeaderContentType(Payload.GetContentType())}); + return Sess.Put(); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Put(std::string_view Url, const KeyValueMap& Parameters) +{ + ZEN_TRACE_CPU("CprHttpClient::Put"); + + return CommonResponse(m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = AllocSession(m_BaseUri, + Url, + m_ConnectionSettings, + {{"Content-Length", "0"}}, + Parameters, + m_SessionId, + GetAccessToken()); + return Sess.Put(); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Get(std::string_view Url, const KeyValueMap& AdditionalHeader, const KeyValueMap& Parameters) +{ + ZEN_TRACE_CPU("CprHttpClient::Get"); + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = + AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, Parameters, m_SessionId, GetAccessToken()); + return Sess.Get(); + }, + m_ConnectionSettings.RetryCount, + [](cpr::Response& Result) { + std::unique_ptr<detail::TempPayloadFile> NoTempFile; + return ValidatePayload(Result, NoTempFile); + })); +} + +CprHttpClient::Response +CprHttpClient::Head(std::string_view Url, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::Head"); + + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + return Sess.Head(); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Delete(std::string_view Url, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::Delete"); + + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + return Sess.Delete(); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Post(std::string_view Url, const KeyValueMap& AdditionalHeader, const KeyValueMap& Parameters) +{ + ZEN_TRACE_CPU("CprHttpClient::PostNoPayload"); + + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = + AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, Parameters, m_SessionId, GetAccessToken()); + return Sess.Post(); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Post(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader) +{ + return Post(Url, Payload, Payload.GetContentType(), AdditionalHeader); +} + +CprHttpClient::Response +CprHttpClient::Post(std::string_view Url, const IoBuffer& Payload, ZenContentType ContentType, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::PostWithPayload"); + + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Sess->UpdateHeader({HeaderContentType(ContentType)}); + + IoBufferFileReference FileRef = {nullptr, 0, 0}; + if (Payload.GetFileReference(FileRef)) + { + uint64_t Offset = 0; + detail::BufferedReadFileStream Buffer(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize, 512u * 1024u); + auto ReadCallback = [&Payload, &Offset, &Buffer](char* buffer, size_t& size, intptr_t) { + size = Min<size_t>(size, Payload.GetSize() - Offset); + Buffer.Read(buffer, size); + Offset += size; + return true; + }; + return Sess.Post(cpr::ReadCallback(gsl::narrow<cpr::cpr_off_t>(Payload.GetSize()), ReadCallback)); + } + Sess->SetBody(AsCprBody(Payload)); + return Sess.Post(); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Post(std::string_view Url, CbObject Payload, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::PostObjectPayload"); + + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + + Sess->SetBody(AsCprBody(Payload)); + Sess->UpdateHeader({HeaderContentType(ZenContentType::kCbObject)}); + return Sess.Post(); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Post(std::string_view Url, CbPackage Pkg, const KeyValueMap& AdditionalHeader) +{ + return Post(Url, zen::FormatPackageMessageBuffer(Pkg), ZenContentType::kCbPackage, AdditionalHeader); +} + +CprHttpClient::Response +CprHttpClient::Post(std::string_view Url, const CompositeBuffer& Payload, ZenContentType ContentType, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::Post"); + + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Sess->UpdateHeader({HeaderContentType(ContentType)}); + + detail::CompositeBufferReadStream Reader(Payload, 512u * 1024u); + auto ReadCallback = [&Reader](char* buffer, size_t& size, intptr_t) { + size = Reader.Read(buffer, size); + return true; + }; + return Sess.Post(cpr::ReadCallback(gsl::narrow<cpr::cpr_off_t>(Payload.GetSize()), ReadCallback)); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Upload(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::Upload"); + + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Sess->UpdateHeader({HeaderContentType(Payload.GetContentType())}); + + IoBufferFileReference FileRef = {nullptr, 0, 0}; + if (Payload.GetFileReference(FileRef)) + { + uint64_t Offset = 0; + detail::BufferedReadFileStream Buffer(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize, 512u * 1024u); + auto ReadCallback = [&Payload, &Offset, &Buffer](char* buffer, size_t& size, intptr_t) { + size = Min<size_t>(size, Payload.GetSize() - Offset); + Buffer.Read(buffer, size); + Offset += size; + return true; + }; + return Sess.Put(cpr::ReadCallback(gsl::narrow<cpr::cpr_off_t>(Payload.GetSize()), ReadCallback)); + } + Sess->SetBody(AsCprBody(Payload)); + return Sess.Put(); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Upload(std::string_view Url, const CompositeBuffer& Payload, ZenContentType ContentType, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::Upload"); + + return CommonResponse( + m_SessionId, + DoWithRetry( + m_SessionId, + [&]() { + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Sess->UpdateHeader({HeaderContentType(ContentType)}); + + detail::CompositeBufferReadStream Reader(Payload, 512u * 1024u); + auto ReadCallback = [&Reader](char* buffer, size_t& size, intptr_t) { + size = Reader.Read(buffer, size); + return true; + }; + return Sess.Put(cpr::ReadCallback(gsl::narrow<cpr::cpr_off_t>(Payload.GetSize()), ReadCallback)); + }, + m_ConnectionSettings.RetryCount)); +} + +CprHttpClient::Response +CprHttpClient::Download(std::string_view Url, const std::filesystem::path& TempFolderPath, const KeyValueMap& AdditionalHeader) +{ + ZEN_TRACE_CPU("CprHttpClient::Download"); + + std::string PayloadString; + std::unique_ptr<detail::TempPayloadFile> PayloadFile; + cpr::Response Response = DoWithRetry( + m_SessionId, + [&]() { + auto GetHeader = [&](std::string header) -> std::pair<std::string, std::string> { + size_t DelimiterPos = header.find(':'); + if (DelimiterPos != std::string::npos) + { + std::string Key = header.substr(0, DelimiterPos); + constexpr AsciiSet WhitespaceCharacters(" \v\f\t\r\n"); + Key = AsciiSet::TrimSuffixWith(Key, WhitespaceCharacters); + Key = AsciiSet::TrimPrefixWith(Key, WhitespaceCharacters); + + std::string Value = header.substr(DelimiterPos + 1); + Value = AsciiSet::TrimSuffixWith(Value, WhitespaceCharacters); + Value = AsciiSet::TrimPrefixWith(Value, WhitespaceCharacters); + + return std::make_pair(Key, Value); + } + return std::make_pair(header, ""); + }; + + auto DownloadCallback = [&](std::string data, intptr_t) { + if (PayloadFile) + { + ZEN_ASSERT(PayloadString.empty()); + std::error_code Ec = PayloadFile->Write(data); + if (Ec) + { + ZEN_WARN("Failed to write to temp file in '{}' for HttpClient::Download. Reason: {}", + TempFolderPath.string(), + Ec.message()); + return false; + } + } + else + { + PayloadString.append(data); + } + return true; + }; + + uint64_t RequestedContentLength = (uint64_t)-1; + if (auto RangeIt = AdditionalHeader.Entries.find("Range"); RangeIt != AdditionalHeader.Entries.end()) + { + if (RangeIt->second.starts_with("bytes")) + { + size_t RangeStartPos = RangeIt->second.find('=', 5); + if (RangeStartPos != std::string::npos) + { + RangeStartPos++; + size_t RangeSplitPos = RangeIt->second.find('-', RangeStartPos); + if (RangeSplitPos != std::string::npos) + { + std::optional<size_t> RequestedRangeStart = + ParseInt<size_t>(RangeIt->second.substr(RangeStartPos, RangeSplitPos - RangeStartPos)); + std::optional<size_t> RequestedRangeEnd = ParseInt<size_t>(RangeIt->second.substr(RangeStartPos + 1)); + if (RequestedRangeStart.has_value() && RequestedRangeEnd.has_value()) + { + RequestedContentLength = RequestedRangeEnd.value() - 1; + } + } + } + } + } + + cpr::Response Response; + { + std::vector<std::pair<std::string, std::string>> ReceivedHeaders; + auto HeaderCallback = [&](std::string header, intptr_t) { + std::pair<std::string, std::string> Header = GetHeader(header); + if (Header.first == "Content-Length"sv) + { + std::optional<size_t> ContentLength = ParseInt<size_t>(Header.second); + if (ContentLength.has_value()) + { + if (ContentLength.value() > 1024 * 1024) + { + PayloadFile = std::make_unique<detail::TempPayloadFile>(); + std::error_code Ec = PayloadFile->Open(TempFolderPath, ContentLength.value()); + if (Ec) + { + ZEN_WARN("Failed to create temp file in '{}' for HttpClient::Download. Reason: {}", + TempFolderPath.string(), + Ec.message()); + PayloadFile.reset(); + } + } + else + { + PayloadString.reserve(ContentLength.value()); + } + } + } + if (!Header.first.empty()) + { + ReceivedHeaders.emplace_back(std::move(Header)); + } + return 1; + }; + + Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Response = Sess.Download(cpr::WriteCallback{DownloadCallback}, cpr::HeaderCallback{HeaderCallback}); + for (const std::pair<std::string, std::string>& H : ReceivedHeaders) + { + Response.header.insert_or_assign(H.first, H.second); + } + } + if (m_ConnectionSettings.AllowResume) + { + auto SupportsRanges = [](const cpr::Response& Response) -> bool { + if (Response.header.find("Content-Range") != Response.header.end()) + { + return true; + } + if (auto It = Response.header.find("Accept-Ranges"); It != Response.header.end()) + { + return It->second == "bytes"sv; + } + return false; + }; + + auto ShouldResume = [&SupportsRanges](const cpr::Response& Response) -> bool { + if (ShouldRetry(Response)) + { + return SupportsRanges(Response); + } + return false; + }; + + if (ShouldResume(Response)) + { + auto It = Response.header.find("Content-Length"); + if (It != Response.header.end()) + { + std::vector<std::pair<std::string, std::string>> ReceivedHeaders; + + auto HeaderCallback = [&](std::string header, intptr_t) { + std::pair<std::string, std::string> Header = GetHeader(header); + if (!Header.first.empty()) + { + ReceivedHeaders.emplace_back(std::move(Header)); + } + + if (Header.first == "Content-Range"sv) + { + if (Header.second.starts_with("bytes "sv)) + { + size_t RangeStartEnd = Header.second.find('-', 6); + if (RangeStartEnd != std::string::npos) + { + const auto Start = ParseInt<uint64_t>(Header.second.substr(6, RangeStartEnd - 6)); + if (Start) + { + uint64_t DownloadedSize = PayloadFile ? PayloadFile->GetSize() : PayloadString.length(); + if (Start.value() == DownloadedSize) + { + return 1; + } + else if (Start.value() > DownloadedSize) + { + return 0; + } + if (PayloadFile) + { + PayloadFile->ResetWritePos(Start.value()); + } + else + { + PayloadString = PayloadString.substr(0, Start.value()); + } + return 1; + } + } + } + return 0; + } + return 1; + }; + + KeyValueMap HeadersWithRange(AdditionalHeader); + do + { + uint64_t DownloadedSize = PayloadFile ? PayloadFile->GetSize() : PayloadString.length(); + + uint64_t ContentLength = RequestedContentLength; + if (ContentLength == uint64_t(-1)) + { + if (auto ParsedContentLength = ParseInt<int64_t>(It->second); ParsedContentLength.has_value()) + { + ContentLength = ParsedContentLength.value(); + } + } + + std::string Range = fmt::format("bytes={}-{}", DownloadedSize, DownloadedSize + ContentLength - 1); + if (auto RangeIt = HeadersWithRange.Entries.find("Range"); RangeIt != HeadersWithRange.Entries.end()) + { + if (RangeIt->second == Range) + { + // If we didn't make any progress, abort + break; + } + } + HeadersWithRange.Entries.insert_or_assign("Range", Range); + + Session Sess = + AllocSession(m_BaseUri, Url, m_ConnectionSettings, HeadersWithRange, {}, m_SessionId, GetAccessToken()); + Response = Sess.Download(cpr::WriteCallback{DownloadCallback}, cpr::HeaderCallback{HeaderCallback}); + for (const std::pair<std::string, std::string>& H : ReceivedHeaders) + { + Response.header.insert_or_assign(H.first, H.second); + } + ReceivedHeaders.clear(); + } while (ShouldResume(Response)); + } + } + } + + if (!PayloadString.empty()) + { + Response.text = std::move(PayloadString); + } + return Response; + }, + PayloadFile, + m_ConnectionSettings.RetryCount); + + return CommonResponse(m_SessionId, std::move(Response), PayloadFile ? PayloadFile->DetachToIoBuffer() : IoBuffer{}); +} + +} // namespace zen
\ No newline at end of file diff --git a/src/zenhttp/clients/httpclientcpr.h b/src/zenhttp/clients/httpclientcpr.h new file mode 100644 index 000000000..ed9d10c27 --- /dev/null +++ b/src/zenhttp/clients/httpclientcpr.h @@ -0,0 +1,151 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "httpclientcommon.h" + +#include <zencore/logging.h> +#include <zenhttp/cprutils.h> +#include <zenhttp/httpclient.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <cpr/body.h> +#include <cpr/session.h> +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen { + +class CprHttpClient : public HttpClientBase +{ +public: + CprHttpClient(std::string_view BaseUri, const HttpClientSettings& Connectionsettings = {}); + ~CprHttpClient(); + + // HttpClientBase + + [[nodiscard]] virtual Response Put(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {}) override; + [[nodiscard]] virtual Response Put(std::string_view Url, const KeyValueMap& Parameters = {}) override; + [[nodiscard]] virtual Response Get(std::string_view Url, + const KeyValueMap& AdditionalHeader = {}, + const KeyValueMap& Parameters = {}) override; + [[nodiscard]] virtual Response Head(std::string_view Url, const KeyValueMap& AdditionalHeader = {}) override; + [[nodiscard]] virtual Response Delete(std::string_view Url, const KeyValueMap& AdditionalHeader = {}) override; + [[nodiscard]] virtual Response Post(std::string_view Url, + const KeyValueMap& AdditionalHeader = {}, + const KeyValueMap& Parameters = {}) override; + [[nodiscard]] virtual Response Post(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {}) override; + [[nodiscard]] virtual Response Post(std::string_view Url, + const IoBuffer& Payload, + ZenContentType ContentType, + const KeyValueMap& AdditionalHeader = {}) override; + [[nodiscard]] virtual Response Post(std::string_view Url, CbObject Payload, const KeyValueMap& AdditionalHeader = {}) override; + [[nodiscard]] virtual Response Post(std::string_view Url, CbPackage Payload, const KeyValueMap& AdditionalHeader = {}) override; + [[nodiscard]] virtual Response Post(std::string_view Url, + const CompositeBuffer& Payload, + ZenContentType ContentType, + const KeyValueMap& AdditionalHeader = {}) override; + [[nodiscard]] virtual Response Upload(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {}) override; + [[nodiscard]] virtual Response Upload(std::string_view Url, + const CompositeBuffer& Payload, + ZenContentType ContentType, + const KeyValueMap& AdditionalHeader = {}) override; + + [[nodiscard]] virtual Response Download(std::string_view Url, + const std::filesystem::path& TempFolderPath, + const KeyValueMap& AdditionalHeader = {}) override; + + [[nodiscard]] virtual Response TransactPackage(std::string_view Url, + CbPackage Package, + const KeyValueMap& AdditionalHeader = {}) override; + +private: + struct Session + { + Session(CprHttpClient* InOuter, cpr::Session* InSession) : Outer(InOuter), CprSession(InSession) {} + ~Session() { Outer->ReleaseSession(CprSession); } + + inline cpr::Session* operator->() const { return CprSession; } + inline cpr::Response Get() + { + ZEN_TRACE_CPU("HttpClient::Impl::Get"); + cpr::Response Result = CprSession->Get(); + ZEN_TRACE("GET {}", Result); + return Result; + } + inline cpr::Response Download(cpr::WriteCallback&& Write, std::optional<cpr::HeaderCallback>&& Header = {}) + { + ZEN_TRACE_CPU("HttpClient::Impl::Download"); + if (Header) + { + CprSession->SetHeaderCallback(std::move(Header.value())); + } + cpr::Response Result = CprSession->Download(Write); + ZEN_TRACE("GET {}", Result); + CprSession->SetHeaderCallback({}); + CprSession->SetWriteCallback({}); + return Result; + } + inline cpr::Response Head() + { + ZEN_TRACE_CPU("HttpClient::Impl::Head"); + cpr::Response Result = CprSession->Head(); + ZEN_TRACE("HEAD {}", Result); + return Result; + } + inline cpr::Response Put(std::optional<cpr::ReadCallback>&& Read = {}) + { + ZEN_TRACE_CPU("HttpClient::Impl::Put"); + if (Read) + { + CprSession->SetReadCallback(std::move(Read.value())); + } + cpr::Response Result = CprSession->Put(); + ZEN_TRACE("PUT {}", Result); + CprSession->SetReadCallback({}); + return Result; + } + inline cpr::Response Post(std::optional<cpr::ReadCallback>&& Read = {}) + { + ZEN_TRACE_CPU("HttpClient::Impl::Post"); + if (Read) + { + CprSession->SetReadCallback(std::move(Read.value())); + } + cpr::Response Result = CprSession->Post(); + ZEN_TRACE("POST {}", Result); + CprSession->SetReadCallback({}); + return Result; + } + inline cpr::Response Delete() + { + ZEN_TRACE_CPU("HttpClient::Impl::Delete"); + cpr::Response Result = CprSession->Delete(); + ZEN_TRACE("DELETE {}", Result); + return Result; + } + + LoggerRef Log() { return Outer->Log(); } + + private: + CprHttpClient* Outer; + cpr::Session* CprSession; + + Session(Session&&) = delete; + Session& operator=(Session&&) = delete; + }; + + Session AllocSession(const std::string_view BaseUrl, + const std::string_view Url, + const HttpClientSettings& ConnectionSettings, + const KeyValueMap& AdditionalHeader, + const KeyValueMap& Parameters, + const std::string_view SessionId, + std::optional<HttpClientAccessToken> AccessToken); + + RwLock m_SessionLock; + std::vector<cpr::Session*> m_Sessions; + + void ReleaseSession(cpr::Session*); +}; + +} // namespace zen diff --git a/src/zenhttp/httpclient.cpp b/src/zenhttp/httpclient.cpp index 30a2bfc65..c5c808c23 100644 --- a/src/zenhttp/httpclient.cpp +++ b/src/zenhttp/httpclient.cpp @@ -5,8 +5,8 @@ #include <zenhttp/httpserver.h> #include <zenhttp/packageformat.h> -#include <zencore/compactbinarybuilder.h> #include <zencore/compactbinarypackage.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compositebuffer.h> #include <zencore/except.h> #include <zencore/filesystem.h> @@ -19,902 +19,44 @@ #include <zencore/string.h> #include <zencore/trace.h> +#include "clients/httpclientcommon.h" + #if ZEN_WITH_TESTS -# include <zencore/basicfile.h> # include <zencore/testing.h> # include <zencore/testutils.h> #endif // ZEN_WITH_TESTS -ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> -ZEN_THIRD_PARTY_INCLUDES_END - -#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC -# include <fcntl.h> -# include <sys/stat.h> -# include <unistd.h> -#endif - -static std::atomic<uint32_t> HttpClientRequestIdCounter{0}; - namespace zen { -using namespace std::literals; - -namespace detail { - - static std::atomic_uint32_t TempFileBaseIndex; - - class TempPayloadFile - { - public: - TempPayloadFile(const TempPayloadFile&) = delete; - TempPayloadFile& operator=(const TempPayloadFile&) = delete; - - TempPayloadFile() : m_FileHandle(nullptr), m_WriteOffset(0) {} - ~TempPayloadFile() - { - ZEN_TRACE_CPU("TempPayloadFile::Close"); - try - { - if (m_FileHandle) - { -#if ZEN_PLATFORM_WINDOWS - // Mark file for deletion when final handle is closed - FILE_DISPOSITION_INFO Fdi{.DeleteFile = TRUE}; - - SetFileInformationByHandle(m_FileHandle, FileDispositionInfo, &Fdi, sizeof Fdi); - BOOL Success = CloseHandle(m_FileHandle); -#else - std::error_code Ec; - std::filesystem::path FilePath = zen::PathFromHandle(m_FileHandle, Ec); - if (Ec) - { - ZEN_WARN("Error reported on get file path from handle {} for temp payload unlink operation, reason '{}'", - m_FileHandle, - Ec.message()); - } - else - { - unlink(FilePath.c_str()); - } - int Fd = int(uintptr_t(m_FileHandle)); - bool Success = (close(Fd) == 0); -#endif - if (!Success) - { - ZEN_WARN("Error reported on file handle close, reason '{}'", GetLastErrorAsString()); - } - - m_FileHandle = nullptr; - } - } - catch (const std::exception& Ex) - { - ZEN_ERROR("Failed deleting temp file {}. Reason '{}'", m_FileHandle, Ex.what()); - } - } - - std::error_code Open(const std::filesystem::path& TempFolderPath, uint64_t FinalSize) - { - ZEN_TRACE_CPU("TempPayloadFile::Open"); - ZEN_ASSERT(m_FileHandle == nullptr); - - std::uint64_t TmpIndex = ((std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()) & 0xffffffffu) << 32) | - detail::TempFileBaseIndex.fetch_add(1); - - std::filesystem::path FileName = TempFolderPath / fmt::to_string(TmpIndex); -#if ZEN_PLATFORM_WINDOWS - LPCWSTR lpFileName = FileName.c_str(); - const DWORD dwDesiredAccess = (GENERIC_READ | GENERIC_WRITE | DELETE); - const DWORD dwShareMode = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; - LPSECURITY_ATTRIBUTES lpSecurityAttributes = nullptr; - const DWORD dwCreationDisposition = CREATE_ALWAYS; - const DWORD dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL; - const HANDLE hTemplateFile = nullptr; - const HANDLE FileHandle = CreateFile(lpFileName, - dwDesiredAccess, - dwShareMode, - lpSecurityAttributes, - dwCreationDisposition, - dwFlagsAndAttributes, - hTemplateFile); - - if (FileHandle == INVALID_HANDLE_VALUE) - { - return MakeErrorCodeFromLastError(); - } -#else // ZEN_PLATFORM_WINDOWS - int OpenFlags = O_RDWR | O_CREAT | O_TRUNC | O_CLOEXEC; - int Fd = open(FileName.c_str(), OpenFlags, 0666); - if (Fd < 0) - { - return MakeErrorCodeFromLastError(); - } - fchmod(Fd, 0666); - - void* FileHandle = (void*)(uintptr_t(Fd)); -#endif // ZEN_PLATFORM_WINDOWS - m_FileHandle = FileHandle; - - PrepareFileForScatteredWrite(m_FileHandle, FinalSize); - - return {}; - } - - std::error_code Write(std::string_view DataString) - { - ZEN_TRACE_CPU("TempPayloadFile::Write"); - const uint8_t* DataPtr = (const uint8_t*)DataString.data(); - size_t DataSize = DataString.size(); - if (DataSize >= CacheBufferSize) - { - std::error_code Ec = Flush(); - if (Ec) - { - return Ec; - } - return AppendData(DataPtr, DataSize); - } - size_t CopySize = Min(DataSize, CacheBufferSize - m_CacheBufferOffset); - memcpy(&m_CacheBuffer[m_CacheBufferOffset], DataPtr, CopySize); - m_CacheBufferOffset += CopySize; - DataSize -= CopySize; - if (m_CacheBufferOffset == CacheBufferSize) - { - AppendData(m_CacheBuffer, CacheBufferSize); - if (DataSize > 0) - { - ZEN_ASSERT(DataSize < CacheBufferSize); - memcpy(m_CacheBuffer, DataPtr + CopySize, DataSize); - } - m_CacheBufferOffset = DataSize; - } - else - { - ZEN_ASSERT(DataSize == 0); - } - return {}; - } - - IoBuffer DetachToIoBuffer() - { - ZEN_TRACE_CPU("TempPayloadFile::DetachToIoBuffer"); - if (std::error_code Ec = Flush(); Ec) - { - ThrowSystemError(Ec.value(), Ec.message()); - } - ZEN_ASSERT(m_FileHandle != nullptr); - void* FileHandle = m_FileHandle; - IoBuffer Buffer(IoBuffer::File, FileHandle, 0, m_WriteOffset, /*IsWholeFile*/ true); - Buffer.SetDeleteOnClose(true); - m_FileHandle = 0; - m_WriteOffset = 0; - return Buffer; - } - - IoBuffer BorrowIoBuffer() - { - ZEN_TRACE_CPU("TempPayloadFile::BorrowIoBuffer"); - if (std::error_code Ec = Flush(); Ec) - { - ThrowSystemError(Ec.value(), Ec.message()); - } - ZEN_ASSERT(m_FileHandle != nullptr); - void* FileHandle = m_FileHandle; - IoBuffer Buffer(IoBuffer::BorrowedFile, FileHandle, 0, m_WriteOffset); - return Buffer; - } - - uint64_t GetSize() const { return m_WriteOffset; } - void ResetWritePos(uint64_t WriteOffset) - { - ZEN_TRACE_CPU("TempPayloadFile::ResetWritePos"); - Flush(); - m_WriteOffset = WriteOffset; - } - - private: - std::error_code Flush() - { - ZEN_TRACE_CPU("TempPayloadFile::Flush"); - if (m_CacheBufferOffset == 0) - { - return {}; - } - std::error_code Res = AppendData(m_CacheBuffer, m_CacheBufferOffset); - m_CacheBufferOffset = 0; - return Res; - } - - std::error_code AppendData(const void* Data, uint64_t Size) - { - ZEN_TRACE_CPU("TempPayloadFile::AppendData"); - ZEN_ASSERT(m_FileHandle != nullptr); - const uint64_t MaxChunkSize = 2u * 1024 * 1024 * 1024; - - while (Size) - { - const uint64_t NumberOfBytesToWrite = Min(Size, MaxChunkSize); - uint64_t NumberOfBytesWritten = 0; -#if ZEN_PLATFORM_WINDOWS - OVERLAPPED Ovl{}; - - Ovl.Offset = DWORD(m_WriteOffset & 0xffff'ffffu); - Ovl.OffsetHigh = DWORD(m_WriteOffset >> 32); - - DWORD dwNumberOfBytesWritten = 0; +extern HttpClientBase* CreateCprHttpClient(std::string_view BaseUri, const HttpClientSettings& ConnectionSettings); - BOOL Success = ::WriteFile(m_FileHandle, Data, DWORD(NumberOfBytesToWrite), &dwNumberOfBytesWritten, &Ovl); - if (Success) - { - NumberOfBytesWritten = static_cast<uint64_t>(dwNumberOfBytesWritten); - } -#else - static_assert(sizeof(off_t) >= sizeof(uint64_t), "sizeof(off_t) does not support large files"); - int Fd = int(uintptr_t(m_FileHandle)); - int BytesWritten = pwrite(Fd, Data, NumberOfBytesToWrite, m_WriteOffset); - bool Success = (BytesWritten > 0); - if (Success) - { - NumberOfBytesWritten = static_cast<uint64_t>(BytesWritten); - } -#endif - - if (!Success) - { - return MakeErrorCodeFromLastError(); - } - - Size -= NumberOfBytesWritten; - m_WriteOffset += NumberOfBytesWritten; - Data = reinterpret_cast<const uint8_t*>(Data) + NumberOfBytesWritten; - } - return {}; - } - - void* m_FileHandle; - std::uint64_t m_WriteOffset; - static constexpr uint64_t CacheBufferSize = 512u * 1024u; - uint8_t m_CacheBuffer[CacheBufferSize]; - std::uint64_t m_CacheBufferOffset = 0; - }; - - class BufferedReadFileStream - { - public: - BufferedReadFileStream(const BufferedReadFileStream&) = delete; - BufferedReadFileStream& operator=(const BufferedReadFileStream&) = delete; - - BufferedReadFileStream(void* FileHandle, uint64_t FileOffset, uint64_t FileSize, uint64_t BufferSize) - : m_FileHandle(FileHandle) - , m_FileSize(FileSize) - , m_FileEnd(FileOffset + FileSize) - , m_BufferSize(Min(BufferSize, FileSize)) - , m_FileOffset(FileOffset) - { - } - - ~BufferedReadFileStream() { Memory::Free(m_Buffer); } - void Read(void* Data, uint64_t Size) - { - ZEN_ASSERT(Data != nullptr); - if (Size > m_BufferSize) - { - Read(Data, Size, m_FileOffset); - m_FileOffset += Size; - return; - } - uint8_t* WritePtr = ((uint8_t*)Data); - uint64_t Begin = m_FileOffset; - uint64_t End = m_FileOffset + Size; - ZEN_ASSERT(m_FileOffset >= m_BufferStart); - if (m_FileOffset < m_BufferEnd) - { - ZEN_ASSERT(m_Buffer != nullptr); - uint64_t Count = Min(m_BufferEnd, End) - m_FileOffset; - memcpy(WritePtr + Begin - m_FileOffset, m_Buffer + Begin - m_BufferStart, Count); - Begin += Count; - if (Begin == End) - { - m_FileOffset = End; - return; - } - } - if (End == m_FileEnd) - { - Read(WritePtr + Begin - m_FileOffset, End - Begin, Begin); - } - else - { - if (!m_Buffer) - { - m_BufferSize = Min(m_FileEnd - m_FileOffset, m_BufferSize); - m_Buffer = (uint8_t*)Memory::Alloc(gsl::narrow<size_t>(m_BufferSize)); - } - m_BufferStart = Begin; - m_BufferEnd = Min(Begin + m_BufferSize, m_FileEnd); - Read(m_Buffer, m_BufferEnd - m_BufferStart, m_BufferStart); - uint64_t Count = Min(m_BufferEnd, End) - m_BufferStart; - memcpy(WritePtr + Begin - m_FileOffset, m_Buffer, Count); - ZEN_ASSERT(Begin + Count == End); - } - m_FileOffset = End; - } - - private: - void Read(void* Data, uint64_t BytesToRead, uint64_t FileOffset) - { - const uint64_t MaxChunkSize = 1u * 1024 * 1024; - std::error_code Ec; - ReadFile(m_FileHandle, Data, BytesToRead, FileOffset, MaxChunkSize, Ec); - - if (Ec) - { - std::error_code DummyEc; - throw std::system_error( - Ec, - fmt::format( - "HttpClient::BufferedReadFileStream ReadFile/pread failed (offset {:#x}, size {:#x}) file: '{}' (size {:#x})", - FileOffset, - BytesToRead, - PathFromHandle(m_FileHandle, DummyEc).generic_string(), - m_FileSize)); - } - } - - void* m_FileHandle = nullptr; - const uint64_t m_FileSize = 0; - const uint64_t m_FileEnd = 0; - uint64_t m_BufferSize = 0; - uint8_t* m_Buffer = nullptr; - uint64_t m_BufferStart = 0; - uint64_t m_BufferEnd = 0; - uint64_t m_FileOffset = 0; - }; - - class CompositeBufferReadStream - { - public: - CompositeBufferReadStream(const CompositeBuffer& Data, uint64_t BufferSize) - : m_Data(Data) - , m_BufferSize(BufferSize) - , m_SegmentIndex(0) - , m_BytesLeftInSegment(0) - { - } - uint64_t Read(void* Data, uint64_t Size) - { - uint64_t Result = 0; - uint8_t* WritePtr = (uint8_t*)Data; - while ((Size > 0) && (m_SegmentIndex < m_Data.GetSegments().size())) - { - if (m_BytesLeftInSegment == 0) - { - const SharedBuffer& Segment = m_Data.GetSegments()[m_SegmentIndex]; - IoBufferFileReference FileRef = {nullptr, 0, 0}; - if (Segment.AsIoBuffer().GetFileReference(FileRef)) - { - m_SegmentDiskBuffer = std::make_unique<BufferedReadFileStream>(FileRef.FileHandle, - FileRef.FileChunkOffset, - FileRef.FileChunkSize, - m_BufferSize); - } - else - { - m_SegmentMemoryBuffer = Segment.GetView(); - } - m_BytesLeftInSegment = Segment.GetSize(); - } - uint64_t BytesToRead = Min(m_BytesLeftInSegment, Size); - if (m_SegmentDiskBuffer) - { - m_SegmentDiskBuffer->Read(WritePtr, BytesToRead); - } - else - { - ZEN_ASSERT_SLOW(m_SegmentMemoryBuffer.GetSize() >= BytesToRead); - memcpy(WritePtr, m_SegmentMemoryBuffer.GetData(), BytesToRead); - m_SegmentMemoryBuffer.MidInline(BytesToRead); - } - WritePtr += BytesToRead; - Size -= BytesToRead; - Result += BytesToRead; - - m_BytesLeftInSegment -= BytesToRead; - if (m_BytesLeftInSegment == 0) - { - m_SegmentDiskBuffer.reset(); - m_SegmentMemoryBuffer.Reset(); - m_SegmentIndex++; - } - } - return Result; - } - - private: - const CompositeBuffer& m_Data; - const uint64_t m_BufferSize; - size_t m_SegmentIndex; - std::unique_ptr<BufferedReadFileStream> m_SegmentDiskBuffer; - MemoryView m_SegmentMemoryBuffer; - uint64_t m_BytesLeftInSegment; - }; - -} // namespace detail - -////////////////////////////////////////////////////////////////////////// -// -// CPR helpers - -static cpr::Body -AsCprBody(const CbObject& Obj) -{ - return cpr::Body((const char*)Obj.GetBuffer().GetData(), Obj.GetBuffer().GetSize()); -} - -static cpr::Body -AsCprBody(const IoBuffer& Obj) -{ - return cpr::Body((const char*)Obj.GetData(), Obj.GetSize()); -} - -////////////////////////////////////////////////////////////////////////// - -static HttpClient::Response -ResponseWithPayload(std::string_view SessionId, cpr::Response& HttpResponse, const HttpResponseCode WorkResponseCode, IoBuffer&& Payload) -{ - // This ends up doing a memcpy, would be good to get rid of it by streaming results - // into buffer directly - IoBuffer ResponseBuffer = Payload ? std::move(Payload) : IoBuffer(IoBuffer::Clone, HttpResponse.text.data(), HttpResponse.text.size()); - - if (auto It = HttpResponse.header.find("Content-Type"); It != HttpResponse.header.end()) - { - const HttpContentType ContentType = ParseContentType(It->second); - - ResponseBuffer.SetContentType(ContentType); - } - - if (!IsHttpSuccessCode(WorkResponseCode) && WorkResponseCode != HttpResponseCode::NotFound) - { - ZEN_WARN("HttpClient request failed (session: {}): {}", SessionId, HttpResponse); - } - - return HttpClient::Response{.StatusCode = WorkResponseCode, - .ResponsePayload = std::move(ResponseBuffer), - .Header = HttpClient::KeyValueMap(HttpResponse.header.begin(), HttpResponse.header.end()), - .UploadedBytes = gsl::narrow<int64_t>(HttpResponse.uploaded_bytes), - .DownloadedBytes = gsl::narrow<int64_t>(HttpResponse.downloaded_bytes), - .ElapsedSeconds = HttpResponse.elapsed}; -} - -static HttpClient::Response -CommonResponse(std::string_view SessionId, cpr::Response&& HttpResponse, IoBuffer&& Payload = {}) -{ - const HttpResponseCode WorkResponseCode = HttpResponseCode(HttpResponse.status_code); - if (HttpResponse.error) - { - if (HttpResponse.error.code != cpr::ErrorCode::OPERATION_TIMEDOUT && - HttpResponse.error.code != cpr::ErrorCode::CONNECTION_FAILURE && HttpResponse.error.code != cpr::ErrorCode::REQUEST_CANCELLED) - { - ZEN_WARN("HttpClient client failure (session: {}): {}", SessionId, HttpResponse); - } - - // Client side failure code - return HttpClient::Response{ - .StatusCode = WorkResponseCode, - .ResponsePayload = IoBufferBuilder::MakeCloneFromMemory(HttpResponse.text.data(), HttpResponse.text.size()), - .Header = HttpClient::KeyValueMap(HttpResponse.header.begin(), HttpResponse.header.end()), - .UploadedBytes = gsl::narrow<int64_t>(HttpResponse.uploaded_bytes), - .DownloadedBytes = gsl::narrow<int64_t>(HttpResponse.downloaded_bytes), - .ElapsedSeconds = HttpResponse.elapsed, - .Error = HttpClient::ErrorContext{.ErrorCode = gsl::narrow<int>(HttpResponse.error.code), - .ErrorMessage = HttpResponse.error.message}}; - } - - if (WorkResponseCode == HttpResponseCode::NoContent || (HttpResponse.text.empty() && !Payload)) - { - return HttpClient::Response{.StatusCode = WorkResponseCode, - .Header = HttpClient::KeyValueMap(HttpResponse.header.begin(), HttpResponse.header.end()), - .UploadedBytes = gsl::narrow<int64_t>(HttpResponse.uploaded_bytes), - .DownloadedBytes = gsl::narrow<int64_t>(HttpResponse.downloaded_bytes), - .ElapsedSeconds = HttpResponse.elapsed}; - } - else - { - return ResponseWithPayload( - SessionId, - HttpResponse, - WorkResponseCode, - Payload ? std::move(Payload) : IoBufferBuilder::MakeCloneFromMemory(HttpResponse.text.data(), HttpResponse.text.size())); - } -} - -static bool -ShouldRetry(const cpr::Response& Response) -{ - switch (Response.error.code) - { - case cpr::ErrorCode::OK: - break; - case cpr::ErrorCode::INTERNAL_ERROR: - case cpr::ErrorCode::NETWORK_RECEIVE_ERROR: - case cpr::ErrorCode::NETWORK_SEND_FAILURE: - case cpr::ErrorCode::OPERATION_TIMEDOUT: - return true; - default: - return false; - } - switch ((HttpResponseCode)Response.status_code) - { - case HttpResponseCode::RequestTimeout: - case HttpResponseCode::TooManyRequests: - case HttpResponseCode::InternalServerError: - case HttpResponseCode::BadGateway: - case HttpResponseCode::ServiceUnavailable: - case HttpResponseCode::GatewayTimeout: - return true; - default: - return false; - } -}; - -static bool -ValidatePayload(cpr::Response& Response, std::unique_ptr<detail::TempPayloadFile>& PayloadFile) -{ - ZEN_TRACE_CPU("ValidatePayload"); - IoBuffer ResponseBuffer = (Response.text.empty() && PayloadFile) ? PayloadFile->BorrowIoBuffer() - : IoBuffer(IoBuffer::Wrap, Response.text.data(), Response.text.size()); - - if (auto ContentLength = Response.header.find("Content-Length"); ContentLength != Response.header.end()) - { - std::optional<uint64_t> ExpectedContentSize = ParseInt<uint64_t>(ContentLength->second); - if (!ExpectedContentSize.has_value()) - { - Response.error = - cpr::Error(/*CURLE_READ_ERROR*/ 26, fmt::format("Can not parse Content-Length header. Value: '{}'", ContentLength->second)); - return false; - } - if (ExpectedContentSize.value() != ResponseBuffer.GetSize()) - { - Response.error = cpr::Error( - /*CURLE_READ_ERROR*/ 26, - fmt::format("Payload size {} does not match Content-Length {}", ResponseBuffer.GetSize(), ContentLength->second)); - return false; - } - } - - if (Response.status_code == (long)HttpResponseCode::PartialContent) - { - return true; - } - - if (auto JupiterHash = Response.header.find("X-Jupiter-IoHash"); JupiterHash != Response.header.end()) - { - IoHash ExpectedPayloadHash; - if (IoHash::TryParse(JupiterHash->second, ExpectedPayloadHash)) - { - IoHash PayloadHash = IoHash::HashBuffer(ResponseBuffer); - if (PayloadHash != ExpectedPayloadHash) - { - Response.error = cpr::Error(/*CURLE_READ_ERROR*/ 26, - fmt::format("Payload hash {} does not match X-Jupiter-IoHash {}", - PayloadHash.ToHexString(), - ExpectedPayloadHash.ToHexString())); - return false; - } - } - } - - if (auto ContentType = Response.header.find("Content-Type"); ContentType != Response.header.end()) - { - if (ContentType->second == "application/x-ue-comp") - { - IoHash RawHash; - uint64_t RawSize; - if (CompressedBuffer::ValidateCompressedHeader(ResponseBuffer, RawHash, RawSize)) - { - return true; - } - else - { - Response.error = cpr::Error(/*CURLE_READ_ERROR*/ 26, "Compressed binary failed validation"); - return false; - } - } - if (ContentType->second == "application/x-ue-cb") - { - if (CbValidateError Error = ValidateCompactBinary(ResponseBuffer.GetView(), CbValidateMode::Default); - Error == CbValidateError::None) - { - return true; - } - else - { - Response.error = cpr::Error(/*CURLE_READ_ERROR*/ 26, fmt::format("Compact binary failed validation: {}", ToString(Error))); - return false; - } - } - } - - return true; -} - -static cpr::Response -DoWithRetry( - std::string_view SessionId, - std::function<cpr::Response()>&& Func, - uint8_t RetryCount, - std::function<bool(cpr::Response& Result)>&& Validate = [](cpr::Response&) { return true; }) -{ - uint8_t Attempt = 0; - cpr::Response Result = Func(); - while (Attempt < RetryCount) - { - if (!ShouldRetry(Result)) - { - if (Result.error || !IsHttpSuccessCode(Result.status_code)) - { - break; - } - if (Validate(Result)) - { - break; - } - } - Sleep(100 * (Attempt + 1)); - Attempt++; - ZEN_INFO("{} Attempt {}/{}", CommonResponse(SessionId, std::move(Result)).ErrorMessage("Retry"), Attempt, RetryCount + 1); - Result = Func(); - } - return Result; -} - -static cpr::Response -DoWithRetry(std::string_view SessionId, - std::function<cpr::Response()>&& Func, - std::unique_ptr<detail::TempPayloadFile>& PayloadFile, - uint8_t RetryCount) -{ - uint8_t Attempt = 0; - cpr::Response Result = Func(); - while (Attempt < RetryCount) - { - if (!ShouldRetry(Result)) - { - if (Result.error || !IsHttpSuccessCode(Result.status_code)) - { - break; - } - if (ValidatePayload(Result, PayloadFile)) - { - break; - } - } - Sleep(100 * (Attempt + 1)); - Attempt++; - ZEN_INFO("{} Attempt {}/{}", CommonResponse(SessionId, std::move(Result)).ErrorMessage("Retry"), Attempt, RetryCount + 1); - Result = Func(); - } - return Result; -} - -static std::pair<std::string, std::string> -HeaderContentType(ZenContentType ContentType) -{ - return std::make_pair("Content-Type", std::string(MapContentTypeToString(ContentType))); -} +using namespace std::literals; ////////////////////////////////////////////////////////////////////////// -struct HttpClient::Impl : public RefCounted -{ - Impl(LoggerRef Log); - ~Impl(); - - // Session allocation - - struct Session - { - Session(Impl* InOuter, cpr::Session* InSession) : Outer(InOuter), CprSession(InSession) {} - ~Session() { Outer->ReleaseSession(CprSession); } - - inline cpr::Session* operator->() const { return CprSession; } - inline cpr::Response Get() - { - ZEN_TRACE_CPU("HttpClient::Impl::Get"); - cpr::Response Result = CprSession->Get(); - ZEN_TRACE("GET {}", Result); - return Result; - } - inline cpr::Response Download(cpr::WriteCallback&& Write, std::optional<cpr::HeaderCallback>&& Header = {}) - { - ZEN_TRACE_CPU("HttpClient::Impl::Download"); - if (Header) - { - CprSession->SetHeaderCallback(std::move(Header.value())); - } - cpr::Response Result = CprSession->Download(Write); - ZEN_TRACE("GET {}", Result); - CprSession->SetHeaderCallback({}); - CprSession->SetWriteCallback({}); - return Result; - } - inline cpr::Response Head() - { - ZEN_TRACE_CPU("HttpClient::Impl::Head"); - cpr::Response Result = CprSession->Head(); - ZEN_TRACE("HEAD {}", Result); - return Result; - } - inline cpr::Response Put(std::optional<cpr::ReadCallback>&& Read = {}) - { - ZEN_TRACE_CPU("HttpClient::Impl::Put"); - if (Read) - { - CprSession->SetReadCallback(std::move(Read.value())); - } - cpr::Response Result = CprSession->Put(); - ZEN_TRACE("PUT {}", Result); - CprSession->SetReadCallback({}); - return Result; - } - inline cpr::Response Post(std::optional<cpr::ReadCallback>&& Read = {}) - { - ZEN_TRACE_CPU("HttpClient::Impl::Post"); - if (Read) - { - CprSession->SetReadCallback(std::move(Read.value())); - } - cpr::Response Result = CprSession->Post(); - ZEN_TRACE("POST {}", Result); - CprSession->SetReadCallback({}); - return Result; - } - inline cpr::Response Delete() - { - ZEN_TRACE_CPU("HttpClient::Impl::Delete"); - cpr::Response Result = CprSession->Delete(); - ZEN_TRACE("DELETE {}", Result); - return Result; - } - - LoggerRef Logger() { return Outer->Logger(); } - - private: - Impl* Outer; - cpr::Session* CprSession; - - Session(Session&&) = delete; - Session& operator=(Session&&) = delete; - }; - - Session AllocSession(const std::string_view BaseUrl, - const std::string_view Url, - const HttpClientSettings& ConnectionSettings, - const KeyValueMap& AdditionalHeader, - const KeyValueMap& Parameters, - const std::string_view SessionId, - std::optional<HttpClientAccessToken> AccessToken); - - LoggerRef Logger() { return m_Log; } - -private: - LoggerRef m_Log; - RwLock m_SessionLock; - std::vector<cpr::Session*> m_Sessions; - - void ReleaseSession(cpr::Session*); -}; - -HttpClient::Impl::Impl(LoggerRef Log) : m_Log(Log) -{ -} - -HttpClient::Impl::~Impl() -{ - ZEN_TRACE_CPU("HttpClient::Impl::~Impl"); - m_SessionLock.WithExclusiveLock([&] { - for (auto CprSession : m_Sessions) - { - delete CprSession; - } - m_Sessions.clear(); - }); -} - -HttpClient::Impl::Session -HttpClient::Impl::AllocSession(const std::string_view BaseUrl, - const std::string_view ResourcePath, - const HttpClientSettings& ConnectionSettings, - const KeyValueMap& AdditionalHeader, - const KeyValueMap& Parameters, - const std::string_view SessionId, - std::optional<HttpClientAccessToken> AccessToken) +HttpClientBase::HttpClientBase(std::string_view BaseUri, const HttpClientSettings& ConnectionSettings) +: m_Log(zen::logging::Get(ConnectionSettings.LogCategory)) +, m_BaseUri(BaseUri) +, m_ConnectionSettings(ConnectionSettings) { - ZEN_TRACE_CPU("HttpClient::Impl::AllocSession"); - cpr::Session* CprSession = nullptr; - m_SessionLock.WithExclusiveLock([&] { - if (!m_Sessions.empty()) - { - CprSession = m_Sessions.back(); - m_Sessions.pop_back(); - } - }); - - if (CprSession == nullptr) - { - CprSession = new cpr::Session(); - CprSession->SetConnectTimeout(ConnectionSettings.ConnectTimeout); - CprSession->SetTimeout(ConnectionSettings.Timeout); - if (ConnectionSettings.AssumeHttp2) - { - CprSession->SetHttpVersion(cpr::HttpVersion{cpr::HttpVersionCode::VERSION_2_0_PRIOR_KNOWLEDGE}); - } - } - - if (!AdditionalHeader->empty()) - { - CprSession->SetHeader(cpr::Header(AdditionalHeader->begin(), AdditionalHeader->end())); - } - if (!SessionId.empty()) - { - CprSession->UpdateHeader({{"UE-Session", std::string(SessionId)}}); - } - if (AccessToken) - { - CprSession->UpdateHeader({{"Authorization", AccessToken->Value}}); - } - if (!Parameters->empty()) + if (ConnectionSettings.SessionId == Oid::Zero) { - cpr::Parameters Tmp; - for (auto It = Parameters->begin(); It != Parameters->end(); It++) - { - Tmp.Add({It->first, It->second}); - } - CprSession->SetParameters(Tmp); + m_SessionId = GetSessionIdString(); } else { - CprSession->SetParameters({}); + m_SessionId = ConnectionSettings.SessionId.ToString(); } - - ExtendableStringBuilder<128> UrlBuffer; - UrlBuffer << BaseUrl << ResourcePath; - CprSession->SetUrl(UrlBuffer.c_str()); - - return Session(this, CprSession); } -void -HttpClient::Impl::ReleaseSession(cpr::Session* CprSession) -{ - ZEN_TRACE_CPU("HttpClient::Impl::ReleaseSession"); - CprSession->SetUrl({}); - CprSession->SetHeader({}); - CprSession->SetBody({}); - m_SessionLock.WithExclusiveLock([&] { m_Sessions.push_back(CprSession); }); -} - -////////////////////////////////////////////////////////////////////////// - -HttpClient::HttpClient(std::string_view BaseUri, const HttpClientSettings& Connectionsettings) -: m_Log(zen::logging::Get(Connectionsettings.LogCategory)) -, m_BaseUri(BaseUri) -, m_ConnectionSettings(Connectionsettings) -, m_Impl(new Impl(m_Log)) -{ - m_SessionId = GetSessionIdString(); -} - -HttpClient::~HttpClient() +HttpClientBase::~HttpClientBase() { } bool -HttpClient::Authenticate() +HttpClientBase::Authenticate() { - ZEN_TRACE_CPU("HttpClient::Authenticate"); + ZEN_TRACE_CPU("HttpClientBase::Authenticate"); std::optional<HttpClientAccessToken> Token = GetAccessToken(); if (!Token) { @@ -924,9 +66,9 @@ HttpClient::Authenticate() } const std::optional<HttpClientAccessToken> -HttpClient::GetAccessToken() +HttpClientBase::GetAccessToken() { - ZEN_TRACE_CPU("HttpClient::GetAccessToken"); + ZEN_TRACE_CPU("HttpClientBase::GetAccessToken"); if (!m_ConnectionSettings.AccessTokenProvider.has_value()) { return {}; @@ -947,614 +89,19 @@ HttpClient::GetAccessToken() return m_CachedAccessToken; } -HttpClient::Response -HttpClient::TransactPackage(std::string_view Url, CbPackage Package, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::TransactPackage"); - - Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - - // First, list of offered chunks for filtering on the server end - - std::vector<IoHash> AttachmentsToSend; - std::span<const CbAttachment> Attachments = Package.GetAttachments(); - - const uint32_t RequestId = ++HttpClientRequestIdCounter; - auto RequestIdString = fmt::to_string(RequestId); - - if (Attachments.empty() == false) - { - CbObjectWriter Writer; - Writer.BeginArray("offer"); - - for (const CbAttachment& Attachment : Attachments) - { - Writer.AddHash(Attachment.GetHash()); - } - - Writer.EndArray(); - - BinaryWriter MemWriter; - Writer.Save(MemWriter); - - Sess->UpdateHeader({HeaderContentType(HttpContentType::kCbPackageOffer), {"UE-Request", RequestIdString}}); - Sess->SetBody(cpr::Body{(const char*)MemWriter.Data(), MemWriter.Size()}); - - cpr::Response FilterResponse = Sess.Post(); - - if (FilterResponse.status_code == 200) - { - IoBuffer ResponseBuffer(IoBuffer::Wrap, FilterResponse.text.data(), FilterResponse.text.size()); - CbObject ResponseObject = LoadCompactBinaryObject(ResponseBuffer); - - for (CbFieldView& Entry : ResponseObject["need"]) - { - ZEN_ASSERT(Entry.IsHash()); - AttachmentsToSend.push_back(Entry.AsHash()); - } - } - } - - // Prepare package for send - - CbPackage SendPackage; - SendPackage.SetObject(Package.GetObject(), Package.GetObjectHash()); - - for (const IoHash& AttachmentCid : AttachmentsToSend) - { - const CbAttachment* Attachment = Package.FindAttachment(AttachmentCid); - - if (Attachment) - { - SendPackage.AddAttachment(*Attachment); - } - else - { - // This should be an error -- server asked to have something we can't find - } - } - - // Transmit package payload - - CompositeBuffer Message = FormatPackageMessageBuffer(SendPackage); - SharedBuffer FlatMessage = Message.Flatten(); - - Sess->UpdateHeader({HeaderContentType(HttpContentType::kCbPackage), {"UE-Request", RequestIdString}}); - Sess->SetBody(cpr::Body{(const char*)FlatMessage.GetData(), FlatMessage.GetSize()}); - - cpr::Response FilterResponse = Sess.Post(); - - if (!IsHttpSuccessCode(FilterResponse.status_code)) - { - return {.StatusCode = HttpResponseCode(FilterResponse.status_code)}; - } - - IoBuffer ResponseBuffer(IoBuffer::Clone, FilterResponse.text.data(), FilterResponse.text.size()); - - if (auto It = FilterResponse.header.find("Content-Type"); It != FilterResponse.header.end()) - { - HttpContentType ContentType = ParseContentType(It->second); - - ResponseBuffer.SetContentType(ContentType); - } - - return {.StatusCode = HttpResponseCode(FilterResponse.status_code), .ResponsePayload = ResponseBuffer}; -} - -////////////////////////////////////////////////////////////////////////// -// -// Standard HTTP verbs -// - -HttpClient::Response -HttpClient::Put(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::Put"); - - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - Sess->SetBody(AsCprBody(Payload)); - Sess->UpdateHeader({HeaderContentType(Payload.GetContentType())}); - return Sess.Put(); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Put(std::string_view Url, const KeyValueMap& Parameters) -{ - ZEN_TRACE_CPU("HttpClient::Put"); - - return CommonResponse(m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, - Url, - m_ConnectionSettings, - {{"Content-Length", "0"}}, - Parameters, - m_SessionId, - GetAccessToken()); - return Sess.Put(); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Get(std::string_view Url, const KeyValueMap& AdditionalHeader, const KeyValueMap& Parameters) -{ - ZEN_TRACE_CPU("HttpClient::Get"); - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, Parameters, m_SessionId, GetAccessToken()); - return Sess.Get(); - }, - m_ConnectionSettings.RetryCount, - [](cpr::Response& Result) { - std::unique_ptr<detail::TempPayloadFile> NoTempFile; - return ValidatePayload(Result, NoTempFile); - })); -} - -HttpClient::Response -HttpClient::Head(std::string_view Url, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::Head"); - - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - return Sess.Head(); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Delete(std::string_view Url, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::Delete"); - - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - return Sess.Delete(); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Post(std::string_view Url, const KeyValueMap& AdditionalHeader, const KeyValueMap& Parameters) -{ - ZEN_TRACE_CPU("HttpClient::PostNoPayload"); - - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, Parameters, m_SessionId, GetAccessToken()); - return Sess.Post(); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Post(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader) -{ - return Post(Url, Payload, Payload.GetContentType(), AdditionalHeader); -} - -HttpClient::Response -HttpClient::Post(std::string_view Url, const IoBuffer& Payload, ZenContentType ContentType, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::PostWithPayload"); - - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - Sess->UpdateHeader({HeaderContentType(ContentType)}); - - IoBufferFileReference FileRef = {nullptr, 0, 0}; - if (Payload.GetFileReference(FileRef)) - { - uint64_t Offset = 0; - detail::BufferedReadFileStream Buffer(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize, 512u * 1024u); - auto ReadCallback = [&Payload, &Offset, &Buffer](char* buffer, size_t& size, intptr_t) { - size = Min<size_t>(size, Payload.GetSize() - Offset); - Buffer.Read(buffer, size); - Offset += size; - return true; - }; - return Sess.Post(cpr::ReadCallback(gsl::narrow<cpr::cpr_off_t>(Payload.GetSize()), ReadCallback)); - } - Sess->SetBody(AsCprBody(Payload)); - return Sess.Post(); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Post(std::string_view Url, CbObject Payload, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::PostObjectPayload"); - - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - - Sess->SetBody(AsCprBody(Payload)); - Sess->UpdateHeader({HeaderContentType(ZenContentType::kCbObject)}); - return Sess.Post(); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Post(std::string_view Url, CbPackage Pkg, const KeyValueMap& AdditionalHeader) -{ - return Post(Url, zen::FormatPackageMessageBuffer(Pkg), ZenContentType::kCbPackage, AdditionalHeader); -} - -HttpClient::Response -HttpClient::Post(std::string_view Url, const CompositeBuffer& Payload, ZenContentType ContentType, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::Post"); - - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - Sess->UpdateHeader({HeaderContentType(ContentType)}); - - detail::CompositeBufferReadStream Reader(Payload, 512u * 1024u); - auto ReadCallback = [&Reader](char* buffer, size_t& size, intptr_t) { - size = Reader.Read(buffer, size); - return true; - }; - return Sess.Post(cpr::ReadCallback(gsl::narrow<cpr::cpr_off_t>(Payload.GetSize()), ReadCallback)); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Upload(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::Upload"); - - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - Sess->UpdateHeader({HeaderContentType(Payload.GetContentType())}); - - IoBufferFileReference FileRef = {nullptr, 0, 0}; - if (Payload.GetFileReference(FileRef)) - { - uint64_t Offset = 0; - detail::BufferedReadFileStream Buffer(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize, 512u * 1024u); - auto ReadCallback = [&Payload, &Offset, &Buffer](char* buffer, size_t& size, intptr_t) { - size = Min<size_t>(size, Payload.GetSize() - Offset); - Buffer.Read(buffer, size); - Offset += size; - return true; - }; - return Sess.Put(cpr::ReadCallback(gsl::narrow<cpr::cpr_off_t>(Payload.GetSize()), ReadCallback)); - } - Sess->SetBody(AsCprBody(Payload)); - return Sess.Put(); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Upload(std::string_view Url, const CompositeBuffer& Payload, ZenContentType ContentType, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::Upload"); - - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() { - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - Sess->UpdateHeader({HeaderContentType(ContentType)}); - - detail::CompositeBufferReadStream Reader(Payload, 512u * 1024u); - auto ReadCallback = [&Reader](char* buffer, size_t& size, intptr_t) { - size = Reader.Read(buffer, size); - return true; - }; - return Sess.Put(cpr::ReadCallback(gsl::narrow<cpr::cpr_off_t>(Payload.GetSize()), ReadCallback)); - }, - m_ConnectionSettings.RetryCount)); -} - -HttpClient::Response -HttpClient::Download(std::string_view Url, const std::filesystem::path& TempFolderPath, const KeyValueMap& AdditionalHeader) -{ - ZEN_TRACE_CPU("HttpClient::Download"); - - std::string PayloadString; - std::unique_ptr<detail::TempPayloadFile> PayloadFile; - cpr::Response Response = DoWithRetry( - m_SessionId, - [&]() { - auto GetHeader = [&](std::string header) -> std::pair<std::string, std::string> { - size_t DelimiterPos = header.find(':'); - if (DelimiterPos != std::string::npos) - { - std::string Key = header.substr(0, DelimiterPos); - constexpr AsciiSet WhitespaceCharacters(" \v\f\t\r\n"); - Key = AsciiSet::TrimSuffixWith(Key, WhitespaceCharacters); - Key = AsciiSet::TrimPrefixWith(Key, WhitespaceCharacters); - - std::string Value = header.substr(DelimiterPos + 1); - Value = AsciiSet::TrimSuffixWith(Value, WhitespaceCharacters); - Value = AsciiSet::TrimPrefixWith(Value, WhitespaceCharacters); - - return std::make_pair(Key, Value); - } - return std::make_pair(header, ""); - }; - - auto DownloadCallback = [&](std::string data, intptr_t) { - if (PayloadFile) - { - ZEN_ASSERT(PayloadString.empty()); - std::error_code Ec = PayloadFile->Write(data); - if (Ec) - { - ZEN_WARN("Failed to write to temp file in '{}' for HttpClient::Download. Reason: {}", - TempFolderPath.string(), - Ec.message()); - return false; - } - } - else - { - PayloadString.append(data); - } - return true; - }; - - uint64_t RequestedContentLength = (uint64_t)-1; - if (auto RangeIt = AdditionalHeader.Entries.find("Range"); RangeIt != AdditionalHeader.Entries.end()) - { - if (RangeIt->second.starts_with("bytes")) - { - size_t RangeStartPos = RangeIt->second.find('=', 5); - if (RangeStartPos != std::string::npos) - { - RangeStartPos++; - size_t RangeSplitPos = RangeIt->second.find('-', RangeStartPos); - if (RangeSplitPos != std::string::npos) - { - std::optional<size_t> RequestedRangeStart = - ParseInt<size_t>(RangeIt->second.substr(RangeStartPos, RangeSplitPos - RangeStartPos)); - std::optional<size_t> RequestedRangeEnd = ParseInt<size_t>(RangeIt->second.substr(RangeStartPos + 1)); - if (RequestedRangeStart.has_value() && RequestedRangeEnd.has_value()) - { - RequestedContentLength = RequestedRangeEnd.value() - 1; - } - } - } - } - } - - cpr::Response Response; - { - std::vector<std::pair<std::string, std::string>> ReceivedHeaders; - auto HeaderCallback = [&](std::string header, intptr_t) { - std::pair<std::string, std::string> Header = GetHeader(header); - if (Header.first == "Content-Length"sv) - { - std::optional<size_t> ContentLength = ParseInt<size_t>(Header.second); - if (ContentLength.has_value()) - { - if (ContentLength.value() > 1024 * 1024) - { - PayloadFile = std::make_unique<detail::TempPayloadFile>(); - std::error_code Ec = PayloadFile->Open(TempFolderPath, ContentLength.value()); - if (Ec) - { - ZEN_WARN("Failed to create temp file in '{}' for HttpClient::Download. Reason: {}", - TempFolderPath.string(), - Ec.message()); - PayloadFile.reset(); - } - } - else - { - PayloadString.reserve(ContentLength.value()); - } - } - } - if (!Header.first.empty()) - { - ReceivedHeaders.emplace_back(std::move(Header)); - } - return 1; - }; - - Impl::Session Sess = - m_Impl->AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - Response = Sess.Download(cpr::WriteCallback{DownloadCallback}, cpr::HeaderCallback{HeaderCallback}); - for (const std::pair<std::string, std::string>& H : ReceivedHeaders) - { - Response.header.insert_or_assign(H.first, H.second); - } - } - if (m_ConnectionSettings.AllowResume) - { - auto SupportsRanges = [](const cpr::Response& Response) -> bool { - if (Response.header.find("Content-Range") != Response.header.end()) - { - return true; - } - if (auto It = Response.header.find("Accept-Ranges"); It != Response.header.end()) - { - return It->second == "bytes"sv; - } - return false; - }; - - auto ShouldResume = [&SupportsRanges](const cpr::Response& Response) -> bool { - if (ShouldRetry(Response)) - { - return SupportsRanges(Response); - } - return false; - }; - - if (ShouldResume(Response)) - { - auto It = Response.header.find("Content-Length"); - if (It != Response.header.end()) - { - std::vector<std::pair<std::string, std::string>> ReceivedHeaders; - - auto HeaderCallback = [&](std::string header, intptr_t) { - std::pair<std::string, std::string> Header = GetHeader(header); - if (!Header.first.empty()) - { - ReceivedHeaders.emplace_back(std::move(Header)); - } - - if (Header.first == "Content-Range"sv) - { - if (Header.second.starts_with("bytes "sv)) - { - size_t RangeStartEnd = Header.second.find('-', 6); - if (RangeStartEnd != std::string::npos) - { - const auto Start = ParseInt<uint64_t>(Header.second.substr(6, RangeStartEnd - 6)); - if (Start) - { - uint64_t DownloadedSize = PayloadFile ? PayloadFile->GetSize() : PayloadString.length(); - if (Start.value() == DownloadedSize) - { - return 1; - } - else if (Start.value() > DownloadedSize) - { - return 0; - } - if (PayloadFile) - { - PayloadFile->ResetWritePos(Start.value()); - } - else - { - PayloadString = PayloadString.substr(0, Start.value()); - } - return 1; - } - } - } - return 0; - } - return 1; - }; - - KeyValueMap HeadersWithRange(AdditionalHeader); - do - { - uint64_t DownloadedSize = PayloadFile ? PayloadFile->GetSize() : PayloadString.length(); - - uint64_t ContentLength = RequestedContentLength; - if (ContentLength == uint64_t(-1)) - { - if (auto ParsedContentLength = ParseInt<int64_t>(It->second); ParsedContentLength.has_value()) - { - ContentLength = ParsedContentLength.value(); - } - } - - std::string Range = fmt::format("bytes={}-{}", DownloadedSize, DownloadedSize + ContentLength - 1); - if (auto RangeIt = HeadersWithRange.Entries.find("Range"); RangeIt != HeadersWithRange.Entries.end()) - { - if (RangeIt->second == Range) - { - // If we didn't make any progress, abort - break; - } - } - HeadersWithRange.Entries.insert_or_assign("Range", Range); - - Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, - Url, - m_ConnectionSettings, - HeadersWithRange, - {}, - m_SessionId, - GetAccessToken()); - Response = Sess.Download(cpr::WriteCallback{DownloadCallback}, cpr::HeaderCallback{HeaderCallback}); - for (const std::pair<std::string, std::string>& H : ReceivedHeaders) - { - Response.header.insert_or_assign(H.first, H.second); - } - ReceivedHeaders.clear(); - } while (ShouldResume(Response)); - } - } - } - - if (!PayloadString.empty()) - { - Response.text = std::move(PayloadString); - } - return Response; - }, - PayloadFile, - m_ConnectionSettings.RetryCount); - - return CommonResponse(m_SessionId, std::move(Response), PayloadFile ? PayloadFile->DetachToIoBuffer() : IoBuffer{}); -} - ////////////////////////////////////////////////////////////////////////// CbObject HttpClient::Response::AsObject() const { - // TODO: sanity check the payload format etc - if (ResponsePayload) { - return LoadCompactBinaryObject(ResponsePayload); + CbValidateError ValidationError = CbValidateError::None; + if (CbObject ResponseObject = ValidateAndReadCompactBinaryObject(IoBuffer(ResponsePayload), ValidationError); + ValidationError == CbValidateError::None) + { + return ResponseObject; + } } return {}; @@ -1653,41 +200,138 @@ HttpClient::Response::ThrowError(std::string_view ErrorPrefix) ////////////////////////////////////////////////////////////////////////// -#if ZEN_WITH_TESTS +HttpClient::HttpClient(std::string_view BaseUri, const HttpClientSettings& ConnectionSettings) +: m_BaseUri(BaseUri) +, m_ConnectionSettings(ConnectionSettings) +{ + m_SessionId = GetSessionIdString(); -namespace testutil { - IoHash HashComposite(const CompositeBuffer& Payload) - { - IoHashStream Hasher; - const uint64_t PayloadSize = Payload.GetSize(); - std::vector<uint8_t> Buffer(64u * 1024u); - detail::CompositeBufferReadStream Stream(Payload, 137u * 1024u); - for (uint64_t Offset = 0; Offset < PayloadSize;) - { - uint64_t Count = Min(64u * 1024u, PayloadSize - Offset); - Stream.Read(Buffer.data(), Count); - Hasher.Append(Buffer.data(), Count); - Offset += Count; - } - return Hasher.GetHash(); - }; + m_Inner = CreateCprHttpClient(BaseUri, ConnectionSettings); +} - IoHash HashFileStream(void* FileHandle, uint64_t FileOffset, uint64_t FileSize) +HttpClient::~HttpClient() +{ + delete m_Inner; +} + +void +HttpClient::SetSessionId(const Oid& SessionId) +{ + if (SessionId == Oid::Zero) { - IoHashStream Hasher; - std::vector<uint8_t> Buffer(64u * 1024u); - detail::BufferedReadFileStream Stream(FileHandle, FileOffset, FileSize, 137u * 1024u); - for (uint64_t Offset = 0; Offset < FileSize;) - { - uint64_t Count = Min(64u * 1024u, FileSize - Offset); - Stream.Read(Buffer.data(), Count); - Hasher.Append(Buffer.data(), Count); - Offset += Count; - } - return Hasher.GetHash(); + m_SessionId = GetSessionIdString(); + } + else + { + m_SessionId = SessionId.ToString(); } +} -} // namespace testutil +HttpClient::Response +HttpClient::Put(std::string_view Url, const IoBuffer& Payload, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Put(Url, Payload, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Put(std::string_view Url, const HttpClient::KeyValueMap& Parameters) +{ + return m_Inner->Put(Url, Parameters); +} + +HttpClient::Response +HttpClient::Get(std::string_view Url, const HttpClient::KeyValueMap& AdditionalHeader, const HttpClient::KeyValueMap& Parameters) +{ + return m_Inner->Get(Url, AdditionalHeader, Parameters); +} + +HttpClient::Response +HttpClient::Head(std::string_view Url, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Head(Url, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Delete(std::string_view Url, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Delete(Url, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Post(std::string_view Url, const HttpClient::KeyValueMap& AdditionalHeader, const HttpClient::KeyValueMap& Parameters) +{ + return m_Inner->Post(Url, AdditionalHeader, Parameters); +} + +HttpClient::Response +HttpClient::Post(std::string_view Url, const IoBuffer& Payload, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Post(Url, Payload, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Post(std::string_view Url, const IoBuffer& Payload, ZenContentType ContentType, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Post(Url, Payload, ContentType, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Post(std::string_view Url, CbObject Payload, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Post(Url, Payload, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Post(std::string_view Url, CbPackage Payload, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Post(Url, Payload, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Post(std::string_view Url, + const CompositeBuffer& Payload, + ZenContentType ContentType, + const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Post(Url, Payload, ContentType, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Upload(std::string_view Url, const IoBuffer& Payload, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Upload(Url, Payload, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Upload(std::string_view Url, + const CompositeBuffer& Payload, + ZenContentType ContentType, + const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Upload(Url, Payload, ContentType, AdditionalHeader); +} + +HttpClient::Response +HttpClient::Download(std::string_view Url, const std::filesystem::path& TempFolderPath, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->Download(Url, TempFolderPath, AdditionalHeader); +} + +HttpClient::Response +HttpClient::TransactPackage(std::string_view Url, CbPackage Package, const HttpClient::KeyValueMap& AdditionalHeader) +{ + return m_Inner->TransactPackage(Url, Package, AdditionalHeader); +} + +bool +HttpClient::Authenticate() +{ + return m_Inner->Authenticate(); +} + +////////////////////////////////////////////////////////////////////////// + +#if ZEN_WITH_TESTS TEST_CASE("responseformat") { @@ -1735,53 +379,6 @@ TEST_CASE("responseformat") } } -TEST_CASE("BufferedReadFileStream") -{ - ScopedTemporaryDirectory TmpDir; - - IoBuffer DiskBuffer = WriteToTempFile(CompositeBuffer(CreateRandomBlob(496 * 5 * 1024)), TmpDir.Path() / "diskbuffer1"); - - IoBufferFileReference FileRef = {nullptr, 0, 0}; - CHECK(DiskBuffer.GetFileReference(FileRef)); - CHECK_EQ(IoHash::HashBuffer(DiskBuffer), testutil::HashFileStream(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize)); - - IoBuffer Partial(DiskBuffer, 37 * 1024, 512 * 1024); - CHECK(Partial.GetFileReference(FileRef)); - CHECK_EQ(IoHash::HashBuffer(Partial), testutil::HashFileStream(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize)); - - IoBuffer SmallDiskBuffer = WriteToTempFile(CompositeBuffer(CreateRandomBlob(63 * 1024)), TmpDir.Path() / "diskbuffer2"); - CHECK(SmallDiskBuffer.GetFileReference(FileRef)); - CHECK_EQ(IoHash::HashBuffer(SmallDiskBuffer), - testutil::HashFileStream(FileRef.FileHandle, FileRef.FileChunkOffset, FileRef.FileChunkSize)); -} - -TEST_CASE("CompositeBufferReadStream") -{ - ScopedTemporaryDirectory TmpDir; - - IoBuffer MemoryBuffer1 = CreateRandomBlob(64); - CHECK_EQ(IoHash::HashBuffer(MemoryBuffer1), testutil::HashComposite(CompositeBuffer(SharedBuffer(MemoryBuffer1)))); - - IoBuffer MemoryBuffer2 = CreateRandomBlob(561 * 1024); - CHECK_EQ(IoHash::HashBuffer(MemoryBuffer2), testutil::HashComposite(CompositeBuffer(SharedBuffer(MemoryBuffer2)))); - - IoBuffer DiskBuffer1 = WriteToTempFile(CompositeBuffer(CreateRandomBlob(267 * 3 * 1024)), TmpDir.Path() / "diskbuffer1"); - CHECK_EQ(IoHash::HashBuffer(DiskBuffer1), testutil::HashComposite(CompositeBuffer(SharedBuffer(DiskBuffer1)))); - - IoBuffer DiskBuffer2 = WriteToTempFile(CompositeBuffer(CreateRandomBlob(3 * 1024)), TmpDir.Path() / "diskbuffer2"); - CHECK_EQ(IoHash::HashBuffer(DiskBuffer2), testutil::HashComposite(CompositeBuffer(SharedBuffer(DiskBuffer2)))); - - IoBuffer DiskBuffer3 = WriteToTempFile(CompositeBuffer(CreateRandomBlob(496 * 5 * 1024)), TmpDir.Path() / "diskbuffer3"); - CHECK_EQ(IoHash::HashBuffer(DiskBuffer3), testutil::HashComposite(CompositeBuffer(SharedBuffer(DiskBuffer3)))); - - CompositeBuffer Data(SharedBuffer(std::move(MemoryBuffer1)), - SharedBuffer(std::move(DiskBuffer1)), - SharedBuffer(std::move(DiskBuffer2)), - SharedBuffer(std::move(MemoryBuffer2)), - SharedBuffer(std::move(DiskBuffer3))); - CHECK_EQ(IoHash::HashBuffer(Data), testutil::HashComposite(Data)); -} - TEST_CASE("httpclient") { using namespace std::literals; diff --git a/src/zenhttp/httpclientauth.cpp b/src/zenhttp/httpclientauth.cpp index 62e1b77bc..8754c57d6 100644 --- a/src/zenhttp/httpclientauth.cpp +++ b/src/zenhttp/httpclientauth.cpp @@ -9,11 +9,11 @@ #include <zencore/timer.h> #include <zencore/uid.h> #include <zenhttp/auth/authmgr.h> +#include <zenhttp/httpclient.h> #include <ctime> ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> #include <fmt/format.h> #include <json11.hpp> ZEN_THIRD_PARTY_INCLUDES_END @@ -47,18 +47,22 @@ namespace zen { namespace httpclientauth { OAuthParams.ClientId, OAuthParams.ClientSecret); - cpr::Response Response = cpr::Post(cpr::Url{OAuthParams.Url}, - cpr::Header{{"Content-Type", "application/x-www-form-urlencoded"}}, - cpr::Body{std::move(Body)}); + HttpClient Http{OAuthParams.Url}; - if (Response.error || Response.status_code != 200) + IoBuffer Payload{IoBuffer::Wrap, Body.data(), Body.size()}; + + // TODO: ensure this gets the right Content-Type passed along + + HttpClient::Response Response = Http.Post("", Payload, {{"Content-Type", "application/x-www-form-urlencoded"}}); + + if (!Response || Response.StatusCode != HttpResponseCode::OK) { - ZEN_WARN("Failed fetching OAuth access token {}. Reason: '{}'", OAuthParams.Url, Response.reason); + ZEN_WARN("Failed fetching OAuth access token {}. Reason: '{}'", OAuthParams.Url, Response.ErrorMessage("")); return HttpClientAccessToken{}; } std::string JsonError; - json11::Json Json = json11::Json::parse(Response.text, JsonError); + json11::Json Json = json11::Json::parse(std::string{Response.AsText()}, JsonError); if (JsonError.empty() == false) { @@ -90,7 +94,8 @@ namespace zen { namespace httpclientauth { static HttpClientAccessToken GetOidcTokenFromExe(const std::filesystem::path& OidcExecutablePath, std::string_view CloudHost, bool Unattended, - bool Quiet) + bool Quiet, + bool Hidden) { Stopwatch Timer; @@ -99,6 +104,10 @@ namespace zen { namespace httpclientauth { { ProcOptions.StdoutFile = std::filesystem::temp_directory_path() / fmt::format(".zen-auth-output-{}", Oid::NewOid()); } + if (Hidden) + { + ProcOptions.Flags |= CreateProcOptions::Flag_NoConsole; + } const std::filesystem::path AuthTokenPath(std::filesystem::temp_directory_path() / fmt::format(".zen-auth-{}", Oid::NewOid())); auto _ = MakeGuard([AuthTokenPath, &ProcOptions]() { @@ -176,14 +185,17 @@ namespace zen { namespace httpclientauth { std::optional<std::function<HttpClientAccessToken()>> CreateFromOidcTokenExecutable(const std::filesystem::path& OidcExecutablePath, std::string_view CloudHost, - bool Quiet) + bool Quiet, + bool Unattended, + bool Hidden) { - HttpClientAccessToken InitialToken = GetOidcTokenFromExe(OidcExecutablePath, CloudHost, /* Unattended */ false, Quiet); + HttpClientAccessToken InitialToken = GetOidcTokenFromExe(OidcExecutablePath, CloudHost, Unattended, Quiet, Hidden); if (InitialToken.IsValid()) { return [OidcExecutablePath = std::filesystem::path(OidcExecutablePath), CloudHost = std::string(CloudHost), Quiet, + Hidden, InitialToken]() mutable { if (InitialToken.IsValid()) { @@ -191,7 +203,7 @@ namespace zen { namespace httpclientauth { InitialToken = {}; return Result; } - return GetOidcTokenFromExe(OidcExecutablePath, CloudHost, /* Unattended */ true, Quiet); + return GetOidcTokenFromExe(OidcExecutablePath, CloudHost, /* Unattended */ true, Quiet, Hidden); }; } return {}; diff --git a/src/zenhttp/httpserver.cpp b/src/zenhttp/httpserver.cpp index 764f2a2a7..2c063d646 100644 --- a/src/zenhttp/httpserver.cpp +++ b/src/zenhttp/httpserver.cpp @@ -18,6 +18,7 @@ #include <zencore/compactbinary.h> #include <zencore/compactbinarybuilder.h> #include <zencore/compactbinarypackage.h> +#include <zencore/compactbinaryutil.h> #include <zencore/iobuffer.h> #include <zencore/logging.h> #include <zencore/stream.h> @@ -665,9 +666,13 @@ HttpServerRequest::ReadPayloadObject() } return CbObject(); } - return LoadCompactBinaryObject(std::move(Payload)); + CbValidateError ValidationError = CbValidateError::None; + if (CbObject ResponseObject = ValidateAndReadCompactBinaryObject(std::move(Payload), ValidationError); + ValidationError == CbValidateError::None) + { + return ResponseObject; + } } - return {}; } @@ -931,42 +936,51 @@ HandlePackageOffers(HttpService& Service, HttpServerRequest& Request, Ref<IHttpP if (PackageHandlerRef) { - CbObject OfferMessage = LoadCompactBinaryObject(Request.ReadPayload()); - - std::vector<IoHash> OfferCids; - - for (auto& CidEntry : OfferMessage["offer"]) + CbValidateError ValidationError = CbValidateError::None; + if (CbObject OfferMessage = ValidateAndReadCompactBinaryObject(IoBuffer(Request.ReadPayload()), ValidationError); + ValidationError == CbValidateError::None) { - if (!CidEntry.IsHash()) + std::vector<IoHash> OfferCids; + + for (auto& CidEntry : OfferMessage["offer"]) { - // Should yield bad request response? + if (!CidEntry.IsHash()) + { + // Should yield bad request response? + + ZEN_WARN("found invalid entry in offer"); - ZEN_WARN("found invalid entry in offer"); + continue; + } - continue; + OfferCids.push_back(CidEntry.AsHash()); } - OfferCids.push_back(CidEntry.AsHash()); - } + ZEN_TRACE("request #{} -> filtering offer of {} entries", Request.RequestId(), OfferCids.size()); + + PackageHandlerRef->FilterOffer(OfferCids); - ZEN_TRACE("request #{} -> filtering offer of {} entries", Request.RequestId(), OfferCids.size()); + ZEN_TRACE("request #{} -> filtered to {} entries", Request.RequestId(), OfferCids.size()); - PackageHandlerRef->FilterOffer(OfferCids); + CbObjectWriter ResponseWriter; + ResponseWriter.BeginArray("need"); - ZEN_TRACE("request #{} -> filtered to {} entries", Request.RequestId(), OfferCids.size()); + for (const IoHash& Cid : OfferCids) + { + ResponseWriter.AddHash(Cid); + } - CbObjectWriter ResponseWriter; - ResponseWriter.BeginArray("need"); + ResponseWriter.EndArray(); - for (const IoHash& Cid : OfferCids) + // Emit filter response + Request.WriteResponse(HttpResponseCode::OK, ResponseWriter.Save()); + } + else { - ResponseWriter.AddHash(Cid); + Request.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Invalid request payload: '{}'", ToString(ValidationError))); } - - ResponseWriter.EndArray(); - - // Emit filter response - Request.WriteResponse(HttpResponseCode::OK, ResponseWriter.Save()); return true; } } diff --git a/src/zenhttp/include/zenhttp/cprutils.h b/src/zenhttp/include/zenhttp/cprutils.h new file mode 100644 index 000000000..a3b870c0f --- /dev/null +++ b/src/zenhttp/include/zenhttp/cprutils.h @@ -0,0 +1,86 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zencore/compactbinary.h> +#include <zencore/compactbinaryvalidation.h> +#include <zencore/iobuffer.h> +#include <zencore/string.h> +#include <zenhttp/formatters.h> +#include <zenhttp/httpclient.h> +#include <zenhttp/httpcommon.h> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <cpr/response.h> +#include <fmt/format.h> +ZEN_THIRD_PARTY_INCLUDES_END + +template<> +struct fmt::formatter<cpr::Response> +{ + constexpr auto parse(format_parse_context& Ctx) -> decltype(Ctx.begin()) { return Ctx.end(); } + + template<typename FormatContext> + auto format(const cpr::Response& Response, FormatContext& Ctx) const -> decltype(Ctx.out()) + { + using namespace std::literals; + + zen::NiceTimeSpanMs NiceResponseTime(uint64_t(Response.elapsed * 1000)); + + if (zen::IsHttpSuccessCode(Response.status_code)) + { + return fmt::format_to(Ctx.out(), + "Url: {}, Status: {}, Error: '{}' ({}), Bytes: {}/{} (Up/Down), Elapsed: {}", + Response.url.str(), + Response.status_code, + Response.error.message, + int(Response.error.code), + Response.uploaded_bytes, + Response.downloaded_bytes, + NiceResponseTime.c_str()); + } + else + { + const auto It = Response.header.find("Content-Type"); + const std::string_view ContentType = It != Response.header.end() ? It->second : "<None>"sv; + + if (ContentType == "application/x-ue-cb"sv) + { + zen::IoBuffer Body(zen::IoBuffer::Wrap, Response.text.data(), Response.text.size()); + zen::CbObjectView Obj(Body.Data()); + zen::ExtendableStringBuilder<256> Sb; + std::string_view Json = Obj.ToJson(Sb).ToView(); + + return fmt::format_to( + Ctx.out(), + "Url: {}, Status: {}, Error: '{}' ({}). Bytes: {}/{} (Up/Down), Elapsed: {}, Response: '{}', Reason: '{}'", + Response.url.str(), + Response.status_code, + Response.error.message, + int(Response.error.code), + Response.uploaded_bytes, + Response.downloaded_bytes, + NiceResponseTime.c_str(), + Json, + Response.reason); + } + else + { + zen::BodyLogFormatter Body(Response.text); + + return fmt::format_to( + Ctx.out(), + "Url: {}, Status: {}, Error: '{}' ({}), Bytes: {}/{} (Up/Down), Elapsed: {}, Response: '{}', Reason: '{}'", + Response.url.str(), + Response.status_code, + Response.error.message, + int(Response.error.code), + Response.uploaded_bytes, + Response.downloaded_bytes, + NiceResponseTime.c_str(), + Body.GetText(), + Response.reason); + } + } + } +}; diff --git a/src/zenhttp/include/zenhttp/formatters.h b/src/zenhttp/include/zenhttp/formatters.h index 05a23d675..0af31fa30 100644 --- a/src/zenhttp/include/zenhttp/formatters.h +++ b/src/zenhttp/include/zenhttp/formatters.h @@ -10,7 +10,6 @@ #include <zenhttp/httpcommon.h> ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> #include <fmt/format.h> ZEN_THIRD_PARTY_INCLUDES_END @@ -59,76 +58,6 @@ public: } // namespace zen template<> -struct fmt::formatter<cpr::Response> -{ - constexpr auto parse(format_parse_context& Ctx) -> decltype(Ctx.begin()) { return Ctx.end(); } - - template<typename FormatContext> - auto format(const cpr::Response& Response, FormatContext& Ctx) const -> decltype(Ctx.out()) - { - using namespace std::literals; - - zen::NiceTimeSpanMs NiceResponseTime(uint64_t(Response.elapsed * 1000)); - - if (zen::IsHttpSuccessCode(Response.status_code)) - { - return fmt::format_to(Ctx.out(), - "Url: {}, Status: {}, Error: '{}' ({}), Bytes: {}/{} (Up/Down), Elapsed: {}", - Response.url.str(), - Response.status_code, - Response.error.message, - int(Response.error.code), - Response.uploaded_bytes, - Response.downloaded_bytes, - NiceResponseTime.c_str()); - } - else - { - const auto It = Response.header.find("Content-Type"); - const std::string_view ContentType = It != Response.header.end() ? It->second : "<None>"sv; - - if (ContentType == "application/x-ue-cb"sv) - { - zen::IoBuffer Body(zen::IoBuffer::Wrap, Response.text.data(), Response.text.size()); - zen::CbObjectView Obj(Body.Data()); - zen::ExtendableStringBuilder<256> Sb; - std::string_view Json = Obj.ToJson(Sb).ToView(); - - return fmt::format_to( - Ctx.out(), - "Url: {}, Status: {}, Error: '{}' ({}). Bytes: {}/{} (Up/Down), Elapsed: {}, Response: '{}', Reason: '{}'", - Response.url.str(), - Response.status_code, - Response.error.message, - int(Response.error.code), - Response.uploaded_bytes, - Response.downloaded_bytes, - NiceResponseTime.c_str(), - Json, - Response.reason); - } - else - { - zen::BodyLogFormatter Body(Response.text); - - return fmt::format_to( - Ctx.out(), - "Url: {}, Status: {}, Error: '{}' ({}), Bytes: {}/{} (Up/Down), Elapsed: {}, Response: '{}', Reason: '{}'", - Response.url.str(), - Response.status_code, - Response.error.message, - int(Response.error.code), - Response.uploaded_bytes, - Response.downloaded_bytes, - NiceResponseTime.c_str(), - Body.GetText(), - Response.reason); - } - } - } -}; - -template<> struct fmt::formatter<zen::HttpClient::Response> { constexpr auto parse(format_parse_context& Ctx) -> decltype(Ctx.begin()) { return Ctx.end(); } diff --git a/src/zenhttp/include/zenhttp/httpclient.h b/src/zenhttp/include/zenhttp/httpclient.h index 50bd5b53a..c1fc1efa6 100644 --- a/src/zenhttp/include/zenhttp/httpclient.h +++ b/src/zenhttp/include/zenhttp/httpclient.h @@ -55,6 +55,7 @@ struct HttpClientSettings bool AssumeHttp2 = false; bool AllowResume = false; uint8_t RetryCount = 0; + Oid SessionId = Oid::Zero; }; class HttpClientError : public std::runtime_error @@ -76,10 +77,40 @@ public: { } + inline int GetInternalErrorCode() const { return m_Error; } + inline HttpResponseCode GetHttpResponseCode() const { return m_ResponseCode; } + + enum class ResponseClass : std::int8_t + { + kSuccess = 0, + + kHttpOtherClientError = 80, + kHttpCantConnectError = 81, // CONNECTION_FAILURE + kHttpNotFound = 66, // NotFound(404) + kHttpUnauthorized = 77, // Unauthorized(401), + kHttpSLLError = + 82, // SSL_CONNECT_ERROR, SSL_LOCAL_CERTIFICATE_ERROR, SSL_REMOTE_CERTIFICATE_ERROR, SSL_CACERT_ERROR, GENERIC_SSL_ERROR + kHttpForbidden = 83, // Forbidden(403) + kHttpTimeout = 84, // NETWORK_RECEIVE_ERROR, NETWORK_SEND_FAILURE, OPERATION_TIMEDOUT, RequestTimeout(408) + kHttpConflict = 85, // Conflict(409) + kHttpNoHost = 86, // HOST_RESOLUTION_FAILURE, PROXY_RESOLUTION_FAILURE + + kHttpOtherServerError = 90, + kHttpInternalServerError = 91, // InternalServerError(500) + kHttpServiceUnavailable = 69, // ServiceUnavailable(503) + kHttpBadGateway = 92, // BadGateway(502) + kHttpGatewayTimeout = 93, // GatewayTimeout(504) + }; + + ResponseClass GetResponseClass() const; + +private: const int m_Error = 0; const HttpResponseCode m_ResponseCode = HttpResponseCode::ImATeapot; }; +class HttpClientBase; + class HttpClient { public: @@ -163,6 +194,11 @@ public: std::string ErrorMessage(std::string_view Prefix) const; }; + static std::pair<std::string_view, std::string_view> Accept(ZenContentType ContentType) + { + return std::make_pair("Accept", MapContentTypeToString(ContentType)); + } + [[nodiscard]] Response Put(std::string_view Url, const IoBuffer& Payload, const KeyValueMap& AdditionalHeader = {}); [[nodiscard]] Response Put(std::string_view Url, const KeyValueMap& Parameters = {}); [[nodiscard]] Response Get(std::string_view Url, const KeyValueMap& AdditionalHeader = {}, const KeyValueMap& Parameters = {}); @@ -192,27 +228,20 @@ public: [[nodiscard]] Response TransactPackage(std::string_view Url, CbPackage Package, const KeyValueMap& AdditionalHeader = {}); - static std::pair<std::string_view, std::string_view> Accept(ZenContentType ContentType) - { - return std::make_pair("Accept", MapContentTypeToString(ContentType)); - } - - LoggerRef Logger() { return m_Log; } + LoggerRef Log() { return m_Log; } std::string_view GetBaseUri() const { return m_BaseUri; } - bool Authenticate(); std::string_view GetSessionId() const { return m_SessionId; } + void SetSessionId(const Oid& SessionId); + + bool Authenticate(); private: - const std::optional<HttpClientAccessToken> GetAccessToken(); - struct Impl; + HttpClientBase* m_Inner; LoggerRef m_Log; std::string m_BaseUri; std::string m_SessionId; const HttpClientSettings m_ConnectionSettings; - RwLock m_AccessTokenLock; - HttpClientAccessToken m_CachedAccessToken; - Ref<Impl> m_Impl; }; void httpclient_forcelink(); // internal diff --git a/src/zenhttp/include/zenhttp/httpclientauth.h b/src/zenhttp/include/zenhttp/httpclientauth.h index 32d00f87f..26f31ed2a 100644 --- a/src/zenhttp/include/zenhttp/httpclientauth.h +++ b/src/zenhttp/include/zenhttp/httpclientauth.h @@ -28,7 +28,9 @@ namespace httpclientauth { std::optional<std::function<HttpClientAccessToken()>> CreateFromOidcTokenExecutable(const std::filesystem::path& OidcExecutablePath, std::string_view CloudHost, - bool Quiet); + bool Quiet, + bool Unattended, + bool Hidden); } // namespace httpclientauth } // namespace zen diff --git a/src/zenhttp/packageformat.cpp b/src/zenhttp/packageformat.cpp index 0b7848f79..708238224 100644 --- a/src/zenhttp/packageformat.cpp +++ b/src/zenhttp/packageformat.cpp @@ -4,6 +4,7 @@ #include <zencore/compactbinarybuilder.h> #include <zencore/compactbinarypackage.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compositebuffer.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> @@ -375,7 +376,7 @@ ParsePackageMessage(IoBuffer Payload, std::function<IoBuffer(const IoHash&, uint if (Hdr->HeaderMagic != kCbPkgMagic) { throw std::invalid_argument( - fmt::format("invalid CbPackage header magic, expected {0:x}, got {0:x}", static_cast<uint32_t>(kCbPkgMagic), Hdr->HeaderMagic)); + fmt::format("invalid CbPackage header magic, expected {0:x}, got {1:x}", static_cast<uint32_t>(kCbPkgMagic), Hdr->HeaderMagic)); } Reader.Skip(sizeof(CbPackageHeader)); @@ -499,6 +500,8 @@ ParsePackageMessage(IoBuffer Payload, std::function<IoBuffer(const IoHash&, uint { if (Entry.Flags & CbAttachmentEntry::kIsObject) { + CbObject AttachmentObject; + CompressedBuffer CompBuf(CompressedBuffer::FromCompressedNoValidate(IoBuffer(AttachmentBuffer))); if (!CompBuf) { @@ -509,7 +512,18 @@ ParsePackageMessage(IoBuffer Payload, std::function<IoBuffer(const IoHash&, uint AttachmentBuffer.GetSize(), Entry.AttachmentHash))); } - CbObject AttachmentObject = LoadCompactBinaryObject(std::move(CompBuf)); + else + { + CbValidateError ValidationError = CbValidateError::None; + AttachmentObject = ValidateAndReadCompactBinaryObject(std::move(CompBuf), ValidationError); + if (ValidationError != CbValidateError::None) + { + MalformedAttachments.push_back(std::make_pair( + i, + fmt::format("Invalid format, CbObject for {}. Reason '{}'", Entry.AttachmentHash, ToString(ValidationError)))); + } + } + if (i == 0) { // First payload is always a compact binary object @@ -541,7 +555,15 @@ ParsePackageMessage(IoBuffer Payload, std::function<IoBuffer(const IoHash&, uint { if (Entry.Flags & CbAttachmentEntry::kIsObject) { - CbObject AttachmentObject = LoadCompactBinaryObject(AttachmentBuffer); + CbValidateError ValidationError = CbValidateError::None; + CbObject AttachmentObject = ValidateAndReadCompactBinaryObject(std::move(AttachmentBuffer), ValidationError); + if (ValidationError != CbValidateError::None) + { + MalformedAttachments.push_back(std::make_pair( + i, + fmt::format("Invalid format, CbObject for {}. Reason '{}'", Entry.AttachmentHash, ToString(ValidationError)))); + } + if (i == 0) { Package.SetObject(AttachmentObject); @@ -709,7 +731,12 @@ CbPackageReader::Finalize() { if (Entry.Flags & CbAttachmentEntry::kIsLocalRef) { - m_RootObject = LoadCompactBinaryObject(MarshalLocalChunkReference(AttachmentBuffer)); + CbValidateError ValidateError = CbValidateError::None; + m_RootObject = ValidateAndReadCompactBinaryObject(MarshalLocalChunkReference(AttachmentBuffer), ValidateError); + if (ValidateError != CbValidateError::None) + { + throw std::runtime_error(fmt::format("Root object format is invalid, reason: '{}'", ToString(ValidateError))); + } } else if (Entry.Flags & CbAttachmentEntry::kIsCompressed) { @@ -718,12 +745,22 @@ CbPackageReader::Finalize() CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(AttachmentBuffer), RawHash, RawSize); if (RawHash == Entry.AttachmentHash) { - m_RootObject = LoadCompactBinaryObject(Compressed); + CbValidateError ValidateError = CbValidateError::None; + m_RootObject = ValidateAndReadCompactBinaryObject(std::move(Compressed), ValidateError); + if (ValidateError != CbValidateError::None) + { + throw std::runtime_error(fmt::format("Root object format is invalid, reason: '{}'", ToString(ValidateError))); + } } } else { - m_RootObject = LoadCompactBinaryObject(std::move(AttachmentBuffer)); + CbValidateError ValidateError = CbValidateError::None; + m_RootObject = ValidateAndReadCompactBinaryObject(std::move(AttachmentBuffer), ValidateError); + if (ValidateError != CbValidateError::None) + { + throw std::runtime_error(fmt::format("Root object format is invalid, reason: '{}'", ToString(ValidateError))); + } } } else diff --git a/src/zenhttp/servers/httpasio.cpp b/src/zenhttp/servers/httpasio.cpp index 5392140d1..2023b6d98 100644 --- a/src/zenhttp/servers/httpasio.cpp +++ b/src/zenhttp/servers/httpasio.cpp @@ -522,9 +522,9 @@ HttpServerConnection::HandleRequest() } else { - ZEN_ERROR("Caught system error exception while handling request: {}. ({})", - SystemError.what(), - SystemError.code().value()); + ZEN_WARN("Caught system error exception while handling request: {}. ({})", + SystemError.what(), + SystemError.code().value()); Request.WriteResponse(HttpResponseCode::InternalServerError, HttpContentType::kText, SystemError.what()); } } @@ -540,7 +540,7 @@ HttpServerConnection::HandleRequest() // Drop any partially formatted response Request.m_Response.reset(); - ZEN_ERROR("Caught exception while handling request: {}", ex.what()); + ZEN_WARN("Caught exception while handling request: {}", ex.what()); Request.WriteResponse(HttpResponseCode::InternalServerError, HttpContentType::kText, ex.what()); } } diff --git a/src/zenhttp/servers/httpparser.cpp b/src/zenhttp/servers/httpparser.cpp index 9bb354a5e..93094e21b 100644 --- a/src/zenhttp/servers/httpparser.cpp +++ b/src/zenhttp/servers/httpparser.cpp @@ -6,6 +6,8 @@ #include <zencore/logging.h> #include <zencore/string.h> +#include <limits> + namespace zen { using namespace std::literals; @@ -69,23 +71,21 @@ HttpRequestParser::ConsumeData(const char* InputData, size_t DataSize) int HttpRequestParser::OnUrl(const char* Data, size_t Bytes) { - if (!m_Url) + const size_t RemainingBufferSpace = std::numeric_limits<std::uint32_t>::max() - m_HeaderData.size(); + if (RemainingBufferSpace < Bytes) { - ZEN_ASSERT_SLOW(m_UrlLength == 0); - m_Url = m_HeaderCursor; + ZEN_WARN("HTTP parser does not have enough space for incoming request headers, need {} more bytes", Bytes - RemainingBufferSpace); + return 1; } - const size_t RemainingBufferSpace = sizeof m_HeaderBuffer + m_HeaderBuffer - m_HeaderCursor; - - if (RemainingBufferSpace < Bytes) + if (m_UrlRange.Length == 0) { - ZEN_WARN("HTTP parser does not have enough space for incoming request, need {} more bytes", Bytes - RemainingBufferSpace); - return 1; + ZEN_ASSERT_SLOW(m_UrlRange.Offset == 0); + m_UrlRange.Offset = (uint32_t)m_HeaderData.size(); } - memcpy(m_HeaderCursor, Data, Bytes); - m_HeaderCursor += Bytes; - m_UrlLength += Bytes; + m_HeaderData.insert(m_HeaderData.end(), Data, &Data[Bytes]); + m_UrlRange.Length += (uint32_t)Bytes; return 0; } @@ -93,59 +93,66 @@ HttpRequestParser::OnUrl(const char* Data, size_t Bytes) int HttpRequestParser::OnHeader(const char* Data, size_t Bytes) { - if (m_CurrentHeaderValueLength) + const size_t RemainingBufferSpace = std::numeric_limits<std::uint32_t>::max() - m_HeaderData.size(); + if (RemainingBufferSpace < Bytes) { - AppendCurrentHeader(); + ZEN_WARN("HTTP parser does not have enough space for incoming request headers, need {} more bytes", Bytes - RemainingBufferSpace); + return 1; + } - m_CurrentHeaderNameLength = 0; - m_CurrentHeaderValueLength = 0; - m_CurrentHeaderName = m_HeaderCursor; + if (m_HeaderEntries.empty()) + { + m_HeaderEntries.resize(1); } - else if (m_CurrentHeaderName == nullptr) + HeaderEntry* CurrentHeaderEntry = &m_HeaderEntries.back(); + if (CurrentHeaderEntry->ValueRange.Length) { - m_CurrentHeaderName = m_HeaderCursor; + ParseCurrentHeader(); + m_HeaderEntries.emplace_back(HeaderEntry{.NameRange = {.Offset = (uint32_t)m_HeaderData.size()}}); + CurrentHeaderEntry = &m_HeaderEntries.back(); } - - const size_t RemainingBufferSpace = sizeof m_HeaderBuffer + m_HeaderBuffer - m_HeaderCursor; - if (RemainingBufferSpace < Bytes) + else if (CurrentHeaderEntry->NameRange.Length == 0) { - ZEN_WARN("HTTP parser does not have enough space for incoming header name, need {} more bytes", Bytes - RemainingBufferSpace); - return 1; + m_HeaderEntries.emplace_back(HeaderEntry{.NameRange = {.Offset = (uint32_t)m_HeaderData.size()}}); + CurrentHeaderEntry = &m_HeaderEntries.back(); } - memcpy(m_HeaderCursor, Data, Bytes); - m_HeaderCursor += Bytes; - m_CurrentHeaderNameLength += Bytes; + m_HeaderData.insert(m_HeaderData.end(), Data, &Data[Bytes]); + CurrentHeaderEntry->NameRange.Length += (uint32_t)Bytes; return 0; } void -HttpRequestParser::AppendCurrentHeader() +HttpRequestParser::ParseCurrentHeader() { - std::string_view HeaderName(m_CurrentHeaderName, m_CurrentHeaderNameLength); - if (m_Headers.size() == std::numeric_limits<int8_t>::max()) + ZEN_ASSERT_SLOW(!m_HeaderEntries.empty()); + const HeaderEntry& CurrentHeaderEntry = m_HeaderEntries.back(); + const size_t CurrentHeaderCount = m_HeaderEntries.size(); + const std::string_view HeaderName(GetHeaderSubString(CurrentHeaderEntry.NameRange)); + if (CurrentHeaderCount > std::numeric_limits<int8_t>::max()) { ZEN_WARN("HttpRequestParser parser only supports up to {} headers, can't store header '{}'. Dropping it.", std::numeric_limits<int8_t>::max(), HeaderName); return; } - std::string_view HeaderValue(m_CurrentHeaderValue, m_CurrentHeaderValueLength); + const std::string_view HeaderValue(GetHeaderSubString(CurrentHeaderEntry.ValueRange)); - const uint32_t HeaderHash = HashStringAsLowerDjb2(HeaderName); + const uint32_t HeaderHash = HashStringAsLowerDjb2(HeaderName); + const int8_t CurrentHeaderIndex = int8_t(CurrentHeaderCount - 1); if (HeaderHash == HashContentLength) { - m_ContentLengthHeaderIndex = (int8_t)m_Headers.size(); + m_ContentLengthHeaderIndex = CurrentHeaderIndex; } else if (HeaderHash == HashAccept) { - m_AcceptHeaderIndex = (int8_t)m_Headers.size(); + m_AcceptHeaderIndex = CurrentHeaderIndex; } else if (HeaderHash == HashContentType) { - m_ContentTypeHeaderIndex = (int8_t)m_Headers.size(); + m_ContentTypeHeaderIndex = CurrentHeaderIndex; } else if (HeaderHash == HashSession) { @@ -169,38 +176,38 @@ HttpRequestParser::AppendCurrentHeader() } else if (HeaderHash == HashRange) { - m_RangeHeaderIndex = (int8_t)m_Headers.size(); + m_RangeHeaderIndex = CurrentHeaderIndex; } - - m_Headers.emplace_back(HeaderName, HeaderValue); } int HttpRequestParser::OnHeaderValue(const char* Data, size_t Bytes) { - if (m_CurrentHeaderValueLength == 0) - { - m_CurrentHeaderValue = m_HeaderCursor; - } - - const size_t RemainingBufferSpace = sizeof m_HeaderBuffer + m_HeaderBuffer - m_HeaderCursor; + const size_t RemainingBufferSpace = std::numeric_limits<std::uint32_t>::max() - m_HeaderData.size(); if (RemainingBufferSpace < Bytes) { - ZEN_WARN("HTTP parser does not have enough space for incoming header value, need {} more bytes", Bytes - RemainingBufferSpace); + ZEN_WARN("HTTP parser does not have enough space for incoming request headers, need {} more bytes", Bytes - RemainingBufferSpace); return 1; } - memcpy(m_HeaderCursor, Data, Bytes); - m_HeaderCursor += Bytes; - m_CurrentHeaderValueLength += Bytes; + ZEN_ASSERT_SLOW(!m_HeaderEntries.empty()); + HeaderEntry& CurrentHeaderEntry = m_HeaderEntries.back(); + if (CurrentHeaderEntry.ValueRange.Length == 0) + { + CurrentHeaderEntry.ValueRange.Offset = (uint32_t)m_HeaderData.size(); + } + m_HeaderData.insert(m_HeaderData.end(), Data, &Data[Bytes]); + CurrentHeaderEntry.ValueRange.Length += (uint32_t)Bytes; return 0; } static void -NormalizeUrlPath(const char* Url, size_t UrlLength, std::string& NormalizedUrl) +NormalizeUrlPath(std::string_view InUrl, std::string& NormalizedUrl) { - bool LastCharWasSeparator = false; + bool LastCharWasSeparator = false; + const char* Url = InUrl.data(); + const size_t UrlLength = InUrl.length(); for (std::string_view::size_type UrlIndex = 0; UrlIndex < UrlLength; ++UrlIndex) { const char UrlChar = Url[UrlIndex]; @@ -233,9 +240,13 @@ HttpRequestParser::OnHeadersComplete() { try { - if (m_CurrentHeaderValueLength) + if (!m_HeaderEntries.empty()) { - AppendCurrentHeader(); + HeaderEntry& CurrentHeaderEntry = m_HeaderEntries.back(); + if (CurrentHeaderEntry.NameRange.Length) + { + ParseCurrentHeader(); + } } m_KeepAlive = !!http_should_keep_alive(&m_Parser); @@ -275,21 +286,21 @@ HttpRequestParser::OnHeadersComplete() break; } - std::string_view Url(m_Url, m_UrlLength); + std::string_view FullUrl(GetHeaderSubString(m_UrlRange)); - if (auto QuerySplit = Url.find_first_of('?'); QuerySplit != std::string_view::npos) + if (auto QuerySplit = FullUrl.find_first_of('?'); QuerySplit != std::string_view::npos) { - m_UrlLength = QuerySplit; - m_QueryString = m_Url + QuerySplit + 1; - m_QueryLength = Url.size() - QuerySplit - 1; + m_UrlRange.Length = uint32_t(QuerySplit); + m_QueryStringRange = {.Offset = uint32_t(m_UrlRange.Offset + QuerySplit + 1), + .Length = uint32_t(FullUrl.size() - QuerySplit - 1)}; } - NormalizeUrlPath(m_Url, m_UrlLength, m_NormalizedUrl); + NormalizeUrlPath(FullUrl, m_NormalizedUrl); - if (m_ContentLengthHeaderIndex >= 0) + std::string_view Value = GetHeaderValue(m_ContentLengthHeaderIndex); + if (!Value.empty()) { - std::string_view& Value = m_Headers[m_ContentLengthHeaderIndex].Value; - uint64_t ContentLength = 0; + uint64_t ContentLength = 0; std::from_chars(Value.data(), Value.data() + Value.size(), ContentLength); if (ContentLength) @@ -337,15 +348,11 @@ HttpRequestParser::OnBody(const char* Data, size_t Bytes) void HttpRequestParser::ResetState() { - m_HeaderCursor = m_HeaderBuffer; - m_CurrentHeaderName = nullptr; - m_CurrentHeaderNameLength = 0; - m_CurrentHeaderValue = nullptr; - m_CurrentHeaderValueLength = 0; - m_Url = nullptr; - m_UrlLength = 0; - m_QueryString = nullptr; - m_QueryLength = 0; + m_UrlRange = {}; + m_QueryStringRange = {}; + + m_HeaderEntries.clear(); + m_ContentLengthHeaderIndex = -1; m_AcceptHeaderIndex = -1; m_ContentTypeHeaderIndex = -1; @@ -353,7 +360,8 @@ HttpRequestParser::ResetState() m_Expect100Continue = false; m_BodyBuffer = {}; m_BodyPosition = 0; - m_Headers.clear(); + + m_HeaderData.clear(); m_NormalizedUrl.clear(); } diff --git a/src/zenhttp/servers/httpparser.h b/src/zenhttp/servers/httpparser.h index bdbcab4d9..0d2664ec5 100644 --- a/src/zenhttp/servers/httpparser.h +++ b/src/zenhttp/servers/httpparser.h @@ -5,6 +5,8 @@ #include <zencore/uid.h> #include <zenhttp/httpcommon.h> +#include <EASTL/fixed_vector.h> + ZEN_THIRD_PARTY_INCLUDES_START #include <http_parser.h> ZEN_THIRD_PARTY_INCLUDES_END @@ -31,73 +33,68 @@ struct HttpRequestParser HttpVerb RequestVerb() const { return m_RequestVerb; } bool IsKeepAlive() const { return m_KeepAlive; } - std::string_view Url() const { return m_NormalizedUrl.empty() ? std::string_view(m_Url, m_UrlLength) : m_NormalizedUrl; } - std::string_view QueryString() const { return std::string_view(m_QueryString, m_QueryLength); } + std::string_view Url() const { return m_NormalizedUrl.empty() ? GetHeaderSubString(m_UrlRange) : m_NormalizedUrl; } + std::string_view QueryString() const { return GetHeaderSubString(m_QueryStringRange); } IoBuffer Body() { return m_BodyBuffer; } - inline HttpContentType ContentType() - { - if (m_ContentTypeHeaderIndex < 0) - { - return HttpContentType::kUnknownContentType; - } - - return ParseContentType(m_Headers[m_ContentTypeHeaderIndex].Value); - } + inline HttpContentType ContentType() { return ParseContentType(GetHeaderValue(m_ContentTypeHeaderIndex)); } - inline HttpContentType AcceptType() - { - if (m_AcceptHeaderIndex < 0) - { - return HttpContentType::kUnknownContentType; - } - - return ParseContentType(m_Headers[m_AcceptHeaderIndex].Value); - } + inline HttpContentType AcceptType() { return ParseContentType(GetHeaderValue(m_AcceptHeaderIndex)); } Oid SessionId() const { return m_SessionId; } int RequestId() const { return m_RequestId; } - std::string_view RangeHeader() const { return m_RangeHeaderIndex != -1 ? m_Headers[m_RangeHeaderIndex].Value : std::string_view(); } + std::string_view RangeHeader() const { return GetHeaderValue(m_RangeHeaderIndex); } private: + struct HeaderRange + { + uint32_t Offset = 0; + uint32_t Length = 0; + }; + struct HeaderEntry { - HeaderEntry() = default; + HeaderRange NameRange; + HeaderRange ValueRange; + }; - HeaderEntry(std::string_view InName, std::string_view InValue) : Name(InName), Value(InValue) {} + inline std::string_view GetHeaderValue(int8_t HeaderIndex) const + { + if (HeaderIndex == -1) + { + return {}; + } + ZEN_ASSERT(size_t(HeaderIndex) < m_HeaderEntries.size()); + return GetHeaderSubString(m_HeaderEntries[HeaderIndex].ValueRange); + } - std::string_view Name; - std::string_view Value; - }; + std::string_view GetHeaderSubString(const HeaderRange& Range) const + { + ZEN_ASSERT_SLOW(Range.Offset + Range.Length <= m_HeaderData.size()); + return std::string_view(m_HeaderData.begin(), m_HeaderData.size()).substr(Range.Offset, Range.Length); + } - HttpRequestParserCallbacks& m_Connection; - char* m_HeaderCursor = m_HeaderBuffer; - char* m_Url = nullptr; - size_t m_UrlLength = 0; - char* m_QueryString = nullptr; - size_t m_QueryLength = 0; - char* m_CurrentHeaderName = nullptr; // Used while parsing headers - size_t m_CurrentHeaderNameLength = 0; - char* m_CurrentHeaderValue = nullptr; // Used while parsing headers - size_t m_CurrentHeaderValueLength = 0; - std::vector<HeaderEntry> m_Headers; - int8_t m_ContentLengthHeaderIndex; - int8_t m_AcceptHeaderIndex; - int8_t m_ContentTypeHeaderIndex; - int8_t m_RangeHeaderIndex; - HttpVerb m_RequestVerb; - std::atomic_bool m_KeepAlive{false}; - bool m_Expect100Continue = false; - int m_RequestId = -1; - Oid m_SessionId{}; - IoBuffer m_BodyBuffer; - uint64_t m_BodyPosition = 0; - http_parser m_Parser; - char m_HeaderBuffer[1024]; - std::string m_NormalizedUrl; - - void AppendCurrentHeader(); + HttpRequestParserCallbacks& m_Connection; + HeaderRange m_UrlRange; + HeaderRange m_QueryStringRange; + eastl::fixed_vector<HeaderEntry, 16> m_HeaderEntries; + int8_t m_ContentLengthHeaderIndex; + int8_t m_AcceptHeaderIndex; + int8_t m_ContentTypeHeaderIndex; + int8_t m_RangeHeaderIndex; + HttpVerb m_RequestVerb; + std::atomic_bool m_KeepAlive{false}; + bool m_Expect100Continue = false; + int m_RequestId = -1; + Oid m_SessionId{}; + IoBuffer m_BodyBuffer; + uint64_t m_BodyPosition = 0; + http_parser m_Parser; + eastl::fixed_vector<char, 512> m_HeaderData; + std::string m_NormalizedUrl; + + void ParseCurrentHeader(); int OnMessageBegin(); int OnUrl(const char* Data, size_t Bytes); diff --git a/src/zenhttp/servers/httpplugin.cpp b/src/zenhttp/servers/httpplugin.cpp index 155f3fa02..d6ca7e1c5 100644 --- a/src/zenhttp/servers/httpplugin.cpp +++ b/src/zenhttp/servers/httpplugin.cpp @@ -396,6 +396,14 @@ HttpPluginConnectionHandler::HandleRequest() { Service->HandleRequest(Request); } + catch (const AssertException& AssertEx) + { + // Drop any partially formatted response + Request.m_Response.reset(); + + ZEN_ERROR("Caught assert exception while handling request: {}", AssertEx.FullDescription()); + Request.WriteResponse(HttpResponseCode::InternalServerError, HttpContentType::kText, AssertEx.FullDescription()); + } catch (const std::system_error& SystemError) { // Drop any partially formatted response @@ -407,9 +415,9 @@ HttpPluginConnectionHandler::HandleRequest() } else { - ZEN_ERROR("Caught system error exception while handling request: {}. ({})", - SystemError.what(), - SystemError.code().value()); + ZEN_WARN("Caught system error exception while handling request: {}. ({})", + SystemError.what(), + SystemError.code().value()); Request.WriteResponse(HttpResponseCode::InternalServerError, HttpContentType::kText, SystemError.what()); } } @@ -425,7 +433,7 @@ HttpPluginConnectionHandler::HandleRequest() // Drop any partially formatted response Request.m_Response.reset(); - ZEN_ERROR("Caught exception while handling request: {}", ex.what()); + ZEN_WARN("Caught exception while handling request: {}", ex.what()); Request.WriteResponse(HttpResponseCode::InternalServerError, HttpContentType::kText, ex.what()); } } diff --git a/src/zenhttp/servers/httpsys.cpp b/src/zenhttp/servers/httpsys.cpp index e57fa8a30..95d83911d 100644 --- a/src/zenhttp/servers/httpsys.cpp +++ b/src/zenhttp/servers/httpsys.cpp @@ -835,7 +835,7 @@ HttpAsyncWorkRequest::IssueRequest(std::error_code& ErrorCode) ZEN_TRACE_CPU("httpsys::AsyncWork::IssueRequest"); ErrorCode.clear(); - Transaction().Server().WorkPool().ScheduleWork(m_WorkItem); + Transaction().Server().WorkPool().ScheduleWork(m_WorkItem, WorkerThreadPool::EMode::EnableBacklog); } HttpSysRequestHandler* @@ -2056,7 +2056,7 @@ InitialRequestHandler::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesT return new HttpMessageResponseRequest(Transaction(), (uint16_t)HttpResponseCode::InsufficientStorage, SystemError.what()); } - ZEN_ERROR("Caught system error exception while handling request: {}. ({})", SystemError.what(), SystemError.code().value()); + ZEN_WARN("Caught system error exception while handling request: {}. ({})", SystemError.what(), SystemError.code().value()); return new HttpMessageResponseRequest(Transaction(), (uint16_t)HttpResponseCode::InternalServerError, SystemError.what()); } catch (const std::bad_alloc& BadAlloc) @@ -2065,7 +2065,7 @@ InitialRequestHandler::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesT } catch (const std::exception& ex) { - ZEN_ERROR("Caught exception while handling request: '{}'", ex.what()); + ZEN_WARN("Caught exception while handling request: '{}'", ex.what()); return new HttpMessageResponseRequest(Transaction(), (uint16_t)HttpResponseCode::InternalServerError, ex.what()); } } diff --git a/src/zenhttp/transports/winsocktransport.cpp b/src/zenhttp/transports/winsocktransport.cpp index 8c82760bb..c06a50c95 100644 --- a/src/zenhttp/transports/winsocktransport.cpp +++ b/src/zenhttp/transports/winsocktransport.cpp @@ -304,18 +304,20 @@ SocketTransportPluginImpl::Initialize(TransportServer* ServerInterface) TransportServerConnection* ConnectionInterface{m_ServerInterface->CreateConnectionHandler(Connection)}; Connection->Initialize(ConnectionInterface, ClientSocket); - m_WorkerThreadpool->ScheduleWork([Connection] { - try - { - Connection->HandleConnection(); - } - catch (const std::exception& Ex) - { - ZEN_WARN("exception caught in connection loop: {}", Ex.what()); - } - - delete Connection; - }); + m_WorkerThreadpool->ScheduleWork( + [Connection] { + try + { + Connection->HandleConnection(); + } + catch (const std::exception& Ex) + { + ZEN_WARN("exception caught in connection loop: {}", Ex.what()); + } + + delete Connection; + }, + WorkerThreadPool::EMode::EnableBacklog); } else { diff --git a/src/zennet-test/xmake.lua b/src/zennet-test/xmake.lua index 8d8c0a598..c84837610 100644 --- a/src/zennet-test/xmake.lua +++ b/src/zennet-test/xmake.lua @@ -5,7 +5,7 @@ target("zennet-test") set_group("tests") add_headerfiles("**.h") add_files("*.cpp") - add_deps("zencore", "zenutil", "zennet") + add_deps("zennet") add_packages("vcpkg::mimalloc") add_packages("vcpkg::doctest") diff --git a/src/zennet-test/zennet-test.cpp b/src/zennet-test/zennet-test.cpp index a341bd344..5e4d29220 100644 --- a/src/zennet-test/zennet-test.cpp +++ b/src/zennet-test/zennet-test.cpp @@ -23,7 +23,15 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char** argv) zen::IgnoreChildSignals(); # endif +# if ZEN_WITH_TRACE zen::TraceInit("zennet-test"); + zen::TraceOptions TraceCommandlineOptions; + if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) + { + TraceConfigure(TraceCommandlineOptions); + } +# endif // ZEN_WITH_TRACE + zen::logging::InitializeLogging(); zen::MaximizeOpenFileCount(); diff --git a/src/zenremotestore-test/xmake.lua b/src/zenremotestore-test/xmake.lua new file mode 100644 index 000000000..edc356b5b --- /dev/null +++ b/src/zenremotestore-test/xmake.lua @@ -0,0 +1,9 @@ +-- Copyright Epic Games, Inc. All Rights Reserved. + +target("zenremotestore-test") + set_kind("binary") + set_group("tests") + add_headerfiles("**.h") + add_files("*.cpp") + add_deps("zenremotestore") + add_packages("vcpkg::doctest") diff --git a/src/zenremotestore-test/zenremotestore-test.cpp b/src/zenremotestore-test/zenremotestore-test.cpp new file mode 100644 index 000000000..a49c6d273 --- /dev/null +++ b/src/zenremotestore-test/zenremotestore-test.cpp @@ -0,0 +1,43 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/filesystem.h> +#include <zencore/logging.h> +#include <zencore/trace.h> +#include <zenremotestore/projectstore/remoteprojectstore.h> +#include <zenremotestore/zenremotestore.h> + +#include <zencore/memory/newdelete.h> + +#if ZEN_WITH_TESTS +# define ZEN_TEST_WITH_RUNNER 1 +# include <zencore/testing.h> +# include <zencore/process.h> +#endif + +int +main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) +{ +#if ZEN_WITH_TESTS + zen::zenremotestore_forcelinktests(); + +# if ZEN_PLATFORM_LINUX + zen::IgnoreChildSignals(); +# endif + +# if ZEN_WITH_TRACE + zen::TraceInit("zenstore-test"); + zen::TraceOptions TraceCommandlineOptions; + if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) + { + TraceConfigure(TraceCommandlineOptions); + } +# endif // ZEN_WITH_TRACE + + zen::logging::InitializeLogging(); + zen::MaximizeOpenFileCount(); + + return ZEN_RUN_TESTS(argc, argv); +#else + return 0; +#endif +} diff --git a/src/zenutil/buildstoragecache.cpp b/src/zenremotestore/builds/buildstoragecache.cpp index 2171f4d62..d36d75480 100644 --- a/src/zenutil/buildstoragecache.cpp +++ b/src/zenremotestore/builds/buildstoragecache.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/buildstoragecache.h> +#include <zenremotestore/builds/buildstoragecache.h> #include <zencore/compactbinarybuilder.h> #include <zencore/compactbinaryvalidation.h> @@ -11,7 +11,6 @@ #include <zencore/workthreadpool.h> #include <zenhttp/httpclient.h> #include <zenhttp/packageformat.h> -#include <zenutil/workerpools.h> ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_set.h> @@ -29,15 +28,13 @@ public: std::string_view Namespace, std::string_view Bucket, const std::filesystem::path& TempFolderPath, - bool BoostBackgroundThreadCount) + WorkerThreadPool& BackgroundWorkerPool) : m_HttpClient(HttpClient) , m_Stats(Stats) , m_Namespace(Namespace.empty() ? "none" : Namespace) , m_Bucket(Bucket.empty() ? "none" : Bucket) , m_TempFolderPath(std::filesystem::path(TempFolderPath).make_preferred()) - , m_BoostBackgroundThreadCount(BoostBackgroundThreadCount) - , m_BackgroundWorkPool(m_BoostBackgroundThreadCount ? GetSmallWorkerPool(EWorkloadType::Background) - : GetTinyWorkerPool(EWorkloadType::Background)) + , m_BackgroundWorkPool(BackgroundWorkerPool) , m_PendingBackgroundWorkCount(1) , m_CancelBackgroundWork(false) { @@ -65,21 +62,23 @@ public: m_PendingBackgroundWorkCount.AddCount(1); try { - m_BackgroundWorkPool.ScheduleWork([this, Work = std::move(Work)]() { - ZEN_TRACE_CPU("ZenBuildStorageCache::BackgroundWork"); - auto _ = MakeGuard([this]() { m_PendingBackgroundWorkCount.CountDown(); }); - if (!m_CancelBackgroundWork) - { - try - { - Work(); - } - catch (const std::exception& Ex) + m_BackgroundWorkPool.ScheduleWork( + [this, Work = std::move(Work)]() { + ZEN_TRACE_CPU("ZenBuildStorageCache::BackgroundWork"); + auto _ = MakeGuard([this]() { m_PendingBackgroundWorkCount.CountDown(); }); + if (!m_CancelBackgroundWork) { - ZEN_ERROR("Failed executing background upload to build cache. Reason: {}", Ex.what()); + try + { + Work(); + } + catch (const std::exception& Ex) + { + ZEN_ERROR("Failed executing background upload to build cache. Reason: {}", Ex.what()); + } } - } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); } catch (const std::exception& Ex) { @@ -392,7 +391,6 @@ private: const std::string m_Namespace; const std::string m_Bucket; const std::filesystem::path m_TempFolderPath; - const bool m_BoostBackgroundThreadCount; bool IsFlushed = false; WorkerThreadPool& m_BackgroundWorkPool; @@ -406,9 +404,27 @@ CreateZenBuildStorageCache(HttpClient& HttpClient, std::string_view Namespace, std::string_view Bucket, const std::filesystem::path& TempFolderPath, - bool BoostBackgroundThreadCount) + WorkerThreadPool& BackgroundWorkerPool) { - return std::make_unique<ZenBuildStorageCache>(HttpClient, Stats, Namespace, Bucket, TempFolderPath, BoostBackgroundThreadCount); + return std::make_unique<ZenBuildStorageCache>(HttpClient, Stats, Namespace, Bucket, TempFolderPath, BackgroundWorkerPool); } +ZenCacheEndpointTestResult +TestZenCacheEndpoint(std::string_view BaseUrl, const bool AssumeHttp2) +{ + HttpClientSettings TestClientSettings{.LogCategory = "httpcacheclient", + .ConnectTimeout = std::chrono::milliseconds{1000}, + .Timeout = std::chrono::milliseconds{2000}, + .AssumeHttp2 = AssumeHttp2, + .AllowResume = true, + .RetryCount = 0}; + HttpClient TestHttpClient(BaseUrl, TestClientSettings); + HttpClient::Response TestResponse = TestHttpClient.Get("/status/builds"); + if (TestResponse.IsSuccess()) + { + return {.Success = true}; + } + return {.Success = false, .FailureReason = TestResponse.ErrorMessage("")}; +}; + } // namespace zen diff --git a/src/zenutil/filebuildstorage.cpp b/src/zenremotestore/builds/filebuildstorage.cpp index f75fe403f..5cfd80666 100644 --- a/src/zenutil/filebuildstorage.cpp +++ b/src/zenremotestore/builds/filebuildstorage.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/filebuildstorage.h> +#include <zenremotestore/builds/filebuildstorage.h> #include <zencore/basicfile.h> #include <zencore/compactbinarybuilder.h> diff --git a/src/zenutil/jupiter/jupiterbuildstorage.cpp b/src/zenremotestore/builds/jupiterbuildstorage.cpp index c9278acb4..14a5ecc85 100644 --- a/src/zenutil/jupiter/jupiterbuildstorage.cpp +++ b/src/zenremotestore/builds/jupiterbuildstorage.cpp @@ -1,18 +1,21 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/jupiter/jupiterbuildstorage.h> +#include <zenremotestore/builds/jupiterbuildstorage.h> #include <zencore/compactbinarybuilder.h> +#include <zencore/compactbinaryutil.h> #include <zencore/fmtutils.h> #include <zencore/scopeguard.h> #include <zencore/timer.h> #include <zencore/trace.h> -#include <zenutil/jupiter/jupitersession.h> +#include <zenremotestore/jupiter/jupitersession.h> ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_map.h> ZEN_THIRD_PARTY_INCLUDES_END +#include <regex> + namespace zen { using namespace std::literals; @@ -430,13 +433,41 @@ private: } else if (Payload.GetContentType() == ZenContentType::kCbObject) { - return LoadCompactBinaryObject(Payload); + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject Object = ValidateAndReadCompactBinaryObject(IoBuffer(Payload), ValidateResult); + ValidateResult == CbValidateError::None) + { + return Object; + } + else + { + throw std::runtime_error(fmt::format("{}: {} ({})", + "Invalid compact binary object: '{}'", + ErrorContext, + ToString(Payload.GetContentType()), + ToString(ValidateResult))); + } } else if (Payload.GetContentType() == ZenContentType::kCompressedBinary) { - IoHash RawHash; - uint64_t RawSize; - return LoadCompactBinaryObject(CompressedBuffer::FromCompressed(SharedBuffer(Payload), RawHash, RawSize)); + IoHash RawHash; + uint64_t RawSize; + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject Object = + ValidateAndReadCompactBinaryObject(CompressedBuffer::FromCompressed(SharedBuffer(Payload), RawHash, RawSize), + ValidateResult); + ValidateResult == CbValidateError::None) + { + return Object; + } + else + { + throw std::runtime_error(fmt::format("{}: {} ({})", + "Invalid compresed compact binary object: '{}'", + ErrorContext, + ToString(Payload.GetContentType()), + ToString(ValidateResult))); + } } else { @@ -482,4 +513,49 @@ CreateJupiterBuildStorage(LoggerRef InLog, return std::make_unique<JupiterBuildStorage>(InLog, InHttpClient, Stats, Namespace, Bucket, AllowRedirect, TempFolderPath); } +bool +ParseBuildStorageUrl(std::string_view InUrl, + std::string& OutHost, + std::string& OutNamespace, + std::string& OutBucket, + std::string& OutBuildId) +{ + std::string Url(InUrl); + const std::string_view ExtendedApiString = "api/v2/builds/"; + if (auto ApiString = ToLower(Url).find(ExtendedApiString); ApiString != std::string::npos) + { + Url.erase(ApiString, ExtendedApiString.length()); + } + + const std::string ArtifactURLRegExString = R"((http[s]?:\/\/.*?)\/(.*?)\/(.*?)\/(.*))"; + const std::regex ArtifactURLRegEx(ArtifactURLRegExString, std::regex::ECMAScript | std::regex::icase); + std::match_results<std::string_view::const_iterator> MatchResults; + std::string_view UrlToParse(Url); + if (regex_match(begin(UrlToParse), end(UrlToParse), MatchResults, ArtifactURLRegEx) && MatchResults.size() == 5) + { + auto GetMatch = [&MatchResults](uint32_t Index) -> std::string_view { + ZEN_ASSERT(Index < MatchResults.size()); + + const auto& Match = MatchResults[Index]; + + return std::string_view(&*Match.first, Match.second - Match.first); + }; + + const std::string_view Host = GetMatch(1); + const std::string_view Namespace = GetMatch(2); + const std::string_view Bucket = GetMatch(3); + const std::string_view BuildId = GetMatch(4); + + OutHost = Host; + OutNamespace = Namespace; + OutBucket = Bucket; + OutBuildId = BuildId; + return true; + } + else + { + return false; + } +} + } // namespace zen diff --git a/src/zenutil/chunkblock.cpp b/src/zenremotestore/chunking/chunkblock.cpp index abfc0fb63..05ae13de1 100644 --- a/src/zenutil/chunkblock.cpp +++ b/src/zenremotestore/chunking/chunkblock.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/chunkblock.h> +#include <zenremotestore/chunking/chunkblock.h> #include <zencore/compactbinarybuilder.h> #include <zencore/fmtutils.h> @@ -8,6 +8,13 @@ #include <vector> +#if ZEN_WITH_TESTS +# include <zencore/testing.h> +# include <zencore/testutils.h> + +# include <unordered_map> +#endif // ZEN_WITH_TESTS + namespace zen { using namespace std::literals; @@ -254,4 +261,60 @@ IterateChunkBlock(const SharedBuffer& BlockPayload, return true; }; +#if ZEN_WITH_TESTS + +namespace testutils { + static std::vector<std::pair<Oid, CompressedBuffer>> CreateAttachments( + const std::span<const size_t>& Sizes, + OodleCompressionLevel CompressionLevel = OodleCompressionLevel::VeryFast, + uint64_t BlockSize = 0) + { + std::vector<std::pair<Oid, CompressedBuffer>> Result; + Result.reserve(Sizes.size()); + for (size_t Size : Sizes) + { + CompressedBuffer Compressed = + CompressedBuffer::Compress(SharedBuffer(CreateSemiRandomBlob(Size)), OodleCompressor::Mermaid, CompressionLevel, BlockSize); + Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed)); + } + return Result; + } + +} // namespace testutils + +TEST_CASE("project.store.block") +{ + using namespace std::literals; + using namespace testutils; + + std::vector<std::size_t> AttachmentSizes({7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 3466, 1093, 4269, 2257, 3685, 3489, + 7194, 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 4024, 1582, 5251, + 491, 5464, 4607, 8135, 3767, 4045, 4415, 5007, 8876, 6761, 3359, 8526, 4097, 4855, 8225}); + + std::vector<std::pair<Oid, CompressedBuffer>> AttachmentsWithId = CreateAttachments(AttachmentSizes); + std::vector<std::pair<IoHash, FetchChunkFunc>> Chunks; + Chunks.reserve(AttachmentSizes.size()); + for (const auto& It : AttachmentsWithId) + { + Chunks.push_back( + std::make_pair(It.second.DecodeRawHash(), [Buffer = It.second](const IoHash&) -> std::pair<uint64_t, CompressedBuffer> { + return {Buffer.DecodeRawSize(), Buffer}; + })); + } + ChunkBlockDescription Block; + CompressedBuffer BlockBuffer = GenerateChunkBlock(std::move(Chunks), Block); + uint64_t HeaderSize; + CHECK(IterateChunkBlock( + BlockBuffer.Decompress(), + [](CompressedBuffer&&, const IoHash&) {}, + HeaderSize)); +} + +void +chunkblock_forcelink() +{ +} + +#endif // ZEN_WITH_TESTS + } // namespace zen diff --git a/src/zenutil/chunkedcontent.cpp b/src/zenremotestore/chunking/chunkedcontent.cpp index cd1bf7dd7..9df7725db 100644 --- a/src/zenutil/chunkedcontent.cpp +++ b/src/zenremotestore/chunking/chunkedcontent.cpp @@ -1,18 +1,16 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/chunkedcontent.h> +#include <zenremotestore/chunking/chunkedcontent.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> +#include <zencore/parallelwork.h> #include <zencore/scopeguard.h> #include <zencore/timer.h> #include <zencore/trace.h> - -#include <zenutil/chunkedfile.h> -#include <zenutil/chunkingcontroller.h> -#include <zenutil/parallelwork.h> -#include <zenutil/workerpools.h> +#include <zenremotestore/chunking/chunkedfile.h> +#include <zenremotestore/chunking/chunkingcontroller.h> ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_set.h> @@ -805,7 +803,7 @@ ChunkFolderContent(ChunkingStatistics& Stats, RwLock Lock; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); for (uint32_t PathIndex : Order) { @@ -930,7 +928,9 @@ BuildChunkedContentLookup(const ChunkedFolderContent& Content) for (uint32_t PathIndex = 0; PathIndex < Content.Paths.size(); PathIndex++) { std::string LowercaseExtension = Content.Paths[PathIndex].extension().string(); - std::transform(LowercaseExtension.begin(), LowercaseExtension.end(), LowercaseExtension.begin(), ::tolower); + std::transform(LowercaseExtension.begin(), LowercaseExtension.end(), LowercaseExtension.begin(), [](char c) { + return (char)::tolower(c); + }); Result.PathExtensionHash[PathIndex] = HashStringDjb2(LowercaseExtension); if (Content.RawSizes[PathIndex] > 0) { diff --git a/src/zenutil/chunkedfile.cpp b/src/zenremotestore/chunking/chunkedfile.cpp index a2c041ffd..652110605 100644 --- a/src/zenutil/chunkedfile.cpp +++ b/src/zenremotestore/chunking/chunkedfile.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/chunkedfile.h> +#include <zenremotestore/chunking/chunkedfile.h> #include <zencore/basicfile.h> #include <zencore/trace.h> diff --git a/src/zenutil/chunking.cpp b/src/zenremotestore/chunking/chunking.cpp index 71f0a06e4..71f0a06e4 100644 --- a/src/zenutil/chunking.cpp +++ b/src/zenremotestore/chunking/chunking.cpp diff --git a/src/zenutil/chunking.h b/src/zenremotestore/chunking/chunking.h index 09c56454f..09c56454f 100644 --- a/src/zenutil/chunking.h +++ b/src/zenremotestore/chunking/chunking.h diff --git a/src/zenutil/chunkingcontroller.cpp b/src/zenremotestore/chunking/chunkingcontroller.cpp index 6fb4182c0..49332c2ce 100644 --- a/src/zenutil/chunkingcontroller.cpp +++ b/src/zenremotestore/chunking/chunkingcontroller.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/chunkingcontroller.h> +#include <zenremotestore/chunking/chunkingcontroller.h> #include <zencore/basicfile.h> #include <zencore/compactbinarybuilder.h> diff --git a/src/zenutil/include/zenutil/buildstorage.h b/src/zenremotestore/include/zenremotestore/builds/buildstorage.h index 46ecd0a11..ee0ddcaa4 100644 --- a/src/zenutil/include/zenutil/buildstorage.h +++ b/src/zenremotestore/include/zenremotestore/builds/buildstorage.h @@ -3,7 +3,7 @@ #pragma once #include <zencore/compactbinary.h> -#include <zenutil/chunkblock.h> +#include <zenremotestore/chunking/chunkblock.h> ZEN_THIRD_PARTY_INCLUDES_START #include <tsl/robin_map.h> diff --git a/src/zenutil/include/zenutil/buildstoragecache.h b/src/zenremotestore/include/zenremotestore/builds/buildstoragecache.h index a0690a16a..e30270848 100644 --- a/src/zenutil/include/zenutil/buildstoragecache.h +++ b/src/zenremotestore/include/zenremotestore/builds/buildstoragecache.h @@ -6,11 +6,12 @@ #include <zencore/compactbinary.h> #include <zencore/compositebuffer.h> -#include <zenutil/chunkblock.h> +#include <zenremotestore/chunking/chunkblock.h> namespace zen { class HttpClient; +class WorkerThreadPool; class BuildStorageCache { @@ -56,5 +57,14 @@ std::unique_ptr<BuildStorageCache> CreateZenBuildStorageCache(HttpClient& H std::string_view Namespace, std::string_view Bucket, const std::filesystem::path& TempFolderPath, - bool BoostBackgroundThreadCount); + WorkerThreadPool& BackgroundWorkerPool); + +struct ZenCacheEndpointTestResult +{ + bool Success = false; + std::string FailureReason; +}; + +ZenCacheEndpointTestResult TestZenCacheEndpoint(std::string_view BaseUrl, const bool AssumeHttp2); + } // namespace zen diff --git a/src/zenutil/include/zenutil/filebuildstorage.h b/src/zenremotestore/include/zenremotestore/builds/filebuildstorage.h index c95fb32e6..8c1e3c5df 100644 --- a/src/zenutil/include/zenutil/filebuildstorage.h +++ b/src/zenremotestore/include/zenremotestore/builds/filebuildstorage.h @@ -3,7 +3,7 @@ #pragma once #include <zencore/logging.h> -#include <zenutil/buildstorage.h> +#include <zenremotestore/builds/buildstorage.h> namespace zen { class HttpClient; diff --git a/src/zenutil/include/zenutil/jupiter/jupiterbuildstorage.h b/src/zenremotestore/include/zenremotestore/builds/jupiterbuildstorage.h index bbf070993..9e25ead7a 100644 --- a/src/zenutil/include/zenutil/jupiter/jupiterbuildstorage.h +++ b/src/zenremotestore/include/zenremotestore/builds/jupiterbuildstorage.h @@ -3,7 +3,7 @@ #pragma once #include <zencore/logging.h> -#include <zenutil/buildstorage.h> +#include <zenremotestore/builds/buildstorage.h> namespace zen { class HttpClient; @@ -15,4 +15,11 @@ std::unique_ptr<BuildStorage> CreateJupiterBuildStorage(LoggerRef InLog, std::string_view Bucket, bool AllowRedirect, const std::filesystem::path& TempFolderPath); + +bool ParseBuildStorageUrl(std::string_view InUrl, + std::string& OutHost, + std::string& OutNamespace, + std::string& OutBucket, + std::string& OutBuildId); + } // namespace zen diff --git a/src/zenutil/include/zenutil/chunkblock.h b/src/zenremotestore/include/zenremotestore/chunking/chunkblock.h index 277580c74..b0d8ef24c 100644 --- a/src/zenutil/include/zenutil/chunkblock.h +++ b/src/zenremotestore/include/zenremotestore/chunking/chunkblock.h @@ -37,4 +37,6 @@ bool IterateChunkBlock(const SharedBuffer& BlockPayload, uint64_t& OutHeaderSize); std::vector<uint32_t> ReadChunkBlockHeader(const MemoryView BlockView, uint64_t& OutHeaderSize); +void chunkblock_forcelink(); + } // namespace zen diff --git a/src/zenutil/include/zenutil/chunkedcontent.h b/src/zenremotestore/include/zenremotestore/chunking/chunkedcontent.h index 306a5d990..306a5d990 100644 --- a/src/zenutil/include/zenutil/chunkedcontent.h +++ b/src/zenremotestore/include/zenremotestore/chunking/chunkedcontent.h diff --git a/src/zenutil/include/zenutil/chunkedfile.h b/src/zenremotestore/include/zenremotestore/chunking/chunkedfile.h index 4cec80fdb..4cec80fdb 100644 --- a/src/zenutil/include/zenutil/chunkedfile.h +++ b/src/zenremotestore/include/zenremotestore/chunking/chunkedfile.h diff --git a/src/zenutil/include/zenutil/chunkingcontroller.h b/src/zenremotestore/include/zenremotestore/chunking/chunkingcontroller.h index 315502265..2d1ba36aa 100644 --- a/src/zenutil/include/zenutil/chunkingcontroller.h +++ b/src/zenremotestore/include/zenremotestore/chunking/chunkingcontroller.h @@ -4,7 +4,7 @@ #include <zencore/compactbinary.h> -#include <zenutil/chunkedfile.h> +#include <zenremotestore/chunking/chunkedfile.h> #include <atomic> #include <filesystem> diff --git a/src/zenutil/include/zenutil/jupiter/jupiterclient.h b/src/zenremotestore/include/zenremotestore/jupiter/jupiterclient.h index 8a51bd60a..8a51bd60a 100644 --- a/src/zenutil/include/zenutil/jupiter/jupiterclient.h +++ b/src/zenremotestore/include/zenremotestore/jupiter/jupiterclient.h diff --git a/src/zenremotestore/include/zenremotestore/jupiter/jupiterhost.h b/src/zenremotestore/include/zenremotestore/jupiter/jupiterhost.h new file mode 100644 index 000000000..3bbc700b8 --- /dev/null +++ b/src/zenremotestore/include/zenremotestore/jupiter/jupiterhost.h @@ -0,0 +1,35 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <string> +#include <string_view> +#include <vector> + +namespace zen { + +struct HttpClientSettings; + +struct JupiterServerDiscovery +{ + struct EndPoint + { + std::string Name; + std::string BaseUrl; + bool AssumeHttp2 = false; + }; + std::vector<EndPoint> ServerEndPoints; + std::vector<EndPoint> CacheEndPoints; +}; + +JupiterServerDiscovery DiscoverJupiterEndpoints(std::string_view Host, const HttpClientSettings& ClientSettings); + +struct JupiterEndpointTestResult +{ + bool Success = false; + std::string FailureReason; +}; + +JupiterEndpointTestResult TestJupiterEndpoint(std::string_view BaseUrl, const bool AssumeHttp2); + +} // namespace zen diff --git a/src/zenutil/include/zenutil/jupiter/jupitersession.h b/src/zenremotestore/include/zenremotestore/jupiter/jupitersession.h index b79790f25..b79790f25 100644 --- a/src/zenutil/include/zenutil/jupiter/jupitersession.h +++ b/src/zenremotestore/include/zenremotestore/jupiter/jupitersession.h diff --git a/src/zenserver/projectstore/buildsremoteprojectstore.h b/src/zenremotestore/include/zenremotestore/projectstore/buildsremoteprojectstore.h index 60b6caef7..037325ed1 100644 --- a/src/zenserver/projectstore/buildsremoteprojectstore.h +++ b/src/zenremotestore/include/zenremotestore/projectstore/buildsremoteprojectstore.h @@ -2,7 +2,7 @@ #pragma once -#include "remoteprojectstore.h" +#include <zenremotestore/projectstore/remoteprojectstore.h> namespace zen { @@ -26,6 +26,8 @@ struct BuildsRemoteStoreOptions : RemoteStoreOptions std::shared_ptr<RemoteProjectStore> CreateJupiterBuildsRemoteStore(const BuildsRemoteStoreOptions& Options, const std::filesystem::path& TempFilePath, - bool Quiet); + bool Quiet, + bool Unattended, + bool Hidden); } // namespace zen diff --git a/src/zenserver/projectstore/fileremoteprojectstore.h b/src/zenremotestore/include/zenremotestore/projectstore/fileremoteprojectstore.h index 8da9692d5..ff2ecb405 100644 --- a/src/zenserver/projectstore/fileremoteprojectstore.h +++ b/src/zenremotestore/include/zenremotestore/projectstore/fileremoteprojectstore.h @@ -2,7 +2,7 @@ #pragma once -#include "remoteprojectstore.h" +#include <zenremotestore/projectstore/remoteprojectstore.h> namespace zen { diff --git a/src/zenserver/projectstore/jupiterremoteprojectstore.h b/src/zenremotestore/include/zenremotestore/projectstore/jupiterremoteprojectstore.h index ac2d25b47..13a346039 100644 --- a/src/zenserver/projectstore/jupiterremoteprojectstore.h +++ b/src/zenremotestore/include/zenremotestore/projectstore/jupiterremoteprojectstore.h @@ -2,7 +2,7 @@ #pragma once -#include "remoteprojectstore.h" +#include <zenremotestore/projectstore/remoteprojectstore.h> namespace zen { @@ -26,6 +26,8 @@ struct JupiterRemoteStoreOptions : RemoteStoreOptions std::shared_ptr<RemoteProjectStore> CreateJupiterRemoteStore(const JupiterRemoteStoreOptions& Options, const std::filesystem::path& TempFilePath, - bool Quiet); + bool Quiet, + bool Unattended, + bool Hidden); } // namespace zen diff --git a/src/zenserver/projectstore/remoteprojectstore.h b/src/zenremotestore/include/zenremotestore/projectstore/remoteprojectstore.h index 1210afc7c..fbcdde955 100644 --- a/src/zenserver/projectstore/remoteprojectstore.h +++ b/src/zenremotestore/include/zenremotestore/projectstore/remoteprojectstore.h @@ -3,9 +3,9 @@ #pragma once #include <zencore/jobqueue.h> -#include "projectstore.h" +#include <zenstore/projectstore.h> -#include <zenutil/chunkblock.h> +#include <zenremotestore/chunking/chunkblock.h> #include <unordered_set> @@ -110,10 +110,12 @@ public: struct RemoteStoreOptions { static const size_t DefaultMaxBlockSize = 64u * 1024u * 1024u; + static const size_t DefaultMaxChunksPerBlock = 4u * 1000u; static const size_t DefaultMaxChunkEmbedSize = 3u * 512u * 1024u; static const size_t DefaultChunkFileSizeLimit = 256u * 1024u * 1024u; size_t MaxBlockSize = DefaultMaxBlockSize; + size_t MaxChunksPerBlock = DefaultMaxChunksPerBlock; size_t MaxChunkEmbedSize = DefaultMaxChunkEmbedSize; size_t ChunkFileSizeLimit = DefaultChunkFileSizeLimit; }; @@ -124,7 +126,9 @@ RemoteProjectStore::LoadContainerResult BuildContainer( CidStore& ChunkStore, ProjectStore::Project& Project, ProjectStore::Oplog& Oplog, + WorkerThreadPool& WorkerPool, size_t MaxBlockSize, + size_t MaxChunksPerBlock, size_t MaxChunkEmbedSize, size_t ChunkFileSizeLimit, bool BuildBlocks, @@ -150,7 +154,10 @@ RemoteProjectStore::Result SaveOplog(CidStore& ChunkStore, RemoteProjectStore& RemoteStore, ProjectStore::Project& Project, ProjectStore::Oplog& Oplog, + WorkerThreadPool& NetworkWorkerPool, + WorkerThreadPool& WorkerPool, size_t MaxBlockSize, + size_t MaxChunksPerBlock, size_t MaxChunkEmbedSize, size_t ChunkFileSizeLimit, bool EmbedLooseFiles, @@ -161,6 +168,8 @@ RemoteProjectStore::Result SaveOplog(CidStore& ChunkStore, RemoteProjectStore::Result LoadOplog(CidStore& ChunkStore, RemoteProjectStore& RemoteStore, ProjectStore::Oplog& Oplog, + WorkerThreadPool& NetworkWorkerPool, + WorkerThreadPool& WorkerPool, bool ForceDownload, bool IgnoreMissingAttachments, bool CleanOplog, @@ -169,4 +178,6 @@ RemoteProjectStore::Result LoadOplog(CidStore& ChunkStore, std::vector<IoHash> GetBlockHashesFromOplog(CbObjectView ContainerObject); std::vector<ThinChunkBlockDescription> GetBlocksFromOplog(CbObjectView ContainerObject, std::span<const IoHash> IncludeBlockHashes); +void remoteprojectstore_forcelink(); + } // namespace zen diff --git a/src/zenserver/projectstore/zenremoteprojectstore.h b/src/zenremotestore/include/zenremotestore/projectstore/zenremoteprojectstore.h index 7c81a597d..d6b62c563 100644 --- a/src/zenserver/projectstore/zenremoteprojectstore.h +++ b/src/zenremotestore/include/zenremotestore/projectstore/zenremoteprojectstore.h @@ -2,7 +2,7 @@ #pragma once -#include "remoteprojectstore.h" +#include <zenremotestore/projectstore/remoteprojectstore.h> namespace zen { diff --git a/src/zenremotestore/include/zenremotestore/zenremotestore.h b/src/zenremotestore/include/zenremotestore/zenremotestore.h new file mode 100644 index 000000000..576c8ee75 --- /dev/null +++ b/src/zenremotestore/include/zenremotestore/zenremotestore.h @@ -0,0 +1,13 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zencore/zencore.h> + +#define ZENSTORE_API + +namespace zen { + +ZENSTORE_API void zenremotestore_forcelinktests(); + +} // namespace zen diff --git a/src/zenutil/jupiter/jupiterclient.cpp b/src/zenremotestore/jupiter/jupiterclient.cpp index dbac218a4..bf9d8e346 100644 --- a/src/zenutil/jupiter/jupiterclient.cpp +++ b/src/zenremotestore/jupiter/jupiterclient.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/jupiter/jupiterclient.h> +#include <zenremotestore/jupiter/jupiterclient.h> namespace zen { diff --git a/src/zenremotestore/jupiter/jupiterhost.cpp b/src/zenremotestore/jupiter/jupiterhost.cpp new file mode 100644 index 000000000..df6be10c9 --- /dev/null +++ b/src/zenremotestore/jupiter/jupiterhost.cpp @@ -0,0 +1,66 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenremotestore/jupiter/jupiterhost.h> + +#include <zencore/compactbinary.h> +#include <zencore/fmtutils.h> +#include <zenhttp/httpclient.h> + +namespace zen { + +JupiterServerDiscovery +DiscoverJupiterEndpoints(std::string_view Host, const HttpClientSettings& ClientSettings) +{ + JupiterServerDiscovery Result; + + HttpClient DiscoveryHttpClient(Host, ClientSettings); + HttpClient::Response ServerInfoResponse = DiscoveryHttpClient.Get("/api/v1/status/servers", HttpClient::Accept(HttpContentType::kJSON)); + if (!ServerInfoResponse.IsSuccess()) + { + ServerInfoResponse.ThrowError(fmt::format("Failed to get list of servers from discovery url '{}'", Host)); + } + std::string_view JsonResponse = ServerInfoResponse.AsText(); + CbObject CbPayload = LoadCompactBinaryFromJson(JsonResponse).AsObject(); + CbArrayView ServerEndpoints = CbPayload["serverEndpoints"].AsArrayView(); + Result.ServerEndPoints.reserve(ServerEndpoints.Num()); + + auto ParseEndPoints = [](CbArrayView ServerEndpoints) -> std::vector<JupiterServerDiscovery::EndPoint> { + std::vector<JupiterServerDiscovery::EndPoint> Result; + + Result.reserve(ServerEndpoints.Num()); + for (CbFieldView ServerEndpointView : ServerEndpoints) + { + CbObjectView ServerEndPoint = ServerEndpointView.AsObjectView(); + Result.push_back(JupiterServerDiscovery::EndPoint{.Name = std::string(ServerEndPoint["name"].AsString()), + .BaseUrl = std::string(ServerEndPoint["baseUrl"].AsString()), + .AssumeHttp2 = ServerEndPoint["baseUrl"].AsBool(false)}); + } + return Result; + }; + + Result.ServerEndPoints = ParseEndPoints(CbPayload["serverEndpoints"].AsArrayView()); + Result.CacheEndPoints = ParseEndPoints(CbPayload["cacheEndpoints"].AsArrayView()); + + return Result; +} + +JupiterEndpointTestResult +TestJupiterEndpoint(std::string_view BaseUrl, const bool AssumeHttp2) +{ + HttpClientSettings TestClientSettings{.LogCategory = "httpbuildsclient", + .ConnectTimeout = std::chrono::milliseconds{1000}, + .Timeout = std::chrono::milliseconds{2000}, + .AssumeHttp2 = AssumeHttp2, + .AllowResume = true, + .RetryCount = 0}; + + HttpClient TestHttpClient(BaseUrl, TestClientSettings); + HttpClient::Response TestResponse = TestHttpClient.Get("/health/live"); + if (TestResponse.IsSuccess()) + { + return {.Success = true}; + } + return {.Success = false, .FailureReason = TestResponse.ErrorMessage("")}; +} + +} // namespace zen diff --git a/src/zenutil/jupiter/jupitersession.cpp b/src/zenremotestore/jupiter/jupitersession.cpp index ab2a2ea55..942e2a9dc 100644 --- a/src/zenutil/jupiter/jupitersession.cpp +++ b/src/zenremotestore/jupiter/jupitersession.cpp @@ -1,9 +1,10 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/jupiter/jupitersession.h> +#include <zenremotestore/jupiter/jupitersession.h> #include <zencore/compactbinary.h> #include <zencore/compactbinarybuilder.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compositebuffer.h> #include <zencore/compress.h> #include <zencore/fmtutils.h> @@ -524,7 +525,15 @@ JupiterSession::PutMultipartBuildBlob(std::string_view Namespace, ZEN_WARN("{}", StartMultipartResponse.ErrorMessage("startMultipartUpload: ")); return detail::ConvertResponse(StartMultipartResponse, "JupiterSession::PutMultipartBuildBlob"sv); } - CbObject ResponseObject = LoadCompactBinaryObject(StartMultipartResponse.ResponsePayload); + CbValidateError ValidateResult = CbValidateError::None; + CbObject ResponseObject = ValidateAndReadCompactBinaryObject(IoBuffer(StartMultipartResponse.ResponsePayload), ValidateResult); + if (ValidateResult != CbValidateError::None) + { + JupiterResult Result = detail::ConvertResponse(StartMultipartResponse, "JupiterSession::PutMultipartBuildBlob"sv); + Result.ErrorCode = (int32)HttpResponseCode::UnsupportedMediaType; + Result.Reason = fmt::format("Invalid multipart response object format: '{}'", ToString(ValidateResult)); + return Result; + } struct WorkloadData { diff --git a/src/zenserver/projectstore/buildsremoteprojectstore.cpp b/src/zenremotestore/projectstore/buildsremoteprojectstore.cpp index a9dd48510..2cc8ed4aa 100644 --- a/src/zenserver/projectstore/buildsremoteprojectstore.cpp +++ b/src/zenremotestore/projectstore/buildsremoteprojectstore.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include "buildsremoteprojectstore.h" +#include <zenremotestore/projectstore/buildsremoteprojectstore.h> #include <zencore/compactbinarybuilder.h> #include <zencore/compress.h> @@ -8,7 +8,7 @@ #include <zencore/scopeguard.h> #include <zenhttp/httpclientauth.h> -#include <zenutil/jupiter/jupiterbuildstorage.h> +#include <zenremotestore/builds/jupiterbuildstorage.h> namespace zen { @@ -255,7 +255,9 @@ public: } catch (const HttpClientError& Ex) { - Result.ErrorCode = Ex.m_Error != 0 ? Ex.m_Error : Ex.m_ResponseCode != HttpResponseCode::ImATeapot ? (int)Ex.m_ResponseCode : 0; + Result.ErrorCode = Ex.GetInternalErrorCode() != 0 ? Ex.GetInternalErrorCode() + : Ex.GetHttpResponseCode() != HttpResponseCode::ImATeapot ? (int)Ex.GetHttpResponseCode() + : 0; Result.Reason = fmt::format("Failed finalizing oplog container build part to {}/{}/{}/{}/{}. Reason: '{}'", m_Url, m_Namespace, @@ -284,9 +286,9 @@ public: } catch (const HttpClientError& Ex) { - Result.ErrorCode = Ex.m_Error != 0 ? Ex.m_Error - : Ex.m_ResponseCode != HttpResponseCode::ImATeapot ? (int)Ex.m_ResponseCode - : 0; + Result.ErrorCode = Ex.GetInternalErrorCode() != 0 ? Ex.GetInternalErrorCode() + : Ex.GetHttpResponseCode() != HttpResponseCode::ImATeapot ? (int)Ex.GetHttpResponseCode() + : 0; Result.Reason = fmt::format("Failed finalizing oplog container build to {}/{}/{}/{}. Reason: '{}'", m_Url, m_Namespace, @@ -452,7 +454,9 @@ public: private: static int MakeErrorCode(const HttpClientError& Ex) { - return Ex.m_Error != 0 ? Ex.m_Error : Ex.m_ResponseCode != HttpResponseCode::ImATeapot ? (int)Ex.m_ResponseCode : 0; + return Ex.GetInternalErrorCode() != 0 ? Ex.GetInternalErrorCode() + : Ex.GetHttpResponseCode() != HttpResponseCode::ImATeapot ? (int)Ex.GetHttpResponseCode() + : 0; } std::unique_ptr<BuildStorage::Statistics> m_BuildStorageStats; @@ -470,7 +474,11 @@ private: }; std::shared_ptr<RemoteProjectStore> -CreateJupiterBuildsRemoteStore(const BuildsRemoteStoreOptions& Options, const std::filesystem::path& TempFilePath, bool Quiet) +CreateJupiterBuildsRemoteStore(const BuildsRemoteStoreOptions& Options, + const std::filesystem::path& TempFilePath, + bool Quiet, + bool Unattended, + bool Hidden) { std::string Url = Options.Url; if (Url.find("://"sv) == std::string::npos) @@ -495,7 +503,8 @@ CreateJupiterBuildsRemoteStore(const BuildsRemoteStoreOptions& Options, const st } else if (!Options.OidcExePath.empty()) { - if (auto TokenProviderMaybe = httpclientauth::CreateFromOidcTokenExecutable(Options.OidcExePath, Url, Quiet); TokenProviderMaybe) + if (auto TokenProviderMaybe = httpclientauth::CreateFromOidcTokenExecutable(Options.OidcExePath, Url, Quiet, Unattended, Hidden); + TokenProviderMaybe) { TokenProvider = TokenProviderMaybe.value(); } diff --git a/src/zenserver/projectstore/fileremoteprojectstore.cpp b/src/zenremotestore/projectstore/fileremoteprojectstore.cpp index 375e44e59..d6e6944f4 100644 --- a/src/zenserver/projectstore/fileremoteprojectstore.cpp +++ b/src/zenremotestore/projectstore/fileremoteprojectstore.cpp @@ -1,12 +1,14 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include "fileremoteprojectstore.h" +#include <zenremotestore/projectstore/fileremoteprojectstore.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compress.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zencore/timer.h> +#include <zenhttp/httpcommon.h> namespace zen { @@ -260,11 +262,14 @@ private: ContainerPayload = ContainerFile.ReadAll(); } AddStats(0, ContainerPayload.GetSize(), Timer.GetElapsedTimeUs() * 1000); - Result.ContainerObject = LoadCompactBinaryObject(ContainerPayload); - if (!Result.ContainerObject) + CbValidateError ValidateResult = CbValidateError::None; + if (Result.ContainerObject = ValidateAndReadCompactBinaryObject(std::move(ContainerPayload), ValidateResult); + ValidateResult != CbValidateError::None || !Result.ContainerObject) { Result.ErrorCode = gsl::narrow<int32_t>(HttpResponseCode::InternalServerError); - Result.Reason = fmt::format("The file {} is not formatted as a compact binary object", SourcePath.string()); + Result.Reason = fmt::format("The file {} is not formatted as a compact binary object ('{}')", + SourcePath.string(), + ToString(ValidateResult)); Result.ElapsedSeconds = Timer.GetElapsedTimeMs() / 1000.0; return Result; } diff --git a/src/zenserver/projectstore/jupiterremoteprojectstore.cpp b/src/zenremotestore/projectstore/jupiterremoteprojectstore.cpp index d79ad3cb7..dda5ef99d 100644 --- a/src/zenserver/projectstore/jupiterremoteprojectstore.cpp +++ b/src/zenremotestore/projectstore/jupiterremoteprojectstore.cpp @@ -1,14 +1,15 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include "jupiterremoteprojectstore.h" +#include <zenremotestore/projectstore/jupiterremoteprojectstore.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compress.h> #include <zencore/fmtutils.h> #include <zenhttp/httpclientauth.h> -#include <zenutil/jupiter/jupiterclient.h> -#include <zenutil/jupiter/jupitersession.h> +#include <zenremotestore/jupiter/jupiterclient.h> +#include <zenremotestore/jupiter/jupitersession.h> namespace zen { @@ -249,8 +250,9 @@ private: return Result; } - CbObject ContainerObject = LoadCompactBinaryObject(GetResult.Response); - if (!ContainerObject) + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject ContainerObject = ValidateAndReadCompactBinaryObject(IoBuffer(GetResult.Response), ValidateResult); + ValidateResult != CbValidateError::None || !ContainerObject) { return LoadContainerResult{ RemoteProjectStore::Result{.ErrorCode = gsl::narrow<int32_t>(HttpResponseCode::InternalServerError), @@ -262,7 +264,10 @@ private: Key)}, {}}; } - return LoadContainerResult{ConvertResult(GetResult), std::move(ContainerObject)}; + else + { + return LoadContainerResult{ConvertResult(GetResult), std::move(ContainerObject)}; + } } void AddStats(const JupiterResult& Result) @@ -338,7 +343,11 @@ private: }; std::shared_ptr<RemoteProjectStore> -CreateJupiterRemoteStore(const JupiterRemoteStoreOptions& Options, const std::filesystem::path& TempFilePath, bool Quiet) +CreateJupiterRemoteStore(const JupiterRemoteStoreOptions& Options, + const std::filesystem::path& TempFilePath, + bool Quiet, + bool Unattended, + bool Hidden) { std::string Url = Options.Url; if (Url.find("://"sv) == std::string::npos) @@ -369,7 +378,8 @@ CreateJupiterRemoteStore(const JupiterRemoteStoreOptions& Options, const std::fi } else if (!Options.OidcExePath.empty()) { - if (auto TokenProviderMaybe = httpclientauth::CreateFromOidcTokenExecutable(Options.OidcExePath, Url, Quiet); TokenProviderMaybe) + if (auto TokenProviderMaybe = httpclientauth::CreateFromOidcTokenExecutable(Options.OidcExePath, Url, Quiet, Unattended, Hidden); + TokenProviderMaybe) { TokenProvider = TokenProviderMaybe.value(); } diff --git a/src/zenserver/projectstore/remoteprojectstore.cpp b/src/zenremotestore/projectstore/remoteprojectstore.cpp index 857e868da..0d1e0d93d 100644 --- a/src/zenserver/projectstore/remoteprojectstore.cpp +++ b/src/zenremotestore/projectstore/remoteprojectstore.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include "remoteprojectstore.h" +#include <zenremotestore/projectstore/remoteprojectstore.h> #include <zencore/compactbinarybuilder.h> #include <zencore/compactbinaryutil.h> @@ -12,12 +12,18 @@ #include <zencore/stream.h> #include <zencore/timer.h> #include <zencore/workthreadpool.h> +#include <zenhttp/httpcommon.h> +#include <zenremotestore/chunking/chunkedfile.h> #include <zenstore/cidstore.h> -#include <zenutil/chunkedfile.h> -#include <zenutil/workerpools.h> #include <unordered_map> +#if ZEN_WITH_TESTS +# include <zencore/testing.h> +# include <zencore/testutils.h> +# include <zenremotestore/projectstore/fileremoteprojectstore.h> +#endif // ZEN_WITH_TESTS + namespace zen { /* @@ -180,7 +186,7 @@ namespace remotestore_impl { { Ops.emplace_back(CbObjectView(&OpsData[OpDataOffset])); } - std::vector<uint32_t> OpLsns = Oplog.AppendNewOplogEntries(Ops); + std::vector<ProjectStore::LogSequenceNumber> OpLsns = Oplog.AppendNewOplogEntries(Ops); OpsCompleteCount += OpLsns.size(); OpsData.clear(); OpDataOffsets.clear(); @@ -245,101 +251,105 @@ namespace remotestore_impl { const std::vector<IoHash>& Chunks) { AttachmentsDownloadLatch.AddCount(1); - NetworkWorkerPool.ScheduleWork([&RemoteStore, - &ChunkStore, - &WorkerPool, - &AttachmentsDownloadLatch, - &AttachmentsWriteLatch, - &RemoteResult, - Chunks = Chunks, - &Info, - &LoadAttachmentsTimer, - &DownloadStartMS, - IgnoreMissingAttachments, - OptionalContext]() { - auto _ = MakeGuard([&AttachmentsDownloadLatch] { AttachmentsDownloadLatch.CountDown(); }); - if (RemoteResult.IsError()) - { - return; - } - try - { - uint64_t Unset = (std::uint64_t)-1; - DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs()); - RemoteProjectStore::LoadAttachmentsResult Result = RemoteStore.LoadAttachments(Chunks); - if (Result.ErrorCode) - { - ReportMessage(OptionalContext, - fmt::format("Failed to load attachments with {} chunks ({}): {}", - Chunks.size(), - RemoteResult.GetError(), - RemoteResult.GetErrorReason())); - Info.MissingAttachmentCount.fetch_add(1); - if (IgnoreMissingAttachments) - { - RemoteResult.SetError(Result.ErrorCode, Result.Reason, Result.Text); - } - return; - } - Info.AttachmentsDownloaded.fetch_add(Chunks.size()); - ZEN_INFO("Loaded {} bulk attachments in {}", - Chunks.size(), - NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000))); + NetworkWorkerPool.ScheduleWork( + [&RemoteStore, + &ChunkStore, + &WorkerPool, + &AttachmentsDownloadLatch, + &AttachmentsWriteLatch, + &RemoteResult, + Chunks = Chunks, + &Info, + &LoadAttachmentsTimer, + &DownloadStartMS, + IgnoreMissingAttachments, + OptionalContext]() { + auto _ = MakeGuard([&AttachmentsDownloadLatch] { AttachmentsDownloadLatch.CountDown(); }); if (RemoteResult.IsError()) { return; } - AttachmentsWriteLatch.AddCount(1); - WorkerPool.ScheduleWork([&AttachmentsWriteLatch, &RemoteResult, &Info, &ChunkStore, Chunks = std::move(Result.Chunks)]() { - auto _ = MakeGuard([&AttachmentsWriteLatch] { AttachmentsWriteLatch.CountDown(); }); - if (RemoteResult.IsError()) + try + { + uint64_t Unset = (std::uint64_t)-1; + DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs()); + RemoteProjectStore::LoadAttachmentsResult Result = RemoteStore.LoadAttachments(Chunks); + if (Result.ErrorCode) { + ReportMessage(OptionalContext, + fmt::format("Failed to load attachments with {} chunks ({}): {}", + Chunks.size(), + RemoteResult.GetError(), + RemoteResult.GetErrorReason())); + Info.MissingAttachmentCount.fetch_add(1); + if (IgnoreMissingAttachments) + { + RemoteResult.SetError(Result.ErrorCode, Result.Reason, Result.Text); + } return; } - if (!Chunks.empty()) + Info.AttachmentsDownloaded.fetch_add(Chunks.size()); + ZEN_INFO("Loaded {} bulk attachments in {}", + Chunks.size(), + NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000))); + if (RemoteResult.IsError()) { - try - { - std::vector<IoBuffer> WriteAttachmentBuffers; - std::vector<IoHash> WriteRawHashes; - WriteAttachmentBuffers.reserve(Chunks.size()); - WriteRawHashes.reserve(Chunks.size()); - - for (const auto& It : Chunks) + return; + } + AttachmentsWriteLatch.AddCount(1); + WorkerPool.ScheduleWork( + [&AttachmentsWriteLatch, &RemoteResult, &Info, &ChunkStore, Chunks = std::move(Result.Chunks)]() { + auto _ = MakeGuard([&AttachmentsWriteLatch] { AttachmentsWriteLatch.CountDown(); }); + if (RemoteResult.IsError()) { - uint64_t ChunkSize = It.second.GetCompressedSize(); - Info.AttachmentBytesDownloaded.fetch_add(ChunkSize); - WriteAttachmentBuffers.push_back(It.second.GetCompressed().Flatten().AsIoBuffer()); - WriteRawHashes.push_back(It.first); + return; } - std::vector<CidStore::InsertResult> InsertResults = - ChunkStore.AddChunks(WriteAttachmentBuffers, WriteRawHashes, CidStore::InsertMode::kCopyOnly); - - for (size_t Index = 0; Index < InsertResults.size(); Index++) + if (!Chunks.empty()) { - if (InsertResults[Index].New) + try { - Info.AttachmentBytesStored.fetch_add(WriteAttachmentBuffers[Index].GetSize()); - Info.AttachmentsStored.fetch_add(1); + std::vector<IoBuffer> WriteAttachmentBuffers; + std::vector<IoHash> WriteRawHashes; + WriteAttachmentBuffers.reserve(Chunks.size()); + WriteRawHashes.reserve(Chunks.size()); + + for (const auto& It : Chunks) + { + uint64_t ChunkSize = It.second.GetCompressedSize(); + Info.AttachmentBytesDownloaded.fetch_add(ChunkSize); + WriteAttachmentBuffers.push_back(It.second.GetCompressed().Flatten().AsIoBuffer()); + WriteRawHashes.push_back(It.first); + } + std::vector<CidStore::InsertResult> InsertResults = + ChunkStore.AddChunks(WriteAttachmentBuffers, WriteRawHashes, CidStore::InsertMode::kCopyOnly); + + for (size_t Index = 0; Index < InsertResults.size(); Index++) + { + if (InsertResults[Index].New) + { + Info.AttachmentBytesStored.fetch_add(WriteAttachmentBuffers[Index].GetSize()); + Info.AttachmentsStored.fetch_add(1); + } + } + } + catch (const std::exception& Ex) + { + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("Failed to bulk save {} attachments", Chunks.size()), + Ex.what()); } } - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("Failed to bulk save {} attachments", Chunks.size()), - Ex.what()); - } - } - }); - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("Failed to bulk load {} attachments", Chunks.size()), - Ex.what()); - } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); + } + catch (const std::exception& Ex) + { + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("Failed to bulk load {} attachments", Chunks.size()), + Ex.what()); + } + }, + WorkerThreadPool::EMode::EnableBacklog); }; void DownloadAndSaveBlock(CidStore& ChunkStore, @@ -359,226 +369,237 @@ namespace remotestore_impl { uint32_t RetriesLeft) { AttachmentsDownloadLatch.AddCount(1); - NetworkWorkerPool.ScheduleWork([&AttachmentsDownloadLatch, - &AttachmentsWriteLatch, - &ChunkStore, - &RemoteStore, - &NetworkWorkerPool, - &WorkerPool, - BlockHash, - &RemoteResult, - &Info, - &LoadAttachmentsTimer, - &DownloadStartMS, - IgnoreMissingAttachments, - OptionalContext, - RetriesLeft, - Chunks = Chunks]() { - auto _ = MakeGuard([&AttachmentsDownloadLatch] { AttachmentsDownloadLatch.CountDown(); }); - if (RemoteResult.IsError()) - { - return; - } - try - { - uint64_t Unset = (std::uint64_t)-1; - DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs()); - RemoteProjectStore::LoadAttachmentResult BlockResult = RemoteStore.LoadAttachment(BlockHash); - if (BlockResult.ErrorCode) - { - ReportMessage(OptionalContext, - fmt::format("Failed to download block attachment {} ({}): {}", - BlockHash, - RemoteResult.GetError(), - RemoteResult.GetErrorReason())); - Info.MissingAttachmentCount.fetch_add(1); - if (!IgnoreMissingAttachments) - { - RemoteResult.SetError(BlockResult.ErrorCode, BlockResult.Reason, BlockResult.Text); - } - return; - } + NetworkWorkerPool.ScheduleWork( + [&AttachmentsDownloadLatch, + &AttachmentsWriteLatch, + &ChunkStore, + &RemoteStore, + &NetworkWorkerPool, + &WorkerPool, + BlockHash, + &RemoteResult, + &Info, + &LoadAttachmentsTimer, + &DownloadStartMS, + IgnoreMissingAttachments, + OptionalContext, + RetriesLeft, + Chunks = Chunks]() { + auto _ = MakeGuard([&AttachmentsDownloadLatch] { AttachmentsDownloadLatch.CountDown(); }); if (RemoteResult.IsError()) { return; } - uint64_t BlockSize = BlockResult.Bytes.GetSize(); - Info.AttachmentBlocksDownloaded.fetch_add(1); - ZEN_INFO("Loaded block attachment '{}' in {} ({})", - BlockHash, - NiceTimeSpanMs(static_cast<uint64_t>(BlockResult.ElapsedSeconds * 1000)), - NiceBytes(BlockSize)); - Info.AttachmentBlockBytesDownloaded.fetch_add(BlockSize); - - AttachmentsWriteLatch.AddCount(1); - WorkerPool.ScheduleWork([&AttachmentsDownloadLatch, - &AttachmentsWriteLatch, - &ChunkStore, - &RemoteStore, - &NetworkWorkerPool, - &WorkerPool, - BlockHash, - &RemoteResult, - &Info, - &LoadAttachmentsTimer, - &DownloadStartMS, - IgnoreMissingAttachments, - OptionalContext, - RetriesLeft, - Chunks = Chunks, - Bytes = std::move(BlockResult.Bytes)]() { - auto _ = MakeGuard([&AttachmentsWriteLatch] { AttachmentsWriteLatch.CountDown(); }); - if (RemoteResult.IsError()) + try + { + uint64_t Unset = (std::uint64_t)-1; + DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs()); + RemoteProjectStore::LoadAttachmentResult BlockResult = RemoteStore.LoadAttachment(BlockHash); + if (BlockResult.ErrorCode) { + ReportMessage(OptionalContext, + fmt::format("Failed to download block attachment {} ({}): {}", + BlockHash, + RemoteResult.GetError(), + RemoteResult.GetErrorReason())); + Info.MissingAttachmentCount.fetch_add(1); + if (!IgnoreMissingAttachments) + { + RemoteResult.SetError(BlockResult.ErrorCode, BlockResult.Reason, BlockResult.Text); + } return; } - try + if (RemoteResult.IsError()) { - ZEN_ASSERT(Bytes.Size() > 0); - std::unordered_set<IoHash, IoHash::Hasher> WantedChunks; - WantedChunks.reserve(Chunks.size()); - WantedChunks.insert(Chunks.begin(), Chunks.end()); - std::vector<IoBuffer> WriteAttachmentBuffers; - std::vector<IoHash> WriteRawHashes; - - IoHash RawHash; - uint64_t RawSize; - CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Bytes), RawHash, RawSize); - if (!Compressed) - { - if (RetriesLeft > 0) + return; + } + uint64_t BlockSize = BlockResult.Bytes.GetSize(); + Info.AttachmentBlocksDownloaded.fetch_add(1); + ZEN_INFO("Loaded block attachment '{}' in {} ({})", + BlockHash, + NiceTimeSpanMs(static_cast<uint64_t>(BlockResult.ElapsedSeconds * 1000)), + NiceBytes(BlockSize)); + Info.AttachmentBlockBytesDownloaded.fetch_add(BlockSize); + + AttachmentsWriteLatch.AddCount(1); + WorkerPool.ScheduleWork( + [&AttachmentsDownloadLatch, + &AttachmentsWriteLatch, + &ChunkStore, + &RemoteStore, + &NetworkWorkerPool, + &WorkerPool, + BlockHash, + &RemoteResult, + &Info, + &LoadAttachmentsTimer, + &DownloadStartMS, + IgnoreMissingAttachments, + OptionalContext, + RetriesLeft, + Chunks = Chunks, + Bytes = std::move(BlockResult.Bytes)]() { + auto _ = MakeGuard([&AttachmentsWriteLatch] { AttachmentsWriteLatch.CountDown(); }); + if (RemoteResult.IsError()) { - ReportMessage( - OptionalContext, - fmt::format("Block attachment {} is malformed, can't parse as compressed binary, retrying download", - BlockHash)); - return DownloadAndSaveBlock(ChunkStore, - RemoteStore, - IgnoreMissingAttachments, - OptionalContext, - NetworkWorkerPool, - WorkerPool, - AttachmentsDownloadLatch, - AttachmentsWriteLatch, - RemoteResult, - Info, - LoadAttachmentsTimer, - DownloadStartMS, - BlockHash, - std::move(Chunks), - RetriesLeft - 1); + return; } - ReportMessage(OptionalContext, - fmt::format("Block attachment {} is malformed, can't parse as compressed binary", BlockHash)); - RemoteResult.SetError( - gsl::narrow<int32_t>(HttpResponseCode::InternalServerError), - fmt::format("Block attachment {} is malformed, can't parse as compressed binary", BlockHash), - {}); - return; - } - SharedBuffer BlockPayload = Compressed.Decompress(); - if (!BlockPayload) - { - if (RetriesLeft > 0) + try { - ReportMessage(OptionalContext, - fmt::format("Block attachment {} is malformed, can't decompress payload, retrying download", - BlockHash)); - return DownloadAndSaveBlock(ChunkStore, - RemoteStore, - IgnoreMissingAttachments, - OptionalContext, - NetworkWorkerPool, - WorkerPool, - AttachmentsDownloadLatch, - AttachmentsWriteLatch, - RemoteResult, - Info, - LoadAttachmentsTimer, - DownloadStartMS, - BlockHash, - std::move(Chunks), - RetriesLeft - 1); - } - ReportMessage(OptionalContext, - fmt::format("Block attachment {} is malformed, can't decompress payload", BlockHash)); - RemoteResult.SetError(gsl::narrow<int32_t>(HttpResponseCode::InternalServerError), - fmt::format("Block attachment {} is malformed, can't decompress payload", BlockHash), - {}); - return; - } - if (RawHash != BlockHash) - { - ReportMessage(OptionalContext, - fmt::format("Block attachment {} has mismatching raw hash ({})", BlockHash, RawHash)); - RemoteResult.SetError(gsl::narrow<int32_t>(HttpResponseCode::InternalServerError), - fmt::format("Block attachment {} has mismatching raw hash ({})", BlockHash, RawHash), - {}); - return; - } + ZEN_ASSERT(Bytes.Size() > 0); + std::unordered_set<IoHash, IoHash::Hasher> WantedChunks; + WantedChunks.reserve(Chunks.size()); + WantedChunks.insert(Chunks.begin(), Chunks.end()); + std::vector<IoBuffer> WriteAttachmentBuffers; + std::vector<IoHash> WriteRawHashes; + + IoHash RawHash; + uint64_t RawSize; + CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Bytes), RawHash, RawSize); + if (!Compressed) + { + if (RetriesLeft > 0) + { + ReportMessage( + OptionalContext, + fmt::format( + "Block attachment {} is malformed, can't parse as compressed binary, retrying download", + BlockHash)); + return DownloadAndSaveBlock(ChunkStore, + RemoteStore, + IgnoreMissingAttachments, + OptionalContext, + NetworkWorkerPool, + WorkerPool, + AttachmentsDownloadLatch, + AttachmentsWriteLatch, + RemoteResult, + Info, + LoadAttachmentsTimer, + DownloadStartMS, + BlockHash, + std::move(Chunks), + RetriesLeft - 1); + } + ReportMessage( + OptionalContext, + fmt::format("Block attachment {} is malformed, can't parse as compressed binary", BlockHash)); + RemoteResult.SetError( + gsl::narrow<int32_t>(HttpResponseCode::InternalServerError), + fmt::format("Block attachment {} is malformed, can't parse as compressed binary", BlockHash), + {}); + return; + } + SharedBuffer BlockPayload = Compressed.Decompress(); + if (!BlockPayload) + { + if (RetriesLeft > 0) + { + ReportMessage( + OptionalContext, + fmt::format("Block attachment {} is malformed, can't decompress payload, retrying download", + BlockHash)); + return DownloadAndSaveBlock(ChunkStore, + RemoteStore, + IgnoreMissingAttachments, + OptionalContext, + NetworkWorkerPool, + WorkerPool, + AttachmentsDownloadLatch, + AttachmentsWriteLatch, + RemoteResult, + Info, + LoadAttachmentsTimer, + DownloadStartMS, + BlockHash, + std::move(Chunks), + RetriesLeft - 1); + } + ReportMessage(OptionalContext, + fmt::format("Block attachment {} is malformed, can't decompress payload", BlockHash)); + RemoteResult.SetError( + gsl::narrow<int32_t>(HttpResponseCode::InternalServerError), + fmt::format("Block attachment {} is malformed, can't decompress payload", BlockHash), + {}); + return; + } + if (RawHash != BlockHash) + { + ReportMessage(OptionalContext, + fmt::format("Block attachment {} has mismatching raw hash ({})", BlockHash, RawHash)); + RemoteResult.SetError( + gsl::narrow<int32_t>(HttpResponseCode::InternalServerError), + fmt::format("Block attachment {} has mismatching raw hash ({})", BlockHash, RawHash), + {}); + return; + } - uint64_t BlockHeaderSize = 0; - bool StoreChunksOK = IterateChunkBlock( - BlockPayload, - [&WantedChunks, &WriteAttachmentBuffers, &WriteRawHashes, &Info](CompressedBuffer&& Chunk, - const IoHash& AttachmentRawHash) { - if (WantedChunks.contains(AttachmentRawHash)) - { - WriteAttachmentBuffers.emplace_back(Chunk.GetCompressed().Flatten().AsIoBuffer()); - IoHash RawHash; - uint64_t RawSize; - ZEN_ASSERT(CompressedBuffer::ValidateCompressedHeader(WriteAttachmentBuffers.back(), RawHash, RawSize)); - ZEN_ASSERT(RawHash == AttachmentRawHash); - WriteRawHashes.emplace_back(AttachmentRawHash); - WantedChunks.erase(AttachmentRawHash); - } - }, - BlockHeaderSize); - - if (!StoreChunksOK) - { - ReportMessage(OptionalContext, - fmt::format("Block attachment {} has invalid format ({}): {}", - BlockHash, - RemoteResult.GetError(), - RemoteResult.GetErrorReason())); - RemoteResult.SetError(gsl::narrow<int32_t>(HttpResponseCode::InternalServerError), - fmt::format("Invalid format for block {}", BlockHash), - {}); - return; - } + uint64_t BlockHeaderSize = 0; + bool StoreChunksOK = IterateChunkBlock( + BlockPayload, + [&WantedChunks, &WriteAttachmentBuffers, &WriteRawHashes, &Info](CompressedBuffer&& Chunk, + const IoHash& AttachmentRawHash) { + if (WantedChunks.contains(AttachmentRawHash)) + { + WriteAttachmentBuffers.emplace_back(Chunk.GetCompressed().Flatten().AsIoBuffer()); + IoHash RawHash; + uint64_t RawSize; + ZEN_ASSERT(CompressedBuffer::ValidateCompressedHeader(WriteAttachmentBuffers.back(), + RawHash, + RawSize)); + ZEN_ASSERT(RawHash == AttachmentRawHash); + WriteRawHashes.emplace_back(AttachmentRawHash); + WantedChunks.erase(AttachmentRawHash); + } + }, + BlockHeaderSize); + + if (!StoreChunksOK) + { + ReportMessage(OptionalContext, + fmt::format("Block attachment {} has invalid format ({}): {}", + BlockHash, + RemoteResult.GetError(), + RemoteResult.GetErrorReason())); + RemoteResult.SetError(gsl::narrow<int32_t>(HttpResponseCode::InternalServerError), + fmt::format("Invalid format for block {}", BlockHash), + {}); + return; + } - ZEN_ASSERT(WantedChunks.empty()); + ZEN_ASSERT(WantedChunks.empty()); - if (!WriteAttachmentBuffers.empty()) - { - auto Results = ChunkStore.AddChunks(WriteAttachmentBuffers, WriteRawHashes); - for (size_t Index = 0; Index < Results.size(); Index++) - { - const auto& Result = Results[Index]; - if (Result.New) + if (!WriteAttachmentBuffers.empty()) { - Info.AttachmentBytesStored.fetch_add(WriteAttachmentBuffers[Index].GetSize()); - Info.AttachmentsStored.fetch_add(1); + auto Results = ChunkStore.AddChunks(WriteAttachmentBuffers, WriteRawHashes); + for (size_t Index = 0; Index < Results.size(); Index++) + { + const auto& Result = Results[Index]; + if (Result.New) + { + Info.AttachmentBytesStored.fetch_add(WriteAttachmentBuffers[Index].GetSize()); + Info.AttachmentsStored.fetch_add(1); + } + } } } - } - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("Failed save block attachment {}", BlockHash), - Ex.what()); - } - }); - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("Failed to block attachment {}", BlockHash), - Ex.what()); - } - }); + catch (const std::exception& Ex) + { + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("Failed save block attachment {}", BlockHash), + Ex.what()); + } + }, + WorkerThreadPool::EMode::EnableBacklog); + } + catch (const std::exception& Ex) + { + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("Failed to block attachment {}", BlockHash), + Ex.what()); + } + }, + WorkerThreadPool::EMode::EnableBacklog); }; void DownloadAndSaveAttachment(CidStore& ChunkStore, @@ -596,92 +617,96 @@ namespace remotestore_impl { const IoHash& RawHash) { AttachmentsDownloadLatch.AddCount(1); - NetworkWorkerPool.ScheduleWork([&RemoteStore, - &ChunkStore, - &WorkerPool, - &RemoteResult, - &AttachmentsDownloadLatch, - &AttachmentsWriteLatch, - RawHash, - &LoadAttachmentsTimer, - &DownloadStartMS, - &Info, - IgnoreMissingAttachments, - OptionalContext]() { - auto _ = MakeGuard([&AttachmentsDownloadLatch] { AttachmentsDownloadLatch.CountDown(); }); - if (RemoteResult.IsError()) - { - return; - } - try - { - uint64_t Unset = (std::uint64_t)-1; - DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs()); - RemoteProjectStore::LoadAttachmentResult AttachmentResult = RemoteStore.LoadAttachment(RawHash); - if (AttachmentResult.ErrorCode) - { - ReportMessage(OptionalContext, - fmt::format("Failed to download large attachment {}: '{}', error code : {}", - RawHash, - AttachmentResult.Reason, - AttachmentResult.ErrorCode)); - Info.MissingAttachmentCount.fetch_add(1); - if (!IgnoreMissingAttachments) - { - RemoteResult.SetError(AttachmentResult.ErrorCode, AttachmentResult.Reason, AttachmentResult.Text); - } - return; - } - uint64_t AttachmentSize = AttachmentResult.Bytes.GetSize(); - ZEN_INFO("Loaded large attachment '{}' in {} ({})", - RawHash, - NiceTimeSpanMs(static_cast<uint64_t>(AttachmentResult.ElapsedSeconds * 1000)), - NiceBytes(AttachmentSize)); - Info.AttachmentsDownloaded.fetch_add(1); + NetworkWorkerPool.ScheduleWork( + [&RemoteStore, + &ChunkStore, + &WorkerPool, + &RemoteResult, + &AttachmentsDownloadLatch, + &AttachmentsWriteLatch, + RawHash, + &LoadAttachmentsTimer, + &DownloadStartMS, + &Info, + IgnoreMissingAttachments, + OptionalContext]() { + auto _ = MakeGuard([&AttachmentsDownloadLatch] { AttachmentsDownloadLatch.CountDown(); }); if (RemoteResult.IsError()) { return; } - Info.AttachmentBytesDownloaded.fetch_add(AttachmentSize); - - AttachmentsWriteLatch.AddCount(1); - WorkerPool.ScheduleWork([&AttachmentsWriteLatch, - &RemoteResult, - &Info, - &ChunkStore, - RawHash, - AttachmentSize, - Bytes = std::move(AttachmentResult.Bytes), - OptionalContext]() { - auto _ = MakeGuard([&AttachmentsWriteLatch] { AttachmentsWriteLatch.CountDown(); }); - if (RemoteResult.IsError()) - { - return; - } - try + try + { + uint64_t Unset = (std::uint64_t)-1; + DownloadStartMS.compare_exchange_strong(Unset, LoadAttachmentsTimer.GetElapsedTimeMs()); + RemoteProjectStore::LoadAttachmentResult AttachmentResult = RemoteStore.LoadAttachment(RawHash); + if (AttachmentResult.ErrorCode) { - CidStore::InsertResult InsertResult = ChunkStore.AddChunk(Bytes, RawHash); - if (InsertResult.New) + ReportMessage(OptionalContext, + fmt::format("Failed to download large attachment {}: '{}', error code : {}", + RawHash, + AttachmentResult.Reason, + AttachmentResult.ErrorCode)); + Info.MissingAttachmentCount.fetch_add(1); + if (!IgnoreMissingAttachments) { - Info.AttachmentBytesStored.fetch_add(AttachmentSize); - Info.AttachmentsStored.fetch_add(1); + RemoteResult.SetError(AttachmentResult.ErrorCode, AttachmentResult.Reason, AttachmentResult.Text); } + return; } - catch (const std::exception& Ex) + uint64_t AttachmentSize = AttachmentResult.Bytes.GetSize(); + ZEN_INFO("Loaded large attachment '{}' in {} ({})", + RawHash, + NiceTimeSpanMs(static_cast<uint64_t>(AttachmentResult.ElapsedSeconds * 1000)), + NiceBytes(AttachmentSize)); + Info.AttachmentsDownloaded.fetch_add(1); + if (RemoteResult.IsError()) { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("Saving attachment {} failed", RawHash), - Ex.what()); + return; } - }); - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("Loading attachment {} failed", RawHash), - Ex.what()); - } - }); + Info.AttachmentBytesDownloaded.fetch_add(AttachmentSize); + + AttachmentsWriteLatch.AddCount(1); + WorkerPool.ScheduleWork( + [&AttachmentsWriteLatch, + &RemoteResult, + &Info, + &ChunkStore, + RawHash, + AttachmentSize, + Bytes = std::move(AttachmentResult.Bytes), + OptionalContext]() { + auto _ = MakeGuard([&AttachmentsWriteLatch] { AttachmentsWriteLatch.CountDown(); }); + if (RemoteResult.IsError()) + { + return; + } + try + { + CidStore::InsertResult InsertResult = ChunkStore.AddChunk(Bytes, RawHash); + if (InsertResult.New) + { + Info.AttachmentBytesStored.fetch_add(AttachmentSize); + Info.AttachmentsStored.fetch_add(1); + } + } + catch (const std::exception& Ex) + { + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("Saving attachment {} failed", RawHash), + Ex.what()); + } + }, + WorkerThreadPool::EMode::EnableBacklog); + } + catch (const std::exception& Ex) + { + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("Loading attachment {} failed", RawHash), + Ex.what()); + } + }, + WorkerThreadPool::EMode::EnableBacklog); }; void CreateBlock(WorkerThreadPool& WorkerPool, @@ -694,45 +719,47 @@ namespace remotestore_impl { AsyncRemoteResult& RemoteResult) { OpSectionsLatch.AddCount(1); - WorkerPool.ScheduleWork([&Blocks, - &SectionsLock, - &OpSectionsLatch, - BlockIndex, - Chunks = std::move(ChunksInBlock), - &AsyncOnBlock, - &RemoteResult]() mutable { - auto _ = MakeGuard([&OpSectionsLatch] { OpSectionsLatch.CountDown(); }); - if (RemoteResult.IsError()) - { - return; - } - size_t ChunkCount = Chunks.size(); - try - { - ZEN_ASSERT(ChunkCount > 0); - Stopwatch Timer; - ChunkBlockDescription Block; - CompressedBuffer CompressedBlock = GenerateChunkBlock(std::move(Chunks), Block); - IoHash BlockHash = CompressedBlock.DecodeRawHash(); + WorkerPool.ScheduleWork( + [&Blocks, + &SectionsLock, + &OpSectionsLatch, + BlockIndex, + Chunks = std::move(ChunksInBlock), + &AsyncOnBlock, + &RemoteResult]() mutable { + auto _ = MakeGuard([&OpSectionsLatch] { OpSectionsLatch.CountDown(); }); + if (RemoteResult.IsError()) { - // We can share the lock as we are not resizing the vector and only touch BlockHash at our own index - RwLock::SharedLockScope __(SectionsLock); - Blocks[BlockIndex] = Block; + return; } - uint64_t BlockSize = CompressedBlock.GetCompressedSize(); - AsyncOnBlock(std::move(CompressedBlock), std::move(Block)); - ZEN_INFO("Generated block with {} attachments in {} ({})", - ChunkCount, - NiceTimeSpanMs(Timer.GetElapsedTimeMs()), - NiceBytes(BlockSize)); - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("Failed creating block {} with {} chunks", BlockIndex, ChunkCount), - Ex.what()); - } - }); + size_t ChunkCount = Chunks.size(); + try + { + ZEN_ASSERT(ChunkCount > 0); + Stopwatch Timer; + ChunkBlockDescription Block; + CompressedBuffer CompressedBlock = GenerateChunkBlock(std::move(Chunks), Block); + IoHash BlockHash = CompressedBlock.DecodeRawHash(); + { + // We can share the lock as we are not resizing the vector and only touch BlockHash at our own index + RwLock::SharedLockScope __(SectionsLock); + Blocks[BlockIndex] = Block; + } + uint64_t BlockSize = CompressedBlock.GetCompressedSize(); + AsyncOnBlock(std::move(CompressedBlock), std::move(Block)); + ZEN_INFO("Generated block with {} attachments in {} ({})", + ChunkCount, + NiceTimeSpanMs(Timer.GetElapsedTimeMs()), + NiceBytes(BlockSize)); + } + catch (const std::exception& Ex) + { + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("Failed creating block {} with {} chunks", BlockIndex, ChunkCount), + Ex.what()); + } + }, + WorkerThreadPool::EMode::EnableBacklog); } struct UploadInfo @@ -861,89 +888,91 @@ namespace remotestore_impl { SaveAttachmentsLatch.AddCount(1); AttachmentsToSave++; - WorkerPool.ScheduleWork([&ChunkStore, - &RemoteStore, - &SaveAttachmentsLatch, - &RemoteResult, - RawHash, - &CreatedBlocks, - &LooseFileAttachments, - &Info, - OptionalContext]() { - auto _ = MakeGuard([&SaveAttachmentsLatch] { SaveAttachmentsLatch.CountDown(); }); - if (RemoteResult.IsError()) - { - return; - } - try - { - IoBuffer Payload; - ChunkBlockDescription Block; - if (auto BlockIt = CreatedBlocks.find(RawHash); BlockIt != CreatedBlocks.end()) - { - Payload = BlockIt->second.Payload; - Block = BlockIt->second.Block; - } - else if (auto LooseTmpFileIt = LooseFileAttachments.find(RawHash); LooseTmpFileIt != LooseFileAttachments.end()) - { - Payload = LooseTmpFileIt->second(RawHash); - } - else - { - Payload = ChunkStore.FindChunkByCid(RawHash); - } - if (!Payload) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::NotFound), - fmt::format("Failed to find attachment {}", RawHash), - {}); - ZEN_WARN("Failed to save attachment '{}' ({}): {}", - RawHash, - RemoteResult.GetError(), - RemoteResult.GetErrorReason()); - return; - } - const bool IsBlock = Block.BlockHash == RawHash; - size_t PayloadSize = Payload.GetSize(); - RemoteProjectStore::SaveAttachmentResult Result = - RemoteStore.SaveAttachment(CompositeBuffer(SharedBuffer(std::move(Payload))), RawHash, std::move(Block)); - if (Result.ErrorCode) + WorkerPool.ScheduleWork( + [&ChunkStore, + &RemoteStore, + &SaveAttachmentsLatch, + &RemoteResult, + RawHash, + &CreatedBlocks, + &LooseFileAttachments, + &Info, + OptionalContext]() { + auto _ = MakeGuard([&SaveAttachmentsLatch] { SaveAttachmentsLatch.CountDown(); }); + if (RemoteResult.IsError()) { - RemoteResult.SetError(Result.ErrorCode, Result.Reason, Result.Text); - ReportMessage(OptionalContext, - fmt::format("Failed to save attachment '{}', {} ({}): {}", - RawHash, - NiceBytes(PayloadSize), - RemoteResult.GetError(), - RemoteResult.GetErrorReason())); return; } - if (IsBlock) + try { - Info.AttachmentBlocksUploaded.fetch_add(1); - Info.AttachmentBlockBytesUploaded.fetch_add(PayloadSize); - ZEN_INFO("Saved block attachment '{}' in {} ({})", - RawHash, - NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000)), - NiceBytes(PayloadSize)); + IoBuffer Payload; + ChunkBlockDescription Block; + if (auto BlockIt = CreatedBlocks.find(RawHash); BlockIt != CreatedBlocks.end()) + { + Payload = BlockIt->second.Payload; + Block = BlockIt->second.Block; + } + else if (auto LooseTmpFileIt = LooseFileAttachments.find(RawHash); LooseTmpFileIt != LooseFileAttachments.end()) + { + Payload = LooseTmpFileIt->second(RawHash); + } + else + { + Payload = ChunkStore.FindChunkByCid(RawHash); + } + if (!Payload) + { + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::NotFound), + fmt::format("Failed to find attachment {}", RawHash), + {}); + ZEN_WARN("Failed to save attachment '{}' ({}): {}", + RawHash, + RemoteResult.GetError(), + RemoteResult.GetErrorReason()); + return; + } + const bool IsBlock = Block.BlockHash == RawHash; + size_t PayloadSize = Payload.GetSize(); + RemoteProjectStore::SaveAttachmentResult Result = + RemoteStore.SaveAttachment(CompositeBuffer(SharedBuffer(std::move(Payload))), RawHash, std::move(Block)); + if (Result.ErrorCode) + { + RemoteResult.SetError(Result.ErrorCode, Result.Reason, Result.Text); + ReportMessage(OptionalContext, + fmt::format("Failed to save attachment '{}', {} ({}): {}", + RawHash, + NiceBytes(PayloadSize), + RemoteResult.GetError(), + RemoteResult.GetErrorReason())); + return; + } + if (IsBlock) + { + Info.AttachmentBlocksUploaded.fetch_add(1); + Info.AttachmentBlockBytesUploaded.fetch_add(PayloadSize); + ZEN_INFO("Saved block attachment '{}' in {} ({})", + RawHash, + NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000)), + NiceBytes(PayloadSize)); + } + else + { + Info.AttachmentsUploaded.fetch_add(1); + Info.AttachmentBytesUploaded.fetch_add(PayloadSize); + ZEN_INFO("Saved large attachment '{}' in {} ({})", + RawHash, + NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000)), + NiceBytes(PayloadSize)); + } } - else + catch (const std::exception& Ex) { - Info.AttachmentsUploaded.fetch_add(1); - Info.AttachmentBytesUploaded.fetch_add(PayloadSize); - ZEN_INFO("Saved large attachment '{}' in {} ({})", - RawHash, - NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000)), - NiceBytes(PayloadSize)); + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("To upload attachment {}", RawHash), + Ex.what()); } - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("To upload attachment {}", RawHash), - Ex.what()); - } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); } if (IsCancelled(OptionalContext)) @@ -982,66 +1011,68 @@ namespace remotestore_impl { SaveAttachmentsLatch.AddCount(1); AttachmentsToSave++; - WorkerPool.ScheduleWork([&RemoteStore, - &ChunkStore, - &SaveAttachmentsLatch, - &RemoteResult, - NeededChunks = std::move(NeededChunks), - &BulkBlockAttachmentsToUpload, - &Info, - OptionalContext]() { - auto _ = MakeGuard([&SaveAttachmentsLatch] { SaveAttachmentsLatch.CountDown(); }); - if (RemoteResult.IsError()) - { - return; - } - try - { - size_t ChunksSize = 0; - std::vector<SharedBuffer> ChunkBuffers; - ChunkBuffers.reserve(NeededChunks.size()); - for (const IoHash& Chunk : NeededChunks) + WorkerPool.ScheduleWork( + [&RemoteStore, + &ChunkStore, + &SaveAttachmentsLatch, + &RemoteResult, + NeededChunks = std::move(NeededChunks), + &BulkBlockAttachmentsToUpload, + &Info, + OptionalContext]() { + auto _ = MakeGuard([&SaveAttachmentsLatch] { SaveAttachmentsLatch.CountDown(); }); + if (RemoteResult.IsError()) + { + return; + } + try { - auto It = BulkBlockAttachmentsToUpload.find(Chunk); - ZEN_ASSERT(It != BulkBlockAttachmentsToUpload.end()); - CompressedBuffer ChunkPayload = It->second(It->first).second; - if (!ChunkPayload) + size_t ChunksSize = 0; + std::vector<SharedBuffer> ChunkBuffers; + ChunkBuffers.reserve(NeededChunks.size()); + for (const IoHash& Chunk : NeededChunks) + { + auto It = BulkBlockAttachmentsToUpload.find(Chunk); + ZEN_ASSERT(It != BulkBlockAttachmentsToUpload.end()); + CompressedBuffer ChunkPayload = It->second(It->first).second; + if (!ChunkPayload) + { + RemoteResult.SetError(static_cast<int32_t>(HttpResponseCode::NotFound), + fmt::format("Missing chunk {}"sv, Chunk), + fmt::format("Unable to fetch attachment {} required by the oplog"sv, Chunk)); + ChunkBuffers.clear(); + break; + } + ChunksSize += ChunkPayload.GetCompressedSize(); + ChunkBuffers.emplace_back(SharedBuffer(std::move(ChunkPayload).GetCompressed().Flatten().AsIoBuffer())); + } + RemoteProjectStore::SaveAttachmentsResult Result = RemoteStore.SaveAttachments(ChunkBuffers); + if (Result.ErrorCode) { - RemoteResult.SetError(static_cast<int32_t>(HttpResponseCode::NotFound), - fmt::format("Missing chunk {}"sv, Chunk), - fmt::format("Unable to fetch attachment {} required by the oplog"sv, Chunk)); - ChunkBuffers.clear(); - break; + RemoteResult.SetError(Result.ErrorCode, Result.Reason, Result.Text); + ReportMessage(OptionalContext, + fmt::format("Failed to save attachments with {} chunks ({}): {}", + NeededChunks.size(), + RemoteResult.GetError(), + RemoteResult.GetErrorReason())); + return; } - ChunksSize += ChunkPayload.GetCompressedSize(); - ChunkBuffers.emplace_back(SharedBuffer(std::move(ChunkPayload).GetCompressed().Flatten().AsIoBuffer())); + Info.AttachmentsUploaded.fetch_add(ChunkBuffers.size()); + Info.AttachmentBytesUploaded.fetch_add(ChunksSize); + + ZEN_INFO("Saved {} bulk attachments in {} ({})", + NeededChunks.size(), + NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000)), + NiceBytes(ChunksSize)); } - RemoteProjectStore::SaveAttachmentsResult Result = RemoteStore.SaveAttachments(ChunkBuffers); - if (Result.ErrorCode) + catch (const std::exception& Ex) { - RemoteResult.SetError(Result.ErrorCode, Result.Reason, Result.Text); - ReportMessage(OptionalContext, - fmt::format("Failed to save attachments with {} chunks ({}): {}", - NeededChunks.size(), - RemoteResult.GetError(), - RemoteResult.GetErrorReason())); - return; + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("Failed to buck upload {} attachments", NeededChunks.size()), + Ex.what()); } - Info.AttachmentsUploaded.fetch_add(ChunkBuffers.size()); - Info.AttachmentBytesUploaded.fetch_add(ChunksSize); - - ZEN_INFO("Saved {} bulk attachments in {} ({})", - NeededChunks.size(), - NiceTimeSpanMs(static_cast<uint64_t>(Result.ElapsedSeconds * 1000)), - NiceBytes(ChunksSize)); - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("Failed to buck upload {} attachments", NeededChunks.size()), - Ex.what()); - } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); } } @@ -1142,6 +1173,7 @@ BuildContainer(CidStore& ChunkStore, ProjectStore::Project& Project, ProjectStore::Oplog& Oplog, size_t MaxBlockSize, + size_t MaxChunksPerBlock, size_t MaxChunkEmbedSize, size_t ChunkFileSizeLimit, bool BuildBlocks, @@ -1516,148 +1548,152 @@ BuildContainer(CidStore& ChunkStore, ResolveAttachmentsLatch.AddCount(1); - WorkerPool.ScheduleWork([&ChunkStore, - UploadAttachment = &It.second, - RawHash = It.first, - &ResolveAttachmentsLatch, - &ResolveLock, - &ChunkedHashes, - &LargeChunkHashes, - &ChunkedUploadAttachments, - &LooseUploadAttachments, - &MissingHashes, - &OnLargeAttachment, - &AttachmentTempPath, - &ChunkFile, - &ChunkedFiles, - MaxChunkEmbedSize, - ChunkFileSizeLimit, - AllowChunking, - &RemoteResult, - OptionalContext]() { - auto _ = MakeGuard([&ResolveAttachmentsLatch] { ResolveAttachmentsLatch.CountDown(); }); - if (remotestore_impl::IsCancelled(OptionalContext)) - { - return; - } + WorkerPool.ScheduleWork( + [&ChunkStore, + UploadAttachment = &It.second, + RawHash = It.first, + &ResolveAttachmentsLatch, + &ResolveLock, + &ChunkedHashes, + &LargeChunkHashes, + &ChunkedUploadAttachments, + &LooseUploadAttachments, + &MissingHashes, + &OnLargeAttachment, + &AttachmentTempPath, + &ChunkFile, + &ChunkedFiles, + MaxChunkEmbedSize, + ChunkFileSizeLimit, + AllowChunking, + &RemoteResult, + OptionalContext]() { + auto _ = MakeGuard([&ResolveAttachmentsLatch] { ResolveAttachmentsLatch.CountDown(); }); + if (remotestore_impl::IsCancelled(OptionalContext)) + { + return; + } - try - { - if (!UploadAttachment->RawPath.empty()) + try { - const std::filesystem::path& FilePath = UploadAttachment->RawPath; - IoBuffer RawData = IoBufferBuilder::MakeFromFile(FilePath); - if (RawData) + if (!UploadAttachment->RawPath.empty()) { - if (AllowChunking && RawData.GetSize() > ChunkFileSizeLimit) + const std::filesystem::path& FilePath = UploadAttachment->RawPath; + IoBuffer RawData = IoBufferBuilder::MakeFromFile(FilePath); + if (RawData) { - IoBufferFileReference FileRef; - (void)RawData.GetFileReference(FileRef); - - ChunkedFile Chunked = ChunkFile(RawHash, RawData, FileRef, OptionalContext); - ResolveLock.WithExclusiveLock( - [RawHash, &ChunkedFiles, &ChunkedUploadAttachments, &ChunkedHashes, &Chunked]() { - ChunkedUploadAttachments.insert_or_assign(RawHash, ChunkedFiles.size()); - ChunkedHashes.reserve(ChunkedHashes.size() + Chunked.Chunked.Info.ChunkHashes.size()); - for (const IoHash& ChunkHash : Chunked.Chunked.Info.ChunkHashes) - { - ChunkedHashes.insert(ChunkHash); - } - ChunkedFiles.emplace_back(std::move(Chunked)); + if (AllowChunking && RawData.GetSize() > ChunkFileSizeLimit) + { + IoBufferFileReference FileRef; + (void)RawData.GetFileReference(FileRef); + + ChunkedFile Chunked = ChunkFile(RawHash, RawData, FileRef, OptionalContext); + ResolveLock.WithExclusiveLock( + [RawHash, &ChunkedFiles, &ChunkedUploadAttachments, &ChunkedHashes, &Chunked]() { + ChunkedUploadAttachments.insert_or_assign(RawHash, ChunkedFiles.size()); + ChunkedHashes.reserve(ChunkedHashes.size() + Chunked.Chunked.Info.ChunkHashes.size()); + for (const IoHash& ChunkHash : Chunked.Chunked.Info.ChunkHashes) + { + ChunkedHashes.insert(ChunkHash); + } + ChunkedFiles.emplace_back(std::move(Chunked)); + }); + } + else if (RawData.GetSize() > (MaxChunkEmbedSize * 2)) + { + // Assume the compressed file is going to be larger than MaxChunkEmbedSize, even if it isn't + // it will be a loose attachment instead of going into a block + OnLargeAttachment(RawHash, [RawData = std::move(RawData), AttachmentTempPath](const IoHash& RawHash) { + size_t RawSize = RawData.GetSize(); + CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(std::move(RawData)), + OodleCompressor::Mermaid, + OodleCompressionLevel::VeryFast); + + std::filesystem::path AttachmentPath = AttachmentTempPath; + AttachmentPath.append(RawHash.ToHexString()); + IoBuffer TempAttachmentBuffer = + WriteToTempFile(std::move(Compressed).GetCompressed(), AttachmentPath); + ZEN_INFO("Saved temp attachment to '{}', {} ({})", + AttachmentPath, + NiceBytes(RawSize), + NiceBytes(TempAttachmentBuffer.GetSize())); + return TempAttachmentBuffer; }); - } - else if (RawData.GetSize() > (MaxChunkEmbedSize * 2)) - { - // Assume the compressed file is going to be larger than MaxChunkEmbedSize, even if it isn't - // it will be a loose attachment instead of going into a block - OnLargeAttachment(RawHash, [RawData = std::move(RawData), AttachmentTempPath](const IoHash& RawHash) { - size_t RawSize = RawData.GetSize(); - CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(std::move(RawData)), + ResolveLock.WithExclusiveLock([RawHash, &LargeChunkHashes]() { LargeChunkHashes.insert(RawHash); }); + } + else + { + uint64_t RawSize = RawData.GetSize(); + CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(RawData), OodleCompressor::Mermaid, OodleCompressionLevel::VeryFast); std::filesystem::path AttachmentPath = AttachmentTempPath; AttachmentPath.append(RawHash.ToHexString()); + + uint64_t CompressedSize = Compressed.GetCompressedSize(); IoBuffer TempAttachmentBuffer = WriteToTempFile(std::move(Compressed).GetCompressed(), AttachmentPath); ZEN_INFO("Saved temp attachment to '{}', {} ({})", AttachmentPath, NiceBytes(RawSize), NiceBytes(TempAttachmentBuffer.GetSize())); - return TempAttachmentBuffer; - }); - ResolveLock.WithExclusiveLock([RawHash, &LargeChunkHashes]() { LargeChunkHashes.insert(RawHash); }); + + if (CompressedSize > MaxChunkEmbedSize) + { + OnLargeAttachment(RawHash, + [Data = std::move(TempAttachmentBuffer)](const IoHash&) { return Data; }); + ResolveLock.WithExclusiveLock([RawHash, &LargeChunkHashes]() { LargeChunkHashes.insert(RawHash); }); + } + else + { + UploadAttachment->Size = CompressedSize; + ResolveLock.WithExclusiveLock( + [RawHash, RawSize, &LooseUploadAttachments, Data = std::move(TempAttachmentBuffer)]() { + LooseUploadAttachments.insert_or_assign(RawHash, std::make_pair(RawSize, std::move(Data))); + }); + } + } } else { - uint64_t RawSize = RawData.GetSize(); - CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(RawData), - OodleCompressor::Mermaid, - OodleCompressionLevel::VeryFast); - - std::filesystem::path AttachmentPath = AttachmentTempPath; - AttachmentPath.append(RawHash.ToHexString()); - - uint64_t CompressedSize = Compressed.GetCompressedSize(); - IoBuffer TempAttachmentBuffer = WriteToTempFile(std::move(Compressed).GetCompressed(), AttachmentPath); - ZEN_INFO("Saved temp attachment to '{}', {} ({})", - AttachmentPath, - NiceBytes(RawSize), - NiceBytes(TempAttachmentBuffer.GetSize())); - - if (CompressedSize > MaxChunkEmbedSize) - { - OnLargeAttachment(RawHash, [Data = std::move(TempAttachmentBuffer)](const IoHash&) { return Data; }); - ResolveLock.WithExclusiveLock([RawHash, &LargeChunkHashes]() { LargeChunkHashes.insert(RawHash); }); - } - else - { - UploadAttachment->Size = CompressedSize; - ResolveLock.WithExclusiveLock( - [RawHash, RawSize, &LooseUploadAttachments, Data = std::move(TempAttachmentBuffer)]() { - LooseUploadAttachments.insert_or_assign(RawHash, std::make_pair(RawSize, std::move(Data))); - }); - } + ResolveLock.WithExclusiveLock([RawHash, &MissingHashes]() { MissingHashes.insert(RawHash); }); } } else { - ResolveLock.WithExclusiveLock([RawHash, &MissingHashes]() { MissingHashes.insert(RawHash); }); - } - } - else - { - IoBuffer Data = ChunkStore.FindChunkByCid(RawHash); - if (Data) - { - auto GetForChunking = - [](size_t ChunkFileSizeLimit, const IoBuffer& Data, IoBufferFileReference& OutFileRef) -> bool { - if (Data.IsWholeFile()) - { - IoHash VerifyRawHash; - uint64_t VerifyRawSize; - CompressedBuffer Compressed = - CompressedBuffer::FromCompressed(SharedBuffer(Data), VerifyRawHash, VerifyRawSize); - if (Compressed) + IoBuffer Data = ChunkStore.FindChunkByCid(RawHash); + if (Data) + { + auto GetForChunking = + [](size_t ChunkFileSizeLimit, const IoBuffer& Data, IoBufferFileReference& OutFileRef) -> bool { + if (Data.IsWholeFile()) { - if (VerifyRawSize > ChunkFileSizeLimit) + IoHash VerifyRawHash; + uint64_t VerifyRawSize; + CompressedBuffer Compressed = + CompressedBuffer::FromCompressed(SharedBuffer(Data), VerifyRawHash, VerifyRawSize); + if (Compressed) { - OodleCompressor Compressor; - OodleCompressionLevel CompressionLevel; - uint64_t BlockSize; - if (Compressed.TryGetCompressParameters(Compressor, CompressionLevel, BlockSize)) + if (VerifyRawSize > ChunkFileSizeLimit) { - if (CompressionLevel == OodleCompressionLevel::None) + OodleCompressor Compressor; + OodleCompressionLevel CompressionLevel; + uint64_t BlockSize; + if (Compressed.TryGetCompressParameters(Compressor, CompressionLevel, BlockSize)) { - CompositeBuffer Decompressed = Compressed.DecompressToComposite(); - if (Decompressed) + if (CompressionLevel == OodleCompressionLevel::None) { - std::span<const SharedBuffer> Segments = Decompressed.GetSegments(); - if (Segments.size() == 1) + CompositeBuffer Decompressed = Compressed.DecompressToComposite(); + if (Decompressed) { - IoBuffer DecompressedData = Segments[0].AsIoBuffer(); - if (DecompressedData.GetFileReference(OutFileRef)) + std::span<const SharedBuffer> Segments = Decompressed.GetSegments(); + if (Segments.size() == 1) { - return true; + IoBuffer DecompressedData = Segments[0].AsIoBuffer(); + if (DecompressedData.GetFileReference(OutFileRef)) + { + return true; + } } } } @@ -1665,49 +1701,49 @@ BuildContainer(CidStore& ChunkStore, } } } - } - return false; - }; + return false; + }; - IoBufferFileReference FileRef; - if (AllowChunking && GetForChunking(ChunkFileSizeLimit, Data, FileRef)) - { - ChunkedFile Chunked = ChunkFile(RawHash, Data, FileRef, OptionalContext); - ResolveLock.WithExclusiveLock( - [RawHash, &ChunkedFiles, &ChunkedUploadAttachments, &ChunkedHashes, &Chunked]() { - ChunkedUploadAttachments.insert_or_assign(RawHash, ChunkedFiles.size()); - ChunkedHashes.reserve(ChunkedHashes.size() + Chunked.Chunked.Info.ChunkHashes.size()); - for (const IoHash& ChunkHash : Chunked.Chunked.Info.ChunkHashes) - { - ChunkedHashes.insert(ChunkHash); - } - ChunkedFiles.emplace_back(std::move(Chunked)); - }); - } - else if (Data.GetSize() > MaxChunkEmbedSize) - { - OnLargeAttachment(RawHash, - [&ChunkStore](const IoHash& RawHash) { return ChunkStore.FindChunkByCid(RawHash); }); - ResolveLock.WithExclusiveLock([RawHash, &LargeChunkHashes]() { LargeChunkHashes.insert(RawHash); }); + IoBufferFileReference FileRef; + if (AllowChunking && GetForChunking(ChunkFileSizeLimit, Data, FileRef)) + { + ChunkedFile Chunked = ChunkFile(RawHash, Data, FileRef, OptionalContext); + ResolveLock.WithExclusiveLock( + [RawHash, &ChunkedFiles, &ChunkedUploadAttachments, &ChunkedHashes, &Chunked]() { + ChunkedUploadAttachments.insert_or_assign(RawHash, ChunkedFiles.size()); + ChunkedHashes.reserve(ChunkedHashes.size() + Chunked.Chunked.Info.ChunkHashes.size()); + for (const IoHash& ChunkHash : Chunked.Chunked.Info.ChunkHashes) + { + ChunkedHashes.insert(ChunkHash); + } + ChunkedFiles.emplace_back(std::move(Chunked)); + }); + } + else if (Data.GetSize() > MaxChunkEmbedSize) + { + OnLargeAttachment(RawHash, + [&ChunkStore](const IoHash& RawHash) { return ChunkStore.FindChunkByCid(RawHash); }); + ResolveLock.WithExclusiveLock([RawHash, &LargeChunkHashes]() { LargeChunkHashes.insert(RawHash); }); + } + else + { + UploadAttachment->Size = Data.GetSize(); + } } else { - UploadAttachment->Size = Data.GetSize(); + ResolveLock.WithExclusiveLock([RawHash, &MissingHashes]() { MissingHashes.insert(RawHash); }); } } - else - { - ResolveLock.WithExclusiveLock([RawHash, &MissingHashes]() { MissingHashes.insert(RawHash); }); - } } - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::NotFound), - fmt::format("Failed to resolve attachment {}", RawHash), - Ex.what()); - } - }); + catch (const std::exception& Ex) + { + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::NotFound), + fmt::format("Failed to resolve attachment {}", RawHash), + Ex.what()); + } + }, + WorkerThreadPool::EMode::EnableBacklog); } ResolveAttachmentsLatch.CountDown(); @@ -1981,17 +2017,31 @@ BuildContainer(CidStore& ChunkStore, { ChunksInBlock.emplace_back( std::make_pair(RawHash, [&ChunkStore](const IoHash& RawHash) -> std::pair<uint64_t, CompressedBuffer> { - IoBuffer Chunk = ChunkStore.FindChunkByCid(RawHash); - IoHash _; - uint64_t RawSize = 0; - CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(std::move(Chunk)), _, RawSize); - ZEN_ASSERT(Compressed); + IoBuffer Chunk = ChunkStore.FindChunkByCid(RawHash); + if (!Chunk) + { + throw std::runtime_error(fmt::format("Failed to find chunk {} in cid store", RawHash)); + } + IoHash ValidateRawHash; + uint64_t RawSize = 0; + CompressedBuffer Compressed = + CompressedBuffer::FromCompressed(SharedBuffer(std::move(Chunk)), ValidateRawHash, RawSize); + if (!Compressed) + { + throw std::runtime_error( + fmt::format("Chunk {} in cid store is malformed (not a compressed buffer)", RawHash)); + } + if (RawHash != ValidateRawHash) + { + throw std::runtime_error( + fmt::format("Chunk {} in cid store is malformed (mismatching raw hash)", RawHash)); + } return {RawSize, Compressed}; })); } BlockSize += PayloadSize; - if (BlockSize >= MaxBlockSize && (CurrentOpKey != LastOpKey)) + if ((BlockSize >= MaxBlockSize || ChunksInBlock.size() > MaxChunksPerBlock) && (CurrentOpKey != LastOpKey)) { NewBlock(); } @@ -2050,7 +2100,7 @@ BuildContainer(CidStore& ChunkStore, BlockSize += CompressedBuffer::GetHeaderSizeForNoneEncoder() + Source.Size; if (BuildBlocks) { - if (BlockSize >= MaxBlockSize) + if (BlockSize >= MaxBlockSize || ChunksInBlock.size() > MaxChunksPerBlock) { NewBlock(); } @@ -2256,7 +2306,9 @@ RemoteProjectStore::LoadContainerResult BuildContainer(CidStore& ChunkStore, ProjectStore::Project& Project, ProjectStore::Oplog& Oplog, + WorkerThreadPool& WorkerPool, size_t MaxBlockSize, + size_t MaxChunksPerBlock, size_t MaxChunkEmbedSize, size_t ChunkFileSizeLimit, bool BuildBlocks, @@ -2267,13 +2319,14 @@ BuildContainer(CidStore& ChunkStore, const std::function<void(std::vector<std::pair<IoHash, FetchChunkFunc>>&&)>& OnBlockChunks, bool EmbedLooseFiles) { - WorkerThreadPool& WorkerPool = GetLargeWorkerPool(EWorkloadType::Background); + // WorkerThreadPool& WorkerPool = GetLargeWorkerPool(EWorkloadType::Background); remotestore_impl::AsyncRemoteResult RemoteResult; CbObject ContainerObject = BuildContainer(ChunkStore, Project, Oplog, MaxBlockSize, + MaxChunksPerBlock, MaxChunkEmbedSize, ChunkFileSizeLimit, BuildBlocks, @@ -2295,7 +2348,10 @@ SaveOplog(CidStore& ChunkStore, RemoteProjectStore& RemoteStore, ProjectStore::Project& Project, ProjectStore::Oplog& Oplog, + WorkerThreadPool& NetworkWorkerPool, + WorkerThreadPool& WorkerPool, size_t MaxBlockSize, + size_t MaxChunksPerBlock, size_t MaxChunkEmbedSize, size_t ChunkFileSizeLimit, bool EmbedLooseFiles, @@ -2309,9 +2365,6 @@ SaveOplog(CidStore& ChunkStore, remotestore_impl::UploadInfo Info; - WorkerThreadPool& WorkerPool = GetLargeWorkerPool(EWorkloadType::Background); - WorkerThreadPool& NetworkWorkerPool = GetMediumWorkerPool(EWorkloadType::Background); - const RemoteProjectStore::RemoteStoreInfo RemoteStoreInfo = RemoteStore.GetInfo(); std::filesystem::path AttachmentTempPath; @@ -2445,6 +2498,7 @@ SaveOplog(CidStore& ChunkStore, Project, Oplog, MaxBlockSize, + MaxChunksPerBlock, MaxChunkEmbedSize, ChunkFileSizeLimit, RemoteStoreInfo.CreateBlocks, @@ -2651,19 +2705,22 @@ ParseOplogContainer(const CbObject& ContainerObject, IoBuffer OpsBuffer(IoBuffer::Wrap, OpsSection.GetData(), OpsSection.GetSize()); IoBuffer SectionPayload = CompressedBuffer::FromCompressedNoValidate(std::move(OpsBuffer)).Decompress().AsIoBuffer(); + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject SectionObject = ValidateAndReadCompactBinaryObject(std::move(SectionPayload), ValidateResult); + ValidateResult == CbValidateError::None && ContainerObject) { - CbObject SectionObject = LoadCompactBinaryObject(SectionPayload); - if (!SectionObject) - { - remotestore_impl::ReportMessage(OptionalContext, - fmt::format("Failed to save oplog container: '{}'", "Section has unexpected data type")); - return RemoteProjectStore::Result{gsl::narrow<int>(HttpResponseCode::BadRequest), - Timer.GetElapsedTimeMs() / 1000.0, - "Section has unexpected data type", - "Failed to save oplog container"}; - } OutOplogSection = SectionObject; } + else + { + remotestore_impl::ReportMessage( + OptionalContext, + fmt::format("Failed to save oplog container: '{}' ('{}')", "Section has unexpected data type", ToString(ValidateResult))); + return RemoteProjectStore::Result{gsl::narrow<int>(HttpResponseCode::BadRequest), + Timer.GetElapsedTimeMs() / 1000.0, + "Section has unexpected data type", + "Failed to save oplog container"}; + } std::unordered_set<IoHash, IoHash::Hasher> OpsAttachments; { CbArrayView OpsArray = OutOplogSection["ops"sv].AsArrayView(); @@ -2711,6 +2768,12 @@ ParseOplogContainer(const CbObject& ContainerObject, NiceBytes(Chunked.RawSize), Chunked.ChunkHashes.size()); } + if (remotestore_impl::IsCancelled(OptionalContext)) + { + return RemoteProjectStore::Result{.ErrorCode = gsl::narrow<int>(HttpResponseCode::OK), + .ElapsedSeconds = Timer.GetElapsedTimeMs() / 1000.0, + .Reason = "Operation cancelled"}; + } } size_t NeedBlockCount = 0; @@ -2755,6 +2818,12 @@ ParseOplogContainer(const CbObject& ContainerObject, NeedBlockCount++; } } + if (remotestore_impl::IsCancelled(OptionalContext)) + { + return RemoteProjectStore::Result{.ErrorCode = gsl::narrow<int>(HttpResponseCode::OK), + .ElapsedSeconds = Timer.GetElapsedTimeMs() / 1000.0, + .Reason = "Operation cancelled"}; + } } remotestore_impl::ReportMessage(OptionalContext, fmt::format("Requesting {} of {} attachment blocks", NeedBlockCount, BlocksArray.Num())); @@ -2769,6 +2838,12 @@ ParseOplogContainer(const CbObject& ContainerObject, OnNeedAttachment(AttachmentHash); NeedAttachmentCount++; } + if (remotestore_impl::IsCancelled(OptionalContext)) + { + return RemoteProjectStore::Result{.ErrorCode = gsl::narrow<int>(HttpResponseCode::OK), + .ElapsedSeconds = Timer.GetElapsedTimeMs() / 1000.0, + .Reason = "Operation cancelled"}; + } }; remotestore_impl::ReportMessage(OptionalContext, fmt::format("Requesting {} of {} large attachments", NeedAttachmentCount, LargeChunksArray.Num())); @@ -2811,6 +2886,8 @@ RemoteProjectStore::Result LoadOplog(CidStore& ChunkStore, RemoteProjectStore& RemoteStore, ProjectStore::Oplog& Oplog, + WorkerThreadPool& NetworkWorkerPool, + WorkerThreadPool& WorkerPool, bool ForceDownload, bool IgnoreMissingAttachments, bool CleanOplog, @@ -2822,9 +2899,6 @@ LoadOplog(CidStore& ChunkStore, Stopwatch Timer; - WorkerThreadPool& WorkerPool = GetLargeWorkerPool(EWorkloadType::Background); - WorkerThreadPool& NetworkWorkerPool = GetSmallWorkerPool(EWorkloadType::Background); - std::unordered_set<IoHash, IoHash::Hasher> Attachments; uint64_t BlockCountToDownload = 0; @@ -3074,101 +3148,103 @@ LoadOplog(CidStore& ChunkStore, { std::filesystem::path TempFileName = TempFilePath / Chunked.RawHash.ToHexString(); DechunkLatch.AddCount(1); - WorkerPool.ScheduleWork([&ChunkStore, - &DechunkLatch, - TempFileName, - &Chunked, - &RemoteResult, - IgnoreMissingAttachments, - &Info, - OptionalContext]() { - auto _ = MakeGuard([&DechunkLatch, &TempFileName] { - std::error_code Ec; - if (IsFile(TempFileName, Ec)) - { - RemoveFile(TempFileName, Ec); - if (Ec) + WorkerPool.ScheduleWork( + [&ChunkStore, + &DechunkLatch, + TempFileName, + &Chunked, + &RemoteResult, + IgnoreMissingAttachments, + &Info, + OptionalContext]() { + auto _ = MakeGuard([&DechunkLatch, &TempFileName] { + std::error_code Ec; + if (IsFile(TempFileName, Ec)) { - ZEN_INFO("Failed to remove temporary file '{}'. Reason: {}", TempFileName, Ec.message()); + RemoveFile(TempFileName, Ec); + if (Ec) + { + ZEN_INFO("Failed to remove temporary file '{}'. Reason: {}", TempFileName, Ec.message()); + } } - } - DechunkLatch.CountDown(); - }); - try - { - if (RemoteResult.IsError()) - { - return; - } - Stopwatch Timer; - IoBuffer TmpBuffer; + DechunkLatch.CountDown(); + }); + try { - BasicFile TmpFile; - TmpFile.Open(TempFileName, BasicFile::Mode::kTruncate); + if (RemoteResult.IsError()) { - BasicFileWriter TmpWriter(TmpFile, 64u * 1024u); - - uint64_t Offset = CompressedBuffer::GetHeaderSizeForNoneEncoder(); - BLAKE3Stream HashingStream; - for (std::uint32_t SequenceIndex : Chunked.ChunkSequence) + return; + } + Stopwatch Timer; + IoBuffer TmpBuffer; + { + BasicFile TmpFile; + TmpFile.Open(TempFileName, BasicFile::Mode::kTruncate); { - const IoHash& ChunkHash = Chunked.ChunkHashes[SequenceIndex]; - IoBuffer Chunk = ChunkStore.FindChunkByCid(ChunkHash); - if (!Chunk) - { - remotestore_impl::ReportMessage( - OptionalContext, - fmt::format("Missing chunk {} for chunked attachment {}", ChunkHash, Chunked.RawHash)); + BasicFileWriter TmpWriter(TmpFile, 64u * 1024u); - // We only add 1 as the resulting missing count will be 1 for the dechunked file - Info.MissingAttachmentCount.fetch_add(1); - if (!IgnoreMissingAttachments) + uint64_t Offset = CompressedBuffer::GetHeaderSizeForNoneEncoder(); + BLAKE3Stream HashingStream; + for (std::uint32_t SequenceIndex : Chunked.ChunkSequence) + { + const IoHash& ChunkHash = Chunked.ChunkHashes[SequenceIndex]; + IoBuffer Chunk = ChunkStore.FindChunkByCid(ChunkHash); + if (!Chunk) { - RemoteResult.SetError( - gsl::narrow<int>(HttpResponseCode::NotFound), - "Missing chunk", + remotestore_impl::ReportMessage( + OptionalContext, fmt::format("Missing chunk {} for chunked attachment {}", ChunkHash, Chunked.RawHash)); + + // We only add 1 as the resulting missing count will be 1 for the dechunked file + Info.MissingAttachmentCount.fetch_add(1); + if (!IgnoreMissingAttachments) + { + RemoteResult.SetError( + gsl::narrow<int>(HttpResponseCode::NotFound), + "Missing chunk", + fmt::format("Missing chunk {} for chunked attachment {}", ChunkHash, Chunked.RawHash)); + } + return; + } + CompositeBuffer Decompressed = + CompressedBuffer::FromCompressedNoValidate(std::move(Chunk)).DecompressToComposite(); + for (const SharedBuffer& Segment : Decompressed.GetSegments()) + { + MemoryView SegmentData = Segment.GetView(); + HashingStream.Append(SegmentData); + TmpWriter.Write(SegmentData.GetData(), SegmentData.GetSize(), Offset); + Offset += SegmentData.GetSize(); } - return; - } - CompositeBuffer Decompressed = - CompressedBuffer::FromCompressedNoValidate(std::move(Chunk)).DecompressToComposite(); - for (const SharedBuffer& Segment : Decompressed.GetSegments()) - { - MemoryView SegmentData = Segment.GetView(); - HashingStream.Append(SegmentData); - TmpWriter.Write(SegmentData.GetData(), SegmentData.GetSize(), Offset); - Offset += SegmentData.GetSize(); } + BLAKE3 RawHash = HashingStream.GetHash(); + ZEN_ASSERT(Chunked.RawHash == IoHash::FromBLAKE3(RawHash)); + UniqueBuffer Header = CompressedBuffer::CreateHeaderForNoneEncoder(Chunked.RawSize, RawHash); + TmpWriter.Write(Header.GetData(), Header.GetSize(), 0); } - BLAKE3 RawHash = HashingStream.GetHash(); - ZEN_ASSERT(Chunked.RawHash == IoHash::FromBLAKE3(RawHash)); - UniqueBuffer Header = CompressedBuffer::CreateHeaderForNoneEncoder(Chunked.RawSize, RawHash); - TmpWriter.Write(Header.GetData(), Header.GetSize(), 0); + TmpFile.Close(); + TmpBuffer = IoBufferBuilder::MakeFromTemporaryFile(TempFileName); + } + CidStore::InsertResult InsertResult = + ChunkStore.AddChunk(TmpBuffer, Chunked.RawHash, CidStore::InsertMode::kMayBeMovedInPlace); + if (InsertResult.New) + { + Info.AttachmentBytesStored.fetch_add(TmpBuffer.GetSize()); + Info.AttachmentsStored.fetch_add(1); } - TmpFile.Close(); - TmpBuffer = IoBufferBuilder::MakeFromTemporaryFile(TempFileName); + + ZEN_INFO("Dechunked attachment {} ({}) in {}", + Chunked.RawHash, + NiceBytes(Chunked.RawSize), + NiceTimeSpanMs(Timer.GetElapsedTimeMs())); } - CidStore::InsertResult InsertResult = - ChunkStore.AddChunk(TmpBuffer, Chunked.RawHash, CidStore::InsertMode::kMayBeMovedInPlace); - if (InsertResult.New) + catch (const std::exception& Ex) { - Info.AttachmentBytesStored.fetch_add(TmpBuffer.GetSize()); - Info.AttachmentsStored.fetch_add(1); + RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), + fmt::format("Failed to dechunck file {}", Chunked.RawHash), + Ex.what()); } - - ZEN_INFO("Dechunked attachment {} ({}) in {}", - Chunked.RawHash, - NiceBytes(Chunked.RawSize), - NiceTimeSpanMs(Timer.GetElapsedTimeMs())); - } - catch (const std::exception& Ex) - { - RemoteResult.SetError(gsl::narrow<int>(HttpResponseCode::InternalServerError), - fmt::format("Failed to dechunck file {}", Chunked.RawHash), - Ex.what()); - } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); } DechunkLatch.CountDown(); @@ -3248,4 +3324,211 @@ RemoteProjectStore::~RemoteProjectStore() { } +#if ZEN_WITH_TESTS + +namespace testutils { + using namespace std::literals; + + static std::string OidAsString(const Oid& Id) + { + StringBuilder<25> OidStringBuilder; + Id.ToString(OidStringBuilder); + return OidStringBuilder.ToString(); + } + + static CbPackage CreateBulkDataOplogPackage(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments) + { + CbPackage Package; + CbObjectWriter Object; + Object << "key"sv << OidAsString(Id); + if (!Attachments.empty()) + { + Object.BeginArray("bulkdata"); + for (const auto& Attachment : Attachments) + { + CbAttachment Attach(Attachment.second, Attachment.second.DecodeRawHash()); + Object.BeginObject(); + Object << "id"sv << Attachment.first; + Object << "type"sv + << "Standard"sv; + Object << "data"sv << Attach; + Object.EndObject(); + + Package.AddAttachment(Attach); + } + Object.EndArray(); + } + Package.SetObject(Object.Save()); + return Package; + }; + + static std::vector<std::pair<Oid, CompressedBuffer>> CreateAttachments( + const std::span<const size_t>& Sizes, + OodleCompressionLevel CompressionLevel = OodleCompressionLevel::VeryFast, + uint64_t BlockSize = 0) + { + std::vector<std::pair<Oid, CompressedBuffer>> Result; + Result.reserve(Sizes.size()); + for (size_t Size : Sizes) + { + CompressedBuffer Compressed = + CompressedBuffer::Compress(SharedBuffer(CreateSemiRandomBlob(Size)), OodleCompressor::Mermaid, CompressionLevel, BlockSize); + Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed)); + } + return Result; + } + +} // namespace testutils + +struct ExportForceDisableBlocksTrue_ForceTempBlocksFalse +{ + static const bool ForceDisableBlocks = true; + static const bool ForceEnableTempBlocks = false; +}; + +struct ExportForceDisableBlocksFalse_ForceTempBlocksFalse +{ + static const bool ForceDisableBlocks = false; + static const bool ForceEnableTempBlocks = false; +}; + +struct ExportForceDisableBlocksFalse_ForceTempBlocksTrue +{ + static const bool ForceDisableBlocks = false; + static const bool ForceEnableTempBlocks = true; +}; + +TEST_CASE_TEMPLATE("project.store.export", + Settings, + ExportForceDisableBlocksTrue_ForceTempBlocksFalse, + ExportForceDisableBlocksFalse_ForceTempBlocksFalse, + ExportForceDisableBlocksFalse_ForceTempBlocksTrue) +{ + using namespace std::literals; + using namespace testutils; + + ScopedTemporaryDirectory TempDir; + ScopedTemporaryDirectory ExportDir; + + GcManager Gc; + CidStore CidStore(Gc); + CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; + CidStore.Initialize(CidConfig); + + std::filesystem::path BasePath = TempDir.Path() / "projectstore"; + ProjectStore ProjectStore(CidStore, BasePath, Gc, ProjectStore::Configuration{}); + std::filesystem::path RootDir = TempDir.Path() / "root"; + std::filesystem::path EngineRootDir = TempDir.Path() / "engine"; + std::filesystem::path ProjectRootDir = TempDir.Path() / "game"; + std::filesystem::path ProjectFilePath = TempDir.Path() / "game" / "game.uproject"; + + Ref<ProjectStore::Project> Project(ProjectStore.NewProject(BasePath / "proj1"sv, + "proj1"sv, + RootDir.string(), + EngineRootDir.string(), + ProjectRootDir.string(), + ProjectFilePath.string())); + Ref<ProjectStore::Oplog> Oplog = Project->NewOplog("oplog1", {}); + CHECK(Oplog); + + Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), {})); + Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{77}))); + Oplog->AppendNewOplogEntry( + CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{7123, 583, 690, 99}))); + Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{55, 122}))); + Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage( + Oid::NewOid(), + CreateAttachments(std::initializer_list<size_t>{256u * 1024u, 92u * 1024u}, OodleCompressionLevel::None))); + + FileRemoteStoreOptions Options = {RemoteStoreOptions{.MaxBlockSize = 64u * 1024, + .MaxChunksPerBlock = 1000, + .MaxChunkEmbedSize = 32 * 1024u, + .ChunkFileSizeLimit = 64u * 1024u}, + /*.FolderPath = */ ExportDir.Path(), + /*.Name = */ std::string("oplog1"), + /*OptionalBaseName = */ std::string(), + /*.ForceDisableBlocks = */ Settings::ForceDisableBlocks, + /*.ForceEnableTempBlocks = */ Settings::ForceEnableTempBlocks}; + std::shared_ptr<RemoteProjectStore> RemoteStore = CreateFileRemoteStore(Options); + RemoteProjectStore::RemoteStoreInfo StoreInfo = RemoteStore->GetInfo(); + + uint32_t NetworkWorkerCount = Max(std::thread::hardware_concurrency() / 4u, 2u); + uint32_t WorkerCount = + (NetworkWorkerCount < std::thread::hardware_concurrency()) ? Max(std::thread::hardware_concurrency() - NetworkWorkerCount, 4u) : 4u; + + WorkerThreadPool WorkerPool(WorkerCount); + WorkerThreadPool NetworkPool(NetworkWorkerCount); + + RemoteProjectStore::Result ExportResult = SaveOplog(CidStore, + *RemoteStore, + *Project.Get(), + *Oplog, + NetworkPool, + WorkerPool, + Options.MaxBlockSize, + Options.MaxChunksPerBlock, + Options.MaxChunkEmbedSize, + Options.ChunkFileSizeLimit, + true, + false, + false, + nullptr); + + CHECK(ExportResult.ErrorCode == 0); + + Ref<ProjectStore::Oplog> OplogImport = Project->NewOplog("oplog2", {}); + CHECK(OplogImport); + + RemoteProjectStore::Result ImportResult = LoadOplog(CidStore, + *RemoteStore, + *OplogImport, + NetworkPool, + WorkerPool, + /*Force*/ false, + /*IgnoreMissingAttachments*/ false, + /*CleanOplog*/ false, + nullptr); + CHECK(ImportResult.ErrorCode == 0); + + RemoteProjectStore::Result ImportForceResult = LoadOplog(CidStore, + *RemoteStore, + *OplogImport, + NetworkPool, + WorkerPool, + /*Force*/ true, + /*IgnoreMissingAttachments*/ false, + /*CleanOplog*/ false, + nullptr); + CHECK(ImportForceResult.ErrorCode == 0); + + RemoteProjectStore::Result ImportCleanResult = LoadOplog(CidStore, + *RemoteStore, + *OplogImport, + NetworkPool, + WorkerPool, + /*Force*/ false, + /*IgnoreMissingAttachments*/ false, + /*CleanOplog*/ true, + nullptr); + CHECK(ImportCleanResult.ErrorCode == 0); + + RemoteProjectStore::Result ImportForceCleanResult = LoadOplog(CidStore, + *RemoteStore, + *OplogImport, + NetworkPool, + WorkerPool, + /*Force*/ true, + /*IgnoreMissingAttachments*/ false, + /*CleanOplog*/ true, + nullptr); + CHECK(ImportForceCleanResult.ErrorCode == 0); +} + +#endif // ZEN_WITH_TESTS + +void +remoteprojectstore_forcelink() +{ +} + } // namespace zen diff --git a/src/zenserver/projectstore/zenremoteprojectstore.cpp b/src/zenremotestore/projectstore/zenremoteprojectstore.cpp index 21ddd6cff..000901e45 100644 --- a/src/zenserver/projectstore/zenremoteprojectstore.cpp +++ b/src/zenremotestore/projectstore/zenremoteprojectstore.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include "zenremoteprojectstore.h" +#include <zenremotestore/projectstore/zenremoteprojectstore.h> #include <zencore/compactbinarybuilder.h> #include <zencore/compactbinarypackage.h> diff --git a/src/zenremotestore/xmake.lua b/src/zenremotestore/xmake.lua new file mode 100644 index 000000000..0818dda7b --- /dev/null +++ b/src/zenremotestore/xmake.lua @@ -0,0 +1,11 @@ +-- Copyright Epic Games, Inc. All Rights Reserved. + +target('zenremotestore') + set_kind("static") + set_group("libs") + add_headerfiles("**.h") + add_files("**.cpp") + add_includedirs("include", {public=true}) + add_deps("zencore", "zenstore") + add_packages("vcpkg::robin-map") + add_packages("vcpkg::eastl", {public=true}); diff --git a/src/zenremotestore/zenremotestore.cpp b/src/zenremotestore/zenremotestore.cpp new file mode 100644 index 000000000..c019bc71d --- /dev/null +++ b/src/zenremotestore/zenremotestore.cpp @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenremotestore/zenremotestore.h> + +#include <zenremotestore/chunking/chunkedfile.h> +#include <zenremotestore/projectstore/remoteprojectstore.h> + +#if ZEN_WITH_TESTS + +namespace zen { + +void +zenremotestore_forcelinktests() +{ + chunkblock_forcelink(); + chunkedfile_forcelink(); + remoteprojectstore_forcelink(); +} + +} // namespace zen + +#endif diff --git a/src/zenserver-test/buildstore-tests.cpp b/src/zenserver-test/buildstore-tests.cpp new file mode 100644 index 000000000..29afd3f9d --- /dev/null +++ b/src/zenserver-test/buildstore-tests.cpp @@ -0,0 +1,506 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#if ZEN_WITH_TESTS +# include "zenserver-test.h" +# include <zencore/testing.h> +# include <zencore/testutils.h> +# include <zencore/workthreadpool.h> +# include <zencore/compactbinarybuilder.h> +# include <zencore/compactbinarypackage.h> +# include <zencore/compress.h> +# include <zencore/filesystem.h> +# include <zencore/stream.h> +# include <zencore/string.h> +# include <zencore/fmtutils.h> +# include <zencore/scopeguard.h> +# include <zenhttp/packageformat.h> +# include <zenremotestore/builds/buildstoragecache.h> +# include <zenutil/workerpools.h> +# include <zenutil/zenserverprocess.h> +# include <zenhttp/httpclient.h> + +ZEN_THIRD_PARTY_INCLUDES_START +# include <tsl/robin_set.h> +ZEN_THIRD_PARTY_INCLUDES_END + +namespace zen::tests { + +using namespace std::literals; + +TEST_CASE("buildstore.blobs") +{ + std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); + auto _ = MakeGuard([&SystemRootPath]() { DeleteDirectories(SystemRootPath); }); + + std::string_view Namespace = "ns"sv; + std::string_view Bucket = "bkt"sv; + Oid BuildId = Oid::NewOid(); + + std::vector<IoHash> CompressedBlobsHashes; + { + ZenServerInstance Instance(TestEnv); + + const uint16_t PortNumber = + Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri() + "/builds/"); + + for (size_t I = 0; I < 5; I++) + { + IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7); + CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob))); + CompressedBlobsHashes.push_back(CompressedBlob.DecodeRawHash()); + IoBuffer Payload = std::move(CompressedBlob).GetCompressed().Flatten().AsIoBuffer(); + Payload.SetContentType(ZenContentType::kCompressedBinary); + + HttpClient::Response Result = + Client.Put(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, CompressedBlobsHashes.back()), Payload); + CHECK(Result); + } + + for (const IoHash& RawHash : CompressedBlobsHashes) + { + HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash), + HttpClient::Accept(ZenContentType::kCompressedBinary)); + CHECK(Result); + IoBuffer Payload = Result.ResponsePayload; + CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary); + IoHash VerifyRawHash; + uint64_t VerifyRawSize; + CompressedBuffer CompressedBlob = + CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize); + CHECK(CompressedBlob); + CHECK(VerifyRawHash == RawHash); + IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer(); + CHECK(IoHash::HashBuffer(Decompressed) == RawHash); + } + } + { + ZenServerInstance Instance(TestEnv); + + const uint16_t PortNumber = + Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri() + "/builds/"); + + for (const IoHash& RawHash : CompressedBlobsHashes) + { + HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash), + HttpClient::Accept(ZenContentType::kCompressedBinary)); + CHECK(Result); + IoBuffer Payload = Result.ResponsePayload; + CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary); + IoHash VerifyRawHash; + uint64_t VerifyRawSize; + CompressedBuffer CompressedBlob = + CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize); + CHECK(CompressedBlob); + CHECK(VerifyRawHash == RawHash); + IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer(); + CHECK(IoHash::HashBuffer(Decompressed) == RawHash); + } + + for (size_t I = 0; I < 5; I++) + { + IoBuffer Blob = CreateSemiRandomBlob(5713 + I * 7); + CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob))); + CompressedBlobsHashes.push_back(CompressedBlob.DecodeRawHash()); + IoBuffer Payload = std::move(CompressedBlob).GetCompressed().Flatten().AsIoBuffer(); + Payload.SetContentType(ZenContentType::kCompressedBinary); + + HttpClient::Response Result = + Client.Put(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, CompressedBlobsHashes.back()), Payload); + CHECK(Result); + } + } + { + ZenServerInstance Instance(TestEnv); + + const uint16_t PortNumber = + Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri() + "/builds/"); + + for (const IoHash& RawHash : CompressedBlobsHashes) + { + HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash), + HttpClient::Accept(ZenContentType::kCompressedBinary)); + CHECK(Result); + IoBuffer Payload = Result.ResponsePayload; + CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary); + IoHash VerifyRawHash; + uint64_t VerifyRawSize; + CompressedBuffer CompressedBlob = + CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize); + CHECK(CompressedBlob); + CHECK(VerifyRawHash == RawHash); + IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer(); + CHECK(IoHash::HashBuffer(Decompressed) == RawHash); + } + } +} + +namespace { + CbObject MakeMetadata(const IoHash& BlobHash, const std::vector<std::pair<std::string, std::string>>& KeyValues) + { + CbObjectWriter Writer; + Writer.AddHash("rawHash"sv, BlobHash); + Writer.BeginObject("values"); + { + for (const auto& V : KeyValues) + { + Writer.AddString(V.first, V.second); + } + } + Writer.EndObject(); // values + return Writer.Save(); + }; + +} // namespace + +TEST_CASE("buildstore.metadata") +{ + std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); + auto _ = MakeGuard([&SystemRootPath]() { DeleteDirectories(SystemRootPath); }); + + std::string_view Namespace = "ns"sv; + std::string_view Bucket = "bkt"sv; + Oid BuildId = Oid::NewOid(); + + std::vector<IoHash> BlobHashes; + std::vector<CbObject> Metadatas; + std::vector<IoHash> MetadataHashes; + + auto GetMetadatas = + [](HttpClient& Client, std::string_view Namespace, std::string_view Bucket, const Oid& BuildId, std::vector<IoHash> BlobHashes) { + CbObjectWriter Request; + + Request.BeginArray("blobHashes"sv); + for (const IoHash& BlobHash : BlobHashes) + { + Request.AddHash(BlobHash); + } + Request.EndArray(); + + IoBuffer Payload = Request.Save().GetBuffer().AsIoBuffer(); + Payload.SetContentType(ZenContentType::kCbObject); + + HttpClient::Response Result = Client.Post(fmt::format("{}/{}/{}/blobs/getBlobMetadata", Namespace, Bucket, BuildId), + Payload, + HttpClient::Accept(ZenContentType::kCbObject)); + CHECK(Result); + + std::vector<CbObject> ResultMetadatas; + + CbPackage ResponsePackage = ParsePackageMessage(Result.ResponsePayload); + CbObject ResponseObject = ResponsePackage.GetObject(); + + CbArrayView BlobHashArray = ResponseObject["blobHashes"sv].AsArrayView(); + CbArrayView MetadatasArray = ResponseObject["metadatas"sv].AsArrayView(); + ResultMetadatas.reserve(MetadatasArray.Num()); + auto BlobHashesIt = BlobHashes.begin(); + auto BlobHashArrayIt = begin(BlobHashArray); + auto MetadataArrayIt = begin(MetadatasArray); + while (MetadataArrayIt != end(MetadatasArray)) + { + const IoHash BlobHash = (*BlobHashArrayIt).AsHash(); + while (BlobHash != *BlobHashesIt) + { + ZEN_ASSERT(BlobHashesIt != BlobHashes.end()); + BlobHashesIt++; + } + + ZEN_ASSERT(BlobHash == *BlobHashesIt); + + const IoHash MetaHash = (*MetadataArrayIt).AsAttachment(); + const CbAttachment* MetaAttachment = ResponsePackage.FindAttachment(MetaHash); + ZEN_ASSERT(MetaAttachment); + + CbObject Metadata = MetaAttachment->AsObject(); + ResultMetadatas.emplace_back(std::move(Metadata)); + + BlobHashArrayIt++; + MetadataArrayIt++; + BlobHashesIt++; + } + return ResultMetadatas; + }; + + { + ZenServerInstance Instance(TestEnv); + + const uint16_t PortNumber = + Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri() + "/builds/"); + + const size_t BlobCount = 5; + + for (size_t I = 0; I < BlobCount; I++) + { + BlobHashes.push_back(IoHash::HashBuffer(&I, sizeof(I))); + Metadatas.push_back(MakeMetadata(BlobHashes.back(), {{"index", fmt::format("{}", I)}})); + MetadataHashes.push_back(IoHash::HashBuffer(Metadatas.back().GetBuffer().AsIoBuffer())); + } + + { + CbPackage RequestPackage; + std::vector<CbAttachment> Attachments; + tsl::robin_set<IoHash, IoHash::Hasher> AttachmentHashes; + Attachments.reserve(BlobCount); + AttachmentHashes.reserve(BlobCount); + { + CbObjectWriter RequestWriter; + RequestWriter.BeginArray("blobHashes"); + for (size_t BlockHashIndex = 0; BlockHashIndex < BlobHashes.size(); BlockHashIndex++) + { + RequestWriter.AddHash(BlobHashes[BlockHashIndex]); + } + RequestWriter.EndArray(); // blobHashes + + RequestWriter.BeginArray("metadatas"); + for (size_t BlockHashIndex = 0; BlockHashIndex < BlobHashes.size(); BlockHashIndex++) + { + const IoHash ObjectHash = Metadatas[BlockHashIndex].GetHash(); + RequestWriter.AddBinaryAttachment(ObjectHash); + if (!AttachmentHashes.contains(ObjectHash)) + { + Attachments.push_back(CbAttachment(Metadatas[BlockHashIndex], ObjectHash)); + AttachmentHashes.insert(ObjectHash); + } + } + + RequestWriter.EndArray(); // metadatas + + RequestPackage.SetObject(RequestWriter.Save()); + } + RequestPackage.AddAttachments(Attachments); + + CompositeBuffer RpcRequestBuffer = FormatPackageMessageBuffer(RequestPackage); + + HttpClient::Response Result = Client.Post(fmt::format("{}/{}/{}/blobs/putBlobMetadata", Namespace, Bucket, BuildId), + RpcRequestBuffer, + ZenContentType::kCbPackage); + CHECK(Result); + } + + { + std::vector<CbObject> ResultMetadatas = GetMetadatas(Client, Namespace, Bucket, BuildId, BlobHashes); + + for (size_t Index = 0; Index < MetadataHashes.size(); Index++) + { + const IoHash& ExpectedHash = MetadataHashes[Index]; + IoHash Hash = IoHash::HashBuffer(ResultMetadatas[Index].GetBuffer().AsIoBuffer()); + CHECK_EQ(ExpectedHash, Hash); + } + } + } + { + ZenServerInstance Instance(TestEnv); + + const uint16_t PortNumber = + Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri() + "/builds/"); + + std::vector<CbObject> ResultMetadatas = GetMetadatas(Client, Namespace, Bucket, BuildId, BlobHashes); + + for (size_t Index = 0; Index < MetadataHashes.size(); Index++) + { + const IoHash& ExpectedHash = MetadataHashes[Index]; + IoHash Hash = IoHash::HashBuffer(ResultMetadatas[Index].GetBuffer().AsIoBuffer()); + CHECK_EQ(ExpectedHash, Hash); + } + } +} + +TEST_CASE("buildstore.cache") +{ + std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); + std::filesystem::path TempDir = TestEnv.CreateNewTestDir(); + auto _ = MakeGuard([&SystemRootPath, &TempDir]() { + DeleteDirectories(SystemRootPath); + DeleteDirectories(TempDir); + }); + + std::string_view Namespace = "ns"sv; + std::string_view Bucket = "bkt"sv; + Oid BuildId = Oid::NewOid(); + + std::vector<IoHash> BlobHashes; + std::vector<CbObject> Metadatas; + std::vector<IoHash> MetadataHashes; + + const size_t BlobCount = 5; + { + ZenServerInstance Instance(TestEnv); + + const uint16_t PortNumber = + Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri()); + + BuildStorageCache::Statistics Stats; + std::unique_ptr<BuildStorageCache> Cache( + CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, GetTinyWorkerPool(EWorkloadType::Background))); + + { + IoHash NoneBlob = IoHash::HashBuffer("data", 4); + std::vector<BuildStorageCache::BlobExistsResult> NoneExists = Cache->BlobsExists(BuildId, std::vector<IoHash>{NoneBlob}); + CHECK(NoneExists.size() == 1); + CHECK(!NoneExists[0].HasBody); + CHECK(!NoneExists[0].HasMetadata); + } + + for (size_t I = 0; I < BlobCount; I++) + { + IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7); + CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob))); + BlobHashes.push_back(CompressedBlob.DecodeRawHash()); + Cache->PutBuildBlob(BuildId, BlobHashes.back(), ZenContentType::kCompressedBinary, CompressedBlob.GetCompressed()); + } + + Cache->Flush(500); + + Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, GetTinyWorkerPool(EWorkloadType::Background)); + + { + std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes); + CHECK(Exists.size() == BlobHashes.size()); + for (size_t I = 0; I < BlobCount; I++) + { + CHECK(Exists[I].HasBody); + CHECK(!Exists[I].HasMetadata); + } + + std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); + CHECK_EQ(0, FetchedMetadatas.size()); + } + + { + for (size_t I = 0; I < BlobCount; I++) + { + IoBuffer BuildBlob = Cache->GetBuildBlob(BuildId, BlobHashes[I]); + CHECK(BuildBlob); + CHECK_EQ(BlobHashes[I], + IoHash::HashBuffer(CompressedBuffer::FromCompressedNoValidate(std::move(BuildBlob)).Decompress().AsIoBuffer())); + } + } + + { + for (size_t I = 0; I < BlobCount; I++) + { + CbObject Metadata = MakeMetadata(BlobHashes[I], + {{"key", fmt::format("{}", I)}, + {"key_plus_one", fmt::format("{}", I + 1)}, + {"block_hash", fmt::format("{}", BlobHashes[I])}}); + Metadatas.push_back(Metadata); + MetadataHashes.push_back(IoHash::HashBuffer(Metadata.GetBuffer().AsIoBuffer())); + } + Cache->PutBlobMetadatas(BuildId, BlobHashes, Metadatas); + } + + Cache->Flush(500); + Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, GetTinyWorkerPool(EWorkloadType::Background)); + + { + std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes); + CHECK(Exists.size() == BlobHashes.size()); + for (size_t I = 0; I < BlobCount; I++) + { + CHECK(Exists[I].HasBody); + CHECK(Exists[I].HasMetadata); + } + + std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); + CHECK_EQ(BlobCount, FetchedMetadatas.size()); + + for (size_t I = 0; I < BlobCount; I++) + { + CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer())); + } + } + + for (size_t I = 0; I < BlobCount; I++) + { + IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7); + CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob))); + BlobHashes.push_back(CompressedBlob.DecodeRawHash()); + Cache->PutBuildBlob(BuildId, BlobHashes.back(), ZenContentType::kCompressedBinary, CompressedBlob.GetCompressed()); + } + + Cache->Flush(500); + Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, GetTinyWorkerPool(EWorkloadType::Background)); + + { + std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes); + CHECK(Exists.size() == BlobHashes.size()); + for (size_t I = 0; I < BlobCount * 2; I++) + { + CHECK(Exists[I].HasBody); + CHECK_EQ(I < BlobCount, Exists[I].HasMetadata); + } + + std::vector<CbObject> MetaDatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); + CHECK_EQ(BlobCount, MetaDatas.size()); + + std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); + CHECK_EQ(BlobCount, FetchedMetadatas.size()); + + for (size_t I = 0; I < BlobCount; I++) + { + CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer())); + } + } + } + + { + ZenServerInstance Instance(TestEnv); + + const uint16_t PortNumber = + Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri()); + + BuildStorageCache::Statistics Stats; + std::unique_ptr<BuildStorageCache> Cache( + CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, GetTinyWorkerPool(EWorkloadType::Background))); + + std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes); + CHECK(Exists.size() == BlobHashes.size()); + for (size_t I = 0; I < BlobCount * 2; I++) + { + CHECK(Exists[I].HasBody); + CHECK_EQ(I < BlobCount, Exists[I].HasMetadata); + } + + for (size_t I = 0; I < BlobCount * 2; I++) + { + IoBuffer BuildBlob = Cache->GetBuildBlob(BuildId, BlobHashes[I]); + CHECK(BuildBlob); + CHECK_EQ(BlobHashes[I], + IoHash::HashBuffer(CompressedBuffer::FromCompressedNoValidate(std::move(BuildBlob)).Decompress().AsIoBuffer())); + } + + std::vector<CbObject> MetaDatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); + CHECK_EQ(BlobCount, MetaDatas.size()); + + std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); + CHECK_EQ(BlobCount, FetchedMetadatas.size()); + + for (size_t I = 0; I < BlobCount; I++) + { + CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer())); + } + } +} + +} // namespace zen::tests +#endif diff --git a/src/zenserver-test/cache-tests.cpp b/src/zenserver-test/cache-tests.cpp new file mode 100644 index 000000000..1ce5f3be4 --- /dev/null +++ b/src/zenserver-test/cache-tests.cpp @@ -0,0 +1,2366 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#if ZEN_WITH_TESTS +# include "zenserver-test.h" +# include <zencore/testing.h> +# include <zencore/testutils.h> +# include <zencore/workthreadpool.h> +# include <zencore/compactbinarybuilder.h> +# include <zencore/compactbinarypackage.h> +# include <zencore/compress.h> +# include <zencore/fmtutils.h> +# include <zenhttp/packageformat.h> +# include <zenstore/cache/cachepolicy.h> +# include <zencore/filesystem.h> +# include <zencore/stream.h> +# include <zencore/string.h> +# include <zenutil/zenserverprocess.h> +# include <zenhttp/httpclient.h> + +# include "cacherequests.h" + +# include <random> + +namespace zen::tests { + +TEST_CASE("zcache.basic") +{ + using namespace std::literals; + + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + + const int kIterationCount = 100; + + auto HashKey = [](int i) -> zen::IoHash { return zen::IoHash::HashBuffer(&i, sizeof i); }; + + { + ZenServerInstance Instance1(TestEnv); + Instance1.SetTestDir(TestDir); + + const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady(); + const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber); + + // Populate with some simple data + + HttpClient Http{BaseUri}; + + for (int i = 0; i < kIterationCount; ++i) + { + zen::CbObjectWriter Cbo; + Cbo << "index" << i; + + IoBuffer Payload = Cbo.Save().GetBuffer().AsIoBuffer(); + Payload.SetContentType(HttpContentType::kCbObject); + + zen::IoHash Key = HashKey(i); + + HttpClient::Response Result = Http.Put(fmt::format("/test/{}", Key), Payload); + + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + // Retrieve data + + for (int i = 0; i < kIterationCount; ++i) + { + zen::IoHash Key = HashKey(i); + + HttpClient::Response Result = Http.Get(fmt::format("/test/{}", Key), {{"Accept", "application/x-ue-cbpkg"}}); + + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + + // Ensure bad bucket identifiers are rejected + + { + zen::CbObjectWriter Cbo; + Cbo << "index" << 42; + + IoBuffer Payload = Cbo.Save().GetBuffer().AsIoBuffer(); + Payload.SetContentType(HttpContentType::kCbObject); + + zen::IoHash Key = HashKey(442); + + HttpClient::Response Result = Http.Put(fmt::format("/te!st/{}", Key), Payload); + + CHECK(Result.StatusCode == HttpResponseCode::BadRequest); + } + } + + // Verify that the data persists between process runs (the previous server has exited at this point) + + { + ZenServerInstance Instance1(TestEnv); + Instance1.SetTestDir(TestDir); + const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady(); + + const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber); + + HttpClient Http{BaseUri}; + + // Retrieve data again + + for (int i = 0; i < kIterationCount; ++i) + { + zen::IoHash Key = HashKey(i); + + HttpClient::Response Result = Http.Get(fmt::format("/{}/{}", "test", Key), {{"Accept", "application/x-ue-cbpkg"}}); + + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + } +} + +TEST_CASE("zcache.cbpackage") +{ + using namespace std::literals; + + auto CreateTestPackage = [](zen::IoHash& OutAttachmentKey) -> zen::CbPackage { + auto Data = zen::SharedBuffer::Clone(zen::MakeMemoryView<uint8_t>({1, 2, 3, 4, 5, 6, 7, 8, 9})); + auto CompressedData = zen::CompressedBuffer::Compress(Data); + + OutAttachmentKey = CompressedData.DecodeRawHash(); + + zen::CbWriter Obj; + Obj.BeginObject("obj"sv); + Obj.AddBinaryAttachment("data", OutAttachmentKey); + Obj.EndObject(); + + zen::CbPackage Package; + Package.SetObject(Obj.Save().AsObject()); + Package.AddAttachment(zen::CbAttachment(CompressedData, OutAttachmentKey)); + + return Package; + }; + + auto IsEqual = [](zen::CbPackage Lhs, zen::CbPackage Rhs) -> bool { + std::span<const zen::CbAttachment> LhsAttachments = Lhs.GetAttachments(); + std::span<const zen::CbAttachment> RhsAttachments = Rhs.GetAttachments(); + + if (LhsAttachments.size() != RhsAttachments.size()) + { + return false; + } + + for (const zen::CbAttachment& LhsAttachment : LhsAttachments) + { + const zen::CbAttachment* RhsAttachment = Rhs.FindAttachment(LhsAttachment.GetHash()); + CHECK(RhsAttachment); + + zen::SharedBuffer LhsBuffer = LhsAttachment.AsCompressedBinary().Decompress(); + CHECK(!LhsBuffer.IsNull()); + + zen::SharedBuffer RhsBuffer = RhsAttachment->AsCompressedBinary().Decompress(); + CHECK(!RhsBuffer.IsNull()); + + if (!LhsBuffer.GetView().EqualBytes(RhsBuffer.GetView())) + { + return false; + } + } + + return true; + }; + + SUBCASE("PUT/GET returns correct package") + { + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + + ZenServerInstance Instance1(TestEnv); + Instance1.SetTestDir(TestDir); + const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady(); + const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber); + + HttpClient Http{BaseUri}; + + const std::string_view Bucket = "mosdef"sv; + zen::IoHash Key; + zen::CbPackage ExpectedPackage = CreateTestPackage(Key); + + // PUT + { + zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage); + HttpClient::Response Result = Http.Put(fmt::format("/{}/{}", Bucket, Key), Body); + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + // GET + { + HttpClient::Response Result = Http.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + + zen::CbPackage Package; + const bool Ok = Package.TryLoad(Result.ResponsePayload); + CHECK(Ok); + CHECK(IsEqual(Package, ExpectedPackage)); + } + } + + SUBCASE("PUT propagates upstream") + { + // Setup local and remote server + std::filesystem::path LocalDataDir = TestEnv.CreateNewTestDir(); + std::filesystem::path RemoteDataDir = TestEnv.CreateNewTestDir(); + + ZenServerInstance RemoteInstance(TestEnv); + RemoteInstance.SetTestDir(RemoteDataDir); + const uint16_t RemotePortNumber = RemoteInstance.SpawnServerAndWaitUntilReady(); + + ZenServerInstance LocalInstance(TestEnv); + LocalInstance.SetTestDir(LocalDataDir); + LocalInstance.SpawnServer(TestEnv.GetNewPortNumber(), + fmt::format("--upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", RemotePortNumber)); + const uint16_t LocalPortNumber = LocalInstance.WaitUntilReady(); + CHECK_MESSAGE(LocalPortNumber != 0, LocalInstance.GetLogOutput()); + + const auto LocalBaseUri = fmt::format("http://localhost:{}/z$", LocalPortNumber); + const auto RemoteBaseUri = fmt::format("http://localhost:{}/z$", RemotePortNumber); + + const std::string_view Bucket = "mosdef"sv; + zen::IoHash Key; + zen::CbPackage ExpectedPackage = CreateTestPackage(Key); + + HttpClient LocalHttp{LocalBaseUri}; + HttpClient RemoteHttp{RemoteBaseUri}; + + // Store the cache record package in the local instance + { + zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage); + HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}", Bucket, Key), Body); + + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + // The cache record can be retrieved as a package from the local instance + { + HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + + zen::CbPackage Package; + const bool Ok = Package.TryLoad(Result.ResponsePayload); + CHECK(Ok); + CHECK(IsEqual(Package, ExpectedPackage)); + } + + // The cache record can be retrieved as a package from the remote instance + { + HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + + zen::CbPackage Package; + const bool Ok = Package.TryLoad(Result.ResponsePayload); + CHECK(Ok); + CHECK(IsEqual(Package, ExpectedPackage)); + } + } + + SUBCASE("GET finds upstream when missing in local") + { + // Setup local and remote server + std::filesystem::path LocalDataDir = TestEnv.CreateNewTestDir(); + std::filesystem::path RemoteDataDir = TestEnv.CreateNewTestDir(); + + ZenServerInstance RemoteInstance(TestEnv); + RemoteInstance.SetTestDir(RemoteDataDir); + const uint16_t RemotePortNumber = RemoteInstance.SpawnServerAndWaitUntilReady(); + + ZenServerInstance LocalInstance(TestEnv); + LocalInstance.SetTestDir(LocalDataDir); + LocalInstance.SpawnServer(TestEnv.GetNewPortNumber(), + fmt::format("--upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", RemotePortNumber)); + const uint16_t LocalPortNumber = LocalInstance.WaitUntilReady(); + CHECK_MESSAGE(LocalPortNumber != 0, LocalInstance.GetLogOutput()); + + const auto LocalBaseUri = fmt::format("http://localhost:{}/z$", LocalPortNumber); + const auto RemoteBaseUri = fmt::format("http://localhost:{}/z$", RemotePortNumber); + + HttpClient LocalHttp{LocalBaseUri}; + HttpClient RemoteHttp{RemoteBaseUri}; + + const std::string_view Bucket = "mosdef"sv; + zen::IoHash Key; + zen::CbPackage ExpectedPackage = CreateTestPackage(Key); + + // Store the cache record package in upstream cache + { + zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage); + HttpClient::Response Result = RemoteHttp.Put(fmt::format("/{}/{}", Bucket, Key), Body); + + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + // The cache record can be retrieved as a package from the local cache + { + HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + + zen::CbPackage Package; + const bool Ok = Package.TryLoad(Result.ResponsePayload); + CHECK(Ok); + CHECK(IsEqual(Package, ExpectedPackage)); + } + } +} + +TEST_CASE("zcache.policy") +{ + using namespace std::literals; + using namespace utils; + + auto GenerateData = [](uint64_t Size, zen::IoHash& OutHash) -> zen::IoBuffer { + auto Buf = zen::UniqueBuffer::Alloc(Size); + uint8_t* Data = reinterpret_cast<uint8_t*>(Buf.GetData()); + for (uint64_t Idx = 0; Idx < Size; Idx++) + { + Data[Idx] = Idx % 256; + } + OutHash = zen::IoHash::HashBuffer(Data, Size); + return Buf.MoveToShared().AsIoBuffer(); + }; + + auto GeneratePackage = [](zen::IoHash& OutRecordKey, zen::IoHash& OutAttachmentKey) -> zen::CbPackage { + auto Data = zen::SharedBuffer::Clone(zen::MakeMemoryView<uint8_t>({1, 2, 3, 4, 5, 6, 7, 8, 9})); + auto CompressedData = zen::CompressedBuffer::Compress(Data); + OutAttachmentKey = CompressedData.DecodeRawHash(); + + zen::CbWriter Writer; + Writer.BeginObject("obj"sv); + Writer.AddBinaryAttachment("data", OutAttachmentKey); + Writer.EndObject(); + CbObject CacheRecord = Writer.Save().AsObject(); + + OutRecordKey = IoHash::HashBuffer(CacheRecord.GetBuffer().GetView()); + + zen::CbPackage Package; + Package.SetObject(CacheRecord); + Package.AddAttachment(zen::CbAttachment(CompressedData, OutAttachmentKey)); + + return Package; + }; + + SUBCASE("query - 'local' does not query upstream (binary)") + { + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamInst(TestEnv); + UpstreamCfg.Spawn(UpstreamInst); + const uint16_t UpstreamPort = UpstreamCfg.Port; + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamPort); + ZenServerInstance LocalInst(TestEnv); + LocalCfg.Spawn(LocalInst); + + const std::string_view Bucket = "legacy"sv; + + zen::IoHash Key; + IoBuffer BinaryValue = GenerateData(1024, Key); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient RemoteHttp{UpstreamCfg.BaseUri}; + + { + HttpClient::Response Result = RemoteHttp.Put(fmt::format("/{}/{}", Bucket, Key), BinaryValue); + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + { + HttpClient::Response Result = + LocalHttp.Get(fmt::format("/{}/{}?Policy=QueryLocal,Store", Bucket, Key), {{"Accept", "application/octet-stream"}}); + CHECK(Result.StatusCode == HttpResponseCode::NotFound); + } + + { + HttpClient::Response Result = + LocalHttp.Get(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), {{"Accept", "application/octet-stream"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + } + + SUBCASE("store - 'local' does not store upstream (binary)") + { + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamInst(TestEnv); + UpstreamCfg.Spawn(UpstreamInst); + const uint16_t UpstreamPort = UpstreamCfg.Port; + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamPort); + ZenServerInstance LocalInst(TestEnv); + LocalCfg.Spawn(LocalInst); + + const auto Bucket = "legacy"sv; + + zen::IoHash Key; + IoBuffer BinaryValue = GenerateData(1024, Key); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient RemoteHttp{UpstreamCfg.BaseUri}; + + // Store binary cache value locally + { + HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,StoreLocal", Bucket, Key), + BinaryValue, + {{"Content-Type", "application/octet-stream"}}); + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + { + HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}}); + CHECK(Result.StatusCode == HttpResponseCode::NotFound); + } + + { + HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + } + + SUBCASE("store - 'local/remote' stores local and upstream (binary)") + { + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamInst(TestEnv); + UpstreamCfg.Spawn(UpstreamInst); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); + ZenServerInstance LocalInst(TestEnv); + LocalCfg.Spawn(LocalInst); + + const auto Bucket = "legacy"sv; + + zen::IoHash Key; + IoBuffer BinaryValue = GenerateData(1024, Key); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient RemoteHttp{UpstreamCfg.BaseUri}; + + // Store binary cache value locally and upstream + { + HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), + BinaryValue, + {{"Content-Type", "application/octet-stream"}}); + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + { + HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + + { + HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/octet-stream"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + } + + SUBCASE("query - 'local' does not query upstream (cbpackage)") + { + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamInst(TestEnv); + UpstreamCfg.Spawn(UpstreamInst); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); + ZenServerInstance LocalInst(TestEnv); + LocalCfg.Spawn(LocalInst); + + const auto Bucket = "legacy"sv; + + zen::IoHash Key; + zen::IoHash PayloadId; + zen::CbPackage Package = GeneratePackage(Key, PayloadId); + IoBuffer Buf = SerializeToBuffer(Package); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient RemoteHttp{UpstreamCfg.BaseUri}; + + // Store package upstream + { + HttpClient::Response Result = RemoteHttp.Put(fmt::format("/{}/{}", Bucket, Key), Buf); + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + { + HttpClient::Response Result = + LocalHttp.Get(fmt::format("/{}/{}?Policy=QueryLocal,Store", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::NotFound); + } + + { + HttpClient::Response Result = + LocalHttp.Get(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + } + + SUBCASE("store - 'local' does not store upstream (cbpackage)") + { + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamInst(TestEnv); + UpstreamCfg.Spawn(UpstreamInst); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); + ZenServerInstance LocalInst(TestEnv); + LocalCfg.Spawn(LocalInst); + + const auto Bucket = "legacy"sv; + + zen::IoHash Key; + zen::IoHash PayloadId; + zen::CbPackage Package = GeneratePackage(Key, PayloadId); + IoBuffer Buf = SerializeToBuffer(Package); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient RemoteHttp{UpstreamCfg.BaseUri}; + + // Store package locally + { + HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,StoreLocal", Bucket, Key), Buf); + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + { + HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::NotFound); + } + + { + HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + } + + SUBCASE("store - 'local/remote' stores local and upstream (cbpackage)") + { + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamInst(TestEnv); + UpstreamCfg.Spawn(UpstreamInst); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); + ZenServerInstance LocalInst(TestEnv); + LocalCfg.Spawn(LocalInst); + + const auto Bucket = "legacy"sv; + + zen::IoHash Key; + zen::IoHash PayloadId; + zen::CbPackage Package = GeneratePackage(Key, PayloadId); + IoBuffer Buf = SerializeToBuffer(Package); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient RemoteHttp{UpstreamCfg.BaseUri}; + + // Store package locally and upstream + { + HttpClient::Response Result = LocalHttp.Put(fmt::format("/{}/{}?Policy=Query,Store", Bucket, Key), Buf); + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + { + HttpClient::Response Result = RemoteHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + + { + HttpClient::Response Result = LocalHttp.Get(fmt::format("/{}/{}", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result.StatusCode == HttpResponseCode::OK); + } + } + + SUBCASE("skip - 'data' returns cache record without attachments/empty payload") + { + ZenConfig Cfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance Instance(TestEnv); + Cfg.Spawn(Instance); + + const auto Bucket = "test"sv; + + zen::IoHash Key; + zen::IoHash PayloadId; + zen::CbPackage Package = GeneratePackage(Key, PayloadId); + IoBuffer Buf = SerializeToBuffer(Package); + + HttpClient Http{Cfg.BaseUri}; + + // Store package + { + HttpClient::Response Result = Http.Put(fmt::format("/{}/{}", Bucket, Key), Buf); + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + // Get package + { + HttpClient::Response Result = + Http.Get(fmt::format("/{}/{}?Policy=Default,SkipData", Bucket, Key), {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Result); + CbPackage ResponsePackage; + CHECK(ResponsePackage.TryLoad(Result.ResponsePayload)); + CHECK(ResponsePackage.GetAttachments().size() == 0); + } + + // Get record + { + HttpClient::Response Result = + Http.Get(fmt::format("/{}/{}?Policy=Default,SkipData", Bucket, Key), {{"Accept", "application/x-ue-cb"}}); + CHECK(Result); + CbObject ResponseObject = zen::LoadCompactBinaryObject(Result.ResponsePayload); + CHECK(ResponseObject); + } + + // Get payload + { + HttpClient::Response Result = + Http.Get(fmt::format("/{}/{}/{}?Policy=Default,SkipData", Bucket, Key, PayloadId), {{"Accept", "application/x-ue-comp"}}); + CHECK(Result); + CHECK(Result.ResponsePayload.GetSize() == 0); + } + } + + SUBCASE("skip - 'data' returns empty binary value") + { + ZenConfig Cfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance Instance(TestEnv); + Cfg.Spawn(Instance); + + const auto Bucket = "test"sv; + + zen::IoHash Key; + IoBuffer BinaryValue = GenerateData(1024, Key); + + HttpClient Http{Cfg.BaseUri}; + + // Store binary cache value + { + HttpClient::Response Result = Http.Put(fmt::format("/{}/{}", Bucket, Key), BinaryValue); + CHECK(Result.StatusCode == HttpResponseCode::Created); + } + + // Get package + { + HttpClient::Response Result = + Http.Get(fmt::format("/{}/{}?Policy=Default,SkipData", Bucket, Key), {{"Accept", "application/octet-stream"}}); + CHECK(Result); + CHECK(Result.ResponsePayload.GetSize() == 0); + } + } +} + +TEST_CASE("zcache.rpc") +{ + using namespace std::literals; + + auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request, + const zen::CacheKey& CacheKey, + size_t PayloadSize, + CachePolicy RecordPolicy) { + std::vector<uint8_t> Data; + Data.resize(PayloadSize); + uint32_t DataSeed = *reinterpret_cast<const uint32_t*>(&CacheKey.Hash.Hash[0]); + uint16_t* DataPtr = reinterpret_cast<uint16_t*>(Data.data()); + for (size_t Idx = 0; Idx < PayloadSize / 2; ++Idx) + { + DataPtr[Idx] = static_cast<uint16_t>((Idx + DataSeed) % 0xffffu); + } + if (PayloadSize & 1) + { + Data[PayloadSize - 1] = static_cast<uint8_t>((PayloadSize - 1) & 0xff); + } + CompressedBuffer Value = zen::CompressedBuffer::Compress(SharedBuffer::MakeView(Data.data(), Data.size())); + Request.Requests.push_back({.Key = CacheKey, .Values = {{.Id = Oid::NewOid(), .Body = std::move(Value)}}, .Policy = RecordPolicy}); + }; + + auto PutCacheRecords = [&AppendCacheRecord](std::string_view BaseUri, + std::string_view Namespace, + std::string_view Bucket, + size_t Num, + size_t PayloadSize = 1024, + size_t KeyOffset = 1, + CachePolicy PutPolicy = CachePolicy::Default, + std::vector<CbPackage>* OutPackages = nullptr) -> std::vector<CacheKey> { + std::vector<zen::CacheKey> OutKeys; + + HttpClient Http{BaseUri}; + + for (uint32_t Key = 1; Key <= Num; ++Key) + { + zen::IoHash KeyHash; + ((uint32_t*)(KeyHash.Hash))[0] = gsl::narrow<uint32_t>(KeyOffset + Key); + const zen::CacheKey CacheKey = zen::CacheKey::Create(Bucket, KeyHash); + + cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; + AppendCacheRecord(Request, CacheKey, PayloadSize, PutPolicy); + OutKeys.push_back(CacheKey); + + CbPackage Package; + CHECK(Request.Format(Package)); + + CompositeBuffer Body(FormatPackageMessageBuffer(Package)); + HttpClient::Response Result = + Http.Post("/$rpc", Body, HttpContentType::kCbPackage, HttpClient::Accept(HttpContentType::kCbPackage)); + + CHECK(Result.StatusCode == HttpResponseCode::OK); + if (OutPackages) + { + OutPackages->emplace_back(std::move(Package)); + } + } + + return OutKeys; + }; + + struct GetCacheRecordResult + { + zen::CbPackage Response; + cacherequests::GetCacheRecordsResult Result; + bool Success; + }; + + auto GetCacheRecords = [](std::string_view BaseUri, + std::string_view Namespace, + std::span<zen::CacheKey> Keys, + zen::CachePolicy Policy, + zen::RpcAcceptOptions AcceptOptions = zen::RpcAcceptOptions::kNone, + int Pid = 0) -> GetCacheRecordResult { + cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, + .AcceptOptions = static_cast<uint16_t>(AcceptOptions), + .ProcessPid = Pid, + .DefaultPolicy = Policy, + .Namespace = std::string(Namespace)}; + for (const CacheKey& Key : Keys) + { + Request.Requests.push_back({.Key = Key}); + } + + CbObjectWriter RequestWriter; + CHECK(Request.Format(RequestWriter)); + + IoBuffer Body = RequestWriter.Save().GetBuffer().AsIoBuffer(); + Body.SetContentType(HttpContentType::kCbObject); + + HttpClient Http{BaseUri}; + + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + + GetCacheRecordResult OutResult; + + if (Result.StatusCode == HttpResponseCode::OK) + { + CbPackage Response = ParsePackageMessage(Result.ResponsePayload); + CHECK(!Response.IsNull()); + OutResult.Response = std::move(Response); + CHECK(OutResult.Result.Parse(OutResult.Response)); + OutResult.Success = true; + } + + return OutResult; + }; + + SUBCASE("get cache records") + { + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + + ZenServerInstance Inst(TestEnv); + Inst.SetTestDir(TestDir); + + const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady(); + const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort); + + CachePolicy Policy = CachePolicy::Default; + std::vector<zen::CacheKey> Keys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 16); + GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, Policy); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record); + CHECK(Record->Key == ExpectedKey); + CHECK(Record->Values.size() == 1); + + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + CHECK(Value.Body); + } + } + } + + SUBCASE("get missing cache records") + { + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + + ZenServerInstance Inst(TestEnv); + Inst.SetTestDir(TestDir); + const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady(); + const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort); + + CachePolicy Policy = CachePolicy::Default; + std::vector<zen::CacheKey> ExistingKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 16); + std::vector<zen::CacheKey> Keys; + + for (const zen::CacheKey& Key : ExistingKeys) + { + Keys.push_back(Key); + Keys.push_back(CacheKey::Create("missing"sv, IoHash::Zero)); + } + + GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, Policy); + + CHECK(Result.Result.Results.size() == Keys.size()); + + size_t KeyIndex = 0; + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + const bool Missing = Index++ % 2 != 0; + + if (Missing) + { + CHECK(!Record); + } + else + { + const CacheKey& ExpectedKey = ExistingKeys[KeyIndex++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + CHECK(Value.Body); + } + } + } + } + + SUBCASE("policy - 'QueryLocal' does not query upstream") + { + using namespace utils; + + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamServer(TestEnv); + SpawnServer(UpstreamServer, UpstreamCfg); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); + ZenServerInstance LocalServer(TestEnv); + SpawnServer(LocalServer, LocalCfg); + + std::vector<zen::CacheKey> Keys = PutCacheRecords(UpstreamCfg.BaseUri, "ue4.ddc"sv, "mastodon"sv, 4); + + CachePolicy Policy = CachePolicy::QueryLocal; + GetCacheRecordResult Result = GetCacheRecords(LocalCfg.BaseUri, "ue4.ddc"sv, Keys, Policy); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(!Record); + } + } + + SUBCASE("policy - 'QueryRemote' does query upstream") + { + using namespace utils; + + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamServer(TestEnv); + SpawnServer(UpstreamServer, UpstreamCfg); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); + ZenServerInstance LocalServer(TestEnv); + SpawnServer(LocalServer, LocalCfg); + + std::vector<zen::CacheKey> Keys = PutCacheRecords(UpstreamCfg.BaseUri, "ue4.ddc"sv, "mastodon"sv, 4); + + CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); + GetCacheRecordResult Result = GetCacheRecords(LocalCfg.BaseUri, "ue4.ddc"sv, Keys, Policy); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + } + } + + SUBCASE("policy - 'QueryLocal' on put allows overwrite with differing value when not limiting overwrites") + { + using namespace utils; + + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamServer(TestEnv); + SpawnServer(UpstreamServer, UpstreamCfg); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); + ZenServerInstance LocalServer(TestEnv); + SpawnServer(LocalServer, LocalCfg); + + size_t PayloadSize = 1024; + std::string_view Namespace("ue4.ddc"sv); + std::string_view Bucket("mastodon"sv); + const size_t NumRecords = 4; + std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient UpstreamHttp{UpstreamCfg.BaseUri}; + + for (const zen::CacheKey& CacheKey : Keys) + { + cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; + AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default); + + CbPackage Package; + CHECK(Request.Format(Package)); + + IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); + Body.SetContentType(HttpContentType::kCbPackage); + HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + + CHECK(Result.StatusCode == HttpResponseCode::OK); + cacherequests::PutCacheRecordsResult ParsedResult; + CbPackage Response = ParsePackageMessage(Result.ResponsePayload); + CHECK(!Response.IsNull()); + CHECK(ParsedResult.Parse(Response)); + for (bool ResponseSuccess : ParsedResult.Success) + { + CHECK(ResponseSuccess); + } + CHECK(ParsedResult.Details.empty()); + } + + auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) { + CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); + GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + CHECK(Value.RawSize == PayloadSize * 2); + } + } + }; + + // Check that the records are present and overwritten in the local server + CheckRecordCorrectness(LocalCfg); + + // Check that the records are present and overwritten in the upstream server + CheckRecordCorrectness(UpstreamCfg); + } + + SUBCASE("policy - 'QueryLocal' on put denies overwrite with differing value when limiting overwrites") + { + using namespace utils; + + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamServer(TestEnv); + SpawnServer(UpstreamServer, UpstreamCfg); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites"); + ZenServerInstance LocalServer(TestEnv); + SpawnServer(LocalServer, LocalCfg); + + size_t PayloadSize = 1024; + std::string_view Namespace("ue4.ddc"sv); + std::string_view Bucket("mastodon"sv); + const size_t NumRecords = 4; + std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient UpstreamHttp{UpstreamCfg.BaseUri}; + + for (const zen::CacheKey& CacheKey : Keys) + { + cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; + AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default); + + CbPackage Package; + CHECK(Request.Format(Package)); + + IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); + Body.SetContentType(HttpContentType::kCbPackage); + + HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + + CHECK(Result.StatusCode == HttpResponseCode::OK); + cacherequests::PutCacheRecordsResult ParsedResult; + CbPackage Response = ParsePackageMessage(Result.ResponsePayload); + CHECK(!Response.IsNull()); + CHECK(ParsedResult.Parse(Response)); + CHECK(Request.Requests.size() == ParsedResult.Success.size()); + for (bool ResponseSuccess : ParsedResult.Success) + { + CHECK(ResponseSuccess); + } + CHECK(Request.Requests.size() == ParsedResult.Details.size()); + for (const CbObjectView& Details : ParsedResult.Details) + { + CHECK(Details); + CHECK(Details["RawHash"sv].IsHash()); + CHECK(Details["RawSize"sv].IsInteger()); + CHECK(Details["Record"sv].IsObject()); + } + } + + auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) { + CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); + GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + CHECK(Value.RawSize == PayloadSize); + } + } + }; + + // Check that the records are present and not overwritten in the local server + CheckRecordCorrectness(LocalCfg); + + // Check that the records are present and not overwritten in the upstream server + CheckRecordCorrectness(UpstreamCfg); + } + + SUBCASE("policy - no 'QueryLocal' on put allows overwrite with differing value when limiting overwrites") + { + using namespace utils; + + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamServer(TestEnv); + SpawnServer(UpstreamServer, UpstreamCfg); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites"); + ZenServerInstance LocalServer(TestEnv); + SpawnServer(LocalServer, LocalCfg); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient UpstreamHttp{UpstreamCfg.BaseUri}; + + size_t PayloadSize = 1024; + std::string_view Namespace("ue4.ddc"sv); + std::string_view Bucket("mastodon"sv); + const size_t NumRecords = 4; + std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize); + + for (const zen::CacheKey& CacheKey : Keys) + { + cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; + AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Store); + + CbPackage Package; + CHECK(Request.Format(Package)); + + IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); + Body.SetContentType(HttpContentType::kCbPackage); + HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + + CHECK(Result.StatusCode == HttpResponseCode::OK); + cacherequests::PutCacheRecordsResult ParsedResult; + CbPackage Response = ParsePackageMessage(Result.ResponsePayload); + CHECK(!Response.IsNull()); + CHECK(ParsedResult.Parse(Response)); + for (bool ResponseSuccess : ParsedResult.Success) + { + CHECK(ResponseSuccess); + } + CHECK(ParsedResult.Details.empty()); + } + + auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) { + CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); + GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + CHECK(Value.RawSize == PayloadSize * 2); + } + } + }; + + // Check that the records are present and overwritten in the local server + CheckRecordCorrectness(LocalCfg); + + // Check that the records are present and overwritten in the upstream server + CheckRecordCorrectness(UpstreamCfg); + } + + SUBCASE("policy - 'QueryLocal' on put allows overwrite with equivalent value when limiting overwrites") + { + using namespace utils; + + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamServer(TestEnv); + SpawnServer(UpstreamServer, UpstreamCfg); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites"); + ZenServerInstance LocalServer(TestEnv); + SpawnServer(LocalServer, LocalCfg); + + HttpClient LocalHttp{LocalCfg.BaseUri}; + HttpClient UpstreamHttp{UpstreamCfg.BaseUri}; + + size_t PayloadSize = 1024; + std::string_view Namespace("ue4.ddc"sv); + std::string_view Bucket("mastodon"sv); + const size_t NumRecords = 4; + std::vector<CbPackage> Packages; + std::vector<zen::CacheKey> Keys = + PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize, 1, CachePolicy::Default, &Packages); + + for (const CbPackage& Package : Packages) + { + IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); + Body.SetContentType(HttpContentType::kCbPackage); + HttpClient::Response Result = LocalHttp.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + + CHECK(Result.StatusCode == HttpResponseCode::OK); + cacherequests::PutCacheRecordsResult ParsedResult; + CbPackage Response = ParsePackageMessage(Result.ResponsePayload); + CHECK(!Response.IsNull()); + CHECK(ParsedResult.Parse(Response)); + for (bool ResponseSuccess : ParsedResult.Success) + { + CHECK(ResponseSuccess); + } + CHECK(ParsedResult.Details.empty()); + } + + auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) { + CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); + GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + CHECK(Value.RawSize == PayloadSize); + } + } + }; + + // Check that the records are present and unchanged in the local server + CheckRecordCorrectness(LocalCfg); + + // Check that the records are present and unchanged in the upstream server + CheckRecordCorrectness(UpstreamCfg); + } + + // TODO: Propagation for rejected PUTs + // SUBCASE("policy - 'QueryLocal' on put denies overwrite with differing value when limiting overwrites but allows propagation to + // upstream") + // { + // using namespace utils; + + // ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + // ZenServerInstance UpstreamServer(TestEnv); + // SpawnServer(UpstreamServer, UpstreamCfg); + + // ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, + // "--cache-bucket-limit-overwrites"); ZenServerInstance LocalServer(TestEnv); SpawnServer(LocalServer, LocalCfg); + + // size_t PayloadSize = 1024; + // std::string_view Namespace("ue4.ddc"sv); + // std::string_view Bucket("mastodon"sv); + // const size_t NumRecords = 4; + // std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize, 1, + // CachePolicy::Local); + + // for (const zen::CacheKey& CacheKey : Keys) + // { + // cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; + // AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default); + + // CbPackage Package; + // CHECK(Request.Format(Package)); + + // IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); + // cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", LocalCfg.BaseUri)}, + // cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, + // cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); + + // CHECK(Result.status_code == 200); + // cacherequests::PutCacheRecordsResult ParsedResult; + // CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); + // CHECK(!Response.IsNull()); + // CHECK(ParsedResult.Parse(Response)); + // for (bool ResponseSuccess : ParsedResult.Success) + // { + // CHECK(!ResponseSuccess); + // } + // } + + // auto CheckRecordCorrectness = [&](const ZenConfig& Cfg, size_t ExpectedPayloadSize) { + // CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); + // GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); + + // CHECK(Result.Result.Results.size() == Keys.size()); + + // for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + // { + // CHECK(Record); + // const CacheKey& ExpectedKey = Keys[Index++]; + // CHECK(Record->Key == ExpectedKey); + // for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + // { + // CHECK(Value.RawSize == ExpectedPayloadSize); + // } + // } + // }; + + // // Check that the records are present and not overwritten in the local server + // CheckRecordCorrectness(LocalCfg, PayloadSize); + + // // Check that the records are present and are the newer size in the upstream server + // CheckRecordCorrectness(UpstreamCfg, PayloadSize*2); + // } + + SUBCASE("RpcAcceptOptions") + { + using namespace utils; + + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + + ZenServerInstance Inst(TestEnv); + Inst.SetTestDir(TestDir); + + const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady(); + const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort); + + std::vector<zen::CacheKey> SmallKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 4, 1024); + std::vector<zen::CacheKey> LargeKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 2, 1024 * 1024 * 16, SmallKeys.size()); + + std::vector<zen::CacheKey> Keys(SmallKeys.begin(), SmallKeys.end()); + Keys.insert(Keys.end(), LargeKeys.begin(), LargeKeys.end()); + + { + GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, CachePolicy::Default); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); + IoBufferFileReference Ref; + bool IsFileRef = Body.GetFileReference(Ref); + CHECK(!IsFileRef); + } + } + } + + // File path, but only for large files + { + GetCacheRecordResult Result = + GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, CachePolicy::Default, RpcAcceptOptions::kAllowLocalReferences); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); + IoBufferFileReference Ref; + bool IsFileRef = Body.GetFileReference(Ref); + CHECK(IsFileRef == (Body.Size() > 1024)); + } + } + } + + // File path, for all files + { + GetCacheRecordResult Result = + GetCacheRecords(BaseUri, + "ue4.ddc"sv, + Keys, + CachePolicy::Default, + RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); + IoBufferFileReference Ref; + bool IsFileRef = Body.GetFileReference(Ref); + CHECK(IsFileRef); + } + } + } + + // File handle, but only for large files + { + GetCacheRecordResult Result = GetCacheRecords(BaseUri, + "ue4.ddc"sv, + Keys, + CachePolicy::Default, + RpcAcceptOptions::kAllowLocalReferences, + GetCurrentProcessId()); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); + IoBufferFileReference Ref; + bool IsFileRef = Body.GetFileReference(Ref); + CHECK(IsFileRef == (Body.Size() > 1024)); + } + } + } + + // File handle, for all files + { + GetCacheRecordResult Result = + GetCacheRecords(BaseUri, + "ue4.ddc"sv, + Keys, + CachePolicy::Default, + RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences, + GetCurrentProcessId()); + + CHECK(Result.Result.Results.size() == Keys.size()); + + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + CHECK(Record); + const CacheKey& ExpectedKey = Keys[Index++]; + CHECK(Record->Key == ExpectedKey); + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); + IoBufferFileReference Ref; + bool IsFileRef = Body.GetFileReference(Ref); + CHECK(IsFileRef); + } + } + } + } +} + +TEST_CASE("zcache.failing.upstream") +{ + // This is an exploratory test that takes a long time to run, so lets skip it by default + if (true) + { + return; + } + + using namespace std::literals; + using namespace utils; + + ZenConfig Upstream1Cfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance Upstream1Server(TestEnv); + SpawnServer(Upstream1Server, Upstream1Cfg); + + ZenConfig Upstream2Cfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance Upstream2Server(TestEnv); + SpawnServer(Upstream2Server, Upstream2Cfg); + + std::vector<std::uint16_t> UpstreamPorts = {Upstream1Cfg.Port, Upstream2Cfg.Port}; + ZenConfig LocalCfg = ZenConfig::NewWithThreadedUpstreams(TestEnv.GetNewPortNumber(), UpstreamPorts, false); + LocalCfg.Args += (" --upstream-thread-count 2"); + ZenServerInstance LocalServer(TestEnv); + SpawnServer(LocalServer, LocalCfg); + + const uint16_t LocalPortNumber = LocalCfg.Port; + const auto LocalUri = fmt::format("http://localhost:{}/z$", LocalPortNumber); + const auto Upstream1Uri = fmt::format("http://localhost:{}/z$", Upstream1Cfg.Port); + const auto Upstream2Uri = fmt::format("http://localhost:{}/z$", Upstream2Cfg.Port); + + bool Upstream1Running = true; + bool Upstream2Running = true; + + using namespace std::literals; + + auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request, + const zen::CacheKey& CacheKey, + size_t PayloadSize, + CachePolicy RecordPolicy) { + std::vector<uint32_t> Data; + Data.resize(PayloadSize / 4); + for (uint32_t Idx = 0; Idx < PayloadSize / 4; ++Idx) + { + Data[Idx] = (*reinterpret_cast<const uint32_t*>(&CacheKey.Hash.Hash[0])) + Idx; + } + + CompressedBuffer Value = zen::CompressedBuffer::Compress(SharedBuffer::MakeView(Data.data(), Data.size() * 4)); + Request.Requests.push_back({.Key = CacheKey, .Values = {{.Id = Oid::NewOid(), .Body = std::move(Value)}}, .Policy = RecordPolicy}); + }; + + auto PutCacheRecords = [&AppendCacheRecord](std::string_view BaseUri, + std::string_view Namespace, + std::string_view Bucket, + size_t Num, + size_t KeyOffset, + size_t PayloadSize = 8192) -> std::vector<CacheKey> { + std::vector<zen::CacheKey> OutKeys; + + cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; + for (size_t Key = 1; Key <= Num; ++Key) + { + zen::IoHash KeyHash; + ((size_t*)(KeyHash.Hash))[0] = KeyOffset + Key; + const zen::CacheKey CacheKey = zen::CacheKey::Create(Bucket, KeyHash); + + AppendCacheRecord(Request, CacheKey, PayloadSize, CachePolicy::Default); + OutKeys.push_back(CacheKey); + } + + CbPackage Package; + CHECK(Request.Format(Package)); + + HttpClient Http{BaseUri}; + + IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + + if (Result.StatusCode != HttpResponseCode::OK) + { + ZEN_DEBUG("PutCacheRecords failed with {}, reason '{}'", ToString(Result.StatusCode), Result.ErrorMessage("")); + OutKeys.clear(); + } + + return OutKeys; + }; + + struct GetCacheRecordResult + { + zen::CbPackage Response; + cacherequests::GetCacheRecordsResult Result; + bool Success = false; + }; + + auto GetCacheRecords = [](std::string_view BaseUri, + std::string_view Namespace, + std::span<zen::CacheKey> Keys, + zen::CachePolicy Policy) -> GetCacheRecordResult { + cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, + .DefaultPolicy = Policy, + .Namespace = std::string(Namespace)}; + for (const CacheKey& Key : Keys) + { + Request.Requests.push_back({.Key = Key}); + } + + CbObjectWriter RequestWriter; + CHECK(Request.Format(RequestWriter)); + + IoBuffer Body = RequestWriter.Save().GetBuffer().AsIoBuffer(); + Body.SetContentType(HttpContentType::kCbObject); + + HttpClient Http{BaseUri}; + + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + + GetCacheRecordResult OutResult; + + if (Result.StatusCode == HttpResponseCode::OK) + { + CbPackage Response = ParsePackageMessage(Result.ResponsePayload); + if (!Response.IsNull()) + { + OutResult.Response = std::move(Response); + CHECK(OutResult.Result.Parse(OutResult.Response)); + OutResult.Success = true; + } + } + else + { + ZEN_DEBUG("GetCacheRecords with {}, reason '{}'", ToString(Result.StatusCode), Result.ErrorMessage("")); + } + + return OutResult; + }; + + // Populate with some simple data + + CachePolicy Policy = CachePolicy::Default; + + const size_t ThreadCount = 128; + const size_t KeyMultiplier = 16384; + const size_t RecordsPerRequest = 64; + WorkerThreadPool Pool(ThreadCount); + + std::atomic_size_t Completed = 0; + + auto Keys = new std::vector<CacheKey>[ThreadCount * KeyMultiplier]; + RwLock KeysLock; + + for (size_t I = 0; I < ThreadCount * KeyMultiplier; I++) + { + size_t Iteration = I; + Pool.ScheduleWork( + [&] { + std::vector<CacheKey> NewKeys = + PutCacheRecords(LocalUri, "ue4.ddc"sv, "mastodon"sv, RecordsPerRequest, I * RecordsPerRequest); + if (NewKeys.size() != RecordsPerRequest) + { + ZEN_DEBUG("PutCacheRecords iteration {} failed", Iteration); + Completed.fetch_add(1); + return; + } + { + RwLock::ExclusiveLockScope _(KeysLock); + Keys[Iteration].swap(NewKeys); + } + Completed.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); + } + bool UseUpstream1 = false; + while (Completed < ThreadCount * KeyMultiplier) + { + Sleep(8000); + + if (UseUpstream1) + { + if (Upstream2Running) + { + Upstream2Server.EnableTermination(); + Upstream2Server.Shutdown(); + Sleep(100); + Upstream2Running = false; + } + if (!Upstream1Running) + { + SpawnServer(Upstream1Server, Upstream1Cfg); + Upstream1Running = true; + } + UseUpstream1 = !UseUpstream1; + } + else + { + if (Upstream1Running) + { + Upstream1Server.EnableTermination(); + Upstream1Server.Shutdown(); + Sleep(100); + Upstream1Running = false; + } + if (!Upstream2Running) + { + SpawnServer(Upstream2Server, Upstream2Cfg); + Upstream2Running = true; + } + UseUpstream1 = !UseUpstream1; + } + } + + Completed = 0; + for (size_t I = 0; I < ThreadCount * KeyMultiplier; I++) + { + size_t Iteration = I; + std::vector<CacheKey>& LocalKeys = Keys[Iteration]; + if (LocalKeys.empty()) + { + Completed.fetch_add(1); + continue; + } + Pool.ScheduleWork( + [&] { + GetCacheRecordResult Result = GetCacheRecords(LocalUri, "ue4.ddc"sv, LocalKeys, Policy); + + if (!Result.Success) + { + ZEN_DEBUG("GetCacheRecords iteration {} failed", Iteration); + Completed.fetch_add(1); + return; + } + + if (Result.Result.Results.size() != LocalKeys.size()) + { + ZEN_DEBUG("GetCacheRecords iteration {} empty records", Iteration); + Completed.fetch_add(1); + return; + } + for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) + { + const CacheKey& ExpectedKey = LocalKeys[Index++]; + if (!Record) + { + continue; + } + if (Record->Key != ExpectedKey) + { + continue; + } + if (Record->Values.size() != 1) + { + continue; + } + + for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) + { + if (!Value.Body) + { + continue; + } + } + } + Completed.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); + } + while (Completed < ThreadCount * KeyMultiplier) + { + Sleep(10); + } +} + +TEST_CASE("zcache.rpc.partialchunks") +{ + using namespace std::literals; + using namespace utils; + + ZenConfig LocalCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance Server(TestEnv); + SpawnServer(Server, LocalCfg); + + std::vector<CompressedBuffer> Attachments; + + const auto BaseUri = fmt::format("http://localhost:{}/z$", Server.GetBasePort()); + + auto GenerateKey = [](std::string_view Bucket, size_t KeyIndex) -> CacheKey { + IoHash KeyHash; + ((size_t*)(KeyHash.Hash))[0] = KeyIndex; + return CacheKey::Create(Bucket, KeyHash); + }; + + auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request, + const CacheKey& CacheKey, + size_t AttachmentCount, + size_t AttachmentsSize, + CachePolicy RecordPolicy) -> std::vector<std::pair<Oid, CompressedBuffer>> { + std::vector<std::pair<Oid, CompressedBuffer>> AttachmentBuffers; + std::vector<cacherequests::PutCacheRecordRequestValue> Attachments; + for (size_t AttachmentIndex = 0; AttachmentIndex < AttachmentCount; AttachmentIndex++) + { + CompressedBuffer Value = CreateSemiRandomBlob(AttachmentsSize); + AttachmentBuffers.push_back(std::make_pair(Oid::NewOid(), Value)); + Attachments.push_back({.Id = AttachmentBuffers.back().first, .Body = std::move(Value)}); + } + Request.Requests.push_back({.Key = CacheKey, .Values = Attachments, .Policy = RecordPolicy}); + return AttachmentBuffers; + }; + + auto PutCacheRecords = [&AppendCacheRecord, &GenerateKey]( + std::string_view BaseUri, + std::string_view Namespace, + std::string_view Bucket, + size_t KeyOffset, + size_t Num, + size_t AttachmentCount, + size_t AttachmentsSize = + 8192) -> std::vector<std::pair<CacheKey, std::vector<std::pair<Oid, CompressedBuffer>>>> { + std::vector<std::pair<CacheKey, std::vector<std::pair<Oid, CompressedBuffer>>>> Keys; + + cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; + for (size_t Key = 1; Key <= Num; ++Key) + { + const CacheKey NewCacheKey = GenerateKey(Bucket, KeyOffset + Key); + std::vector<std::pair<Oid, CompressedBuffer>> Attachments = + AppendCacheRecord(Request, NewCacheKey, AttachmentCount, AttachmentsSize, CachePolicy::Default); + Keys.push_back(std::make_pair(NewCacheKey, std::move(Attachments))); + } + + CbPackage Package; + CHECK(Request.Format(Package)); + + HttpClient Http{BaseUri}; + + IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); + Body.SetContentType(HttpContentType::kCbPackage); + + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + + if (Result.StatusCode != HttpResponseCode::OK) + { + ZEN_DEBUG("PutCacheRecords failed with {}, reason '{}'", ToString(Result.StatusCode), Result.ErrorMessage("")); + Keys.clear(); + } + + return Keys; + }; + + std::string_view TestBucket = "partialcachevaluetests"sv; + std::string_view TestNamespace = "ue4.ddc"sv; + auto RecordsWithSmallAttachments = PutCacheRecords(BaseUri, TestNamespace, TestBucket, 0, 3, 2, 4096u); + CHECK(RecordsWithSmallAttachments.size() == 3); + auto RecordsWithLargeAttachments = PutCacheRecords(BaseUri, TestNamespace, TestBucket, 10, 1, 2, 8u * 1024u * 1024u); + CHECK(RecordsWithLargeAttachments.size() == 1); + + struct PartialOptions + { + uint64_t Offset = 0ull; + uint64_t Size = ~0ull; + RpcAcceptOptions AcceptOptions = RpcAcceptOptions::kNone; + }; + + auto GetCacheChunk = [](std::string_view BaseUri, + std::string_view Namespace, + const CacheKey& Key, + const Oid& ValueId, + const PartialOptions& Options = {}) -> cacherequests::GetCacheChunksResult { + cacherequests::GetCacheChunksRequest Request = { + .AcceptMagic = kCbPkgMagic, + .AcceptOptions = (uint16_t)Options.AcceptOptions, + .Namespace = std::string(Namespace), + .Requests = {{.Key = Key, .ValueId = ValueId, .RawOffset = Options.Offset, .RawSize = Options.Size}}}; + CbPackage Package; + CHECK(Request.Format(Package)); + IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); + Body.SetContentType(HttpContentType::kCbPackage); + + HttpClient Http{BaseUri}; + + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + + CHECK(Result.StatusCode == HttpResponseCode::OK); + + CbPackage Response = ParsePackageMessage(Result.ResponsePayload); + bool Loaded = !Response.IsNull(); + CHECK_MESSAGE(Loaded, "GetCacheChunks response failed to load."); + cacherequests::GetCacheChunksResult GetCacheChunksResult; + CHECK(GetCacheChunksResult.Parse(Response)); + return GetCacheChunksResult; + }; + + auto GetAndVerifyChunk = [&GetCacheChunk](std::string_view BaseUri, + std::string_view Namespace, + const CacheKey& Key, + const Oid& ChunkId, + const CompressedBuffer& VerifyData, + const PartialOptions& Options = {}) { + cacherequests::GetCacheChunksResult Result = GetCacheChunk(BaseUri, Namespace, Key, ChunkId, Options); + CHECK(Result.Results.size() == 1); + bool CanGetPartial = ((uint16_t)Options.AcceptOptions & (uint16_t)RpcAcceptOptions::kAllowPartialCacheChunks); + if (!CanGetPartial) + { + CHECK(Result.Results[0].FragmentOffset == 0); + CHECK(Result.Results[0].Body.GetCompressedSize() == VerifyData.GetCompressedSize()); + } + IoBuffer SourceDecompressed = VerifyData.Decompress(Options.Offset, Options.Size).AsIoBuffer(); + IoBuffer ReceivedDecompressed = + Result.Results[0].Body.Decompress(Options.Offset - Result.Results[0].FragmentOffset, Options.Size).AsIoBuffer(); + CHECK(SourceDecompressed.GetView().EqualBytes(ReceivedDecompressed.GetView())); + }; + + GetAndVerifyChunk(BaseUri, + TestNamespace, + RecordsWithSmallAttachments[0].first, + RecordsWithSmallAttachments[0].second[0].first, + RecordsWithSmallAttachments[0].second[0].second); + GetAndVerifyChunk(BaseUri, + TestNamespace, + RecordsWithSmallAttachments[0].first, + RecordsWithSmallAttachments[0].second[0].first, + RecordsWithSmallAttachments[0].second[0].second, + PartialOptions{.Offset = 378, .Size = 519, .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences}); + GetAndVerifyChunk( + BaseUri, + TestNamespace, + RecordsWithSmallAttachments[0].first, + RecordsWithSmallAttachments[0].second[0].first, + RecordsWithSmallAttachments[0].second[0].second, + PartialOptions{.Offset = 378, + .Size = 519, + .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialCacheChunks}); + GetAndVerifyChunk(BaseUri, + TestNamespace, + RecordsWithLargeAttachments[0].first, + RecordsWithLargeAttachments[0].second[0].first, + RecordsWithLargeAttachments[0].second[0].second, + PartialOptions{.AcceptOptions = RpcAcceptOptions::kAllowLocalReferences}); + GetAndVerifyChunk(BaseUri, + TestNamespace, + RecordsWithLargeAttachments[0].first, + RecordsWithLargeAttachments[0].second[0].first, + RecordsWithLargeAttachments[0].second[0].second, + PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u}); + GetAndVerifyChunk( + BaseUri, + TestNamespace, + RecordsWithLargeAttachments[0].first, + RecordsWithLargeAttachments[0].second[0].first, + RecordsWithLargeAttachments[0].second[0].second, + PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u, .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences}); + GetAndVerifyChunk( + BaseUri, + TestNamespace, + RecordsWithLargeAttachments[0].first, + RecordsWithLargeAttachments[0].second[0].first, + RecordsWithLargeAttachments[0].second[0].second, + PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u, .AcceptOptions = RpcAcceptOptions::kAllowPartialCacheChunks}); + GetAndVerifyChunk( + BaseUri, + TestNamespace, + RecordsWithLargeAttachments[0].first, + RecordsWithLargeAttachments[0].second[0].first, + RecordsWithLargeAttachments[0].second[0].second, + PartialOptions{.Offset = 1024u * 1024u, + .Size = 512u * 1024u, + .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialCacheChunks}); + GetAndVerifyChunk( + BaseUri, + TestNamespace, + RecordsWithLargeAttachments[0].first, + RecordsWithLargeAttachments[0].second[0].first, + RecordsWithLargeAttachments[0].second[0].second, + PartialOptions{.Offset = 1024u * 1024u, + .Size = 512u * 1024u, + .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences | + RpcAcceptOptions::kAllowPartialCacheChunks}); +} + +IoBuffer +FormatPackageBody(const CbPackage& Package) +{ + IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); + Body.SetContentType(HttpContentType::kCbPackage); + return Body; +} + +TEST_CASE("zcache.rpc.allpolicies") +{ + using namespace std::literals; + using namespace utils; + + ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); + ZenServerInstance UpstreamServer(TestEnv); + SpawnServer(UpstreamServer, UpstreamCfg); + + ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); + ZenServerInstance LocalServer(TestEnv); + SpawnServer(LocalServer, LocalCfg); + + const auto BaseUri = fmt::format("http://localhost:{}/z$", LocalServer.GetBasePort()); + HttpClient Http{BaseUri}; + + std::string_view TestVersion = "F72150A02AE34B57A9EC91D36BA1CE08"sv; + std::string_view TestBucket = "allpoliciestest"sv; + std::string_view TestNamespace = "ue4.ddc"sv; + + // NumKeys = (2 Value vs Record)*(2 SkipData vs Default)*(2 ForceMiss vs Not)*(2 use local) + // *(2 use remote)*(2 UseValue Policy vs not)*(4 cases per type) + constexpr int NumKeys = 256; + constexpr int NumValues = 4; + Oid ValueIds[NumValues]; + IoHash Hash; + for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) + { + ExtendableStringBuilder<16> ValueName; + ValueName << "ValueId_"sv << ValueIndex; + static_assert(sizeof(IoHash) >= sizeof(Oid)); + ValueIds[ValueIndex] = Oid::FromMemory(IoHash::HashBuffer(ValueName.Data(), ValueName.Size() * sizeof(ValueName.Data()[0])).Hash); + } + + struct KeyData; + struct UserData + { + UserData& Set(KeyData* InKeyData, int InValueIndex) + { + Data = InKeyData; + ValueIndex = InValueIndex; + return *this; + } + KeyData* Data = nullptr; + int ValueIndex = 0; + }; + struct KeyData + { + CompressedBuffer BufferValues[NumValues]; + uint64_t IntValues[NumValues]; + UserData ValueUserData[NumValues]; + bool ReceivedChunk[NumValues]; + CacheKey Key; + UserData KeyUserData; + uint32_t KeyIndex = 0; + bool GetRequestsData = true; + bool UseValueAPI = false; + bool UseValuePolicy = false; + bool ForceMiss = false; + bool UseLocal = true; + bool UseRemote = true; + bool ShouldBeHit = true; + bool ReceivedPut = false; + bool ReceivedGet = false; + bool ReceivedPutValue = false; + bool ReceivedGetValue = false; + }; + struct CachePutRequest + { + CacheKey Key; + CbObject Record; + CacheRecordPolicy Policy; + KeyData* Values; + UserData* Data; + }; + struct CachePutValueRequest + { + CacheKey Key; + CompressedBuffer Value; + CachePolicy Policy; + UserData* Data; + }; + struct CacheGetRequest + { + CacheKey Key; + CacheRecordPolicy Policy; + UserData* Data; + }; + struct CacheGetValueRequest + { + CacheKey Key; + CachePolicy Policy; + UserData* Data; + }; + struct CacheGetChunkRequest + { + CacheKey Key; + Oid ValueId; + uint64_t RawOffset; + uint64_t RawSize; + IoHash RawHash; + CachePolicy Policy; + UserData* Data; + }; + + KeyData KeyDatas[NumKeys]; + std::vector<CachePutRequest> PutRequests; + std::vector<CachePutValueRequest> PutValueRequests; + std::vector<CacheGetRequest> GetRequests; + std::vector<CacheGetValueRequest> GetValueRequests; + std::vector<CacheGetChunkRequest> ChunkRequests; + + for (uint32_t KeyIndex = 0; KeyIndex < NumKeys; ++KeyIndex) + { + IoHashStream KeyWriter; + KeyWriter.Append(TestVersion.data(), TestVersion.length() * sizeof(TestVersion.data()[0])); + KeyWriter.Append(&KeyIndex, sizeof(KeyIndex)); + IoHash KeyHash = KeyWriter.GetHash(); + KeyData& KeyData = KeyDatas[KeyIndex]; + + KeyData.Key = CacheKey::Create(TestBucket, KeyHash); + KeyData.KeyIndex = KeyIndex; + KeyData.GetRequestsData = (KeyIndex & (1 << 1)) == 0; + KeyData.UseValueAPI = (KeyIndex & (1 << 2)) != 0; + KeyData.UseValuePolicy = (KeyIndex & (1 << 3)) != 0; + KeyData.ForceMiss = (KeyIndex & (1 << 4)) == 0; + KeyData.UseLocal = (KeyIndex & (1 << 5)) == 0; + KeyData.UseRemote = (KeyIndex & (1 << 6)) == 0; + KeyData.ShouldBeHit = !KeyData.ForceMiss && (KeyData.UseLocal || KeyData.UseRemote); + CachePolicy SharedPolicy = KeyData.UseLocal ? CachePolicy::Local : CachePolicy::None; + SharedPolicy |= KeyData.UseRemote ? CachePolicy::Remote : CachePolicy::None; + CachePolicy PutPolicy = SharedPolicy; + CachePolicy GetPolicy = SharedPolicy; + GetPolicy |= !KeyData.GetRequestsData ? CachePolicy::SkipData : CachePolicy::None; + CacheKey& Key = KeyData.Key; + + for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) + { + KeyData.IntValues[ValueIndex] = static_cast<uint64_t>(KeyIndex) | (static_cast<uint64_t>(ValueIndex) << 32); + KeyData.BufferValues[ValueIndex] = + CompressedBuffer::Compress(SharedBuffer::MakeView(&KeyData.IntValues[ValueIndex], sizeof(KeyData.IntValues[ValueIndex]))); + KeyData.ReceivedChunk[ValueIndex] = false; + } + + UserData& KeyUserData = KeyData.KeyUserData.Set(&KeyData, -1); + for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) + { + KeyData.ValueUserData[ValueIndex].Set(&KeyData, ValueIndex); + } + if (!KeyData.UseValueAPI) + { + CbObjectWriter Builder; + Builder.BeginObject("key"sv); + Builder << "Bucket"sv << Key.Bucket << "Hash"sv << Key.Hash; + Builder.EndObject(); + Builder.BeginArray("Values"sv); + for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) + { + Builder.BeginObject(); + Builder.AddObjectId("Id"sv, ValueIds[ValueIndex]); + Builder.AddBinaryAttachment("RawHash"sv, KeyData.BufferValues[ValueIndex].DecodeRawHash()); + Builder.AddInteger("RawSize"sv, KeyData.BufferValues[ValueIndex].DecodeRawSize()); + Builder.EndObject(); + } + Builder.EndArray(); + + CacheRecordPolicy PutRecordPolicy; + CacheRecordPolicy GetRecordPolicy; + if (!KeyData.UseValuePolicy) + { + PutRecordPolicy = CacheRecordPolicy(PutPolicy); + GetRecordPolicy = CacheRecordPolicy(GetPolicy); + } + else + { + // Switch the SkipData field in the Record policy so that if the CacheStore ignores the ValuePolicies + // it will use the wrong value for SkipData and fail our tests. + CacheRecordPolicyBuilder PutBuilder(PutPolicy ^ CachePolicy::SkipData); + CacheRecordPolicyBuilder GetBuilder(GetPolicy ^ CachePolicy::SkipData); + for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) + { + PutBuilder.AddValuePolicy(ValueIds[ValueIndex], PutPolicy); + GetBuilder.AddValuePolicy(ValueIds[ValueIndex], GetPolicy); + } + PutRecordPolicy = PutBuilder.Build(); + GetRecordPolicy = GetBuilder.Build(); + } + if (!KeyData.ForceMiss) + { + PutRequests.push_back({Key, Builder.Save(), PutRecordPolicy, &KeyData, &KeyUserData}); + } + GetRequests.push_back({Key, GetRecordPolicy, &KeyUserData}); + for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) + { + UserData& ValueUserData = KeyData.ValueUserData[ValueIndex]; + ChunkRequests.push_back({Key, ValueIds[ValueIndex], 0, UINT64_MAX, IoHash(), GetPolicy, &ValueUserData}); + } + } + else + { + if (!KeyData.ForceMiss) + { + PutValueRequests.push_back({Key, KeyData.BufferValues[0], PutPolicy, &KeyUserData}); + } + GetValueRequests.push_back({Key, GetPolicy, &KeyUserData}); + ChunkRequests.push_back({Key, Oid::Zero, 0, UINT64_MAX, IoHash(), GetPolicy, &KeyUserData}); + } + } + + // PutCacheRecords + { + CachePolicy BatchDefaultPolicy = CachePolicy::Default; + cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, + .DefaultPolicy = BatchDefaultPolicy, + .Namespace = std::string(TestNamespace)}; + Request.Requests.reserve(PutRequests.size()); + for (CachePutRequest& PutRequest : PutRequests) + { + cacherequests::PutCacheRecordRequest& RecordRequest = Request.Requests.emplace_back(); + RecordRequest.Key = PutRequest.Key; + RecordRequest.Policy = PutRequest.Policy; + RecordRequest.Values.reserve(NumValues); + for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) + { + RecordRequest.Values.push_back({.Id = ValueIds[ValueIndex], .Body = PutRequest.Values->BufferValues[ValueIndex]}); + } + PutRequest.Data->Data->ReceivedPut = true; + } + + CbPackage Package; + CHECK(Request.Format(Package)); + IoBuffer Body = FormatPackageBody(Package); + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "PutCacheRecords unexpectedly failed."); + } + + // PutCacheValues + { + CachePolicy BatchDefaultPolicy = CachePolicy::Default; + + cacherequests::PutCacheValuesRequest Request = {.AcceptMagic = kCbPkgMagic, + .DefaultPolicy = BatchDefaultPolicy, + .Namespace = std::string(TestNamespace)}; + Request.Requests.reserve(PutValueRequests.size()); + for (CachePutValueRequest& PutRequest : PutValueRequests) + { + Request.Requests.push_back({.Key = PutRequest.Key, .Body = PutRequest.Value, .Policy = PutRequest.Policy}); + PutRequest.Data->Data->ReceivedPutValue = true; + } + + CbPackage Package; + CHECK(Request.Format(Package)); + + IoBuffer Body = FormatPackageBody(Package); + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "PutCacheValues unexpectedly failed."); + } + + for (KeyData& KeyData : KeyDatas) + { + if (!KeyData.ForceMiss) + { + if (!KeyData.UseValueAPI) + { + CHECK_MESSAGE(KeyData.ReceivedPut, WriteToString<32>("Key ", KeyData.KeyIndex, " was unexpectedly not put.").c_str()); + } + else + { + CHECK_MESSAGE(KeyData.ReceivedPutValue, + WriteToString<32>("Key ", KeyData.KeyIndex, " was unexpectedly not put to ValueAPI.").c_str()); + } + } + } + + // GetCacheRecords + { + CachePolicy BatchDefaultPolicy = CachePolicy::Default; + cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, + .DefaultPolicy = BatchDefaultPolicy, + .Namespace = std::string(TestNamespace)}; + Request.Requests.reserve(GetRequests.size()); + for (CacheGetRequest& GetRequest : GetRequests) + { + Request.Requests.push_back({.Key = GetRequest.Key, .Policy = GetRequest.Policy}); + } + + CbPackage Package; + CHECK(Request.Format(Package)); + IoBuffer Body = FormatPackageBody(Package); + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "GetCacheRecords unexpectedly failed."); + CbPackage Response = ParsePackageMessage(Result.ResponsePayload); + bool Loaded = !Response.IsNull(); + CHECK_MESSAGE(Loaded, "GetCacheRecords response failed to load."); + cacherequests::GetCacheRecordsResult RequestResult; + CHECK(RequestResult.Parse(Response)); + CHECK_MESSAGE(RequestResult.Results.size() == GetRequests.size(), "GetCacheRecords response count did not match request count."); + for (int Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& RecordResult : RequestResult.Results) + { + bool Succeeded = RecordResult.has_value(); + CacheGetRequest& GetRequest = GetRequests[Index++]; + KeyData* KeyData = GetRequest.Data->Data; + KeyData->ReceivedGet = true; + WriteToString<32> Name("Get(", KeyData->KeyIndex, ")"); + if (KeyData->ShouldBeHit) + { + CHECK_MESSAGE(Succeeded, WriteToString<32>(Name, " unexpectedly failed.").c_str()); + } + else if (KeyData->ForceMiss) + { + CHECK_MESSAGE(!Succeeded, WriteToString<32>(Name, " unexpectedly succeeded.").c_str()); + } + if (!KeyData->ForceMiss && Succeeded) + { + CHECK_MESSAGE(RecordResult->Values.size() == NumValues, + WriteToString<32>(Name, " number of values did not match.").c_str()); + for (const cacherequests::GetCacheRecordResultValue& Value : RecordResult->Values) + { + int ExpectedValueIndex = 0; + for (; ExpectedValueIndex < NumValues; ++ExpectedValueIndex) + { + if (ValueIds[ExpectedValueIndex] == Value.Id) + { + break; + } + } + CHECK_MESSAGE(ExpectedValueIndex < NumValues, WriteToString<32>(Name, " could not find matching ValueId.").c_str()); + + WriteToString<32> ValueName("Get(", KeyData->KeyIndex, ",", ExpectedValueIndex, ")"); + + CompressedBuffer ExpectedValue = KeyData->BufferValues[ExpectedValueIndex]; + CHECK_MESSAGE(Value.RawHash == ExpectedValue.DecodeRawHash(), + WriteToString<32>(ValueName, " RawHash did not match.").c_str()); + CHECK_MESSAGE(Value.RawSize == ExpectedValue.DecodeRawSize(), + WriteToString<32>(ValueName, " RawSize did not match.").c_str()); + + if (KeyData->GetRequestsData) + { + SharedBuffer Buffer = Value.Body.Decompress(); + CHECK_MESSAGE(Buffer.GetSize() == Value.RawSize, + WriteToString<32>(ValueName, " BufferSize did not match RawSize.").c_str()); + uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0]; + uint64_t ExpectedIntValue = KeyData->IntValues[ExpectedValueIndex]; + CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(ValueName, " had unexpected data.").c_str()); + } + } + } + } + } + + // GetCacheValues + { + CachePolicy BatchDefaultPolicy = CachePolicy::Default; + + cacherequests::GetCacheValuesRequest GetCacheValuesRequest = {.AcceptMagic = kCbPkgMagic, + .DefaultPolicy = BatchDefaultPolicy, + .Namespace = std::string(TestNamespace)}; + GetCacheValuesRequest.Requests.reserve(GetValueRequests.size()); + for (CacheGetValueRequest& GetRequest : GetValueRequests) + { + GetCacheValuesRequest.Requests.push_back({.Key = GetRequest.Key, .Policy = GetRequest.Policy}); + } + + CbPackage Package; + CHECK(GetCacheValuesRequest.Format(Package)); + + IoBuffer Body = FormatPackageBody(Package); + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "GetCacheValues unexpectedly failed."); + IoBuffer MessageBuffer(Result.ResponsePayload); + CbPackage Response = ParsePackageMessage(MessageBuffer); + bool Loaded = !Response.IsNull(); + CHECK_MESSAGE(Loaded, "GetCacheValues response failed to load."); + cacherequests::GetCacheValuesResult GetCacheValuesResult; + CHECK(GetCacheValuesResult.Parse(Response)); + for (int Index = 0; const cacherequests::CacheValueResult& ValueResult : GetCacheValuesResult.Results) + { + bool Succeeded = ValueResult.RawHash != IoHash::Zero; + CacheGetValueRequest& Request = GetValueRequests[Index++]; + KeyData* KeyData = Request.Data->Data; + KeyData->ReceivedGetValue = true; + WriteToString<32> Name("GetValue("sv, KeyData->KeyIndex, ")"sv); + + if (KeyData->ShouldBeHit) + { + CHECK_MESSAGE(Succeeded, WriteToString<32>(Name, " unexpectedly failed.").c_str()); + } + else if (KeyData->ForceMiss) + { + CHECK_MESSAGE(!Succeeded, WriteToString<32>(Name, "unexpectedly succeeded.").c_str()); + } + if (!KeyData->ForceMiss && Succeeded) + { + CompressedBuffer ExpectedValue = KeyData->BufferValues[0]; + CHECK_MESSAGE(ValueResult.RawHash == ExpectedValue.DecodeRawHash(), + WriteToString<32>(Name, " RawHash did not match.").c_str()); + CHECK_MESSAGE(ValueResult.RawSize == ExpectedValue.DecodeRawSize(), + WriteToString<32>(Name, " RawSize did not match.").c_str()); + + if (KeyData->GetRequestsData) + { + SharedBuffer Buffer = ValueResult.Body.Decompress(); + CHECK_MESSAGE(Buffer.GetSize() == ValueResult.RawSize, + WriteToString<32>(Name, " BufferSize did not match RawSize.").c_str()); + uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0]; + uint64_t ExpectedIntValue = KeyData->IntValues[0]; + CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(Name, " had unexpected data.").c_str()); + } + } + } + } + + // GetCacheChunks + { + std::sort(ChunkRequests.begin(), ChunkRequests.end(), [](CacheGetChunkRequest& A, CacheGetChunkRequest& B) { + return A.Key.Hash < B.Key.Hash; + }); + CachePolicy BatchDefaultPolicy = CachePolicy::Default; + cacherequests::GetCacheChunksRequest GetCacheChunksRequest = {.AcceptMagic = kCbPkgMagic, + .DefaultPolicy = BatchDefaultPolicy, + .Namespace = std::string(TestNamespace)}; + GetCacheChunksRequest.Requests.reserve(ChunkRequests.size()); + for (CacheGetChunkRequest& ChunkRequest : ChunkRequests) + { + GetCacheChunksRequest.Requests.push_back({.Key = ChunkRequest.Key, + .ValueId = ChunkRequest.ValueId, + .ChunkId = IoHash(), + .RawOffset = ChunkRequest.RawOffset, + .RawSize = ChunkRequest.RawSize, + .Policy = ChunkRequest.Policy}); + } + CbPackage Package; + CHECK(GetCacheChunksRequest.Format(Package)); + + IoBuffer Body = FormatPackageBody(Package); + HttpClient::Response Result = Http.Post("/$rpc", Body, {{"Accept", "application/x-ue-cbpkg"}}); + CHECK_MESSAGE(Result.StatusCode == HttpResponseCode::OK, "GetCacheChunks unexpectedly failed."); + CbPackage Response = ParsePackageMessage(Result.ResponsePayload); + bool Loaded = !Response.IsNull(); + CHECK_MESSAGE(Loaded, "GetCacheChunks response failed to load."); + cacherequests::GetCacheChunksResult GetCacheChunksResult; + CHECK(GetCacheChunksResult.Parse(Response)); + CHECK_MESSAGE(GetCacheChunksResult.Results.size() == ChunkRequests.size(), + "GetCacheChunks response count did not match request count."); + + for (int Index = 0; const cacherequests::CacheValueResult& ValueResult : GetCacheChunksResult.Results) + { + bool Succeeded = ValueResult.RawHash != IoHash::Zero; + + CacheGetChunkRequest& Request = ChunkRequests[Index++]; + KeyData* KeyData = Request.Data->Data; + int ValueIndex = Request.Data->ValueIndex >= 0 ? Request.Data->ValueIndex : 0; + KeyData->ReceivedChunk[ValueIndex] = true; + WriteToString<32> Name("GetChunks("sv, KeyData->KeyIndex, ","sv, ValueIndex, ")"sv); + + if (KeyData->ShouldBeHit) + { + CHECK_MESSAGE(Succeeded, WriteToString<256>(Name, " unexpectedly failed."sv).c_str()); + } + else if (KeyData->ForceMiss) + { + CHECK_MESSAGE(!Succeeded, WriteToString<256>(Name, " unexpectedly succeeded."sv).c_str()); + } + if (KeyData->ShouldBeHit && Succeeded) + { + CompressedBuffer ExpectedValue = KeyData->BufferValues[ValueIndex]; + CHECK_MESSAGE(ValueResult.RawHash == ExpectedValue.DecodeRawHash(), + WriteToString<32>(Name, " had unexpected RawHash.").c_str()); + CHECK_MESSAGE(ValueResult.RawSize == ExpectedValue.DecodeRawSize(), + WriteToString<32>(Name, " had unexpected RawSize.").c_str()); + + if (KeyData->GetRequestsData) + { + SharedBuffer Buffer = ValueResult.Body.Decompress(); + CHECK_MESSAGE(Buffer.GetSize() == ValueResult.RawSize, + WriteToString<32>(Name, " BufferSize did not match RawSize.").c_str()); + uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0]; + uint64_t ExpectedIntValue = KeyData->IntValues[ValueIndex]; + CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(Name, " had unexpected data.").c_str()); + } + } + } + } + + for (KeyData& KeyData : KeyDatas) + { + if (!KeyData.UseValueAPI) + { + CHECK_MESSAGE(KeyData.ReceivedGet, WriteToString<32>("Get(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str()); + for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) + { + CHECK_MESSAGE( + KeyData.ReceivedChunk[ValueIndex], + WriteToString<32>("GetChunks(", KeyData.KeyIndex, ",", ValueIndex, ") was unexpectedly not received.").c_str()); + } + } + else + { + CHECK_MESSAGE(KeyData.ReceivedGetValue, + WriteToString<32>("GetValue(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str()); + CHECK_MESSAGE(KeyData.ReceivedChunk[0], + WriteToString<32>("GetChunks(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str()); + } + } +} + +} // namespace zen::tests + +#endif diff --git a/src/zenutil/cache/cacherequests.cpp b/src/zenserver-test/cacherequests.cpp index 7c6f493f2..46339aebb 100644 --- a/src/zenutil/cache/cacherequests.cpp +++ b/src/zenserver-test/cacherequests.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/cache/cacherequests.h> +#include "cacherequests.h" #include <zencore/compactbinary.h> #include <zencore/compactbinarybuilder.h> @@ -8,6 +8,7 @@ #include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zencore/zencore.h> +#include <zenstore/cache/cache.h> #include <string> #include <string_view> @@ -17,124 +18,13 @@ # include <zencore/testing.h> #endif -namespace zen { - -namespace cacherequests { - - 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; - } +namespace zen { namespace cacherequests { std::optional<CacheRecordPolicy> Convert(const OptionalCacheRecordPolicy& Policy) { return Policy.IsValid() ? Policy.Get() : std::optional<CacheRecordPolicy>{}; }; - std::optional<std::string> GetRequestNamespace(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 GetRequestCacheKey(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; - } - void WriteCacheRequestKey(CbObjectWriter& Writer, const CacheKey& Value) { Writer.BeginObject("Key"); @@ -179,7 +69,7 @@ namespace cacherequests { AcceptMagic = BatchObject["AcceptType"].AsUInt32(0); CbObjectView Params = BatchObject["Params"].AsObjectView(); - std::optional<std::string> RequestNamespace = GetRequestNamespace(Params); + std::optional<std::string> RequestNamespace = GetCacheRequestNamespace(Params); if (!RequestNamespace) { return false; @@ -197,7 +87,7 @@ namespace cacherequests { PutCacheRecordRequest& Request = Requests[RequestIndex++]; - if (!GetRequestCacheKey(KeyView, Request.Key)) + if (!GetCacheRequestCacheKey(KeyView, Request.Key)) { return false; } @@ -313,6 +203,17 @@ namespace cacherequests { Success.push_back(It.AsBool()); It++; } + + CbArrayView DetailsArray = Package.GetObject()["Details"].AsArrayView(); + if (DetailsArray) + { + It = DetailsArray.CreateViewIterator(); + while (It.HasValue()) + { + Details.push_back(It.AsObjectView()); + It++; + } + } return true; } @@ -325,7 +226,15 @@ namespace cacherequests { ResponseObject.AddBool(Value); } ResponseObject.EndArray(); - + if (!Details.empty()) + { + ResponseObject.BeginArray("Details"); + for (CbObjectView Value : Details) + { + ResponseObject.AddObject(Value); + } + ResponseObject.EndArray(); + } OutPackage.SetObject(ResponseObject.Save()); return true; } @@ -338,7 +247,7 @@ namespace cacherequests { ProcessPid = RpcRequest["Pid"].AsInt32(0); CbObjectView Params = RpcRequest["Params"].AsObjectView(); - std::optional<std::string> RequestNamespace = GetRequestNamespace(Params); + std::optional<std::string> RequestNamespace = GetCacheRequestNamespace(Params); if (!RequestNamespace) { return false; @@ -356,7 +265,7 @@ namespace cacherequests { GetCacheRecordRequest& Request = Requests.emplace_back(); - if (!GetRequestCacheKey(KeyObject, Request.Key)) + if (!GetCacheRequestCacheKey(KeyObject, Request.Key)) { return false; } @@ -464,7 +373,7 @@ namespace cacherequests { GetCacheRecordResult& Request = Results[ResultIndex].value(); CbObjectView RecordObject = RecordView.AsObjectView(); CbObjectView KeyObject = RecordObject["Key"].AsObjectView(); - if (!GetRequestCacheKey(KeyObject, Request.Key)) + if (!GetCacheRequestCacheKey(KeyObject, Request.Key)) { return false; } @@ -541,7 +450,7 @@ namespace cacherequests { AcceptMagic = BatchObject["AcceptType"].AsUInt32(0); CbObjectView Params = BatchObject["Params"].AsObjectView(); - std::optional<std::string> RequestNamespace = cacherequests::GetRequestNamespace(Params); + std::optional<std::string> RequestNamespace = GetCacheRequestNamespace(Params); if (!RequestNamespace) { return false; @@ -559,7 +468,7 @@ namespace cacherequests { PutCacheValueRequest& Request = Requests.emplace_back(); - if (!GetRequestCacheKey(KeyObject, Request.Key)) + if (!GetCacheRequestCacheKey(KeyObject, Request.Key)) { return false; } @@ -672,7 +581,7 @@ namespace cacherequests { ProcessPid = BatchObject["Pid"].AsInt32(0); CbObjectView Params = BatchObject["Params"].AsObjectView(); - std::optional<std::string> RequestNamespace = cacherequests::GetRequestNamespace(Params); + std::optional<std::string> RequestNamespace = GetCacheRequestNamespace(Params); if (!RequestNamespace) { return false; @@ -690,7 +599,7 @@ namespace cacherequests { GetCacheValueRequest& Request = Requests.emplace_back(); - if (!GetRequestCacheKey(KeyObject, Request.Key)) + if (!GetCacheRequestCacheKey(KeyObject, Request.Key)) { return false; } @@ -846,7 +755,7 @@ namespace cacherequests { ProcessPid = BatchObject["Pid"].AsInt32(0); CbObjectView Params = BatchObject["Params"].AsObjectView(); - std::optional<std::string> RequestNamespace = cacherequests::GetRequestNamespace(Params); + std::optional<std::string> RequestNamespace = GetCacheRequestNamespace(Params); if (!RequestNamespace) { return false; @@ -864,7 +773,7 @@ namespace cacherequests { GetCacheChunkRequest& Request = Requests.emplace_back(); - if (!GetRequestCacheKey(KeyObject, Request.Key)) + if (!GetCacheRequestCacheKey(KeyObject, Request.Key)) { return false; } @@ -926,122 +835,11 @@ namespace cacherequests { return true; } - bool HttpRequestParseRelativeUri(std::string_view Key, std::string_view DefaultNamespace, HttpRequestData& 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; - } - } - // bool CacheRecord::Parse(CbObjectView& Reader) // { // CbObjectView KeyView = Reader["Key"].AsObjectView(); // - // if (!GetRequestCacheKey(KeyView, Key)) + // if (!GetCacheRequestCacheKey(KeyView, Key)) // { // return false; // } @@ -1563,80 +1361,80 @@ namespace cacherequests { { using namespace std::literals; - HttpRequestData RootRequest; - CHECK(HttpRequestParseRelativeUri("", "!default!", RootRequest)); + HttpCacheRequestData RootRequest; + CHECK(HttpCacheRequestParseRelativeUri("", "!default!", RootRequest)); CHECK(!RootRequest.Namespace.has_value()); CHECK(!RootRequest.Bucket.has_value()); CHECK(!RootRequest.HashKey.has_value()); CHECK(!RootRequest.ValueContentId.has_value()); RootRequest = {}; - CHECK(HttpRequestParseRelativeUri("/", "!default!", RootRequest)); + CHECK(HttpCacheRequestParseRelativeUri("/", "!default!", RootRequest)); CHECK(!RootRequest.Namespace.has_value()); CHECK(!RootRequest.Bucket.has_value()); CHECK(!RootRequest.HashKey.has_value()); CHECK(!RootRequest.ValueContentId.has_value()); - HttpRequestData LegacyBucketRequestBecomesNamespaceRequest; - CHECK(HttpRequestParseRelativeUri("test", "!default!", LegacyBucketRequestBecomesNamespaceRequest)); + HttpCacheRequestData LegacyBucketRequestBecomesNamespaceRequest; + CHECK(HttpCacheRequestParseRelativeUri("test", "!default!", LegacyBucketRequestBecomesNamespaceRequest)); CHECK(LegacyBucketRequestBecomesNamespaceRequest.Namespace == "test"sv); CHECK(!LegacyBucketRequestBecomesNamespaceRequest.Bucket.has_value()); CHECK(!LegacyBucketRequestBecomesNamespaceRequest.HashKey.has_value()); CHECK(!LegacyBucketRequestBecomesNamespaceRequest.ValueContentId.has_value()); - HttpRequestData LegacyHashKeyRequest; - CHECK(HttpRequestParseRelativeUri("test/0123456789abcdef12340123456789abcdef1234", "!default!", LegacyHashKeyRequest)); + HttpCacheRequestData LegacyHashKeyRequest; + CHECK(HttpCacheRequestParseRelativeUri("test/0123456789abcdef12340123456789abcdef1234", "!default!", LegacyHashKeyRequest)); CHECK(LegacyHashKeyRequest.Namespace == "!default!"); CHECK(LegacyHashKeyRequest.Bucket == "test"sv); CHECK(LegacyHashKeyRequest.HashKey == IoHash::FromHexString("0123456789abcdef12340123456789abcdef1234"sv)); CHECK(!LegacyHashKeyRequest.ValueContentId.has_value()); - HttpRequestData LegacyValueContentIdRequest; - CHECK(HttpRequestParseRelativeUri("test/0123456789abcdef12340123456789abcdef1234/56789abcdef12345678956789abcdef123456789", - "!default!", - LegacyValueContentIdRequest)); + HttpCacheRequestData LegacyValueContentIdRequest; + CHECK(HttpCacheRequestParseRelativeUri("test/0123456789abcdef12340123456789abcdef1234/56789abcdef12345678956789abcdef123456789", + "!default!", + LegacyValueContentIdRequest)); CHECK(LegacyValueContentIdRequest.Namespace == "!default!"); CHECK(LegacyValueContentIdRequest.Bucket == "test"sv); CHECK(LegacyValueContentIdRequest.HashKey == IoHash::FromHexString("0123456789abcdef12340123456789abcdef1234"sv)); CHECK(LegacyValueContentIdRequest.ValueContentId == IoHash::FromHexString("56789abcdef12345678956789abcdef123456789"sv)); - HttpRequestData V2DefaultNamespaceRequest; - CHECK(HttpRequestParseRelativeUri("ue4.ddc", "!default!", V2DefaultNamespaceRequest)); + HttpCacheRequestData V2DefaultNamespaceRequest; + CHECK(HttpCacheRequestParseRelativeUri("ue4.ddc", "!default!", V2DefaultNamespaceRequest)); CHECK(V2DefaultNamespaceRequest.Namespace == "ue4.ddc"); CHECK(!V2DefaultNamespaceRequest.Bucket.has_value()); CHECK(!V2DefaultNamespaceRequest.HashKey.has_value()); CHECK(!V2DefaultNamespaceRequest.ValueContentId.has_value()); - HttpRequestData V2NamespaceRequest; - CHECK(HttpRequestParseRelativeUri("nicenamespace", "!default!", V2NamespaceRequest)); + HttpCacheRequestData V2NamespaceRequest; + CHECK(HttpCacheRequestParseRelativeUri("nicenamespace", "!default!", V2NamespaceRequest)); CHECK(V2NamespaceRequest.Namespace == "nicenamespace"sv); CHECK(!V2NamespaceRequest.Bucket.has_value()); CHECK(!V2NamespaceRequest.HashKey.has_value()); CHECK(!V2NamespaceRequest.ValueContentId.has_value()); - HttpRequestData V2BucketRequestWithDefaultNamespace; - CHECK(HttpRequestParseRelativeUri("ue4.ddc/test", "!default!", V2BucketRequestWithDefaultNamespace)); + HttpCacheRequestData V2BucketRequestWithDefaultNamespace; + CHECK(HttpCacheRequestParseRelativeUri("ue4.ddc/test", "!default!", V2BucketRequestWithDefaultNamespace)); CHECK(V2BucketRequestWithDefaultNamespace.Namespace == "ue4.ddc"); CHECK(V2BucketRequestWithDefaultNamespace.Bucket == "test"sv); CHECK(!V2BucketRequestWithDefaultNamespace.HashKey.has_value()); CHECK(!V2BucketRequestWithDefaultNamespace.ValueContentId.has_value()); - HttpRequestData V2BucketRequestWithNamespace; - CHECK(HttpRequestParseRelativeUri("nicenamespace/test", "!default!", V2BucketRequestWithNamespace)); + HttpCacheRequestData V2BucketRequestWithNamespace; + CHECK(HttpCacheRequestParseRelativeUri("nicenamespace/test", "!default!", V2BucketRequestWithNamespace)); CHECK(V2BucketRequestWithNamespace.Namespace == "nicenamespace"sv); CHECK(V2BucketRequestWithNamespace.Bucket == "test"sv); CHECK(!V2BucketRequestWithNamespace.HashKey.has_value()); CHECK(!V2BucketRequestWithNamespace.ValueContentId.has_value()); - HttpRequestData V2HashKeyRequest; - CHECK(HttpRequestParseRelativeUri("test/0123456789abcdef12340123456789abcdef1234", "!default!", V2HashKeyRequest)); + HttpCacheRequestData V2HashKeyRequest; + CHECK(HttpCacheRequestParseRelativeUri("test/0123456789abcdef12340123456789abcdef1234", "!default!", V2HashKeyRequest)); CHECK(V2HashKeyRequest.Namespace == "!default!"); CHECK(V2HashKeyRequest.Bucket == "test"); CHECK(V2HashKeyRequest.HashKey == IoHash::FromHexString("0123456789abcdef12340123456789abcdef1234"sv)); CHECK(!V2HashKeyRequest.ValueContentId.has_value()); - HttpRequestData V2ValueContentIdRequest; - CHECK(HttpRequestParseRelativeUri( + HttpCacheRequestData V2ValueContentIdRequest; + CHECK(HttpCacheRequestParseRelativeUri( "nicenamespace/test/0123456789abcdef12340123456789abcdef1234/56789abcdef12345678956789abcdef123456789", "!default!", V2ValueContentIdRequest)); @@ -1645,26 +1443,20 @@ namespace cacherequests { CHECK(V2ValueContentIdRequest.HashKey == IoHash::FromHexString("0123456789abcdef12340123456789abcdef1234"sv)); CHECK(V2ValueContentIdRequest.ValueContentId == IoHash::FromHexString("56789abcdef12345678956789abcdef123456789"sv)); - HttpRequestData Invalid; - CHECK(!HttpRequestParseRelativeUri("bad\2_namespace", "!default!", Invalid)); - CHECK(!HttpRequestParseRelativeUri("nice/\2\1bucket", "!default!", Invalid)); - CHECK(!HttpRequestParseRelativeUri("namespace/bucket/0123456789a", "!default!", Invalid)); - CHECK(!HttpRequestParseRelativeUri("namespace/bucket/0123456789abcdef12340123456789abcdef1234/56789abcdef1234", - "!default!", - Invalid)); - CHECK(!HttpRequestParseRelativeUri("namespace/bucket/pppppppp89abcdef12340123456789abcdef1234", "!default!", Invalid)); - CHECK(!HttpRequestParseRelativeUri("namespace/bucket/0123456789abcdef12340123456789abcdef1234/56789abcd", "!default!", Invalid)); - CHECK(!HttpRequestParseRelativeUri( + HttpCacheRequestData Invalid; + CHECK(!HttpCacheRequestParseRelativeUri("bad\2_namespace", "!default!", Invalid)); + CHECK(!HttpCacheRequestParseRelativeUri("nice/\2\1bucket", "!default!", Invalid)); + CHECK(!HttpCacheRequestParseRelativeUri("namespace/bucket/0123456789a", "!default!", Invalid)); + CHECK(!HttpCacheRequestParseRelativeUri("namespace/bucket/0123456789abcdef12340123456789abcdef1234/56789abcdef1234", + "!default!", + Invalid)); + CHECK(!HttpCacheRequestParseRelativeUri("namespace/bucket/pppppppp89abcdef12340123456789abcdef1234", "!default!", Invalid)); + CHECK( + !HttpCacheRequestParseRelativeUri("namespace/bucket/0123456789abcdef12340123456789abcdef1234/56789abcd", "!default!", Invalid)); + CHECK(!HttpCacheRequestParseRelativeUri( "namespace/bucket/0123456789abcdef12340123456789abcdef1234/ppppppppdef12345678956789abcdef123456789", "!default!", Invalid)); } #endif -} // namespace cacherequests - -void -cacherequests_forcelink() -{ -} - -} // namespace zen +}} // namespace zen::cacherequests diff --git a/src/zenutil/include/zenutil/cache/cacherequests.h b/src/zenserver-test/cacherequests.h index fbf3e08cc..5b7a53390 100644 --- a/src/zenutil/include/zenutil/cache/cacherequests.h +++ b/src/zenserver-test/cacherequests.h @@ -4,8 +4,8 @@ #include <zencore/compress.h> -#include "cachekey.h" -#include "cachepolicy.h" +#include <zenstore/cache/cachekey.h> +#include <zenstore/cache/cachepolicy.h> #include <functional> @@ -85,7 +85,8 @@ namespace cacherequests { struct PutCacheRecordsResult { - std::vector<bool> Success; + std::vector<bool> Success; + std::vector<CbObjectView> Details; bool Parse(const CbPackage& Package); bool Format(CbPackage& OutPackage) const; @@ -242,26 +243,6 @@ namespace cacherequests { ////////////////////////////////////////////////////////////////////////// - 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 HttpRequestData - { - std::optional<std::string> Namespace; - std::optional<std::string> Bucket; - std::optional<IoHash> HashKey; - std::optional<IoHash> ValueContentId; - }; - - bool HttpRequestParseRelativeUri(std::string_view Key, std::string_view DefaultNamespace, HttpRequestData& Data); - - // Temporarily public - std::optional<std::string> GetRequestNamespace(const CbObjectView& Params); - bool GetRequestCacheKey(const CbObjectView& KeyView, CacheKey& Key); - - ////////////////////////////////////////////////////////////////////////// - // struct CacheRecordValue // { // Oid Id = Oid::Zero; @@ -280,6 +261,4 @@ namespace cacherequests { } // namespace cacherequests -void cacherequests_forcelink(); // internal - } // namespace zen diff --git a/src/zenserver-test/projectclient.cpp b/src/zenserver-test/projectclient.cpp deleted file mode 100644 index cb493be77..000000000 --- a/src/zenserver-test/projectclient.cpp +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#include "projectclient.h" - -#if 0 - -# include <zencore/compactbinary.h> -# include <zencore/logging.h> -# include <zencore/sharedbuffer.h> -# include <zencore/string.h> -# include <zencore/zencore.h> - -# include <asio.hpp> -# include <gsl/gsl-lite.hpp> - -namespace zen { - -struct ProjectClientConnection -{ - ProjectClientConnection(int BasePort) { Connect(BasePort); } - - void Connect(int BasePort) - { - ZEN_UNUSED(BasePort); - - WideStringBuilder<64> PipeName; - PipeName << "\\\\.\\pipe\\zenprj"; // TODO: this should use an instance-specific identifier! - - HANDLE hPipe = CreateFileW(PipeName.c_str(), - GENERIC_READ | GENERIC_WRITE, - 0, // Sharing doesn't make any sense - nullptr, // No security attributes - OPEN_EXISTING, // Open existing pipe - 0, // Attributes - nullptr // Template file - ); - - if (hPipe == INVALID_HANDLE_VALUE) - { - ZEN_WARN("failed while creating named pipe {}", WideToUtf8(PipeName)); - - throw std::system_error(GetLastError(), std::system_category(), fmt::format("Failed to open named pipe '{}'", WideToUtf8(PipeName))); - } - - // Change to message mode - DWORD dwMode = PIPE_READMODE_MESSAGE; - BOOL Success = SetNamedPipeHandleState(hPipe, &dwMode, nullptr, nullptr); - - if (!Success) - { - throw std::system_error(GetLastError(), - std::system_category(), - fmt::format("Failed to change named pipe '{}' to message mode", WideToUtf8(PipeName))); - } - - m_hPipe.Attach(hPipe); // This now owns the handle and will close it - } - - ~ProjectClientConnection() {} - - CbObject MessageTransaction(CbObject Request) - { - DWORD dwWrittenBytes = 0; - - MemoryView View = Request.GetView(); - - BOOL Success = ::WriteFile(m_hPipe, View.GetData(), gsl::narrow_cast<DWORD>(View.GetSize()), &dwWrittenBytes, nullptr); - - if (!Success) - { - throw std::system_error(GetLastError(), std::system_category(), "Failed to write pipe message"); - } - - ZEN_ASSERT(dwWrittenBytes == View.GetSize()); - - DWORD dwReadBytes = 0; - - Success = ReadFile(m_hPipe, m_Buffer, sizeof m_Buffer, &dwReadBytes, nullptr); - - if (!Success) - { - DWORD ErrorCode = GetLastError(); - - if (ERROR_MORE_DATA == ErrorCode) - { - // Response message is larger than our buffer - handle it by allocating a larger - // buffer on the heap and read the remainder into that buffer - - DWORD dwBytesAvail = 0, dwLeftThisMessage = 0; - - Success = PeekNamedPipe(m_hPipe, nullptr, 0, nullptr, &dwBytesAvail, &dwLeftThisMessage); - - if (Success) - { - UniqueBuffer MessageBuffer = UniqueBuffer::Alloc(dwReadBytes + dwLeftThisMessage); - - memcpy(MessageBuffer.GetData(), m_Buffer, dwReadBytes); - - Success = ReadFile(m_hPipe, - reinterpret_cast<uint8_t*>(MessageBuffer.GetData()) + dwReadBytes, - dwLeftThisMessage, - &dwReadBytes, - nullptr); - - if (Success) - { - return CbObject(SharedBuffer(std::move(MessageBuffer))); - } - } - } - - throw std::system_error(GetLastError(), std::system_category(), "Failed to read pipe message"); - } - - return CbObject(SharedBuffer::MakeView(MakeMemoryView(m_Buffer))); - } - -private: - static const int kEmbeddedBufferSize = 512 - 16; - - CHandle m_hPipe; - uint8_t m_Buffer[kEmbeddedBufferSize]; -}; - -struct LocalProjectClient::ClientImpl -{ - ClientImpl(int BasePort) : m_BasePort(BasePort) {} - ~ClientImpl() {} - - void Start() {} - void Stop() {} - - inline int BasePort() const { return m_BasePort; } - -private: - int m_BasePort = 0; -}; - -LocalProjectClient::LocalProjectClient(int BasePort) -{ - m_Impl = std::make_unique<ClientImpl>(BasePort); - m_Impl->Start(); -} - -LocalProjectClient::~LocalProjectClient() -{ - m_Impl->Stop(); -} - -CbObject -LocalProjectClient::MessageTransaction(CbObject Request) -{ - ProjectClientConnection Cx(m_Impl->BasePort()); - - return Cx.MessageTransaction(Request); -} - -} // namespace zen - -#endif // 0 diff --git a/src/zenserver-test/projectclient.h b/src/zenserver-test/projectclient.h deleted file mode 100644 index 8362ee0ee..000000000 --- a/src/zenserver-test/projectclient.h +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include <memory> - -#include <zenbase/refcount.h> -#include <zencore/compactbinary.h> - -namespace zen { - -/** - * Client for communication with local project service - * - * This is WIP and not yet functional! - */ - -class LocalProjectClient : public RefCounted -{ -public: - LocalProjectClient(int BasePort = 0); - ~LocalProjectClient(); - - CbObject MessageTransaction(CbObject Request); - -private: - struct ClientImpl; - - std::unique_ptr<ClientImpl> m_Impl; -}; - -} // namespace zen diff --git a/src/zenserver-test/projectstore-tests.cpp b/src/zenserver-test/projectstore-tests.cpp new file mode 100644 index 000000000..c8c96dbbb --- /dev/null +++ b/src/zenserver-test/projectstore-tests.cpp @@ -0,0 +1,1058 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#if ZEN_WITH_TESTS +# include "zenserver-test.h" +# include <zencore/testing.h> +# include <zencore/testutils.h> +# include <zencore/workthreadpool.h> +# include <zencore/compactbinarybuilder.h> +# include <zencore/compactbinarypackage.h> +# include <zencore/compress.h> +# include <zencore/filesystem.h> +# include <zencore/fmtutils.h> +# include <zencore/stream.h> +# include <zencore/string.h> +# include <zencore/xxhash.h> +# include <zenhttp/packageformat.h> +# include <zenutil/zenserverprocess.h> +# include <zenhttp/httpclient.h> + +ZEN_THIRD_PARTY_INCLUDES_START +# include <tsl/robin_set.h> +ZEN_THIRD_PARTY_INCLUDES_END + +# include <random> + +namespace zen::tests { + +using namespace std::literals; + +TEST_CASE("project.basic") +{ + using namespace std::literals; + + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + + ZenServerInstance Instance1(TestEnv); + Instance1.SetTestDir(TestDir); + + const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady(); + + std::mt19937_64 mt; + + zen::StringBuilder<64> BaseUri; + BaseUri << fmt::format("http://localhost:{}", PortNumber); + + std::filesystem::path BinPath = zen::GetRunningExecutablePath(); + std::filesystem::path RootPath = BinPath.parent_path().parent_path(); + BinPath = BinPath.lexically_relative(RootPath); + + SUBCASE("build store init") + { + { + HttpClient Http{BaseUri}; + + { + zen::CbObjectWriter Body; + Body << "id" + << "test"; + Body << "root" << RootPath.c_str(); + Body << "project" + << "/zooom"; + Body << "engine" + << "/zooom"; + + zen::BinaryWriter MemOut; + IoBuffer BodyBuf = Body.Save().GetBuffer().AsIoBuffer(); + + auto Response = Http.Post("/prj/test"sv, BodyBuf); + CHECK(Response.StatusCode == HttpResponseCode::Created); + } + + { + auto Response = Http.Get("/prj/test"sv); + CHECK(Response.StatusCode == HttpResponseCode::OK); + + CbObject ResponseObject = Response.AsObject(); + + CHECK(ResponseObject["id"].AsString() == "test"sv); + CHECK(ResponseObject["root"].AsString() == PathToUtf8(RootPath.c_str())); + } + } + + BaseUri << "/prj/test/oplog/foobar"; + + { + HttpClient Http{BaseUri}; + + { + auto Response = Http.Post(""sv); + CHECK(Response.StatusCode == HttpResponseCode::Created); + } + + { + auto Response = Http.Get(""sv); + CHECK(Response.StatusCode == HttpResponseCode::OK); + + CbObject ResponseObject = Response.AsObject(); + + CHECK(ResponseObject["id"].AsString() == "foobar"sv); + CHECK(ResponseObject["project"].AsString() == "test"sv); + } + } + + SUBCASE("build store persistence") + { + uint8_t AttachData[] = {1, 2, 3}; + + zen::CompressedBuffer Attachment = zen::CompressedBuffer::Compress(zen::SharedBuffer::Clone(zen::MemoryView{AttachData, 3})); + zen::CbAttachment Attach{Attachment, Attachment.DecodeRawHash()}; + + zen::CbObjectWriter OpWriter; + OpWriter << "key" + << "foo" + << "attachment" << Attach; + + const std::string_view ChunkId{ + "00000000" + "00000000" + "00010000"}; + auto FileOid = zen::Oid::FromHexString(ChunkId); + + OpWriter.BeginArray("files"); + OpWriter.BeginObject(); + OpWriter << "id" << FileOid; + OpWriter << "clientpath" + << "/{engine}/client/side/path"; + OpWriter << "serverpath" << BinPath.c_str(); + OpWriter.EndObject(); + OpWriter.EndArray(); + + zen::CbObject Op = OpWriter.Save(); + + zen::CbPackage OpPackage(Op); + OpPackage.AddAttachment(Attach); + + zen::BinaryWriter MemOut; + legacy::SaveCbPackage(OpPackage, MemOut); + + HttpClient Http{BaseUri}; + + { + auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView())); + + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::Created); + } + + // Read file data + + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << ChunkId; + auto Response = Http.Get(ChunkGetUri); + + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + } + + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << ChunkId << "?offset=1&size=10"; + auto Response = Http.Get(ChunkGetUri); + + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + CHECK(Response.ResponsePayload.GetSize() == 10); + } + + ZEN_INFO("+++++++"); + } + + SUBCASE("snapshot") + { + zen::CbObjectWriter OpWriter; + OpWriter << "key" + << "foo"; + + const std::string_view ChunkId{ + "00000000" + "00000000" + "00010000"}; + auto FileOid = zen::Oid::FromHexString(ChunkId); + + OpWriter.BeginArray("files"); + OpWriter.BeginObject(); + OpWriter << "id" << FileOid; + OpWriter << "clientpath" + << "/{engine}/client/side/path"; + OpWriter << "serverpath" << BinPath.c_str(); + OpWriter.EndObject(); + OpWriter.EndArray(); + + zen::CbObject Op = OpWriter.Save(); + + zen::CbPackage OpPackage(Op); + + zen::BinaryWriter MemOut; + legacy::SaveCbPackage(OpPackage, MemOut); + + HttpClient Http{BaseUri}; + + { + auto Response = Http.Post("/new", IoBufferBuilder::MakeFromMemory(MemOut.GetView())); + + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::Created); + } + + // Read file data, it is raw and uncompressed + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << ChunkId; + auto Response = Http.Get(ChunkGetUri); + + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + + IoBuffer Data = Response.ResponsePayload; + IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath); + CHECK(ReferenceData.GetSize() == Data.GetSize()); + CHECK(ReferenceData.GetView().EqualBytes(Data.GetView())); + } + + { + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); + auto Response = Http.Post("/rpc"sv, Payload); + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + } + + // Read chunk data, it is now compressed + { + zen::StringBuilder<128> ChunkGetUri; + ChunkGetUri << "/" << ChunkId; + auto Response = Http.Get(ChunkGetUri, {{"Accept-Type", "application/x-ue-comp"}}); + + REQUIRE(Response); + CHECK(Response.StatusCode == HttpResponseCode::OK); + + IoBuffer Data = Response.ResponsePayload; + IoHash RawHash; + uint64_t RawSize; + CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize); + CHECK(Compressed); + IoBuffer DataDecompressed = Compressed.Decompress().AsIoBuffer(); + IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath); + CHECK(RawSize == ReferenceData.GetSize()); + CHECK(ReferenceData.GetSize() == DataDecompressed.GetSize()); + CHECK(ReferenceData.GetView().EqualBytes(DataDecompressed.GetView())); + } + + ZEN_INFO("+++++++"); + } + + SUBCASE("test chunk not found error") + { + HttpClient Http{BaseUri}; + + for (size_t I = 0; I < 65; I++) + { + zen::StringBuilder<128> PostUri; + PostUri << "/f77c781846caead318084604/info"; + auto Response = Http.Get(PostUri); + + REQUIRE(!Response.Error); + CHECK(Response.StatusCode == HttpResponseCode::NotFound); + } + } + } +} + +CbPackage +CreateOplogPackage(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments) +{ + CbPackage Package; + CbObjectWriter Object; + Object << "key"sv << OidAsString(Id); + if (!Attachments.empty()) + { + Object.BeginArray("bulkdata"); + for (const auto& Attachment : Attachments) + { + CbAttachment Attach(Attachment.second, Attachment.second.DecodeRawHash()); + Object.BeginObject(); + Object << "id"sv << Attachment.first; + Object << "type"sv + << "Standard"sv; + Object << "data"sv << Attach; + Object.EndObject(); + + Package.AddAttachment(Attach); + ZEN_DEBUG("Added attachment {}", Attach.GetHash()); + } + Object.EndArray(); + } + Package.SetObject(Object.Save()); + return Package; +}; + +CbObject +CreateOplogOp(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments) +{ + CbObjectWriter Object; + Object << "key"sv << OidAsString(Id); + if (!Attachments.empty()) + { + Object.BeginArray("bulkdata"); + for (const auto& Attachment : Attachments) + { + CbAttachment Attach(Attachment.second, Attachment.second.DecodeRawHash()); + Object.BeginObject(); + Object << "id"sv << Attachment.first; + Object << "type"sv + << "Standard"sv; + Object << "data"sv << Attach; + Object.EndObject(); + + ZEN_DEBUG("Added attachment {}", Attach.GetHash()); + } + Object.EndArray(); + } + return Object.Save(); +}; + +enum CbWriterMeta +{ + BeginObject, + EndObject, + BeginArray, + EndArray +}; + +inline CbWriter& +operator<<(CbWriter& Writer, CbWriterMeta Meta) +{ + switch (Meta) + { + case BeginObject: + Writer.BeginObject(); + break; + case EndObject: + Writer.EndObject(); + break; + case BeginArray: + Writer.BeginArray(); + break; + case EndArray: + Writer.EndArray(); + break; + default: + ZEN_ASSERT(false); + } + return Writer; +} + +TEST_CASE("project.remote") +{ + using namespace std::literals; + using namespace utils; + + ZenServerTestHelper Servers("remote", 3); + Servers.SpawnServers("--debug"); + + std::vector<Oid> OpIds; + const size_t OpCount = 24; + OpIds.reserve(OpCount); + for (size_t I = 0; I < OpCount; ++I) + { + OpIds.emplace_back(Oid::NewOid()); + } + + std::unordered_map<Oid, std::vector<std::pair<Oid, CompressedBuffer>>, Oid::Hasher> Attachments; + { + std::vector<std::size_t> AttachmentSizes( + {7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 33466, 1093, 4269, 2257, 3685, 13489, 97194, + 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 224024, 51582, 5251, 491, 2u * 1024u * 1024u + 124u, + 74607, 18135, 3767, 154045, 4415, 5007, 8876, 96761, 3359, 8526, 4097, 4855, 48225}); + auto It = AttachmentSizes.begin(); + Attachments[OpIds[0]] = {}; + Attachments[OpIds[1]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[2]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[3]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[4]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++}); + Attachments[OpIds[5]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[6]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[7]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[8]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{}); + Attachments[OpIds[9]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[10]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[11]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++}); + Attachments[OpIds[12]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[13]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[14]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); + Attachments[OpIds[15]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); + Attachments[OpIds[16]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{}); + Attachments[OpIds[17]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); + Attachments[OpIds[18]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); + Attachments[OpIds[19]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{}); + Attachments[OpIds[20]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[21]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[22]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++}); + Attachments[OpIds[23]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + ZEN_ASSERT(It == AttachmentSizes.end()); + } + + // Note: This is a clone of the function in projectstore.cpp + auto ComputeOpKey = [](const CbObjectView& Op) -> Oid { + using namespace std::literals; + + XXH3_128Stream_deprecated KeyHasher; + Op["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); }); + XXH3_128 KeyHash128 = KeyHasher.GetHash(); + + Oid KeyHash; + memcpy(&KeyHash, KeyHash128.Hash, sizeof KeyHash); + + return KeyHash; + }; + + auto AddOp = [ComputeOpKey](const CbObject& Op, std::unordered_map<Oid, uint32_t, Oid::Hasher>& Ops) { + const Oid Id = ComputeOpKey(Op); + IoBuffer Buffer = Op.GetBuffer().AsIoBuffer(); + const uint32_t OpCoreHash = uint32_t(XXH3_64bits(Buffer.GetData(), Buffer.GetSize()) & 0xffffFFFF); + Ops.insert({Id, OpCoreHash}); + }; + + auto MakeProject = [](std::string_view UrlBase, std::string_view ProjectName) { + CbObjectWriter Project; + Project.AddString("id"sv, ProjectName); + Project.AddString("root"sv, ""sv); + Project.AddString("engine"sv, ""sv); + Project.AddString("project"sv, ""sv); + Project.AddString("projectfile"sv, ""sv); + IoBuffer ProjectPayload = Project.Save().GetBuffer().AsIoBuffer(); + ProjectPayload.SetContentType(HttpContentType::kCbObject); + + HttpClient Http{UrlBase}; + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}", ProjectName), ProjectPayload); + CHECK(Response); + }; + + auto MakeOplog = [](std::string_view UrlBase, std::string_view ProjectName, std::string_view OplogName) { + HttpClient Http{UrlBase}; + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}", ProjectName, OplogName), IoBuffer{}); + CHECK(Response); + }; + + auto MakeOp = [](std::string_view UrlBase, std::string_view ProjectName, std::string_view OplogName, const CbPackage& OpPackage) { + zen::BinaryWriter MemOut; + legacy::SaveCbPackage(OpPackage, MemOut); + IoBuffer Body{IoBuffer::Wrap, MemOut.GetData(), MemOut.GetSize()}; + Body.SetContentType(HttpContentType::kCbPackage); + + HttpClient Http{UrlBase}; + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/new", ProjectName, OplogName), Body); + CHECK(Response); + }; + + MakeProject(Servers.GetInstance(0).GetBaseUri(), "proj0"); + MakeOplog(Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0"); + + std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps; + for (const Oid& OpId : OpIds) + { + CbPackage OpPackage = CreateOplogPackage(OpId, Attachments[OpId]); + CHECK(OpPackage.GetAttachments().size() == Attachments[OpId].size()); + AddOp(OpPackage.GetObject(), SourceOps); + MakeOp(Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0", OpPackage); + } + + std::vector<IoHash> AttachmentHashes; + AttachmentHashes.reserve(Attachments.size()); + for (const auto& AttachmentOplog : Attachments) + { + for (const auto& Attachment : AttachmentOplog.second) + { + AttachmentHashes.emplace_back(Attachment.second.DecodeRawHash()); + } + } + + auto MakeCbObjectPayload = [](std::function<void(CbObjectWriter & Writer)> Write) -> IoBuffer { + CbObjectWriter Writer; + Write(Writer); + IoBuffer Result = Writer.Save().GetBuffer().AsIoBuffer(); + Result.MakeOwned(); + Result.SetContentType(HttpContentType::kCbObject); + return Result; + }; + + auto ValidateAttachments = + [&MakeCbObjectPayload, &AttachmentHashes, &Servers](int ServerIndex, std::string_view Project, std::string_view Oplog) { + HttpClient Http{Servers.GetInstance(ServerIndex).GetBaseUri()}; + + IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes](CbObjectWriter& Writer) { + Writer << "method"sv + << "getchunks"sv; + Writer << "chunks"sv << BeginArray; + for (const IoHash& Chunk : AttachmentHashes) + { + Writer << Chunk; + } + Writer << EndArray; // chunks + }); + + HttpClient::Response Response = + Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", Project, Oplog), Payload, {{"Accept", "application/x-ue-cbpkg"}}); + CHECK(Response); + CbPackage ResponsePackage = ParsePackageMessage(Response.ResponsePayload); + CHECK(ResponsePackage.GetAttachments().size() == AttachmentHashes.size()); + for (auto A : ResponsePackage.GetAttachments()) + { + CHECK(IoHash::HashBuffer(A.AsCompressedBinary().DecompressToComposite()) == A.GetHash()); + } + }; + + auto ValidateOplog = [&SourceOps, &AddOp, &Servers](int ServerIndex, std::string_view Project, std::string_view Oplog) { + std::unordered_map<Oid, uint32_t, Oid::Hasher> TargetOps; + std::vector<CbObject> ResultingOplog; + + HttpClient Http{Servers.GetInstance(ServerIndex).GetBaseUri()}; + HttpClient::Response Response = Http.Get(fmt::format("/prj/{}/oplog/{}/entries", Project, Oplog)); + CHECK(Response); + + IoBuffer Payload(Response.ResponsePayload); + CbObject OplogResonse = LoadCompactBinaryObject(Payload); + CbArrayView EntriesArray = OplogResonse["entries"sv].AsArrayView(); + + for (CbFieldView OpEntry : EntriesArray) + { + CbObjectView Core = OpEntry.AsObjectView(); + BinaryWriter Writer; + Core.CopyTo(Writer); + MemoryView OpView = Writer.GetView(); + IoBuffer OpBuffer(IoBuffer::Wrap, OpView.GetData(), OpView.GetSize()); + CbObject Op(SharedBuffer(OpBuffer), CbFieldType::HasFieldType); + AddOp(Op, TargetOps); + } + CHECK(SourceOps == TargetOps); + }; + + auto HttpWaitForCompletion = [](ZenServerInstance& Server, const HttpClient::Response& Response) { + REQUIRE(Response); + const uint64_t JobId = ParseInt<uint64_t>(Response.AsText()).value_or(0); + CHECK(JobId != 0); + + HttpClient Http{Server.GetBaseUri()}; + + while (true) + { + HttpClient::Response StatusResponse = + Http.Get(fmt::format("/admin/jobs/{}", JobId), {{"Accept", ToString(ZenContentType::kCbObject)}}); + CHECK(StatusResponse); + CbObject ResponseObject = StatusResponse.AsObject(); + std::string_view Status = ResponseObject["Status"sv].AsString(); + CHECK(Status != "Aborted"sv); + if (Status == "Complete"sv) + { + return; + } + Sleep(10); + } + }; + + SUBCASE("File") + { + ScopedTemporaryDirectory TempDir; + { + IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) { + Writer << "method"sv + << "export"sv; + Writer << "params" << BeginObject; + { + Writer << "maxblocksize"sv << 3072u; + Writer << "maxchunkembedsize"sv << 1296u; + Writer << "chunkfilesizelimit"sv << 5u * 1024u; + Writer << "force"sv << false; + Writer << "file"sv << BeginObject; + { + Writer << "path"sv << path; + Writer << "name"sv + << "proj0_oplog0"sv; + } + Writer << EndObject; // "file" + } + Writer << EndObject; // "params" + }); + + HttpClient Http{Servers.GetInstance(0).GetBaseUri()}; + + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload); + HttpWaitForCompletion(Servers.GetInstance(0), Response); + } + { + MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy"); + MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy"); + + IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) { + Writer << "method"sv + << "import"sv; + Writer << "params" << BeginObject; + { + Writer << "force"sv << false; + Writer << "file"sv << BeginObject; + { + Writer << "path"sv << path; + Writer << "name"sv + << "proj0_oplog0"sv; + } + Writer << EndObject; // "file" + } + Writer << EndObject; // "params" + }); + + HttpClient Http{Servers.GetInstance(1).GetBaseUri()}; + + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload); + HttpWaitForCompletion(Servers.GetInstance(1), Response); + } + ValidateAttachments(1, "proj0_copy", "oplog0_copy"); + ValidateOplog(1, "proj0_copy", "oplog0_copy"); + } + + SUBCASE("File disable blocks") + { + ScopedTemporaryDirectory TempDir; + { + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { + Writer << "method"sv + << "export"sv; + Writer << "params" << BeginObject; + { + Writer << "maxblocksize"sv << 3072u; + Writer << "maxchunkembedsize"sv << 1296u; + Writer << "chunkfilesizelimit"sv << 5u * 1024u; + Writer << "force"sv << false; + Writer << "file"sv << BeginObject; + { + Writer << "path"sv << TempDir.Path().string(); + Writer << "name"sv + << "proj0_oplog0"sv; + Writer << "disableblocks"sv << true; + } + Writer << EndObject; // "file" + } + Writer << EndObject; // "params" + }); + + HttpClient Http{Servers.GetInstance(0).GetBaseUri()}; + + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload); + HttpWaitForCompletion(Servers.GetInstance(0), Response); + } + { + MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy"); + MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy"); + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { + Writer << "method"sv + << "import"sv; + Writer << "params" << BeginObject; + { + Writer << "force"sv << false; + Writer << "file"sv << BeginObject; + { + Writer << "path"sv << TempDir.Path().string(); + Writer << "name"sv + << "proj0_oplog0"sv; + } + Writer << EndObject; // "file" + } + Writer << EndObject; // "params" + }); + + HttpClient Http{Servers.GetInstance(1).GetBaseUri()}; + + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload); + HttpWaitForCompletion(Servers.GetInstance(1), Response); + } + ValidateAttachments(1, "proj0_copy", "oplog0_copy"); + ValidateOplog(1, "proj0_copy", "oplog0_copy"); + } + + SUBCASE("File force temp blocks") + { + ScopedTemporaryDirectory TempDir; + { + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { + Writer << "method"sv + << "export"sv; + Writer << "params" << BeginObject; + { + Writer << "maxblocksize"sv << 3072u; + Writer << "maxchunkembedsize"sv << 1296u; + Writer << "chunkfilesizelimit"sv << 5u * 1024u; + Writer << "force"sv << false; + Writer << "file"sv << BeginObject; + { + Writer << "path"sv << TempDir.Path().string(); + Writer << "name"sv + << "proj0_oplog0"sv; + Writer << "enabletempblocks"sv << true; + } + Writer << EndObject; // "file" + } + Writer << EndObject; // "params" + }); + + HttpClient Http{Servers.GetInstance(0).GetBaseUri()}; + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload); + HttpWaitForCompletion(Servers.GetInstance(0), Response); + } + { + MakeProject(Servers.GetInstance(1).GetBaseUri(), "proj0_copy"); + MakeOplog(Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy"); + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { + Writer << "method"sv + << "import"sv; + Writer << "params" << BeginObject; + { + Writer << "force"sv << false; + Writer << "file"sv << BeginObject; + { + Writer << "path"sv << TempDir.Path().string(); + Writer << "name"sv + << "proj0_oplog0"sv; + } + Writer << EndObject; // "file" + } + Writer << EndObject; // "params" + }); + + HttpClient Http{Servers.GetInstance(1).GetBaseUri()}; + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0_copy", "oplog0_copy"), Payload); + HttpWaitForCompletion(Servers.GetInstance(1), Response); + } + ValidateAttachments(1, "proj0_copy", "oplog0_copy"); + ValidateOplog(1, "proj0_copy", "oplog0_copy"); + } + + SUBCASE("Zen") + { + ScopedTemporaryDirectory TempDir; + { + std::string ExportSourceUri = Servers.GetInstance(0).GetBaseUri(); + std::string ExportTargetUri = Servers.GetInstance(1).GetBaseUri(); + MakeProject(ExportTargetUri, "proj0_copy"); + MakeOplog(ExportTargetUri, "proj0_copy", "oplog0_copy"); + + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { + Writer << "method"sv + << "export"sv; + Writer << "params" << BeginObject; + { + Writer << "maxblocksize"sv << 3072u; + Writer << "maxchunkembedsize"sv << 1296u; + Writer << "chunkfilesizelimit"sv << 5u * 1024u; + Writer << "force"sv << false; + Writer << "zen"sv << BeginObject; + { + Writer << "url"sv << ExportTargetUri.substr(7); + Writer << "project" + << "proj0_copy"; + Writer << "oplog" + << "oplog0_copy"; + } + Writer << EndObject; // "file" + } + Writer << EndObject; // "params" + }); + + HttpClient Http{Servers.GetInstance(0).GetBaseUri()}; + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj0", "oplog0"), Payload); + HttpWaitForCompletion(Servers.GetInstance(0), Response); + } + ValidateAttachments(1, "proj0_copy", "oplog0_copy"); + ValidateOplog(1, "proj0_copy", "oplog0_copy"); + + { + std::string ImportSourceUri = Servers.GetInstance(1).GetBaseUri(); + std::string ImportTargetUri = Servers.GetInstance(2).GetBaseUri(); + MakeProject(ImportTargetUri, "proj1"); + MakeOplog(ImportTargetUri, "proj1", "oplog1"); + + IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { + Writer << "method"sv + << "import"sv; + Writer << "params" << BeginObject; + { + Writer << "force"sv << false; + Writer << "zen"sv << BeginObject; + { + Writer << "url"sv << ImportSourceUri.substr(7); + Writer << "project" + << "proj0_copy"; + Writer << "oplog" + << "oplog0_copy"; + } + Writer << EndObject; // "file" + } + Writer << EndObject; // "params" + }); + + HttpClient Http{Servers.GetInstance(2).GetBaseUri()}; + HttpClient::Response Response = Http.Post(fmt::format("/prj/{}/oplog/{}/rpc", "proj1", "oplog1"), Payload); + HttpWaitForCompletion(Servers.GetInstance(2), Response); + } + ValidateAttachments(2, "proj1", "oplog1"); + ValidateOplog(2, "proj1", "oplog1"); + } +} + +TEST_CASE("project.rpcappendop") +{ + using namespace std::literals; + using namespace utils; + + ZenServerTestHelper Servers("remote", 2); + Servers.SpawnServers("--debug"); + + std::vector<Oid> OpIds; + const size_t OpCount = 24; + OpIds.reserve(OpCount); + for (size_t I = 0; I < OpCount; ++I) + { + OpIds.emplace_back(Oid::NewOid()); + } + + std::unordered_map<Oid, std::vector<std::pair<Oid, CompressedBuffer>>, Oid::Hasher> Attachments; + { + std::vector<std::size_t> AttachmentSizes( + {7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 33466, 1093, 4269, 2257, 3685, 13489, 97194, + 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 224024, 51582, 5251, 491, 2u * 1024u * 1024u + 124u, + 74607, 18135, 3767, 154045, 4415, 5007, 8876, 96761, 3359, 8526, 4097, 4855, 48225}); + auto It = AttachmentSizes.begin(); + Attachments[OpIds[0]] = {}; + Attachments[OpIds[1]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[2]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[3]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[4]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++}); + Attachments[OpIds[5]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[6]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[7]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[8]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{}); + Attachments[OpIds[9]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[10]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[11]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++}); + Attachments[OpIds[12]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); + Attachments[OpIds[13]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[14]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); + Attachments[OpIds[15]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); + Attachments[OpIds[16]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{}); + Attachments[OpIds[17]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); + Attachments[OpIds[18]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); + Attachments[OpIds[19]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{}); + Attachments[OpIds[20]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[21]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + Attachments[OpIds[22]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++}); + Attachments[OpIds[23]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); + ZEN_ASSERT(It == AttachmentSizes.end()); + } + + // Note: This is a clone of the function in projectstore.cpp + auto ComputeOpKey = [](const CbObjectView& Op) -> Oid { + using namespace std::literals; + + XXH3_128Stream_deprecated KeyHasher; + Op["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); }); + XXH3_128 KeyHash128 = KeyHasher.GetHash(); + + Oid KeyHash; + memcpy(&KeyHash, KeyHash128.Hash, sizeof KeyHash); + + return KeyHash; + }; + + auto AddOp = [ComputeOpKey](const CbObject& Op, std::unordered_map<Oid, uint32_t, Oid::Hasher>& Ops) { + const Oid Id = ComputeOpKey(Op); + IoBuffer Buffer = Op.GetBuffer().AsIoBuffer(); + const uint32_t OpCoreHash = uint32_t(XXH3_64bits(Buffer.GetData(), Buffer.GetSize()) & 0xffffFFFF); + Ops.insert({Id, OpCoreHash}); + }; + + auto MakeProject = [](HttpClient& Client, std::string_view ProjectName) { + CbObjectWriter Project; + Project.AddString("id"sv, ProjectName); + Project.AddString("root"sv, ""sv); + Project.AddString("engine"sv, ""sv); + Project.AddString("project"sv, ""sv); + Project.AddString("projectfile"sv, ""sv); + HttpClient::Response Response = Client.Post(fmt::format("/prj/{}", ProjectName), Project.Save()); + CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("")); + }; + + auto MakeOplog = [](HttpClient& Client, std::string_view ProjectName, std::string_view OplogName) { + HttpClient::Response Response = Client.Post(fmt::format("/prj/{}/oplog/{}", ProjectName, OplogName)); + CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("")); + }; + auto GetOplog = [](HttpClient& Client, std::string_view ProjectName, std::string_view OplogName) { + HttpClient::Response Response = Client.Get(fmt::format("/prj/{}/oplog/{}", ProjectName, OplogName)); + CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("")); + return Response.AsObject(); + }; + + auto MakeOp = + [](HttpClient& Client, std::string_view ProjectName, std::string_view OplogName, const CbObjectView& Op) -> std::vector<IoHash> { + CbObjectWriter Request; + Request.AddString("method"sv, "appendops"sv); + Request.BeginArray("ops"sv); + { + Request.AddObject(Op); + } + Request.EndArray(); // "ops" + HttpClient::Response Response = Client.Post(fmt::format("/prj/{}/oplog/{}/rpc", ProjectName, OplogName), Request.Save()); + CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("")); + + CbObjectView ResponsePayload = Response.AsPackage().GetObject(); + CbArrayView NeedArray = ResponsePayload["need"sv].AsArrayView(); + std::vector<IoHash> Needs; + Needs.reserve(NeedArray.Num()); + for (CbFieldView NeedView : NeedArray) + { + Needs.push_back(NeedView.AsHash()); + } + return Needs; + }; + + auto SendAttachments = [](HttpClient& Client, + std::string_view ProjectName, + std::string_view OplogName, + std::span<const CompressedBuffer> Attachments, + void* ServerProcessHandle, + const std::filesystem::path& TempPath) { + CompositeBuffer PackageMessage; + { + CbPackage RequestPackage; + CbObjectWriter Request; + Request.AddString("method"sv, "putchunks"sv); + Request.AddBool("usingtmpfiles"sv, true); + Request.BeginArray("chunks"sv); + for (CompressedBuffer AttachmentPayload : Attachments) + { + if (AttachmentPayload.DecodeRawSize() > 16u * 1024u) + { + std::filesystem::path TempAttachmentPath = TempPath / (Oid::NewOid().ToString() + ".tmp"); + WriteFile(TempAttachmentPath, AttachmentPayload.GetCompressed()); + IoBuffer OnDiskAttachment = IoBufferBuilder::MakeFromFile(TempAttachmentPath); + AttachmentPayload = CompressedBuffer::FromCompressedNoValidate(std::move(OnDiskAttachment)); + } + + CbAttachment Attachment(AttachmentPayload, AttachmentPayload.DecodeRawHash()); + + Request.AddAttachment(Attachment); + RequestPackage.AddAttachment(Attachment); + } + Request.EndArray(); // "chunks" + RequestPackage.SetObject(Request.Save()); + + PackageMessage = CompositeBuffer(FormatPackageMessage(RequestPackage, FormatFlags::kAllowLocalReferences, ServerProcessHandle)); + } + + HttpClient::Response Response = + Client.Post(fmt::format("/prj/{}/oplog/{}/rpc", ProjectName, OplogName), PackageMessage, HttpContentType::kCbPackage); + CHECK_MESSAGE(Response.IsSuccess(), Response.ErrorMessage("")); + }; + + { + HttpClient Client(Servers.GetInstance(0).GetBaseUri()); + void* ServerProcessHandle = Servers.GetInstance(0).GetProcessHandle(); + + MakeProject(Client, "proj0"); + MakeOplog(Client, "proj0", "oplog0"); + CbObject Oplog = GetOplog(Client, "proj0", "oplog0"); + std::filesystem::path TempPath = Oplog["tempdir"sv].AsU8String(); + + std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps; + for (const Oid& OpId : OpIds) + { + CbObject Op = CreateOplogOp(OpId, Attachments[OpId]); + AddOp(Op, SourceOps); + std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op); + + if (!MissingAttachments.empty()) + { + CHECK(MissingAttachments.size() <= Attachments[OpId].size()); + tsl::robin_set<IoHash, IoHash::Hasher> MissingAttachmentSet(MissingAttachments.begin(), MissingAttachments.end()); + std::vector<CompressedBuffer> PutAttachments; + for (const auto& Attachment : Attachments[OpId]) + { + CompressedBuffer Payload = Attachment.second; + const IoHash AttachmentHash = Payload.DecodeRawHash(); + if (auto It = MissingAttachmentSet.find(AttachmentHash); It != MissingAttachmentSet.end()) + { + PutAttachments.push_back(Payload); + } + } + SendAttachments(Client, "proj0", "oplog0", PutAttachments, ServerProcessHandle, TempPath); + } + } + + // Do it again, but now we should not need any attachments + + for (const Oid& OpId : OpIds) + { + CbObject Op = CreateOplogOp(OpId, Attachments[OpId]); + AddOp(Op, SourceOps); + std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op); + CHECK(MissingAttachments.empty()); + } + } + + { + HttpClient Client(Servers.GetInstance(1).GetBaseUri()); + void* ServerProcessHandle = nullptr; // Force use of path for attachments passed on disk + + MakeProject(Client, "proj0"); + MakeOplog(Client, "proj0", "oplog0"); + CbObject Oplog = GetOplog(Client, "proj0", "oplog0"); + std::filesystem::path TempPath = Oplog["tempdir"sv].AsU8String(); + + std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps; + for (const Oid& OpId : OpIds) + { + CbObject Op = CreateOplogOp(OpId, Attachments[OpId]); + AddOp(Op, SourceOps); + std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op); + + if (!MissingAttachments.empty()) + { + CHECK(MissingAttachments.size() <= Attachments[OpId].size()); + tsl::robin_set<IoHash, IoHash::Hasher> MissingAttachmentSet(MissingAttachments.begin(), MissingAttachments.end()); + std::vector<CompressedBuffer> PutAttachments; + for (const auto& Attachment : Attachments[OpId]) + { + CompressedBuffer Payload = Attachment.second; + const IoHash AttachmentHash = Payload.DecodeRawHash(); + if (auto It = MissingAttachmentSet.find(AttachmentHash); It != MissingAttachmentSet.end()) + { + PutAttachments.push_back(Payload); + } + } + SendAttachments(Client, "proj0", "oplog0", PutAttachments, ServerProcessHandle, TempPath); + } + } + + // Do it again, but now we should not need any attachments + + for (const Oid& OpId : OpIds) + { + CbObject Op = CreateOplogOp(OpId, Attachments[OpId]); + AddOp(Op, SourceOps); + std::vector<IoHash> MissingAttachments = MakeOp(Client, "proj0", "oplog0", Op); + CHECK(MissingAttachments.empty()); + } + } +} + +} // namespace zen::tests + +#endif diff --git a/src/zenserver-test/workspace-tests.cpp b/src/zenserver-test/workspace-tests.cpp new file mode 100644 index 000000000..f299b6dcf --- /dev/null +++ b/src/zenserver-test/workspace-tests.cpp @@ -0,0 +1,541 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#if ZEN_WITH_TESTS +# include "zenserver-test.h" +# include <zencore/testing.h> +# include <zencore/testutils.h> +# include <zencore/workthreadpool.h> +# include <zencore/compactbinarybuilder.h> +# include <zencore/compactbinarypackage.h> +# include <zencore/compress.h> +# include <zencore/fmtutils.h> +# include <zencore/filesystem.h> +# include <zencore/stream.h> +# include <zencore/string.h> +# include <zenutil/zenserverprocess.h> +# include <zenutil/chunkrequests.h> +# include <zenhttp/httpclient.h> + +namespace zen::tests { + +using namespace std::literals; + +std::vector<std::pair<std::filesystem::path, IoBuffer>> +GenerateFolderContent(const std::filesystem::path& RootPath) +{ + CreateDirectories(RootPath); + std::vector<std::pair<std::filesystem::path, IoBuffer>> Result; + Result.push_back(std::make_pair(RootPath / "root_blob_1.bin", CreateRandomBlob(4122))); + Result.push_back(std::make_pair(RootPath / "root_blob_2.bin", CreateRandomBlob(2122))); + + std::filesystem::path EmptyFolder(RootPath / "empty_folder"); + + std::filesystem::path FirstFolder(RootPath / "first_folder"); + CreateDirectories(FirstFolder); + Result.push_back(std::make_pair(FirstFolder / "first_folder_blob1.bin", CreateRandomBlob(22))); + Result.push_back(std::make_pair(FirstFolder / "first_folder_blob2.bin", CreateRandomBlob(122))); + + std::filesystem::path SecondFolder(RootPath / "second_folder"); + CreateDirectories(SecondFolder); + Result.push_back(std::make_pair(SecondFolder / "second_folder_blob1.bin", CreateRandomBlob(522))); + Result.push_back(std::make_pair(SecondFolder / "second_folder_blob2.bin", CreateRandomBlob(122))); + Result.push_back(std::make_pair(SecondFolder / "second_folder_blob3.bin", CreateRandomBlob(225))); + + std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second"); + CreateDirectories(SecondFolderChild); + Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob1.bin", CreateRandomBlob(622))); + + for (const auto& It : Result) + { + WriteFile(It.first, It.second); + } + + return Result; +} + +std::vector<std::pair<std::filesystem::path, IoBuffer>> +GenerateFolderContent2(const std::filesystem::path& RootPath) +{ + std::vector<std::pair<std::filesystem::path, IoBuffer>> Result; + Result.push_back(std::make_pair(RootPath / "root_blob_3.bin", CreateRandomBlob(312))); + std::filesystem::path FirstFolder(RootPath / "first_folder"); + Result.push_back(std::make_pair(FirstFolder / "first_folder_blob3.bin", CreateRandomBlob(722))); + std::filesystem::path SecondFolder(RootPath / "second_folder"); + std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second"); + Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob2.bin", CreateRandomBlob(962))); + Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob3.bin", CreateRandomBlob(561))); + + for (const auto& It : Result) + { + WriteFile(It.first, It.second); + } + + return Result; +} + +TEST_CASE("workspaces.create") +{ + using namespace std::literals; + + std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); + + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + ZenServerInstance Instance(TestEnv); + Instance.SetTestDir(TestDir); + const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( + fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + ScopedTemporaryDirectory TempDir; + std::filesystem::path Root1Path = TempDir.Path() / "root1"; + std::filesystem::path Root2Path = TempDir.Path() / "root2"; + DeleteDirectories(Root1Path); + DeleteDirectories(Root2Path); + + std::filesystem::path Share1Path = "shared_1"; + std::filesystem::path Share2Path = "shared_2"; + CreateDirectories(Root1Path / Share1Path); + CreateDirectories(Root1Path / Share2Path); + CreateDirectories(Root2Path / Share1Path); + CreateDirectories(Root2Path / Share2Path); + + Oid Root1Id = Oid::Zero; + Oid Root2Id = Oid::NewOid(); + + HttpClient Client(Instance.GetBaseUri()); + + CHECK(Client.Put(fmt::format("/ws/{}", Root1Id)).StatusCode == HttpResponseCode::BadRequest); + + if (HttpClient::Response Root1Response = + Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}); + Root1Response.StatusCode == HttpResponseCode::Created) + { + Root1Id = Oid::TryFromHexString(Root1Response.AsText()); + CHECK(Root1Id != Oid::Zero); + } + else + { + CHECK(false); + } + if (HttpClient::Response Root1Response = + Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}); + Root1Response.StatusCode == HttpResponseCode::OK) + { + CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText())); + } + else + { + CHECK(false); + } + if (HttpClient::Response Root1Response = + Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}); + Root1Response.StatusCode == HttpResponseCode::OK) + { + CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText())); + } + else + { + CHECK(false); + } + CHECK(Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}}).StatusCode == + HttpResponseCode::Conflict); + + CHECK( + Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == + HttpResponseCode::Created); + + CHECK( + Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == + HttpResponseCode::NotFound); + + CHECK(Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode == + HttpResponseCode::Conflict); + + if (HttpClient::Response Root2Response = + Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}}); + Root2Response.StatusCode == HttpResponseCode::Created) + { + CHECK(Root2Id == Oid::TryFromHexString(Root2Response.AsText())); + } + else + { + CHECK(false); + } + + CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero)).StatusCode == HttpResponseCode::BadRequest); + + Oid Share2Id = Oid::Zero; + if (HttpClient::Response Share2Response = + Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}); + Share2Response.StatusCode == HttpResponseCode::Created) + { + Share2Id = Oid::TryFromHexString(Share2Response.AsText()); + CHECK(Share2Id != Oid::Zero); + } + else + { + CHECK(false); + } + + CHECK( + Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == + HttpResponseCode::OK); + + CHECK( + Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == + HttpResponseCode::OK); + + CHECK( + Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode == + HttpResponseCode::Conflict); + + CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::NewOid()), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}) + .StatusCode == HttpResponseCode::Conflict); + + CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", "idonotexist"}}).StatusCode != + HttpResponseCode::OK); + + while (true) + { + std::error_code Ec; + DeleteDirectories(Root2Path / Share2Path, Ec); + if (!Ec) + break; + } + + CHECK(Client.Get(fmt::format("/ws/{}/{}/files", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound); +} + +TEST_CASE("workspaces.restricted") +{ + using namespace std::literals; + + std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); + + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + ZenServerInstance Instance(TestEnv); + Instance.SetTestDir(TestDir); + const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + ScopedTemporaryDirectory TempDir; + std::filesystem::path Root1Path = TempDir.Path() / "root1"; + std::filesystem::path Root2Path = TempDir.Path() / "root2"; + DeleteDirectories(Root1Path); + DeleteDirectories(Root2Path); + + std::filesystem::path Share1Path = "shared_1"; + std::filesystem::path Share2Path = "shared_2"; + CreateDirectories(Root1Path / Share1Path); + CreateDirectories(Root1Path / Share2Path); + CreateDirectories(Root2Path / Share1Path); + CreateDirectories(Root2Path / Share2Path); + + Oid Root1Id = Oid::NewOid(); + Oid Root2Id = Oid::NewOid(); + Oid Share1Id = Oid::NewOid(); + Oid Share2Id = Oid::NewOid(); + + HttpClient Client(Instance.GetBaseUri()); + CHECK(Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode == + HttpResponseCode::Unauthorized); + + CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::NotFound); + + std::string Config1; + { + CbObjectWriter Config; + Config.BeginArray("workspaces"); + Config.BeginObject(); + Config << "id"sv << Root1Id.ToString(); + Config << "root_path"sv << Root1Path.string(); + Config << "allow_share_creation_from_http"sv << false; + Config.EndObject(); + Config.EndArray(); + ExtendableStringBuilder<256> SB; + CompactBinaryToJson(Config.Save(), SB); + Config1 = SB.ToString(); + } + WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config1.data(), Config1.size())); + + CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode)); + + CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::OK); + + CHECK(Client.Get(fmt::format("/ws/{}/{}", Root1Id, Share1Id)).StatusCode == HttpResponseCode::NotFound); + CHECK( + Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode == + HttpResponseCode::Unauthorized); + + std::string Config2; + { + CbObjectWriter Config; + Config.BeginArray("workspaces"); + Config.BeginObject(); + Config << "id"sv << Root1Id.ToString(); + Config << "root_path"sv << Root1Path.string(); + Config << "allow_share_creation_from_http"sv << false; + Config.EndObject(); + Config.BeginObject(); + Config << "id"sv << Root2Id.ToString(); + Config << "root_path"sv << Root2Path.string(); + Config << "allow_share_creation_from_http"sv << true; + Config.EndObject(); + Config.EndArray(); + ExtendableStringBuilder<256> SB; + CompactBinaryToJson(Config.Save(), SB); + Config2 = SB.ToString(); + } + WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config2.data(), Config2.size())); + + CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode)); + + CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root2Id)).StatusCode, HttpResponseCode::OK); + + CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound); + CHECK( + Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == + HttpResponseCode::Created); + CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::OK); + + CHECK(IsHttpSuccessCode(Client.Delete(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode)); +} + +TEST_CASE("workspaces.lifetimes") +{ + using namespace std::literals; + + std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); + + Oid WorkspaceId = Oid::NewOid(); + Oid ShareId = Oid::NewOid(); + + ScopedTemporaryDirectory TempDir; + std::filesystem::path RootPath = TempDir.Path(); + DeleteDirectories(RootPath); + std::filesystem::path SharePath = RootPath / "shared_folder"; + CreateDirectories(SharePath); + + { + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + ZenServerInstance Instance(TestEnv); + Instance.SetTestDir(TestDir); + const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( + fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri()); + CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode == + HttpResponseCode::Created); + CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId); + CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode == + HttpResponseCode::OK); + + CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}}) + .StatusCode == HttpResponseCode::Created); + CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId); + CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}}) + .StatusCode == HttpResponseCode::OK); + } + + // Restart + + { + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + ZenServerInstance Instance(TestEnv); + Instance.SetTestDir(TestDir); + const uint16_t PortNumber = + Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri()); + CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId); + + CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId); + } + + // Wipe system config + DeleteDirectories(SystemRootPath); + + // Restart + + { + std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); + ZenServerInstance Instance(TestEnv); + Instance.SetTestDir(TestDir); + const uint16_t PortNumber = + Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + HttpClient Client(Instance.GetBaseUri()); + CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound); + CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound); + } +} + +TEST_CASE("workspaces.share") +{ + std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); + + ZenServerInstance Instance(TestEnv); + + const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( + fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath)); + CHECK(PortNumber != 0); + + ScopedTemporaryDirectory TempDir; + std::filesystem::path RootPath = TempDir.Path(); + DeleteDirectories(RootPath); + std::filesystem::path SharePath = RootPath / "shared_folder"; + GenerateFolderContent(SharePath); + + HttpClient Client(Instance.GetBaseUri()); + + Oid WorkspaceId = Oid::NewOid(); + CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode == + HttpResponseCode::Created); + CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId); + + Oid ShareId = Oid::NewOid(); + CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}}).StatusCode == + HttpResponseCode::Created); + CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId); + + CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8); + GenerateFolderContent2(SharePath); + CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8); + HttpClient::Response FilesResponse = + Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId), + {}, + HttpClient::KeyValueMap{{"refresh", ToString(true)}, {"fieldnames", "id,clientpath,size"}}); + CHECK(FilesResponse); + std::unordered_map<Oid, std::pair<std::filesystem::path, uint64_t>, Oid::Hasher> Files; + { + CbArrayView FilesArray = FilesResponse.AsObject()["files"sv].AsArrayView(); + CHECK(FilesArray.Num() == 12); + for (CbFieldView Field : FilesArray) + { + CbObjectView FileObject = Field.AsObjectView(); + Oid ChunkId = FileObject["id"sv].AsObjectId(); + CHECK(ChunkId != Oid::Zero); + uint64_t Size = FileObject["size"sv].AsUInt64(); + std::u8string_view Path = FileObject["clientpath"sv].AsU8String(); + std::filesystem::path AbsFilePath = SharePath / Path; + CHECK(IsFile(AbsFilePath)); + CHECK(FileSizeFromPath(AbsFilePath) == Size); + Files.insert_or_assign(ChunkId, std::make_pair(AbsFilePath, Size)); + } + } + + HttpClient::Response EntriesResponse = + Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId), {}, HttpClient::KeyValueMap{{"fieldfilter", "id,clientpath"}}); + CHECK(EntriesResponse); + { + CbArrayView EntriesArray = EntriesResponse.AsObject()["entries"sv].AsArrayView(); + CHECK(EntriesArray.Num() == 1); + for (CbFieldView EntryField : EntriesArray) + { + CbObjectView EntryObject = EntryField.AsObjectView(); + CbArrayView FilesArray = EntryObject["files"sv].AsArrayView(); + CHECK(FilesArray.Num() == 12); + for (CbFieldView FileField : FilesArray) + { + CbObjectView FileObject = FileField.AsObjectView(); + Oid ChunkId = FileObject["id"sv].AsObjectId(); + CHECK(ChunkId != Oid::Zero); + std::u8string_view Path = FileObject["clientpath"sv].AsU8String(); + std::filesystem::path AbsFilePath = SharePath / Path; + CHECK(IsFile(AbsFilePath)); + } + } + } + + HttpClient::Response FileManifestResponse = + Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId), + {}, + HttpClient::KeyValueMap{{"opkey", "file_manifest"}, {"fieldfilter", "id,clientpath"}}); + CHECK(FileManifestResponse); + { + CbArrayView EntriesArray = FileManifestResponse.AsObject()["entry"sv].AsObjectView()["files"sv].AsArrayView(); + CHECK(EntriesArray.Num() == 12); + for (CbFieldView Field : EntriesArray) + { + CbObjectView FileObject = Field.AsObjectView(); + Oid ChunkId = FileObject["id"sv].AsObjectId(); + CHECK(ChunkId != Oid::Zero); + std::u8string_view Path = FileObject["clientpath"sv].AsU8String(); + std::filesystem::path AbsFilePath = SharePath / Path; + CHECK(IsFile(AbsFilePath)); + } + } + + for (auto It : Files) + { + const Oid& ChunkId = It.first; + const std::filesystem::path& Path = It.second.first; + const uint64_t Size = It.second.second; + + CHECK(Client.Get(fmt::format("/ws/{}/{}/{}/info", WorkspaceId, ShareId, ChunkId)).AsObject()["size"sv].AsUInt64() == Size); + + { + IoBuffer Payload = Client.Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId)).ResponsePayload; + CHECK(Payload); + CHECK(Payload.GetSize() == Size); + IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path); + CHECK(FileContent); + CHECK(FileContent.GetView().EqualBytes(Payload.GetView())); + } + + { + IoBuffer Payload = + Client + .Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId), + {}, + HttpClient::KeyValueMap{{"offset", fmt::format("{}", Size / 4)}, {"size", fmt::format("{}", Size / 2)}}) + .ResponsePayload; + CHECK(Payload); + CHECK(Payload.GetSize() == Size / 2); + IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Size / 4, Size / 2); + CHECK(FileContent); + CHECK(FileContent.GetView().EqualBytes(Payload.GetView())); + } + } + + { + uint32_t CorrelationId = gsl::narrow<uint32_t>(Files.size()); + std::vector<RequestChunkEntry> BatchEntries; + for (auto It : Files) + { + const Oid& ChunkId = It.first; + const uint64_t Size = It.second.second; + + BatchEntries.push_back( + RequestChunkEntry{.ChunkId = ChunkId, .CorrelationId = --CorrelationId, .Offset = Size / 4, .RequestBytes = Size / 2}); + } + IoBuffer BatchResponse = + Client.Post(fmt::format("/ws/{}/{}/batch", WorkspaceId, ShareId), BuildChunkBatchRequest(BatchEntries)).ResponsePayload; + CHECK(BatchResponse); + std::vector<IoBuffer> BatchResult = ParseChunkBatchResponse(BatchResponse); + CHECK(BatchResult.size() == Files.size()); + for (const RequestChunkEntry& Request : BatchEntries) + { + IoBuffer Result = BatchResult[Request.CorrelationId]; + auto It = Files.find(Request.ChunkId); + const std::filesystem::path& Path = It->second.first; + CHECK(Result.GetSize() == Request.RequestBytes); + IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Request.Offset, Request.RequestBytes); + CHECK(FileContent); + CHECK(FileContent.GetView().EqualBytes(Result.GetView())); + } + } + + CHECK(Client.Delete(fmt::format("/ws/{}/{}", WorkspaceId, ShareId))); + CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound); + CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId))); + + CHECK(Client.Delete(fmt::format("/ws/{}", WorkspaceId))); + CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound); +} + +} // namespace zen::tests +#endif diff --git a/src/zenserver-test/xmake.lua b/src/zenserver-test/xmake.lua index 17d0a3cb7..7832e1f72 100644 --- a/src/zenserver-test/xmake.lua +++ b/src/zenserver-test/xmake.lua @@ -6,7 +6,7 @@ target("zenserver-test") add_headerfiles("**.h") add_files("*.cpp") add_files("zenserver-test.cpp", {unity_ignored = true }) - add_deps("zencore", "zenutil", "zenhttp") + add_deps("zencore", "zenremotestore", "zenhttp") add_deps("zenserver", {inherit=false}) add_packages("vcpkg::cpr", "vcpkg::http-parser", "vcpkg::mimalloc") diff --git a/src/zenserver-test/zenserver-test.cpp b/src/zenserver-test/zenserver-test.cpp index 77ed87cb1..42296cbe1 100644 --- a/src/zenserver-test/zenserver-test.cpp +++ b/src/zenserver-test/zenserver-test.cpp @@ -2,78 +2,32 @@ #define _SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING -#include <zenbase/refcount.h> -#include <zencore/compactbinary.h> -#include <zencore/compactbinarybuilder.h> -#include <zencore/compactbinarypackage.h> -#include <zencore/compress.h> -#include <zencore/except.h> -#include <zencore/filesystem.h> -#include <zencore/fmtutils.h> -#include <zencore/iohash.h> -#include <zencore/logging.h> -#include <zencore/memoryview.h> -#include <zencore/scopeguard.h> -#include <zencore/stream.h> -#include <zencore/string.h> -#include <zencore/testutils.h> -#include <zencore/thread.h> -#include <zencore/timer.h> -#include <zencore/xxhash.h> -#include <zenhttp/httpclient.h> -#include <zenhttp/packageformat.h> -#include <zenhttp/zenhttp.h> -#include <zenutil/buildstoragecache.h> -#include <zenutil/cache/cache.h> -#include <zenutil/cache/cacherequests.h> -#include <zenutil/chunkrequests.h> -#include <zenutil/logging/testformatter.h> -#include <zenutil/zenserverprocess.h> - -#include <http_parser.h> - -#if ZEN_PLATFORM_WINDOWS -# pragma comment(lib, "Crypt32.lib") -# pragma comment(lib, "Wldap32.lib") -#endif - -ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> -#include <tsl/robin_set.h> -#undef GetObject -ZEN_THIRD_PARTY_INCLUDES_END - -#include <atomic> -#include <filesystem> -#include <map> -#include <random> -#include <span> -#include <thread> -#include <typeindex> -#include <unordered_map> - -#if ZEN_PLATFORM_WINDOWS -# include <ppl.h> -# include <process.h> -#endif - -#include <zencore/memory/newdelete.h> - -////////////////////////////////////////////////////////////////////////// - -#include "projectclient.h" - -////////////////////////////////////////////////////////////////////////// - #if ZEN_WITH_TESTS -# define ZEN_TEST_WITH_RUNNER 1 -# include <zencore/testing.h> -# include <zencore/workthreadpool.h> -#endif - -using namespace std::literals; -#if ZEN_PLATFORM_LINUX || ZEN_PLATFORM_MAC +# define ZEN_TEST_WITH_RUNNER 1 +# include "zenserver-test.h" + +# include <zencore/except.h> +# include <zencore/fmtutils.h> +# include <zencore/logging.h> +# include <zencore/stream.h> +# include <zencore/string.h> +# include <zencore/testutils.h> +# include <zencore/thread.h> +# include <zencore/timer.h> +# include <zenhttp/httpclient.h> +# include <zenhttp/packageformat.h> +# include <zenutil/logging/testformatter.h> +# include <zenutil/zenserverprocess.h> + +# include <atomic> +# include <filesystem> + +# if ZEN_PLATFORM_WINDOWS +# include <ppl.h> +# include <process.h> +# else +# include <thread> struct Concurrency { template<typename... T> @@ -90,12 +44,19 @@ struct Concurrency } } }; -#endif +# endif + +# include <zencore/memory/newdelete.h> ////////////////////////////////////////////////////////////////////////// -#if ZEN_WITH_TESTS +using namespace std::literals; + +////////////////////////////////////////////////////////////////////////// + +namespace zen::tests { zen::ZenServerEnvironment TestEnv; +} int main(int argc, char** argv) @@ -107,7 +68,15 @@ main(int argc, char** argv) IgnoreChildSignals(); # endif +# if ZEN_WITH_TRACE zen::TraceInit("zenserver-test"); + TraceOptions TraceCommandlineOptions; + if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) + { + TraceConfigure(TraceCommandlineOptions); + } +# endif // ZEN_WITH_TRACE + zen::logging::InitializeLogging(); zen::logging::SetLogLevel(zen::logging::level::Debug); @@ -133,7 +102,7 @@ main(int argc, char** argv) } } - TestEnv.InitializeForTest(ProgramBaseDir, TestBaseDir, ServerClass); + zen::tests::TestEnv.InitializeForTest(ProgramBaseDir, TestBaseDir, ServerClass); ZEN_INFO("Running tests...(base dir: '{}')", TestBaseDir); @@ -145,16 +114,6 @@ main(int argc, char** argv) namespace zen::tests { -IoBuffer -MakeCbObjectPayload(std::function<void(CbObjectWriter& Writer)> WriteCB) -{ - CbObjectWriter Writer; - WriteCB(Writer); - IoBuffer Payload = Writer.Save().GetBuffer().AsIoBuffer(); - Payload.SetContentType(ZenContentType::kCbObject); - return Payload; -}; - TEST_CASE("default.single") { std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); @@ -172,12 +131,12 @@ TEST_CASE("default.single") const int ThreadId = zen::GetCurrentThreadId(); ZEN_INFO("query batch {} started (thread {})", BatchNo, ThreadId); - cpr::Session cli; - cli.SetUrl(cpr::Url{fmt::format("http://localhost:{}/test/hello", PortNumber)}); - for (int i = 0; i < 10000; ++i) + HttpClient Http{fmt::format("http://localhost:{}", PortNumber)}; + + for (int i = 0; i < 100; ++i) { - auto res = cli.Get(); + auto res = Http.Get("/test/hello"sv); ++RequestCount; } ZEN_INFO("query batch {} ended (thread {})", BatchNo, ThreadId); @@ -216,18 +175,20 @@ TEST_CASE("default.loopback") SUBCASE("ipv4 endpoint connectivity") { - cpr::Session cli; - cli.SetUrl(cpr::Url{fmt::format("http://127.0.0.1:{}/test/hello", PortNumber)}); - auto res = cli.Get(); - CHECK(!res.error); + HttpClient Http{fmt::format("http://127.0.0.1:{}", PortNumber)}; + + auto res = Http.Get("/test/hello"sv); + + CHECK(res); } SUBCASE("ipv6 endpoint connectivity") { - cpr::Session cli; - cli.SetUrl(cpr::Url{fmt::format("http://[::1]:{}/test/hello", PortNumber)}); - auto res = cli.Get(); - CHECK(!res.error); + HttpClient Http{fmt::format("http://[::1]:{}", PortNumber)}; + + auto res = Http.Get("/test/hello"sv); + + CHECK(res); } } @@ -259,14 +220,14 @@ TEST_CASE("multi.basic") ZEN_INFO("query batch {} started (thread {}) for port {}", BatchNo, ThreadId, PortNumber); - cpr::Session cli; - cli.SetUrl(cpr::Url{fmt::format("http://localhost:{}/test/hello", PortNumber)}); + HttpClient Http{fmt::format("http://localhost:{}", PortNumber)}; - for (int i = 0; i < 10000; ++i) + for (int i = 0; i < 100; ++i) { - auto res = cli.Get(); + auto res = Http.Get("/test/hello"sv); ++RequestCount; } + ZEN_INFO("query batch {} ended (thread {})", BatchNo, ThreadId); }; @@ -287,2742 +248,6 @@ TEST_CASE("multi.basic") zen::NiceRate(RequestCount, (uint32_t)Elapsed, "req")); } -TEST_CASE("project.basic") -{ - using namespace std::literals; - - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - - ZenServerInstance Instance1(TestEnv); - Instance1.SetTestDir(TestDir); - - const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady(); - - std::atomic<uint64_t> RequestCount{0}; - - zen::Stopwatch timer; - - std::mt19937_64 mt; - - zen::StringBuilder<64> BaseUri; - BaseUri << fmt::format("http://localhost:{}/prj/test", PortNumber); - - std::filesystem::path BinPath = zen::GetRunningExecutablePath(); - std::filesystem::path RootPath = BinPath.parent_path().parent_path(); - BinPath = BinPath.lexically_relative(RootPath); - - SUBCASE("build store init") - { - { - { - zen::CbObjectWriter Body; - Body << "id" - << "test"; - Body << "root" << RootPath.c_str(); - Body << "project" - << "/zooom"; - Body << "engine" - << "/zooom"; - - zen::BinaryWriter MemOut; - Body.Save(MemOut); - - auto Response = cpr::Post(cpr::Url{BaseUri.c_str()}, cpr::Body{(const char*)MemOut.Data(), MemOut.Size()}); - CHECK(Response.status_code == 201); - } - - { - auto Response = cpr::Get(cpr::Url{BaseUri.c_str()}); - CHECK(Response.status_code == 200); - - zen::CbObjectView ResponseObject = zen::CbFieldView(Response.text.data()).AsObjectView(); - - CHECK(ResponseObject["id"].AsString() == "test"sv); - CHECK(ResponseObject["root"].AsString() == PathToUtf8(RootPath.c_str())); - } - } - - BaseUri << "/oplog/foobar"; - - { - { - zen::StringBuilder<64> PostUri; - PostUri << BaseUri; - auto Response = cpr::Post(cpr::Url{PostUri.c_str()}); - CHECK(Response.status_code == 201); - } - - { - auto Response = cpr::Get(cpr::Url{BaseUri.c_str()}); - CHECK(Response.status_code == 200); - - zen::CbObjectView ResponseObject = zen::CbFieldView(Response.text.data()).AsObjectView(); - - CHECK(ResponseObject["id"].AsString() == "foobar"sv); - CHECK(ResponseObject["project"].AsString() == "test"sv); - } - } - - SUBCASE("build store persistence") - { - uint8_t AttachData[] = {1, 2, 3}; - - zen::CompressedBuffer Attachment = zen::CompressedBuffer::Compress(zen::SharedBuffer::Clone(zen::MemoryView{AttachData, 3})); - zen::CbAttachment Attach{Attachment, Attachment.DecodeRawHash()}; - - zen::CbObjectWriter OpWriter; - OpWriter << "key" - << "foo" - << "attachment" << Attach; - - const std::string_view ChunkId{ - "00000000" - "00000000" - "00010000"}; - auto FileOid = zen::Oid::FromHexString(ChunkId); - - OpWriter.BeginArray("files"); - OpWriter.BeginObject(); - OpWriter << "id" << FileOid; - OpWriter << "clientpath" - << "/{engine}/client/side/path"; - OpWriter << "serverpath" << BinPath.c_str(); - OpWriter.EndObject(); - OpWriter.EndArray(); - - zen::CbObject Op = OpWriter.Save(); - - zen::CbPackage OpPackage(Op); - OpPackage.AddAttachment(Attach); - - zen::BinaryWriter MemOut; - legacy::SaveCbPackage(OpPackage, MemOut); - - { - zen::StringBuilder<64> PostUri; - PostUri << BaseUri << "/new"; - auto Response = cpr::Post(cpr::Url{PostUri.c_str()}, cpr::Body{(const char*)MemOut.Data(), MemOut.Size()}); - - REQUIRE(!Response.error); - CHECK(Response.status_code == 201); - } - - // Read file data - - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << BaseUri << "/" << ChunkId; - auto Response = cpr::Get(cpr::Url{ChunkGetUri.c_str()}); - - REQUIRE(!Response.error); - CHECK(Response.status_code == 200); - } - - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << BaseUri << "/" << ChunkId << "?offset=1&size=10"; - auto Response = cpr::Get(cpr::Url{ChunkGetUri.c_str()}); - - REQUIRE(!Response.error); - CHECK(Response.status_code == 200); - CHECK(Response.text.size() == 10); - } - - ZEN_INFO("+++++++"); - } - - SUBCASE("snapshot") - { - zen::CbObjectWriter OpWriter; - OpWriter << "key" - << "foo"; - - const std::string_view ChunkId{ - "00000000" - "00000000" - "00010000"}; - auto FileOid = zen::Oid::FromHexString(ChunkId); - - OpWriter.BeginArray("files"); - OpWriter.BeginObject(); - OpWriter << "id" << FileOid; - OpWriter << "clientpath" - << "/{engine}/client/side/path"; - OpWriter << "serverpath" << BinPath.c_str(); - OpWriter.EndObject(); - OpWriter.EndArray(); - - zen::CbObject Op = OpWriter.Save(); - - zen::CbPackage OpPackage(Op); - - zen::BinaryWriter MemOut; - legacy::SaveCbPackage(OpPackage, MemOut); - - { - zen::StringBuilder<64> PostUri; - PostUri << BaseUri << "/new"; - auto Response = cpr::Post(cpr::Url{PostUri.c_str()}, cpr::Body{(const char*)MemOut.Data(), MemOut.Size()}); - - REQUIRE(!Response.error); - CHECK(Response.status_code == 201); - } - - // Read file data, it is raw and uncompressed - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << BaseUri << "/" << ChunkId; - auto Response = cpr::Get(cpr::Url{ChunkGetUri.c_str()}); - - REQUIRE(!Response.error); - CHECK(Response.status_code == 200); - IoBuffer Data(IoBuffer::Wrap, Response.text.data(), Response.text.length()); - IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath); - CHECK(ReferenceData.GetSize() == Data.GetSize()); - CHECK(ReferenceData.GetView().EqualBytes(Data.GetView())); - } - - { - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { Writer.AddString("method"sv, "snapshot"sv); }); - zen::StringBuilder<64> PostUri; - PostUri << BaseUri << "/rpc"; - auto Response = cpr::Post(cpr::Url{PostUri.c_str()}, - cpr::Body{(const char*)Payload.Data(), Payload.Size()}, - cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - REQUIRE(!Response.error); - CHECK(Response.status_code == 200); - } - - // Read chunk data, it is now compressed - { - zen::StringBuilder<128> ChunkGetUri; - ChunkGetUri << BaseUri << "/" << ChunkId; - auto Response = cpr::Get(cpr::Url{ChunkGetUri.c_str()}, cpr::Header{{"Accept-Type", "application/x-ue-comp"}}); - - REQUIRE(!Response.error); - CHECK(Response.status_code == 200); - IoBuffer Data(IoBuffer::Wrap, Response.text.data(), Response.text.length()); - IoHash RawHash; - uint64_t RawSize; - CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Data), RawHash, RawSize); - CHECK(Compressed); - IoBuffer DataDecompressed = Compressed.Decompress().AsIoBuffer(); - IoBuffer ReferenceData = IoBufferBuilder::MakeFromFile(RootPath / BinPath); - CHECK(RawSize == ReferenceData.GetSize()); - CHECK(ReferenceData.GetSize() == DataDecompressed.GetSize()); - CHECK(ReferenceData.GetView().EqualBytes(DataDecompressed.GetView())); - } - - ZEN_INFO("+++++++"); - } - - SUBCASE("test chunk not found error") - { - for (size_t I = 0; I < 65; I++) - { - zen::StringBuilder<128> PostUri; - PostUri << BaseUri << "/f77c781846caead318084604/info"; - auto Response = cpr::Get(cpr::Url{PostUri.c_str()}); - - REQUIRE(!Response.error); - CHECK(Response.status_code == 404); - } - } - } - - const uint64_t Elapsed = timer.GetElapsedTimeMs(); - - ZEN_INFO("{} requests in {} ({})", - RequestCount.load(), - zen::NiceTimeSpanMs(Elapsed), - zen::NiceRate(RequestCount, (uint32_t)Elapsed, "req")); -} - -namespace utils { - - struct ZenConfig - { - std::filesystem::path DataDir; - uint16_t Port; - std::string BaseUri; - std::string Args; - - static ZenConfig New(std::string Args = "") - { - return ZenConfig{.DataDir = TestEnv.CreateNewTestDir(), .Port = TestEnv.GetNewPortNumber(), .Args = std::move(Args)}; - } - - static ZenConfig New(uint16_t Port, std::string Args = "") - { - return ZenConfig{.DataDir = TestEnv.CreateNewTestDir(), .Port = Port, .Args = std::move(Args)}; - } - - static ZenConfig NewWithUpstream(uint16_t Port, uint16_t UpstreamPort, std::string Args = "") - { - return New(Port, - fmt::format("{}{}--debug --upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", - Args, - Args.length() > 0 ? " " : "", - UpstreamPort)); - } - - static ZenConfig NewWithThreadedUpstreams(uint16_t NewPort, std::span<uint16_t> UpstreamPorts, bool Debug) - { - std::string Args = Debug ? "--debug" : ""; - for (uint16_t Port : UpstreamPorts) - { - Args = fmt::format("{}{}--upstream-zen-url=http://localhost:{}", Args, Args.length() > 0 ? " " : "", Port); - } - return New(NewPort, Args); - } - - void Spawn(ZenServerInstance& Inst) - { - Inst.SetTestDir(DataDir); - Inst.SpawnServer(Port, Args); - const uint16_t InstancePort = Inst.WaitUntilReady(); - CHECK_MESSAGE(InstancePort != 0, Inst.GetLogOutput()); - - if (Port != InstancePort) - ZEN_DEBUG("relocation detected from {} to {}", Port, InstancePort); - - Port = InstancePort; - BaseUri = fmt::format("http://localhost:{}/z$", Port); - } - }; - - void SpawnServer(ZenServerInstance& Server, ZenConfig& Cfg) { Cfg.Spawn(Server); } - - CompressedBuffer CreateSemiRandomBlob(size_t AttachmentSize, OodleCompressionLevel CompressionLevel = OodleCompressionLevel::VeryFast) - { - // Convoluted way to get a compressed buffer whose result it large enough to be a separate file - // but also does actually compress - const size_t PartCount = (AttachmentSize / (1u * 1024u * 64)) + 1; - const size_t PartSize = AttachmentSize / PartCount; - auto Part = SharedBuffer(CreateRandomBlob(PartSize)); - std::vector<SharedBuffer> Parts(PartCount, Part); - size_t RemainPartSize = AttachmentSize - (PartSize * PartCount); - if (RemainPartSize > 0) - { - Parts.push_back(SharedBuffer(CreateRandomBlob(RemainPartSize))); - } - CompressedBuffer Value = CompressedBuffer::Compress(CompositeBuffer(std::move(Parts)), OodleCompressor::Mermaid, CompressionLevel); - return Value; - }; - - std::vector<std::pair<Oid, CompressedBuffer>> CreateAttachments(const std::span<const size_t>& Sizes) - { - std::vector<std::pair<Oid, CompressedBuffer>> Result; - Result.reserve(Sizes.size()); - for (size_t Size : Sizes) - { - CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(CreateRandomBlob(Size))); - Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed)); - } - return Result; - } - - std::vector<std::pair<Oid, CompressedBuffer>> CreateSemiRandomAttachments(const std::span<const size_t>& Sizes) - { - std::vector<std::pair<Oid, CompressedBuffer>> Result; - Result.reserve(Sizes.size()); - for (size_t Size : Sizes) - { - CompressedBuffer Compressed = - CreateSemiRandomBlob(Size, Size > 1024u * 1024u ? OodleCompressionLevel::None : OodleCompressionLevel::VeryFast); - Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed)); - } - return Result; - } - -} // namespace utils - -TEST_CASE("zcache.basic") -{ - using namespace std::literals; - - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - - const int kIterationCount = 100; - - auto HashKey = [](int i) -> zen::IoHash { return zen::IoHash::HashBuffer(&i, sizeof i); }; - - { - ZenServerInstance Instance1(TestEnv); - Instance1.SetTestDir(TestDir); - - const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady(); - const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber); - - // Populate with some simple data - - for (int i = 0; i < kIterationCount; ++i) - { - zen::CbObjectWriter Cbo; - Cbo << "index" << i; - - zen::BinaryWriter MemOut; - Cbo.Save(MemOut); - - zen::IoHash Key = HashKey(i); - - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}", BaseUri, "test", Key)}, - cpr::Body{(const char*)MemOut.Data(), MemOut.Size()}, - cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - - CHECK(Result.status_code == 201); - } - - // Retrieve data - - for (int i = 0; i < kIterationCount; ++i) - { - zen::IoHash Key = zen::IoHash::HashBuffer(&i, sizeof i); - - cpr::Response Result = - cpr::Get(cpr::Url{fmt::format("{}/{}/{}", BaseUri, "test", Key)}, cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - - CHECK(Result.status_code == 200); - } - - // Ensure bad bucket identifiers are rejected - - { - zen::CbObjectWriter Cbo; - Cbo << "index" << 42; - - zen::BinaryWriter MemOut; - Cbo.Save(MemOut); - - zen::IoHash Key = HashKey(442); - - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}", BaseUri, "te!st", Key)}, - cpr::Body{(const char*)MemOut.Data(), MemOut.Size()}, - cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - - CHECK(Result.status_code == 400); - } - } - - // Verify that the data persists between process runs (the previous server has exited at this point) - - { - ZenServerInstance Instance1(TestEnv); - Instance1.SetTestDir(TestDir); - const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady(); - - const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber); - - // Retrieve data again - - for (int i = 0; i < kIterationCount; ++i) - { - zen::IoHash Key = HashKey(i); - - cpr::Response Result = - cpr::Get(cpr::Url{fmt::format("{}/{}/{}", BaseUri, "test", Key)}, cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - - CHECK(Result.status_code == 200); - } - } -} - -TEST_CASE("zcache.cbpackage") -{ - using namespace std::literals; - - auto CreateTestPackage = [](zen::IoHash& OutAttachmentKey) -> zen::CbPackage { - auto Data = zen::SharedBuffer::Clone(zen::MakeMemoryView<uint8_t>({1, 2, 3, 4, 5, 6, 7, 8, 9})); - auto CompressedData = zen::CompressedBuffer::Compress(Data); - - OutAttachmentKey = CompressedData.DecodeRawHash(); - - zen::CbWriter Obj; - Obj.BeginObject("obj"sv); - Obj.AddBinaryAttachment("data", OutAttachmentKey); - Obj.EndObject(); - - zen::CbPackage Package; - Package.SetObject(Obj.Save().AsObject()); - Package.AddAttachment(zen::CbAttachment(CompressedData, OutAttachmentKey)); - - return Package; - }; - - auto SerializeToBuffer = [](zen::CbPackage Package) -> zen::IoBuffer { - zen::BinaryWriter MemStream; - - Package.Save(MemStream); - - return zen::IoBuffer(zen::IoBuffer::Clone, MemStream.Data(), MemStream.Size()); - }; - - auto IsEqual = [](zen::CbPackage Lhs, zen::CbPackage Rhs) -> bool { - std::span<const zen::CbAttachment> LhsAttachments = Lhs.GetAttachments(); - std::span<const zen::CbAttachment> RhsAttachments = Rhs.GetAttachments(); - - if (LhsAttachments.size() != RhsAttachments.size()) - { - return false; - } - - for (const zen::CbAttachment& LhsAttachment : LhsAttachments) - { - const zen::CbAttachment* RhsAttachment = Rhs.FindAttachment(LhsAttachment.GetHash()); - CHECK(RhsAttachment); - - zen::SharedBuffer LhsBuffer = LhsAttachment.AsCompressedBinary().Decompress(); - CHECK(!LhsBuffer.IsNull()); - - zen::SharedBuffer RhsBuffer = RhsAttachment->AsCompressedBinary().Decompress(); - CHECK(!RhsBuffer.IsNull()); - - if (!LhsBuffer.GetView().EqualBytes(RhsBuffer.GetView())) - { - return false; - } - } - - return true; - }; - - SUBCASE("PUT/GET returns correct package") - { - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - - ZenServerInstance Instance1(TestEnv); - Instance1.SetTestDir(TestDir); - const uint16_t PortNumber = Instance1.SpawnServerAndWaitUntilReady(); - const std::string BaseUri = fmt::format("http://localhost:{}/z$", PortNumber); - - const std::string_view Bucket = "mosdef"sv; - zen::IoHash Key; - zen::CbPackage ExpectedPackage = CreateTestPackage(Key); - - // PUT - { - zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage); - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}", BaseUri, Bucket, Key)}, - cpr::Body{(const char*)Body.Data(), Body.Size()}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 201); - } - - // GET - { - cpr::Response Result = - cpr::Get(cpr::Url{fmt::format("{}/{}/{}", BaseUri, Bucket, Key)}, cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 200); - - zen::IoBuffer Response(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size()); - - zen::CbPackage Package; - const bool Ok = Package.TryLoad(Response); - CHECK(Ok); - CHECK(IsEqual(Package, ExpectedPackage)); - } - } - - SUBCASE("PUT propagates upstream") - { - // Setup local and remote server - std::filesystem::path LocalDataDir = TestEnv.CreateNewTestDir(); - std::filesystem::path RemoteDataDir = TestEnv.CreateNewTestDir(); - - ZenServerInstance RemoteInstance(TestEnv); - RemoteInstance.SetTestDir(RemoteDataDir); - const uint16_t RemotePortNumber = RemoteInstance.SpawnServerAndWaitUntilReady(); - - ZenServerInstance LocalInstance(TestEnv); - LocalInstance.SetTestDir(LocalDataDir); - LocalInstance.SpawnServer(TestEnv.GetNewPortNumber(), - fmt::format("--upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", RemotePortNumber)); - const uint16_t LocalPortNumber = LocalInstance.WaitUntilReady(); - CHECK_MESSAGE(LocalPortNumber != 0, LocalInstance.GetLogOutput()); - - const auto LocalBaseUri = fmt::format("http://localhost:{}/z$", LocalPortNumber); - const auto RemoteBaseUri = fmt::format("http://localhost:{}/z$", RemotePortNumber); - - const std::string_view Bucket = "mosdef"sv; - zen::IoHash Key; - zen::CbPackage ExpectedPackage = CreateTestPackage(Key); - - // Store the cache record package in the local instance - { - zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage); - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}", LocalBaseUri, Bucket, Key)}, - cpr::Body{(const char*)Body.Data(), Body.Size()}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}}); - - CHECK(Result.status_code == 201); - } - - // The cache record can be retrieved as a package from the local instance - { - cpr::Response Result = - cpr::Get(cpr::Url{fmt::format("{}/{}/{}", LocalBaseUri, Bucket, Key)}, cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 200); - - zen::IoBuffer Body(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size()); - zen::CbPackage Package; - const bool Ok = Package.TryLoad(Body); - CHECK(Ok); - CHECK(IsEqual(Package, ExpectedPackage)); - } - - // The cache record can be retrieved as a package from the remote instance - { - cpr::Response Result = - cpr::Get(cpr::Url{fmt::format("{}/{}/{}", RemoteBaseUri, Bucket, Key)}, cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 200); - - zen::IoBuffer Body(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size()); - zen::CbPackage Package; - const bool Ok = Package.TryLoad(Body); - CHECK(Ok); - CHECK(IsEqual(Package, ExpectedPackage)); - } - } - - SUBCASE("GET finds upstream when missing in local") - { - // Setup local and remote server - std::filesystem::path LocalDataDir = TestEnv.CreateNewTestDir(); - std::filesystem::path RemoteDataDir = TestEnv.CreateNewTestDir(); - - ZenServerInstance RemoteInstance(TestEnv); - RemoteInstance.SetTestDir(RemoteDataDir); - const uint16_t RemotePortNumber = RemoteInstance.SpawnServerAndWaitUntilReady(); - - ZenServerInstance LocalInstance(TestEnv); - LocalInstance.SetTestDir(LocalDataDir); - LocalInstance.SpawnServer(TestEnv.GetNewPortNumber(), - fmt::format("--upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", RemotePortNumber)); - const uint16_t LocalPortNumber = LocalInstance.WaitUntilReady(); - CHECK_MESSAGE(LocalPortNumber != 0, LocalInstance.GetLogOutput()); - - const auto LocalBaseUri = fmt::format("http://localhost:{}/z$", LocalPortNumber); - const auto RemoteBaseUri = fmt::format("http://localhost:{}/z$", RemotePortNumber); - - const std::string_view Bucket = "mosdef"sv; - zen::IoHash Key; - zen::CbPackage ExpectedPackage = CreateTestPackage(Key); - - // Store the cache record package in upstream cache - { - zen::IoBuffer Body = SerializeToBuffer(ExpectedPackage); - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}", RemoteBaseUri, Bucket, Key)}, - cpr::Body{(const char*)Body.Data(), Body.Size()}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}}); - - CHECK(Result.status_code == 201); - } - - // The cache record can be retrieved as a package from the local cache - { - cpr::Response Result = - cpr::Get(cpr::Url{fmt::format("{}/{}/{}", LocalBaseUri, Bucket, Key)}, cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 200); - - zen::IoBuffer Body(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size()); - zen::CbPackage Package; - const bool Ok = Package.TryLoad(Body); - CHECK(Ok); - CHECK(IsEqual(Package, ExpectedPackage)); - } - } -} - -TEST_CASE("zcache.policy") -{ - using namespace std::literals; - using namespace utils; - - auto GenerateData = [](uint64_t Size, zen::IoHash& OutHash) -> zen::UniqueBuffer { - auto Buf = zen::UniqueBuffer::Alloc(Size); - uint8_t* Data = reinterpret_cast<uint8_t*>(Buf.GetData()); - for (uint64_t Idx = 0; Idx < Size; Idx++) - { - Data[Idx] = Idx % 256; - } - OutHash = zen::IoHash::HashBuffer(Data, Size); - return Buf; - }; - - auto GeneratePackage = [](zen::IoHash& OutRecordKey, zen::IoHash& OutAttachmentKey) -> zen::CbPackage { - auto Data = zen::SharedBuffer::Clone(zen::MakeMemoryView<uint8_t>({1, 2, 3, 4, 5, 6, 7, 8, 9})); - auto CompressedData = zen::CompressedBuffer::Compress(Data); - OutAttachmentKey = CompressedData.DecodeRawHash(); - - zen::CbWriter Writer; - Writer.BeginObject("obj"sv); - Writer.AddBinaryAttachment("data", OutAttachmentKey); - Writer.EndObject(); - CbObject CacheRecord = Writer.Save().AsObject(); - - OutRecordKey = IoHash::HashBuffer(CacheRecord.GetBuffer().GetView()); - - zen::CbPackage Package; - Package.SetObject(CacheRecord); - Package.AddAttachment(zen::CbAttachment(CompressedData, OutAttachmentKey)); - - return Package; - }; - - auto ToBuffer = [](zen::CbPackage Package) -> zen::IoBuffer { - zen::BinaryWriter MemStream; - Package.Save(MemStream); - - return zen::IoBuffer(zen::IoBuffer::Clone, MemStream.Data(), MemStream.Size()); - }; - - SUBCASE("query - 'local' does not query upstream (binary)") - { - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamInst(TestEnv); - UpstreamCfg.Spawn(UpstreamInst); - const uint16_t UpstreamPort = UpstreamCfg.Port; - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamPort); - ZenServerInstance LocalInst(TestEnv); - LocalCfg.Spawn(LocalInst); - - const std::string_view Bucket = "legacy"sv; - - zen::IoHash Key; - auto BinaryValue = GenerateData(1024, Key); - - // Store binary cache value upstream - { - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}", UpstreamCfg.BaseUri, Bucket, Key)}, - cpr::Body{(const char*)BinaryValue.GetData(), BinaryValue.GetSize()}, - cpr::Header{{"Content-Type", "application/octet-stream"}}); - CHECK(Result.status_code == 201); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}?Policy=QueryLocal,Store", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/octet-stream"}}); - CHECK(Result.status_code == 404); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}?Policy=Query,Store", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/octet-stream"}}); - CHECK(Result.status_code == 200); - } - } - - SUBCASE("store - 'local' does not store upstream (binary)") - { - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamInst(TestEnv); - UpstreamCfg.Spawn(UpstreamInst); - const uint16_t UpstreamPort = UpstreamCfg.Port; - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamPort); - ZenServerInstance LocalInst(TestEnv); - LocalCfg.Spawn(LocalInst); - - const auto Bucket = "legacy"sv; - - zen::IoHash Key; - auto BinaryValue = GenerateData(1024, Key); - - // Store binary cache value locally - { - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}?Policy=Query,StoreLocal", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Body{(const char*)BinaryValue.GetData(), BinaryValue.GetSize()}, - cpr::Header{{"Content-Type", "application/octet-stream"}}); - CHECK(Result.status_code == 201); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}", UpstreamCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/octet-stream"}}); - CHECK(Result.status_code == 404); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/octet-stream"}}); - CHECK(Result.status_code == 200); - } - } - - SUBCASE("store - 'local/remote' stores local and upstream (binary)") - { - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamInst(TestEnv); - UpstreamCfg.Spawn(UpstreamInst); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); - ZenServerInstance LocalInst(TestEnv); - LocalCfg.Spawn(LocalInst); - - const auto Bucket = "legacy"sv; - - zen::IoHash Key; - auto BinaryValue = GenerateData(1024, Key); - - // Store binary cache value locally and upstream - { - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}?Policy=Query,Store", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Body{(const char*)BinaryValue.GetData(), BinaryValue.GetSize()}, - cpr::Header{{"Content-Type", "application/octet-stream"}}); - CHECK(Result.status_code == 201); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}", UpstreamCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/octet-stream"}}); - CHECK(Result.status_code == 200); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/octet-stream"}}); - CHECK(Result.status_code == 200); - } - } - - SUBCASE("query - 'local' does not query upstream (cppackage)") - { - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamInst(TestEnv); - UpstreamCfg.Spawn(UpstreamInst); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); - ZenServerInstance LocalInst(TestEnv); - LocalCfg.Spawn(LocalInst); - - const auto Bucket = "legacy"sv; - - zen::IoHash Key; - zen::IoHash PayloadId; - zen::CbPackage Package = GeneratePackage(Key, PayloadId); - auto Buf = ToBuffer(Package); - - // Store package upstream - { - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}", UpstreamCfg.BaseUri, Bucket, Key)}, - cpr::Body{(const char*)Buf.GetData(), Buf.GetSize()}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 201); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}?Policy=QueryLocal,Store", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 404); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}?Policy=Query,Store", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 200); - } - } - - SUBCASE("store - 'local' does not store upstream (cbpackge)") - { - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamInst(TestEnv); - UpstreamCfg.Spawn(UpstreamInst); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); - ZenServerInstance LocalInst(TestEnv); - LocalCfg.Spawn(LocalInst); - - const auto Bucket = "legacy"sv; - - zen::IoHash Key; - zen::IoHash PayloadId; - zen::CbPackage Package = GeneratePackage(Key, PayloadId); - auto Buf = ToBuffer(Package); - - // Store packge locally - { - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}?Policy=Query,StoreLocal", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Body{(const char*)Buf.GetData(), Buf.GetSize()}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 201); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}", UpstreamCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 404); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 200); - } - } - - SUBCASE("store - 'local/remote' stores local and upstream (cbpackage)") - { - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamInst(TestEnv); - UpstreamCfg.Spawn(UpstreamInst); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); - ZenServerInstance LocalInst(TestEnv); - LocalCfg.Spawn(LocalInst); - - const auto Bucket = "legacy"sv; - - zen::IoHash Key; - zen::IoHash PayloadId; - zen::CbPackage Package = GeneratePackage(Key, PayloadId); - auto Buf = ToBuffer(Package); - - // Store package locally and upstream - { - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}?Policy=Query,Store", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Body{(const char*)Buf.GetData(), Buf.GetSize()}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 201); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}", UpstreamCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 200); - } - - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}", LocalCfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 200); - } - } - - SUBCASE("skip - 'data' returns cache record without attachments/empty payload") - { - ZenConfig Cfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance Instance(TestEnv); - Cfg.Spawn(Instance); - - const auto Bucket = "test"sv; - - zen::IoHash Key; - zen::IoHash PayloadId; - zen::CbPackage Package = GeneratePackage(Key, PayloadId); - auto Buf = ToBuffer(Package); - - // Store package - { - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}", Cfg.BaseUri, Bucket, Key)}, - cpr::Body{(const char*)Buf.GetData(), Buf.GetSize()}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}}); - CHECK(Result.status_code == 201); - } - - // Get package - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}?Policy=Default,SkipData", Cfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/x-ue-cbpkg"}}); - CHECK(IsHttpSuccessCode(Result.status_code)); - IoBuffer Buffer(IoBuffer::Wrap, Result.text.c_str(), Result.text.size()); - CbPackage ResponsePackage; - CHECK(ResponsePackage.TryLoad(Buffer)); - CHECK(ResponsePackage.GetAttachments().size() == 0); - } - - // Get record - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}?Policy=Default,SkipData", Cfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/x-ue-cb"}}); - CHECK(IsHttpSuccessCode(Result.status_code)); - IoBuffer Buffer(IoBuffer::Wrap, Result.text.c_str(), Result.text.size()); - CbObject ResponseObject = zen::LoadCompactBinaryObject(Buffer); - CHECK((bool)ResponseObject); - } - - // Get payload - { - cpr::Response Result = - cpr::Get(cpr::Url{fmt::format("{}/{}/{}/{}?Policy=Default,SkipData", Cfg.BaseUri, Bucket, Key, PayloadId)}, - cpr::Header{{"Accept", "application/x-ue-comp"}}); - CHECK(IsHttpSuccessCode(Result.status_code)); - CHECK(Result.text.size() == 0); - } - } - - SUBCASE("skip - 'data' returns empty binary value") - { - ZenConfig Cfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance Instance(TestEnv); - Cfg.Spawn(Instance); - - const auto Bucket = "test"sv; - - zen::IoHash Key; - auto BinaryValue = GenerateData(1024, Key); - - // Store binary cache value - { - cpr::Response Result = cpr::Put(cpr::Url{fmt::format("{}/{}/{}", Cfg.BaseUri, Bucket, Key)}, - cpr::Body{(const char*)BinaryValue.GetData(), BinaryValue.GetSize()}, - cpr::Header{{"Content-Type", "application/octet-stream"}}); - CHECK(Result.status_code == 201); - } - - // Get package - { - cpr::Response Result = cpr::Get(cpr::Url{fmt::format("{}/{}/{}?Policy=Default,SkipData", Cfg.BaseUri, Bucket, Key)}, - cpr::Header{{"Accept", "application/octet-stream"}}); - CHECK(IsHttpSuccessCode(Result.status_code)); - CHECK(Result.text.size() == 0); - } - } -} - -TEST_CASE("zcache.rpc") -{ - using namespace std::literals; - - auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request, - const zen::CacheKey& CacheKey, - size_t PayloadSize, - CachePolicy RecordPolicy) { - std::vector<uint8_t> Data; - Data.resize(PayloadSize); - uint32_t DataSeed = *reinterpret_cast<const uint32_t*>(&CacheKey.Hash.Hash[0]); - uint16_t* DataPtr = reinterpret_cast<uint16_t*>(Data.data()); - for (size_t Idx = 0; Idx < PayloadSize / 2; ++Idx) - { - DataPtr[Idx] = static_cast<uint16_t>((Idx + DataSeed) % 0xffffu); - } - if (PayloadSize & 1) - { - Data[PayloadSize - 1] = static_cast<uint8_t>((PayloadSize - 1) & 0xff); - } - CompressedBuffer Value = zen::CompressedBuffer::Compress(SharedBuffer::MakeView(Data.data(), Data.size())); - Request.Requests.push_back({.Key = CacheKey, .Values = {{.Id = Oid::NewOid(), .Body = std::move(Value)}}, .Policy = RecordPolicy}); - }; - - auto PutCacheRecords = [&AppendCacheRecord](std::string_view BaseUri, - std::string_view Namespace, - std::string_view Bucket, - size_t Num, - size_t PayloadSize = 1024, - size_t KeyOffset = 1, - CachePolicy PutPolicy = CachePolicy::Default, - std::vector<CbPackage>* OutPackages = nullptr) -> std::vector<CacheKey> { - std::vector<zen::CacheKey> OutKeys; - - for (uint32_t Key = 1; Key <= Num; ++Key) - { - zen::IoHash KeyHash; - ((uint32_t*)(KeyHash.Hash))[0] = gsl::narrow<uint32_t>(KeyOffset + Key); - const zen::CacheKey CacheKey = zen::CacheKey::Create(Bucket, KeyHash); - - cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; - AppendCacheRecord(Request, CacheKey, PayloadSize, PutPolicy); - OutKeys.push_back(CacheKey); - - CbPackage Package; - CHECK(Request.Format(Package)); - - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - CHECK(Result.status_code == 200); - if (OutPackages) - { - OutPackages->emplace_back(std::move(Package)); - } - } - - return OutKeys; - }; - - struct GetCacheRecordResult - { - zen::CbPackage Response; - cacherequests::GetCacheRecordsResult Result; - bool Success; - }; - - auto GetCacheRecords = [](std::string_view BaseUri, - std::string_view Namespace, - std::span<zen::CacheKey> Keys, - zen::CachePolicy Policy, - zen::RpcAcceptOptions AcceptOptions = zen::RpcAcceptOptions::kNone, - int Pid = 0) -> GetCacheRecordResult { - cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, - .AcceptOptions = static_cast<uint16_t>(AcceptOptions), - .ProcessPid = Pid, - .DefaultPolicy = Policy, - .Namespace = std::string(Namespace)}; - for (const CacheKey& Key : Keys) - { - Request.Requests.push_back({.Key = Key}); - } - - CbObjectWriter RequestWriter; - CHECK(Request.Format(RequestWriter)); - - BinaryWriter Body; - RequestWriter.Save(Body); - - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cb"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - GetCacheRecordResult OutResult; - - if (Result.status_code == 200) - { - CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - CHECK(!Response.IsNull()); - OutResult.Response = std::move(Response); - CHECK(OutResult.Result.Parse(OutResult.Response)); - OutResult.Success = true; - } - - return OutResult; - }; - - SUBCASE("get cache records") - { - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - - ZenServerInstance Inst(TestEnv); - Inst.SetTestDir(TestDir); - - const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady(); - const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort); - - CachePolicy Policy = CachePolicy::Default; - std::vector<zen::CacheKey> Keys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 128); - GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, Policy); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record); - CHECK(Record->Key == ExpectedKey); - CHECK(Record->Values.size() == 1); - - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - CHECK(Value.Body); - } - } - } - - SUBCASE("get missing cache records") - { - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - - ZenServerInstance Inst(TestEnv); - Inst.SetTestDir(TestDir); - const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady(); - const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort); - - CachePolicy Policy = CachePolicy::Default; - std::vector<zen::CacheKey> ExistingKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 128); - std::vector<zen::CacheKey> Keys; - - for (const zen::CacheKey& Key : ExistingKeys) - { - Keys.push_back(Key); - Keys.push_back(CacheKey::Create("missing"sv, IoHash::Zero)); - } - - GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, Policy); - - CHECK(Result.Result.Results.size() == Keys.size()); - - size_t KeyIndex = 0; - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - const bool Missing = Index++ % 2 != 0; - - if (Missing) - { - CHECK(!Record); - } - else - { - const CacheKey& ExpectedKey = ExistingKeys[KeyIndex++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - CHECK(Value.Body); - } - } - } - } - - SUBCASE("policy - 'QueryLocal' does not query upstream") - { - using namespace utils; - - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamServer(TestEnv); - SpawnServer(UpstreamServer, UpstreamCfg); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); - ZenServerInstance LocalServer(TestEnv); - SpawnServer(LocalServer, LocalCfg); - - std::vector<zen::CacheKey> Keys = PutCacheRecords(UpstreamCfg.BaseUri, "ue4.ddc"sv, "mastodon"sv, 4); - - CachePolicy Policy = CachePolicy::QueryLocal; - GetCacheRecordResult Result = GetCacheRecords(LocalCfg.BaseUri, "ue4.ddc"sv, Keys, Policy); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(!Record); - } - } - - SUBCASE("policy - 'QueryRemote' does query upstream") - { - using namespace utils; - - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamServer(TestEnv); - SpawnServer(UpstreamServer, UpstreamCfg); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); - ZenServerInstance LocalServer(TestEnv); - SpawnServer(LocalServer, LocalCfg); - - std::vector<zen::CacheKey> Keys = PutCacheRecords(UpstreamCfg.BaseUri, "ue4.ddc"sv, "mastodon"sv, 4); - - CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); - GetCacheRecordResult Result = GetCacheRecords(LocalCfg.BaseUri, "ue4.ddc"sv, Keys, Policy); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - } - } - - SUBCASE("policy - 'QueryLocal' on put allows overwrite with differing value when not limiting overwrites") - { - using namespace utils; - - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamServer(TestEnv); - SpawnServer(UpstreamServer, UpstreamCfg); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); - ZenServerInstance LocalServer(TestEnv); - SpawnServer(LocalServer, LocalCfg); - - size_t PayloadSize = 1024; - std::string_view Namespace("ue4.ddc"sv); - std::string_view Bucket("mastodon"sv); - const size_t NumRecords = 4; - std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize); - - for (const zen::CacheKey& CacheKey : Keys) - { - cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; - AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default); - - CbPackage Package; - CHECK(Request.Format(Package)); - - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", LocalCfg.BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - CHECK(Result.status_code == 200); - cacherequests::PutCacheRecordsResult ParsedResult; - CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - CHECK(!Response.IsNull()); - CHECK(ParsedResult.Parse(Response)); - for (bool ResponseSuccess : ParsedResult.Success) - { - CHECK(ResponseSuccess); - } - } - - auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) { - CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); - GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - CHECK(Value.RawSize == PayloadSize * 2); - } - } - }; - - // Check that the records are present and overwritten in the local server - CheckRecordCorrectness(LocalCfg); - - // Check that the records are present and overwritten in the upstream server - CheckRecordCorrectness(UpstreamCfg); - } - - SUBCASE("policy - 'QueryLocal' on put denies overwrite with differing value when limiting overwrites") - { - using namespace utils; - - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamServer(TestEnv); - SpawnServer(UpstreamServer, UpstreamCfg); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites"); - ZenServerInstance LocalServer(TestEnv); - SpawnServer(LocalServer, LocalCfg); - - size_t PayloadSize = 1024; - std::string_view Namespace("ue4.ddc"sv); - std::string_view Bucket("mastodon"sv); - const size_t NumRecords = 4; - std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize); - - for (const zen::CacheKey& CacheKey : Keys) - { - cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; - AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default); - - CbPackage Package; - CHECK(Request.Format(Package)); - - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", LocalCfg.BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - CHECK(Result.status_code == 200); - cacherequests::PutCacheRecordsResult ParsedResult; - CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - CHECK(!Response.IsNull()); - CHECK(ParsedResult.Parse(Response)); - for (bool ResponseSuccess : ParsedResult.Success) - { - CHECK(!ResponseSuccess); - } - } - - auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) { - CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); - GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - CHECK(Value.RawSize == PayloadSize); - } - } - }; - - // Check that the records are present and not overwritten in the local server - CheckRecordCorrectness(LocalCfg); - - // Check that the records are present and not overwritten in the upstream server - CheckRecordCorrectness(UpstreamCfg); - } - - SUBCASE("policy - no 'QueryLocal' on put allows overwrite with differing value when limiting overwrites") - { - using namespace utils; - - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamServer(TestEnv); - SpawnServer(UpstreamServer, UpstreamCfg); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites"); - ZenServerInstance LocalServer(TestEnv); - SpawnServer(LocalServer, LocalCfg); - - size_t PayloadSize = 1024; - std::string_view Namespace("ue4.ddc"sv); - std::string_view Bucket("mastodon"sv); - const size_t NumRecords = 4; - std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize); - - for (const zen::CacheKey& CacheKey : Keys) - { - cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; - AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Store); - - CbPackage Package; - CHECK(Request.Format(Package)); - - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", LocalCfg.BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - CHECK(Result.status_code == 200); - cacherequests::PutCacheRecordsResult ParsedResult; - CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - CHECK(!Response.IsNull()); - CHECK(ParsedResult.Parse(Response)); - for (bool ResponseSuccess : ParsedResult.Success) - { - CHECK(ResponseSuccess); - } - } - - auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) { - CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); - GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - CHECK(Value.RawSize == PayloadSize * 2); - } - } - }; - - // Check that the records are present and overwritten in the local server - CheckRecordCorrectness(LocalCfg); - - // Check that the records are present and overwritten in the upstream server - CheckRecordCorrectness(UpstreamCfg); - } - - SUBCASE("policy - 'QueryLocal' on put allows overwrite with equivalent value when limiting overwrites") - { - using namespace utils; - - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamServer(TestEnv); - SpawnServer(UpstreamServer, UpstreamCfg); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, "--cache-bucket-limit-overwrites"); - ZenServerInstance LocalServer(TestEnv); - SpawnServer(LocalServer, LocalCfg); - - size_t PayloadSize = 1024; - std::string_view Namespace("ue4.ddc"sv); - std::string_view Bucket("mastodon"sv); - const size_t NumRecords = 4; - std::vector<CbPackage> Packages; - std::vector<zen::CacheKey> Keys = - PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize, 1, CachePolicy::Default, &Packages); - - for (const CbPackage& Package : Packages) - { - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", LocalCfg.BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - CHECK(Result.status_code == 200); - cacherequests::PutCacheRecordsResult ParsedResult; - CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - CHECK(!Response.IsNull()); - CHECK(ParsedResult.Parse(Response)); - for (bool ResponseSuccess : ParsedResult.Success) - { - CHECK(ResponseSuccess); - } - } - - auto CheckRecordCorrectness = [&](const ZenConfig& Cfg) { - CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); - GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - CHECK(Value.RawSize == PayloadSize); - } - } - }; - - // Check that the records are present and unchanged in the local server - CheckRecordCorrectness(LocalCfg); - - // Check that the records are present and unchanged in the upstream server - CheckRecordCorrectness(UpstreamCfg); - } - - // TODO: Propagation for rejected PUTs - // SUBCASE("policy - 'QueryLocal' on put denies overwrite with differing value when limiting overwrites but allows propagation to - // upstream") - // { - // using namespace utils; - - // ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - // ZenServerInstance UpstreamServer(TestEnv); - // SpawnServer(UpstreamServer, UpstreamCfg); - - // ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port, - // "--cache-bucket-limit-overwrites"); ZenServerInstance LocalServer(TestEnv); SpawnServer(LocalServer, LocalCfg); - - // size_t PayloadSize = 1024; - // std::string_view Namespace("ue4.ddc"sv); - // std::string_view Bucket("mastodon"sv); - // const size_t NumRecords = 4; - // std::vector<zen::CacheKey> Keys = PutCacheRecords(LocalCfg.BaseUri, Namespace, Bucket, NumRecords, PayloadSize, 1, - // CachePolicy::Local); - - // for (const zen::CacheKey& CacheKey : Keys) - // { - // cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; - // AppendCacheRecord(Request, CacheKey, PayloadSize * 2, CachePolicy::Default); - - // CbPackage Package; - // CHECK(Request.Format(Package)); - - // IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - // cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", LocalCfg.BaseUri)}, - // cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - // cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - // CHECK(Result.status_code == 200); - // cacherequests::PutCacheRecordsResult ParsedResult; - // CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - // CHECK(!Response.IsNull()); - // CHECK(ParsedResult.Parse(Response)); - // for (bool ResponseSuccess : ParsedResult.Success) - // { - // CHECK(!ResponseSuccess); - // } - // } - - // auto CheckRecordCorrectness = [&](const ZenConfig& Cfg, size_t ExpectedPayloadSize) { - // CachePolicy Policy = (CachePolicy::QueryLocal | CachePolicy::QueryRemote); - // GetCacheRecordResult Result = GetCacheRecords(Cfg.BaseUri, "ue4.ddc"sv, Keys, Policy); - - // CHECK(Result.Result.Results.size() == Keys.size()); - - // for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - // { - // CHECK(Record); - // const CacheKey& ExpectedKey = Keys[Index++]; - // CHECK(Record->Key == ExpectedKey); - // for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - // { - // CHECK(Value.RawSize == ExpectedPayloadSize); - // } - // } - // }; - - // // Check that the records are present and not overwritten in the local server - // CheckRecordCorrectness(LocalCfg, PayloadSize); - - // // Check that the records are present and are the newer size in the upstream server - // CheckRecordCorrectness(UpstreamCfg, PayloadSize*2); - // } - - SUBCASE("RpcAcceptOptions") - { - using namespace utils; - - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - - ZenServerInstance Inst(TestEnv); - Inst.SetTestDir(TestDir); - - const uint16_t BasePort = Inst.SpawnServerAndWaitUntilReady(); - const std::string BaseUri = fmt::format("http://localhost:{}/z$", BasePort); - - std::vector<zen::CacheKey> SmallKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 4, 1024); - std::vector<zen::CacheKey> LargeKeys = PutCacheRecords(BaseUri, "ue4.ddc"sv, "mastodon"sv, 4, 1024 * 1024 * 16, SmallKeys.size()); - - std::vector<zen::CacheKey> Keys(SmallKeys.begin(), SmallKeys.end()); - Keys.insert(Keys.end(), LargeKeys.begin(), LargeKeys.end()); - - { - GetCacheRecordResult Result = GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, CachePolicy::Default); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); - IoBufferFileReference Ref; - bool IsFileRef = Body.GetFileReference(Ref); - CHECK(!IsFileRef); - } - } - } - - // File path, but only for large files - { - GetCacheRecordResult Result = - GetCacheRecords(BaseUri, "ue4.ddc"sv, Keys, CachePolicy::Default, RpcAcceptOptions::kAllowLocalReferences); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); - IoBufferFileReference Ref; - bool IsFileRef = Body.GetFileReference(Ref); - CHECK(IsFileRef == (Body.Size() > 1024)); - } - } - } - - // File path, for all files - { - GetCacheRecordResult Result = - GetCacheRecords(BaseUri, - "ue4.ddc"sv, - Keys, - CachePolicy::Default, - RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); - IoBufferFileReference Ref; - bool IsFileRef = Body.GetFileReference(Ref); - CHECK(IsFileRef); - } - } - } - - // File handle, but only for large files - { - GetCacheRecordResult Result = GetCacheRecords(BaseUri, - "ue4.ddc"sv, - Keys, - CachePolicy::Default, - RpcAcceptOptions::kAllowLocalReferences, - GetCurrentProcessId()); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); - IoBufferFileReference Ref; - bool IsFileRef = Body.GetFileReference(Ref); - CHECK(IsFileRef == (Body.Size() > 1024)); - } - } - } - - // File handle, for all files - { - GetCacheRecordResult Result = - GetCacheRecords(BaseUri, - "ue4.ddc"sv, - Keys, - CachePolicy::Default, - RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences, - GetCurrentProcessId()); - - CHECK(Result.Result.Results.size() == Keys.size()); - - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - CHECK(Record); - const CacheKey& ExpectedKey = Keys[Index++]; - CHECK(Record->Key == ExpectedKey); - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - const IoBuffer& Body = Value.Body.GetCompressed().Flatten().AsIoBuffer(); - IoBufferFileReference Ref; - bool IsFileRef = Body.GetFileReference(Ref); - CHECK(IsFileRef); - } - } - } - } -} - -TEST_CASE("zcache.failing.upstream") -{ - // This is an exploratory test that takes a long time to run, so lets skip it by default - if (true) - { - return; - } - - using namespace std::literals; - using namespace utils; - - ZenConfig Upstream1Cfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance Upstream1Server(TestEnv); - SpawnServer(Upstream1Server, Upstream1Cfg); - - ZenConfig Upstream2Cfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance Upstream2Server(TestEnv); - SpawnServer(Upstream2Server, Upstream2Cfg); - - std::vector<std::uint16_t> UpstreamPorts = {Upstream1Cfg.Port, Upstream2Cfg.Port}; - ZenConfig LocalCfg = ZenConfig::NewWithThreadedUpstreams(TestEnv.GetNewPortNumber(), UpstreamPorts, false); - LocalCfg.Args += (" --upstream-thread-count 2"); - ZenServerInstance LocalServer(TestEnv); - SpawnServer(LocalServer, LocalCfg); - - const uint16_t LocalPortNumber = LocalCfg.Port; - const auto LocalUri = fmt::format("http://localhost:{}/z$", LocalPortNumber); - const auto Upstream1Uri = fmt::format("http://localhost:{}/z$", Upstream1Cfg.Port); - const auto Upstream2Uri = fmt::format("http://localhost:{}/z$", Upstream2Cfg.Port); - - bool Upstream1Running = true; - bool Upstream2Running = true; - - using namespace std::literals; - - auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request, - const zen::CacheKey& CacheKey, - size_t PayloadSize, - CachePolicy RecordPolicy) { - std::vector<uint32_t> Data; - Data.resize(PayloadSize / 4); - for (uint32_t Idx = 0; Idx < PayloadSize / 4; ++Idx) - { - Data[Idx] = (*reinterpret_cast<const uint32_t*>(&CacheKey.Hash.Hash[0])) + Idx; - } - - CompressedBuffer Value = zen::CompressedBuffer::Compress(SharedBuffer::MakeView(Data.data(), Data.size() * 4)); - Request.Requests.push_back({.Key = CacheKey, .Values = {{.Id = Oid::NewOid(), .Body = std::move(Value)}}, .Policy = RecordPolicy}); - }; - - auto PutCacheRecords = [&AppendCacheRecord](std::string_view BaseUri, - std::string_view Namespace, - std::string_view Bucket, - size_t Num, - size_t KeyOffset, - size_t PayloadSize = 8192) -> std::vector<CacheKey> { - std::vector<zen::CacheKey> OutKeys; - - cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; - for (size_t Key = 1; Key <= Num; ++Key) - { - zen::IoHash KeyHash; - ((size_t*)(KeyHash.Hash))[0] = KeyOffset + Key; - const zen::CacheKey CacheKey = zen::CacheKey::Create(Bucket, KeyHash); - - AppendCacheRecord(Request, CacheKey, PayloadSize, CachePolicy::Default); - OutKeys.push_back(CacheKey); - } - - CbPackage Package; - CHECK(Request.Format(Package)); - - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - if (Result.status_code != 200) - { - ZEN_DEBUG("PutCacheRecords failed with {}, reason '{}'", Result.status_code, Result.reason); - OutKeys.clear(); - } - - return OutKeys; - }; - - struct GetCacheRecordResult - { - zen::CbPackage Response; - cacherequests::GetCacheRecordsResult Result; - bool Success = false; - }; - - auto GetCacheRecords = [](std::string_view BaseUri, - std::string_view Namespace, - std::span<zen::CacheKey> Keys, - zen::CachePolicy Policy) -> GetCacheRecordResult { - cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, - .DefaultPolicy = Policy, - .Namespace = std::string(Namespace)}; - for (const CacheKey& Key : Keys) - { - Request.Requests.push_back({.Key = Key}); - } - - CbObjectWriter RequestWriter; - CHECK(Request.Format(RequestWriter)); - - BinaryWriter Body; - RequestWriter.Save(Body); - - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cb"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - GetCacheRecordResult OutResult; - - if (Result.status_code == 200) - { - CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - if (!Response.IsNull()) - { - OutResult.Response = std::move(Response); - CHECK(OutResult.Result.Parse(OutResult.Response)); - OutResult.Success = true; - } - } - else - { - ZEN_DEBUG("GetCacheRecords with {}, reason '{}'", Result.reason, Result.status_code); - } - - return OutResult; - }; - - // Populate with some simple data - - CachePolicy Policy = CachePolicy::Default; - - const size_t ThreadCount = 128; - const size_t KeyMultiplier = 16384; - const size_t RecordsPerRequest = 64; - WorkerThreadPool Pool(ThreadCount); - - std::atomic_size_t Completed = 0; - - auto Keys = new std::vector<CacheKey>[ThreadCount * KeyMultiplier]; - RwLock KeysLock; - - for (size_t I = 0; I < ThreadCount * KeyMultiplier; I++) - { - size_t Iteration = I; - Pool.ScheduleWork([&] { - std::vector<CacheKey> NewKeys = PutCacheRecords(LocalUri, "ue4.ddc"sv, "mastodon"sv, RecordsPerRequest, I * RecordsPerRequest); - if (NewKeys.size() != RecordsPerRequest) - { - ZEN_DEBUG("PutCacheRecords iteration {} failed", Iteration); - Completed.fetch_add(1); - return; - } - { - RwLock::ExclusiveLockScope _(KeysLock); - Keys[Iteration].swap(NewKeys); - } - Completed.fetch_add(1); - }); - } - bool UseUpstream1 = false; - while (Completed < ThreadCount * KeyMultiplier) - { - Sleep(8000); - - if (UseUpstream1) - { - if (Upstream2Running) - { - Upstream2Server.EnableTermination(); - Upstream2Server.Shutdown(); - Sleep(100); - Upstream2Running = false; - } - if (!Upstream1Running) - { - SpawnServer(Upstream1Server, Upstream1Cfg); - Upstream1Running = true; - } - UseUpstream1 = !UseUpstream1; - } - else - { - if (Upstream1Running) - { - Upstream1Server.EnableTermination(); - Upstream1Server.Shutdown(); - Sleep(100); - Upstream1Running = false; - } - if (!Upstream2Running) - { - SpawnServer(Upstream2Server, Upstream2Cfg); - Upstream2Running = true; - } - UseUpstream1 = !UseUpstream1; - } - } - - Completed = 0; - for (size_t I = 0; I < ThreadCount * KeyMultiplier; I++) - { - size_t Iteration = I; - std::vector<CacheKey>& LocalKeys = Keys[Iteration]; - if (LocalKeys.empty()) - { - Completed.fetch_add(1); - continue; - } - Pool.ScheduleWork([&] { - GetCacheRecordResult Result = GetCacheRecords(LocalUri, "ue4.ddc"sv, LocalKeys, Policy); - - if (!Result.Success) - { - ZEN_DEBUG("GetCacheRecords iteration {} failed", Iteration); - Completed.fetch_add(1); - return; - } - - if (Result.Result.Results.size() != LocalKeys.size()) - { - ZEN_DEBUG("GetCacheRecords iteration {} empty records", Iteration); - Completed.fetch_add(1); - return; - } - for (size_t Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& Record : Result.Result.Results) - { - const CacheKey& ExpectedKey = LocalKeys[Index++]; - if (!Record) - { - continue; - } - if (Record->Key != ExpectedKey) - { - continue; - } - if (Record->Values.size() != 1) - { - continue; - } - - for (const cacherequests::GetCacheRecordResultValue& Value : Record->Values) - { - if (!Value.Body) - { - continue; - } - } - } - Completed.fetch_add(1); - }); - } - while (Completed < ThreadCount * KeyMultiplier) - { - Sleep(10); - } -} - -TEST_CASE("zcache.rpc.partialchunks") -{ - using namespace std::literals; - using namespace utils; - - ZenConfig LocalCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance Server(TestEnv); - SpawnServer(Server, LocalCfg); - - std::vector<CompressedBuffer> Attachments; - - const auto BaseUri = fmt::format("http://localhost:{}/z$", Server.GetBasePort()); - - auto GenerateKey = [](std::string_view Bucket, size_t KeyIndex) -> CacheKey { - IoHash KeyHash; - ((size_t*)(KeyHash.Hash))[0] = KeyIndex; - return CacheKey::Create(Bucket, KeyHash); - }; - - auto AppendCacheRecord = [](cacherequests::PutCacheRecordsRequest& Request, - const CacheKey& CacheKey, - size_t AttachmentCount, - size_t AttachmentsSize, - CachePolicy RecordPolicy) -> std::vector<std::pair<Oid, CompressedBuffer>> { - std::vector<std::pair<Oid, CompressedBuffer>> AttachmentBuffers; - std::vector<cacherequests::PutCacheRecordRequestValue> Attachments; - for (size_t AttachmentIndex = 0; AttachmentIndex < AttachmentCount; AttachmentIndex++) - { - CompressedBuffer Value = CreateSemiRandomBlob(AttachmentsSize); - AttachmentBuffers.push_back(std::make_pair(Oid::NewOid(), Value)); - Attachments.push_back({.Id = AttachmentBuffers.back().first, .Body = std::move(Value)}); - } - Request.Requests.push_back({.Key = CacheKey, .Values = Attachments, .Policy = RecordPolicy}); - return AttachmentBuffers; - }; - - auto PutCacheRecords = [&AppendCacheRecord, &GenerateKey]( - std::string_view BaseUri, - std::string_view Namespace, - std::string_view Bucket, - size_t KeyOffset, - size_t Num, - size_t AttachmentCount, - size_t AttachmentsSize = - 8192) -> std::vector<std::pair<CacheKey, std::vector<std::pair<Oid, CompressedBuffer>>>> { - std::vector<std::pair<CacheKey, std::vector<std::pair<Oid, CompressedBuffer>>>> Keys; - - cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, .Namespace = std::string(Namespace)}; - for (size_t Key = 1; Key <= Num; ++Key) - { - const CacheKey NewCacheKey = GenerateKey(Bucket, KeyOffset + Key); - std::vector<std::pair<Oid, CompressedBuffer>> Attachments = - AppendCacheRecord(Request, NewCacheKey, AttachmentCount, AttachmentsSize, CachePolicy::Default); - Keys.push_back(std::make_pair(NewCacheKey, std::move(Attachments))); - } - - CbPackage Package; - CHECK(Request.Format(Package)); - - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - if (Result.status_code != 200) - { - ZEN_DEBUG("PutCacheRecords failed with {}, reason '{}'", Result.status_code, Result.reason); - Keys.clear(); - } - - return Keys; - }; - - std::string_view TestBucket = "partialcachevaluetests"sv; - std::string_view TestNamespace = "ue4.ddc"sv; - auto RecordsWithSmallAttachments = PutCacheRecords(BaseUri, TestNamespace, TestBucket, 0, 3, 2, 4096u); - CHECK(RecordsWithSmallAttachments.size() == 3); - auto RecordsWithLargeAttachments = PutCacheRecords(BaseUri, TestNamespace, TestBucket, 10, 1, 2, 8u * 1024u * 1024u); - CHECK(RecordsWithLargeAttachments.size() == 1); - - struct PartialOptions - { - uint64_t Offset = 0ull; - uint64_t Size = ~0ull; - RpcAcceptOptions AcceptOptions = RpcAcceptOptions::kNone; - }; - - auto GetCacheChunk = [](std::string_view BaseUri, - std::string_view Namespace, - const CacheKey& Key, - const Oid& ValueId, - const PartialOptions& Options = {}) -> cacherequests::GetCacheChunksResult { - cacherequests::GetCacheChunksRequest Request = { - .AcceptMagic = kCbPkgMagic, - .AcceptOptions = (uint16_t)Options.AcceptOptions, - .Namespace = std::string(Namespace), - .Requests = {{.Key = Key, .ValueId = ValueId, .RawOffset = Options.Offset, .RawSize = Options.Size}}}; - CbPackage Package; - CHECK(Request.Format(Package)); - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - - CHECK(Result.status_code == 200); - - CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - bool Loaded = !Response.IsNull(); - CHECK_MESSAGE(Loaded, "GetCacheChunks response failed to load."); - cacherequests::GetCacheChunksResult GetCacheChunksResult; - CHECK(GetCacheChunksResult.Parse(Response)); - return GetCacheChunksResult; - }; - - auto GetAndVerifyChunk = [&GetCacheChunk](std::string_view BaseUri, - std::string_view Namespace, - const CacheKey& Key, - const Oid& ChunkId, - const CompressedBuffer& VerifyData, - const PartialOptions& Options = {}) { - cacherequests::GetCacheChunksResult Result = GetCacheChunk(BaseUri, Namespace, Key, ChunkId, Options); - CHECK(Result.Results.size() == 1); - bool CanGetPartial = ((uint16_t)Options.AcceptOptions & (uint16_t)RpcAcceptOptions::kAllowPartialCacheChunks); - if (!CanGetPartial) - { - CHECK(Result.Results[0].FragmentOffset == 0); - CHECK(Result.Results[0].Body.GetCompressedSize() == VerifyData.GetCompressedSize()); - } - IoBuffer SourceDecompressed = VerifyData.Decompress(Options.Offset, Options.Size).AsIoBuffer(); - IoBuffer ReceivedDecompressed = - Result.Results[0].Body.Decompress(Options.Offset - Result.Results[0].FragmentOffset, Options.Size).AsIoBuffer(); - CHECK(SourceDecompressed.GetView().EqualBytes(ReceivedDecompressed.GetView())); - }; - - GetAndVerifyChunk(BaseUri, - TestNamespace, - RecordsWithSmallAttachments[0].first, - RecordsWithSmallAttachments[0].second[0].first, - RecordsWithSmallAttachments[0].second[0].second); - GetAndVerifyChunk(BaseUri, - TestNamespace, - RecordsWithSmallAttachments[0].first, - RecordsWithSmallAttachments[0].second[0].first, - RecordsWithSmallAttachments[0].second[0].second, - PartialOptions{.Offset = 378, .Size = 519, .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences}); - GetAndVerifyChunk( - BaseUri, - TestNamespace, - RecordsWithSmallAttachments[0].first, - RecordsWithSmallAttachments[0].second[0].first, - RecordsWithSmallAttachments[0].second[0].second, - PartialOptions{.Offset = 378, - .Size = 519, - .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialCacheChunks}); - GetAndVerifyChunk(BaseUri, - TestNamespace, - RecordsWithLargeAttachments[0].first, - RecordsWithLargeAttachments[0].second[0].first, - RecordsWithLargeAttachments[0].second[0].second, - PartialOptions{.AcceptOptions = RpcAcceptOptions::kAllowLocalReferences}); - GetAndVerifyChunk(BaseUri, - TestNamespace, - RecordsWithLargeAttachments[0].first, - RecordsWithLargeAttachments[0].second[0].first, - RecordsWithLargeAttachments[0].second[0].second, - PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u}); - GetAndVerifyChunk( - BaseUri, - TestNamespace, - RecordsWithLargeAttachments[0].first, - RecordsWithLargeAttachments[0].second[0].first, - RecordsWithLargeAttachments[0].second[0].second, - PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u, .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences}); - GetAndVerifyChunk( - BaseUri, - TestNamespace, - RecordsWithLargeAttachments[0].first, - RecordsWithLargeAttachments[0].second[0].first, - RecordsWithLargeAttachments[0].second[0].second, - PartialOptions{.Offset = 1024u * 1024u, .Size = 512u * 1024u, .AcceptOptions = RpcAcceptOptions::kAllowPartialCacheChunks}); - GetAndVerifyChunk( - BaseUri, - TestNamespace, - RecordsWithLargeAttachments[0].first, - RecordsWithLargeAttachments[0].second[0].first, - RecordsWithLargeAttachments[0].second[0].second, - PartialOptions{.Offset = 1024u * 1024u, - .Size = 512u * 1024u, - .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialCacheChunks}); - GetAndVerifyChunk( - BaseUri, - TestNamespace, - RecordsWithLargeAttachments[0].first, - RecordsWithLargeAttachments[0].second[0].first, - RecordsWithLargeAttachments[0].second[0].second, - PartialOptions{.Offset = 1024u * 1024u, - .Size = 512u * 1024u, - .AcceptOptions = RpcAcceptOptions::kAllowLocalReferences | RpcAcceptOptions::kAllowPartialLocalReferences | - RpcAcceptOptions::kAllowPartialCacheChunks}); -} - -TEST_CASE("zcache.rpc.allpolicies") -{ - using namespace std::literals; - using namespace utils; - - ZenConfig UpstreamCfg = ZenConfig::New(TestEnv.GetNewPortNumber()); - ZenServerInstance UpstreamServer(TestEnv); - SpawnServer(UpstreamServer, UpstreamCfg); - - ZenConfig LocalCfg = ZenConfig::NewWithUpstream(TestEnv.GetNewPortNumber(), UpstreamCfg.Port); - ZenServerInstance LocalServer(TestEnv); - SpawnServer(LocalServer, LocalCfg); - - const auto BaseUri = fmt::format("http://localhost:{}/z$", LocalServer.GetBasePort()); - - std::string_view TestVersion = "F72150A02AE34B57A9EC91D36BA1CE08"sv; - std::string_view TestBucket = "allpoliciestest"sv; - std::string_view TestNamespace = "ue4.ddc"sv; - - // NumKeys = (2 Value vs Record)*(2 SkipData vs Default)*(2 ForceMiss vs Not)*(2 use local) - // *(2 use remote)*(2 UseValue Policy vs not)*(4 cases per type) - constexpr int NumKeys = 256; - constexpr int NumValues = 4; - Oid ValueIds[NumValues]; - IoHash Hash; - for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) - { - ExtendableStringBuilder<16> ValueName; - ValueName << "ValueId_"sv << ValueIndex; - static_assert(sizeof(IoHash) >= sizeof(Oid)); - ValueIds[ValueIndex] = Oid::FromMemory(IoHash::HashBuffer(ValueName.Data(), ValueName.Size() * sizeof(ValueName.Data()[0])).Hash); - } - - struct KeyData; - struct UserData - { - UserData& Set(KeyData* InKeyData, int InValueIndex) - { - Data = InKeyData; - ValueIndex = InValueIndex; - return *this; - } - KeyData* Data = nullptr; - int ValueIndex = 0; - }; - struct KeyData - { - CompressedBuffer BufferValues[NumValues]; - uint64_t IntValues[NumValues]; - UserData ValueUserData[NumValues]; - bool ReceivedChunk[NumValues]; - CacheKey Key; - UserData KeyUserData; - uint32_t KeyIndex = 0; - bool GetRequestsData = true; - bool UseValueAPI = false; - bool UseValuePolicy = false; - bool ForceMiss = false; - bool UseLocal = true; - bool UseRemote = true; - bool ShouldBeHit = true; - bool ReceivedPut = false; - bool ReceivedGet = false; - bool ReceivedPutValue = false; - bool ReceivedGetValue = false; - }; - struct CachePutRequest - { - CacheKey Key; - CbObject Record; - CacheRecordPolicy Policy; - KeyData* Values; - UserData* Data; - }; - struct CachePutValueRequest - { - CacheKey Key; - CompressedBuffer Value; - CachePolicy Policy; - UserData* Data; - }; - struct CacheGetRequest - { - CacheKey Key; - CacheRecordPolicy Policy; - UserData* Data; - }; - struct CacheGetValueRequest - { - CacheKey Key; - CachePolicy Policy; - UserData* Data; - }; - struct CacheGetChunkRequest - { - CacheKey Key; - Oid ValueId; - uint64_t RawOffset; - uint64_t RawSize; - IoHash RawHash; - CachePolicy Policy; - UserData* Data; - }; - - KeyData KeyDatas[NumKeys]; - std::vector<CachePutRequest> PutRequests; - std::vector<CachePutValueRequest> PutValueRequests; - std::vector<CacheGetRequest> GetRequests; - std::vector<CacheGetValueRequest> GetValueRequests; - std::vector<CacheGetChunkRequest> ChunkRequests; - - for (uint32_t KeyIndex = 0; KeyIndex < NumKeys; ++KeyIndex) - { - IoHashStream KeyWriter; - KeyWriter.Append(TestVersion.data(), TestVersion.length() * sizeof(TestVersion.data()[0])); - KeyWriter.Append(&KeyIndex, sizeof(KeyIndex)); - IoHash KeyHash = KeyWriter.GetHash(); - KeyData& KeyData = KeyDatas[KeyIndex]; - - KeyData.Key = CacheKey::Create(TestBucket, KeyHash); - KeyData.KeyIndex = KeyIndex; - KeyData.GetRequestsData = (KeyIndex & (1 << 1)) == 0; - KeyData.UseValueAPI = (KeyIndex & (1 << 2)) != 0; - KeyData.UseValuePolicy = (KeyIndex & (1 << 3)) != 0; - KeyData.ForceMiss = (KeyIndex & (1 << 4)) == 0; - KeyData.UseLocal = (KeyIndex & (1 << 5)) == 0; - KeyData.UseRemote = (KeyIndex & (1 << 6)) == 0; - KeyData.ShouldBeHit = !KeyData.ForceMiss && (KeyData.UseLocal || KeyData.UseRemote); - CachePolicy SharedPolicy = KeyData.UseLocal ? CachePolicy::Local : CachePolicy::None; - SharedPolicy |= KeyData.UseRemote ? CachePolicy::Remote : CachePolicy::None; - CachePolicy PutPolicy = SharedPolicy; - CachePolicy GetPolicy = SharedPolicy; - GetPolicy |= !KeyData.GetRequestsData ? CachePolicy::SkipData : CachePolicy::None; - CacheKey& Key = KeyData.Key; - - for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) - { - KeyData.IntValues[ValueIndex] = static_cast<uint64_t>(KeyIndex) | (static_cast<uint64_t>(ValueIndex) << 32); - KeyData.BufferValues[ValueIndex] = - CompressedBuffer::Compress(SharedBuffer::MakeView(&KeyData.IntValues[ValueIndex], sizeof(KeyData.IntValues[ValueIndex]))); - KeyData.ReceivedChunk[ValueIndex] = false; - } - - UserData& KeyUserData = KeyData.KeyUserData.Set(&KeyData, -1); - for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) - { - KeyData.ValueUserData[ValueIndex].Set(&KeyData, ValueIndex); - } - if (!KeyData.UseValueAPI) - { - CbObjectWriter Builder; - Builder.BeginObject("key"sv); - Builder << "Bucket"sv << Key.Bucket << "Hash"sv << Key.Hash; - Builder.EndObject(); - Builder.BeginArray("Values"sv); - for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) - { - Builder.BeginObject(); - Builder.AddObjectId("Id"sv, ValueIds[ValueIndex]); - Builder.AddBinaryAttachment("RawHash"sv, KeyData.BufferValues[ValueIndex].DecodeRawHash()); - Builder.AddInteger("RawSize"sv, KeyData.BufferValues[ValueIndex].DecodeRawSize()); - Builder.EndObject(); - } - Builder.EndArray(); - - CacheRecordPolicy PutRecordPolicy; - CacheRecordPolicy GetRecordPolicy; - if (!KeyData.UseValuePolicy) - { - PutRecordPolicy = CacheRecordPolicy(PutPolicy); - GetRecordPolicy = CacheRecordPolicy(GetPolicy); - } - else - { - // Switch the SkipData field in the Record policy so that if the CacheStore ignores the ValuePolicies - // it will use the wrong value for SkipData and fail our tests. - CacheRecordPolicyBuilder PutBuilder(PutPolicy ^ CachePolicy::SkipData); - CacheRecordPolicyBuilder GetBuilder(GetPolicy ^ CachePolicy::SkipData); - for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) - { - PutBuilder.AddValuePolicy(ValueIds[ValueIndex], PutPolicy); - GetBuilder.AddValuePolicy(ValueIds[ValueIndex], GetPolicy); - } - PutRecordPolicy = PutBuilder.Build(); - GetRecordPolicy = GetBuilder.Build(); - } - if (!KeyData.ForceMiss) - { - PutRequests.push_back({Key, Builder.Save(), PutRecordPolicy, &KeyData, &KeyUserData}); - } - GetRequests.push_back({Key, GetRecordPolicy, &KeyUserData}); - for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) - { - UserData& ValueUserData = KeyData.ValueUserData[ValueIndex]; - ChunkRequests.push_back({Key, ValueIds[ValueIndex], 0, UINT64_MAX, IoHash(), GetPolicy, &ValueUserData}); - } - } - else - { - if (!KeyData.ForceMiss) - { - PutValueRequests.push_back({Key, KeyData.BufferValues[0], PutPolicy, &KeyUserData}); - } - GetValueRequests.push_back({Key, GetPolicy, &KeyUserData}); - ChunkRequests.push_back({Key, Oid::Zero, 0, UINT64_MAX, IoHash(), GetPolicy, &KeyUserData}); - } - } - - // PutCacheRecords - { - CachePolicy BatchDefaultPolicy = CachePolicy::Default; - cacherequests::PutCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, - .DefaultPolicy = BatchDefaultPolicy, - .Namespace = std::string(TestNamespace)}; - Request.Requests.reserve(PutRequests.size()); - for (CachePutRequest& PutRequest : PutRequests) - { - cacherequests::PutCacheRecordRequest& RecordRequest = Request.Requests.emplace_back(); - RecordRequest.Key = PutRequest.Key; - RecordRequest.Policy = PutRequest.Policy; - RecordRequest.Values.reserve(NumValues); - for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) - { - RecordRequest.Values.push_back({.Id = ValueIds[ValueIndex], .Body = PutRequest.Values->BufferValues[ValueIndex]}); - } - PutRequest.Data->Data->ReceivedPut = true; - } - - CbPackage Package; - CHECK(Request.Format(Package)); - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - CHECK_MESSAGE(Result.status_code == 200, "PutCacheRecords unexpectedly failed."); - } - - // PutCacheValues - { - CachePolicy BatchDefaultPolicy = CachePolicy::Default; - - cacherequests::PutCacheValuesRequest Request = {.AcceptMagic = kCbPkgMagic, - .DefaultPolicy = BatchDefaultPolicy, - .Namespace = std::string(TestNamespace)}; - Request.Requests.reserve(PutValueRequests.size()); - for (CachePutValueRequest& PutRequest : PutValueRequests) - { - Request.Requests.push_back({.Key = PutRequest.Key, .Body = PutRequest.Value, .Policy = PutRequest.Policy}); - PutRequest.Data->Data->ReceivedPutValue = true; - } - - CbPackage Package; - CHECK(Request.Format(Package)); - - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - CHECK_MESSAGE(Result.status_code == 200, "PutCacheValues unexpectedly failed."); - } - - for (KeyData& KeyData : KeyDatas) - { - if (!KeyData.ForceMiss) - { - if (!KeyData.UseValueAPI) - { - CHECK_MESSAGE(KeyData.ReceivedPut, WriteToString<32>("Key ", KeyData.KeyIndex, " was unexpectedly not put.").c_str()); - } - else - { - CHECK_MESSAGE(KeyData.ReceivedPutValue, - WriteToString<32>("Key ", KeyData.KeyIndex, " was unexpectedly not put to ValueAPI.").c_str()); - } - } - } - - // GetCacheRecords - { - CachePolicy BatchDefaultPolicy = CachePolicy::Default; - cacherequests::GetCacheRecordsRequest Request = {.AcceptMagic = kCbPkgMagic, - .DefaultPolicy = BatchDefaultPolicy, - .Namespace = std::string(TestNamespace)}; - Request.Requests.reserve(GetRequests.size()); - for (CacheGetRequest& GetRequest : GetRequests) - { - Request.Requests.push_back({.Key = GetRequest.Key, .Policy = GetRequest.Policy}); - } - - CbPackage Package; - CHECK(Request.Format(Package)); - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - CHECK_MESSAGE(Result.status_code == 200, "GetCacheRecords unexpectedly failed."); - CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - bool Loaded = !Response.IsNull(); - CHECK_MESSAGE(Loaded, "GetCacheRecords response failed to load."); - cacherequests::GetCacheRecordsResult RequestResult; - CHECK(RequestResult.Parse(Response)); - CHECK_MESSAGE(RequestResult.Results.size() == GetRequests.size(), "GetCacheRecords response count did not match request count."); - for (int Index = 0; const std::optional<cacherequests::GetCacheRecordResult>& RecordResult : RequestResult.Results) - { - bool Succeeded = RecordResult.has_value(); - CacheGetRequest& GetRequest = GetRequests[Index++]; - KeyData* KeyData = GetRequest.Data->Data; - KeyData->ReceivedGet = true; - WriteToString<32> Name("Get(", KeyData->KeyIndex, ")"); - if (KeyData->ShouldBeHit) - { - CHECK_MESSAGE(Succeeded, WriteToString<32>(Name, " unexpectedly failed.").c_str()); - } - else if (KeyData->ForceMiss) - { - CHECK_MESSAGE(!Succeeded, WriteToString<32>(Name, " unexpectedly succeeded.").c_str()); - } - if (!KeyData->ForceMiss && Succeeded) - { - CHECK_MESSAGE(RecordResult->Values.size() == NumValues, - WriteToString<32>(Name, " number of values did not match.").c_str()); - for (const cacherequests::GetCacheRecordResultValue& Value : RecordResult->Values) - { - int ExpectedValueIndex = 0; - for (; ExpectedValueIndex < NumValues; ++ExpectedValueIndex) - { - if (ValueIds[ExpectedValueIndex] == Value.Id) - { - break; - } - } - CHECK_MESSAGE(ExpectedValueIndex < NumValues, WriteToString<32>(Name, " could not find matching ValueId.").c_str()); - - WriteToString<32> ValueName("Get(", KeyData->KeyIndex, ",", ExpectedValueIndex, ")"); - - CompressedBuffer ExpectedValue = KeyData->BufferValues[ExpectedValueIndex]; - CHECK_MESSAGE(Value.RawHash == ExpectedValue.DecodeRawHash(), - WriteToString<32>(ValueName, " RawHash did not match.").c_str()); - CHECK_MESSAGE(Value.RawSize == ExpectedValue.DecodeRawSize(), - WriteToString<32>(ValueName, " RawSize did not match.").c_str()); - - if (KeyData->GetRequestsData) - { - SharedBuffer Buffer = Value.Body.Decompress(); - CHECK_MESSAGE(Buffer.GetSize() == Value.RawSize, - WriteToString<32>(ValueName, " BufferSize did not match RawSize.").c_str()); - uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0]; - uint64_t ExpectedIntValue = KeyData->IntValues[ExpectedValueIndex]; - CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(ValueName, " had unexpected data.").c_str()); - } - } - } - } - } - - // GetCacheValues - { - CachePolicy BatchDefaultPolicy = CachePolicy::Default; - - cacherequests::GetCacheValuesRequest GetCacheValuesRequest = {.AcceptMagic = kCbPkgMagic, - .DefaultPolicy = BatchDefaultPolicy, - .Namespace = std::string(TestNamespace)}; - GetCacheValuesRequest.Requests.reserve(GetValueRequests.size()); - for (CacheGetValueRequest& GetRequest : GetValueRequests) - { - GetCacheValuesRequest.Requests.push_back({.Key = GetRequest.Key, .Policy = GetRequest.Policy}); - } - - CbPackage Package; - CHECK(GetCacheValuesRequest.Format(Package)); - - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - CHECK_MESSAGE(Result.status_code == 200, "GetCacheValues unexpectedly failed."); - IoBuffer MessageBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size()); - CbPackage Response = ParsePackageMessage(MessageBuffer); - bool Loaded = !Response.IsNull(); - CHECK_MESSAGE(Loaded, "GetCacheValues response failed to load."); - cacherequests::GetCacheValuesResult GetCacheValuesResult; - CHECK(GetCacheValuesResult.Parse(Response)); - for (int Index = 0; const cacherequests::CacheValueResult& ValueResult : GetCacheValuesResult.Results) - { - bool Succeeded = ValueResult.RawHash != IoHash::Zero; - CacheGetValueRequest& Request = GetValueRequests[Index++]; - KeyData* KeyData = Request.Data->Data; - KeyData->ReceivedGetValue = true; - WriteToString<32> Name("GetValue("sv, KeyData->KeyIndex, ")"sv); - - if (KeyData->ShouldBeHit) - { - CHECK_MESSAGE(Succeeded, WriteToString<32>(Name, " unexpectedly failed.").c_str()); - } - else if (KeyData->ForceMiss) - { - CHECK_MESSAGE(!Succeeded, WriteToString<32>(Name, "unexpectedly succeeded.").c_str()); - } - if (!KeyData->ForceMiss && Succeeded) - { - CompressedBuffer ExpectedValue = KeyData->BufferValues[0]; - CHECK_MESSAGE(ValueResult.RawHash == ExpectedValue.DecodeRawHash(), - WriteToString<32>(Name, " RawHash did not match.").c_str()); - CHECK_MESSAGE(ValueResult.RawSize == ExpectedValue.DecodeRawSize(), - WriteToString<32>(Name, " RawSize did not match.").c_str()); - - if (KeyData->GetRequestsData) - { - SharedBuffer Buffer = ValueResult.Body.Decompress(); - CHECK_MESSAGE(Buffer.GetSize() == ValueResult.RawSize, - WriteToString<32>(Name, " BufferSize did not match RawSize.").c_str()); - uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0]; - uint64_t ExpectedIntValue = KeyData->IntValues[0]; - CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(Name, " had unexpected data.").c_str()); - } - } - } - } - - // GetCacheChunks - { - std::sort(ChunkRequests.begin(), ChunkRequests.end(), [](CacheGetChunkRequest& A, CacheGetChunkRequest& B) { - return A.Key.Hash < B.Key.Hash; - }); - CachePolicy BatchDefaultPolicy = CachePolicy::Default; - cacherequests::GetCacheChunksRequest GetCacheChunksRequest = {.AcceptMagic = kCbPkgMagic, - .DefaultPolicy = BatchDefaultPolicy, - .Namespace = std::string(TestNamespace)}; - GetCacheChunksRequest.Requests.reserve(ChunkRequests.size()); - for (CacheGetChunkRequest& ChunkRequest : ChunkRequests) - { - GetCacheChunksRequest.Requests.push_back({.Key = ChunkRequest.Key, - .ValueId = ChunkRequest.ValueId, - .ChunkId = IoHash(), - .RawOffset = ChunkRequest.RawOffset, - .RawSize = ChunkRequest.RawSize, - .Policy = ChunkRequest.Policy}); - } - CbPackage Package; - CHECK(GetCacheChunksRequest.Format(Package)); - - IoBuffer Body = FormatPackageMessageBuffer(Package).Flatten().AsIoBuffer(); - cpr::Response Result = cpr::Post(cpr::Url{fmt::format("{}/$rpc", BaseUri)}, - cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}, - cpr::Body{(const char*)Body.GetData(), Body.GetSize()}); - CHECK_MESSAGE(Result.status_code == 200, "GetCacheChunks unexpectedly failed."); - CbPackage Response = ParsePackageMessage(zen::IoBuffer(zen::IoBuffer::Wrap, Result.text.data(), Result.text.size())); - bool Loaded = !Response.IsNull(); - CHECK_MESSAGE(Loaded, "GetCacheChunks response failed to load."); - cacherequests::GetCacheChunksResult GetCacheChunksResult; - CHECK(GetCacheChunksResult.Parse(Response)); - CHECK_MESSAGE(GetCacheChunksResult.Results.size() == ChunkRequests.size(), - "GetCacheChunks response count did not match request count."); - - for (int Index = 0; const cacherequests::CacheValueResult& ValueResult : GetCacheChunksResult.Results) - { - bool Succeeded = ValueResult.RawHash != IoHash::Zero; - - CacheGetChunkRequest& Request = ChunkRequests[Index++]; - KeyData* KeyData = Request.Data->Data; - int ValueIndex = Request.Data->ValueIndex >= 0 ? Request.Data->ValueIndex : 0; - KeyData->ReceivedChunk[ValueIndex] = true; - WriteToString<32> Name("GetChunks("sv, KeyData->KeyIndex, ","sv, ValueIndex, ")"sv); - - if (KeyData->ShouldBeHit) - { - CHECK_MESSAGE(Succeeded, WriteToString<256>(Name, " unexpectedly failed."sv).c_str()); - } - else if (KeyData->ForceMiss) - { - CHECK_MESSAGE(!Succeeded, WriteToString<256>(Name, " unexpectedly succeeded."sv).c_str()); - } - if (KeyData->ShouldBeHit && Succeeded) - { - CompressedBuffer ExpectedValue = KeyData->BufferValues[ValueIndex]; - CHECK_MESSAGE(ValueResult.RawHash == ExpectedValue.DecodeRawHash(), - WriteToString<32>(Name, " had unexpected RawHash.").c_str()); - CHECK_MESSAGE(ValueResult.RawSize == ExpectedValue.DecodeRawSize(), - WriteToString<32>(Name, " had unexpected RawSize.").c_str()); - - if (KeyData->GetRequestsData) - { - SharedBuffer Buffer = ValueResult.Body.Decompress(); - CHECK_MESSAGE(Buffer.GetSize() == ValueResult.RawSize, - WriteToString<32>(Name, " BufferSize did not match RawSize.").c_str()); - uint64_t ActualIntValue = ((const uint64_t*)Buffer.GetData())[0]; - uint64_t ExpectedIntValue = KeyData->IntValues[ValueIndex]; - CHECK_MESSAGE(ActualIntValue == ExpectedIntValue, WriteToString<32>(Name, " had unexpected data.").c_str()); - } - } - } - } - - for (KeyData& KeyData : KeyDatas) - { - if (!KeyData.UseValueAPI) - { - CHECK_MESSAGE(KeyData.ReceivedGet, WriteToString<32>("Get(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str()); - for (int ValueIndex = 0; ValueIndex < NumValues; ++ValueIndex) - { - CHECK_MESSAGE( - KeyData.ReceivedChunk[ValueIndex], - WriteToString<32>("GetChunks(", KeyData.KeyIndex, ",", ValueIndex, ") was unexpectedly not received.").c_str()); - } - } - else - { - CHECK_MESSAGE(KeyData.ReceivedGetValue, - WriteToString<32>("GetValue(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str()); - CHECK_MESSAGE(KeyData.ReceivedChunk[0], - WriteToString<32>("GetChunks(", KeyData.KeyIndex, ") was unexpectedly not received.").c_str()); - } - } -} - -class ZenServerTestHelper -{ -public: - ZenServerTestHelper(std::string_view HelperId, int ServerCount) : m_HelperId{HelperId}, m_ServerCount{ServerCount} {} - ~ZenServerTestHelper() {} - - void SpawnServers(std::string_view AdditionalServerArgs = std::string_view()) - { - SpawnServers([](ZenServerInstance&) {}, AdditionalServerArgs); - } - - void SpawnServers(auto&& Callback, std::string_view AdditionalServerArgs) - { - ZEN_INFO("{}: spawning {} server instances", m_HelperId, m_ServerCount); - - m_Instances.resize(m_ServerCount); - - for (int i = 0; i < m_ServerCount; ++i) - { - auto& Instance = m_Instances[i]; - Instance = std::make_unique<ZenServerInstance>(TestEnv); - Instance->SetTestDir(TestEnv.CreateNewTestDir()); - } - - for (int i = 0; i < m_ServerCount; ++i) - { - auto& Instance = m_Instances[i]; - Callback(*Instance); - } - - for (int i = 0; i < m_ServerCount; ++i) - { - auto& Instance = m_Instances[i]; - Instance->SpawnServer(TestEnv.GetNewPortNumber(), AdditionalServerArgs); - } - - for (int i = 0; i < m_ServerCount; ++i) - { - auto& Instance = m_Instances[i]; - uint16_t PortNumber = Instance->WaitUntilReady(); - CHECK_MESSAGE(PortNumber != 0, Instance->GetLogOutput()); - } - } - - ZenServerInstance& GetInstance(int Index) { return *m_Instances[Index]; } - -private: - std::string m_HelperId; - int m_ServerCount = 0; - std::vector<std::unique_ptr<ZenServerInstance>> m_Instances; -}; - TEST_CASE("http.basics") { using namespace std::literals; @@ -3033,20 +258,23 @@ TEST_CASE("http.basics") ZenServerInstance& Instance = Servers.GetInstance(0); const std::string BaseUri = Instance.GetBaseUri(); + HttpClient Http{BaseUri}; + { - cpr::Response r = cpr::Get(cpr::Url{fmt::format("{}/testing/hello", BaseUri)}); - CHECK(IsHttpSuccessCode(r.status_code)); + HttpClient::Response r = Http.Get("/testing/hello"); + CHECK(r); } { - cpr::Response r = cpr::Post(cpr::Url{fmt::format("{}/testing/hello", BaseUri)}); - CHECK_EQ(r.status_code, 404); + HttpClient::Response r = Http.Post("/testing/hello"); + CHECK_EQ(r.StatusCode, HttpResponseCode::NotFound); } { - cpr::Response r = cpr::Post(cpr::Url{fmt::format("{}/testing/echo", BaseUri)}, cpr::Body{"yoyoyoyo"}); - CHECK_EQ(r.status_code, 200); - CHECK_EQ(r.text, "yoyoyoyo"); + IoBuffer Body{IoBuffer::Wrap, "yoyoyoyo", 8}; + HttpClient::Response r = Http.Post("/testing/echo", Body); + CHECK_EQ(r.StatusCode, HttpResponseCode::OK); + CHECK(r.ResponsePayload.GetView().EqualBytes(Body.GetView())); } } @@ -3092,1549 +320,6 @@ TEST_CASE("http.package") CHECK_EQ(ResponsePackage, TestPackage); } -std::string -OidAsString(const Oid& Id) -{ - StringBuilder<25> OidStringBuilder; - Id.ToString(OidStringBuilder); - return OidStringBuilder.ToString(); -} - -CbPackage -CreateOplogPackage(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments) -{ - CbPackage Package; - CbObjectWriter Object; - Object << "key"sv << OidAsString(Id); - if (!Attachments.empty()) - { - Object.BeginArray("bulkdata"); - for (const auto& Attachment : Attachments) - { - CbAttachment Attach(Attachment.second, Attachment.second.DecodeRawHash()); - Object.BeginObject(); - Object << "id"sv << Attachment.first; - Object << "type"sv - << "Standard"sv; - Object << "data"sv << Attach; - Object.EndObject(); - - Package.AddAttachment(Attach); - ZEN_DEBUG("Added attachment {}", Attach.GetHash()); - } - Object.EndArray(); - } - Package.SetObject(Object.Save()); - return Package; -}; - -cpr::Body -AsBody(const IoBuffer& Payload) -{ - return cpr::Body{(const char*)Payload.GetData(), Payload.Size()}; -}; - -enum CbWriterMeta -{ - BeginObject, - EndObject, - BeginArray, - EndArray -}; - -inline CbWriter& -operator<<(CbWriter& Writer, CbWriterMeta Meta) -{ - switch (Meta) - { - case BeginObject: - Writer.BeginObject(); - break; - case EndObject: - Writer.EndObject(); - break; - case BeginArray: - Writer.BeginArray(); - break; - case EndArray: - Writer.EndArray(); - break; - default: - ZEN_ASSERT(false); - } - return Writer; -} - -TEST_CASE("project.remote") -{ - using namespace std::literals; - using namespace utils; - - ZenServerTestHelper Servers("remote", 3); - Servers.SpawnServers("--debug"); - - std::vector<Oid> OpIds; - const size_t OpCount = 24; - OpIds.reserve(OpCount); - for (size_t I = 0; I < OpCount; ++I) - { - OpIds.emplace_back(Oid::NewOid()); - } - - std::unordered_map<Oid, std::vector<std::pair<Oid, CompressedBuffer>>, Oid::Hasher> Attachments; - { - std::vector<std::size_t> AttachmentSizes( - {7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 33466, 1093, 4269, 2257, 3685, 13489, 97194, - 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 224024, 51582, 5251, 491, 2u * 1024u * 1024u + 124u, - 74607, 18135, 3767, 154045, 4415, 5007, 8876, 96761, 3359, 8526, 4097, 4855, 48225}); - auto It = AttachmentSizes.begin(); - Attachments[OpIds[0]] = {}; - Attachments[OpIds[1]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); - Attachments[OpIds[2]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); - Attachments[OpIds[3]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); - Attachments[OpIds[4]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++}); - Attachments[OpIds[5]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); - Attachments[OpIds[6]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); - Attachments[OpIds[7]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); - Attachments[OpIds[8]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{}); - Attachments[OpIds[9]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); - Attachments[OpIds[10]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); - Attachments[OpIds[11]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++}); - Attachments[OpIds[12]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++, *It++}); - Attachments[OpIds[13]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); - Attachments[OpIds[14]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); - Attachments[OpIds[15]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); - Attachments[OpIds[16]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{}); - Attachments[OpIds[17]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); - Attachments[OpIds[18]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++}); - Attachments[OpIds[19]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{}); - Attachments[OpIds[20]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); - Attachments[OpIds[21]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); - Attachments[OpIds[22]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++, *It++, *It++}); - Attachments[OpIds[23]] = CreateSemiRandomAttachments(std::initializer_list<size_t>{*It++}); - ZEN_ASSERT(It == AttachmentSizes.end()); - } - - // Note: This is a clone of the function in projectstore.cpp - auto ComputeOpKey = [](const CbObjectView& Op) -> Oid { - using namespace std::literals; - - XXH3_128Stream_deprecated KeyHasher; - Op["key"sv].WriteToStream([&](const void* Data, size_t Size) { KeyHasher.Append(Data, Size); }); - XXH3_128 KeyHash128 = KeyHasher.GetHash(); - - Oid KeyHash; - memcpy(&KeyHash, KeyHash128.Hash, sizeof KeyHash); - - return KeyHash; - }; - - auto AddOp = [ComputeOpKey](const CbObject& Op, std::unordered_map<Oid, uint32_t, Oid::Hasher>& Ops) { - const Oid Id = ComputeOpKey(Op); - IoBuffer Buffer = Op.GetBuffer().AsIoBuffer(); - const uint32_t OpCoreHash = uint32_t(XXH3_64bits(Buffer.GetData(), Buffer.GetSize()) & 0xffffFFFF); - Ops.insert({Id, OpCoreHash}); - }; - - auto MakeProject = [](cpr::Session& Session, std::string_view UrlBase, std::string_view ProjectName) { - CbObjectWriter Project; - Project.AddString("id"sv, ProjectName); - Project.AddString("root"sv, ""sv); - Project.AddString("engine"sv, ""sv); - Project.AddString("project"sv, ""sv); - Project.AddString("projectfile"sv, ""sv); - IoBuffer ProjectPayload = Project.Save().GetBuffer().AsIoBuffer(); - std::string ProjectRequest = fmt::format("{}/prj/{}", UrlBase, ProjectName); - Session.SetUrl({ProjectRequest}); - Session.SetBody(cpr::Body{(const char*)ProjectPayload.GetData(), ProjectPayload.GetSize()}); - cpr::Response Response = Session.Post(); - CHECK(IsHttpSuccessCode(Response.status_code)); - }; - - auto MakeOplog = [](cpr::Session& Session, std::string_view UrlBase, std::string_view ProjectName, std::string_view OplogName) { - std::string CreateOplogRequest = fmt::format("{}/prj/{}/oplog/{}", UrlBase, ProjectName, OplogName); - Session.SetUrl({CreateOplogRequest}); - Session.SetBody(cpr::Body{}); - cpr::Response Response = Session.Post(); - CHECK(IsHttpSuccessCode(Response.status_code)); - }; - - auto MakeOp = [](cpr::Session& Session, - std::string_view UrlBase, - std::string_view ProjectName, - std::string_view OplogName, - const CbPackage& OpPackage) { - std::string CreateOpRequest = fmt::format("{}/prj/{}/oplog/{}/new", UrlBase, ProjectName, OplogName); - Session.SetUrl({CreateOpRequest}); - zen::BinaryWriter MemOut; - legacy::SaveCbPackage(OpPackage, MemOut); - Session.SetBody(cpr::Body{(const char*)MemOut.Data(), MemOut.Size()}); - cpr::Response Response = Session.Post(); - CHECK(IsHttpSuccessCode(Response.status_code)); - }; - - cpr::Session Session; - MakeProject(Session, Servers.GetInstance(0).GetBaseUri(), "proj0"); - MakeOplog(Session, Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0"); - - std::unordered_map<Oid, uint32_t, Oid::Hasher> SourceOps; - for (const Oid& OpId : OpIds) - { - CbPackage OpPackage = CreateOplogPackage(OpId, Attachments[OpId]); - CHECK(OpPackage.GetAttachments().size() == Attachments[OpId].size()); - AddOp(OpPackage.GetObject(), SourceOps); - MakeOp(Session, Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0", OpPackage); - } - - std::vector<IoHash> AttachmentHashes; - AttachmentHashes.reserve(Attachments.size()); - for (const auto& AttachmentOplog : Attachments) - { - for (const auto& Attachment : AttachmentOplog.second) - { - AttachmentHashes.emplace_back(Attachment.second.DecodeRawHash()); - } - } - - auto MakeCbObjectPayload = [](std::function<void(CbObjectWriter & Writer)> Write) -> IoBuffer { - CbObjectWriter Writer; - Write(Writer); - IoBuffer Result = Writer.Save().GetBuffer().AsIoBuffer(); - Result.MakeOwned(); - return Result; - }; - - auto ValidateAttachments = [&MakeCbObjectPayload, &AttachmentHashes, &Servers, &Session](int ServerIndex, - std::string_view Project, - std::string_view Oplog) { - std::string GetChunksRequest = fmt::format("{}/prj/{}/oplog/{}/rpc", Servers.GetInstance(ServerIndex).GetBaseUri(), Project, Oplog); - Session.SetUrl({GetChunksRequest}); - IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes](CbObjectWriter& Writer) { - Writer << "method"sv - << "getchunks"sv; - Writer << "chunks"sv << BeginArray; - for (const IoHash& Chunk : AttachmentHashes) - { - Writer << Chunk; - } - Writer << EndArray; // chunks - }); - Session.SetBody(AsBody(Payload)); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}, {"Accept", "application/x-ue-cbpkg"}}); - cpr::Response Response = Session.Post(); - CHECK(IsHttpSuccessCode(Response.status_code)); - CbPackage ResponsePackage = ParsePackageMessage(IoBuffer(IoBuffer::Wrap, Response.text.data(), Response.text.size())); - CHECK(ResponsePackage.GetAttachments().size() == AttachmentHashes.size()); - for (auto A : ResponsePackage.GetAttachments()) - { - CHECK(IoHash::HashBuffer(A.AsCompressedBinary().DecompressToComposite()) == A.GetHash()); - } - }; - - auto ValidateOplog = [&SourceOps, &AddOp, &Servers, &Session](int ServerIndex, std::string_view Project, std::string_view Oplog) { - std::unordered_map<Oid, uint32_t, Oid::Hasher> TargetOps; - std::vector<CbObject> ResultingOplog; - - std::string GetOpsRequest = - fmt::format("{}/prj/{}/oplog/{}/entries", Servers.GetInstance(ServerIndex).GetBaseUri(), Project, Oplog); - Session.SetUrl({GetOpsRequest}); - cpr::Response Response = Session.Get(); - CHECK(IsHttpSuccessCode(Response.status_code)); - - IoBuffer Payload(IoBuffer::Wrap, Response.text.data(), Response.text.size()); - CbObject OplogResonse = LoadCompactBinaryObject(Payload); - CbArrayView EntriesArray = OplogResonse["entries"sv].AsArrayView(); - - for (CbFieldView OpEntry : EntriesArray) - { - CbObjectView Core = OpEntry.AsObjectView(); - BinaryWriter Writer; - Core.CopyTo(Writer); - MemoryView OpView = Writer.GetView(); - IoBuffer OpBuffer(IoBuffer::Wrap, OpView.GetData(), OpView.GetSize()); - CbObject Op(SharedBuffer(OpBuffer), CbFieldType::HasFieldType); - AddOp(Op, TargetOps); - } - CHECK(SourceOps == TargetOps); - }; - - auto WaitForCompletion = [&Session](ZenServerInstance& Server, const cpr::Response& Response) { - CHECK(IsHttpSuccessCode(Response.status_code)); - uint64_t JobId = ParseInt<uint64_t>(Response.text).value_or(0); - CHECK(JobId != 0); - Session.SetUrl(fmt::format("{}/admin/jobs/{}", Server.GetBaseUri(), JobId)); - Session.SetHeader(cpr::Header{{"Accept", std::string(ToString(ZenContentType::kCbObject))}}); - while (true) - { - cpr::Response StatusResponse = Session.Get(); - CHECK(IsHttpSuccessCode(StatusResponse.status_code)); - CbObject ResponseObject = - LoadCompactBinaryObject(IoBuffer(IoBuffer::Wrap, StatusResponse.text.data(), StatusResponse.text.size())); - std::string_view Status = ResponseObject["Status"sv].AsString(); - CHECK(Status != "Aborted"sv); - if (Status == "Complete"sv) - { - return; - } - Sleep(10); - } - }; - - SUBCASE("File") - { - ScopedTemporaryDirectory TempDir; - { - std::string SaveOplogRequest = fmt::format("{}/prj/{}/oplog/{}/rpc", Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0"); - Session.SetUrl({SaveOplogRequest}); - - IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) { - Writer << "method"sv - << "export"sv; - Writer << "params" << BeginObject; - { - Writer << "maxblocksize"sv << 3072u; - Writer << "maxchunkembedsize"sv << 1296u; - Writer << "chunkfilesizelimit"sv << 5u * 1024u; - Writer << "force"sv << false; - Writer << "file"sv << BeginObject; - { - Writer << "path"sv << path; - Writer << "name"sv - << "proj0_oplog0"sv; - } - Writer << EndObject; // "file" - } - Writer << EndObject; // "params" - }); - Session.SetBody(AsBody(Payload)); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - cpr::Response Response = Session.Post(); - WaitForCompletion(Servers.GetInstance(0), Response); - } - { - MakeProject(Session, Servers.GetInstance(1).GetBaseUri(), "proj0_copy"); - MakeOplog(Session, Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy"); - std::string LoadOplogRequest = - fmt::format("{}/prj/{}/oplog/{}/rpc", Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy"); - Session.SetUrl({LoadOplogRequest}); - - IoBuffer Payload = MakeCbObjectPayload([&AttachmentHashes, path = TempDir.Path().string()](CbObjectWriter& Writer) { - Writer << "method"sv - << "import"sv; - Writer << "params" << BeginObject; - { - Writer << "force"sv << false; - Writer << "file"sv << BeginObject; - { - Writer << "path"sv << path; - Writer << "name"sv - << "proj0_oplog0"sv; - } - Writer << EndObject; // "file" - } - Writer << EndObject; // "params" - }); - Session.SetBody(AsBody(Payload)); - - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - cpr::Response Response = Session.Post(); - WaitForCompletion(Servers.GetInstance(1), Response); - } - ValidateAttachments(1, "proj0_copy", "oplog0_copy"); - ValidateOplog(1, "proj0_copy", "oplog0_copy"); - } - - SUBCASE("File disable blocks") - { - ScopedTemporaryDirectory TempDir; - { - std::string SaveOplogRequest = fmt::format("{}/prj/{}/oplog/{}/rpc", Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0"); - Session.SetUrl({SaveOplogRequest}); - - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { - Writer << "method"sv - << "export"sv; - Writer << "params" << BeginObject; - { - Writer << "maxblocksize"sv << 3072u; - Writer << "maxchunkembedsize"sv << 1296u; - Writer << "chunkfilesizelimit"sv << 5u * 1024u; - Writer << "force"sv << false; - Writer << "file"sv << BeginObject; - { - Writer << "path"sv << TempDir.Path().string(); - Writer << "name"sv - << "proj0_oplog0"sv; - Writer << "disableblocks"sv << true; - } - Writer << EndObject; // "file" - } - Writer << EndObject; // "params" - }); - Session.SetBody(AsBody(Payload)); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - cpr::Response Response = Session.Post(); - WaitForCompletion(Servers.GetInstance(0), Response); - } - { - MakeProject(Session, Servers.GetInstance(1).GetBaseUri(), "proj0_copy"); - MakeOplog(Session, Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy"); - std::string LoadOplogRequest = - fmt::format("{}/prj/{}/oplog/{}/rpc", Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy"); - Session.SetUrl({LoadOplogRequest}); - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { - Writer << "method"sv - << "import"sv; - Writer << "params" << BeginObject; - { - Writer << "force"sv << false; - Writer << "file"sv << BeginObject; - { - Writer << "path"sv << TempDir.Path().string(); - Writer << "name"sv - << "proj0_oplog0"sv; - } - Writer << EndObject; // "file" - } - Writer << EndObject; // "params" - }); - Session.SetBody(AsBody(Payload)); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - cpr::Response Response = Session.Post(); - WaitForCompletion(Servers.GetInstance(1), Response); - } - ValidateAttachments(1, "proj0_copy", "oplog0_copy"); - ValidateOplog(1, "proj0_copy", "oplog0_copy"); - } - - SUBCASE("File force temp blocks") - { - ScopedTemporaryDirectory TempDir; - { - std::string SaveOplogRequest = fmt::format("{}/prj/{}/oplog/{}/rpc", Servers.GetInstance(0).GetBaseUri(), "proj0", "oplog0"); - Session.SetUrl({SaveOplogRequest}); - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { - Writer << "method"sv - << "export"sv; - Writer << "params" << BeginObject; - { - Writer << "maxblocksize"sv << 3072u; - Writer << "maxchunkembedsize"sv << 1296u; - Writer << "chunkfilesizelimit"sv << 5u * 1024u; - Writer << "force"sv << false; - Writer << "file"sv << BeginObject; - { - Writer << "path"sv << TempDir.Path().string(); - Writer << "name"sv - << "proj0_oplog0"sv; - Writer << "enabletempblocks"sv << true; - } - Writer << EndObject; // "file" - } - Writer << EndObject; // "params" - }); - Session.SetBody(AsBody(Payload)); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - cpr::Response Response = Session.Post(); - WaitForCompletion(Servers.GetInstance(0), Response); - } - { - MakeProject(Session, Servers.GetInstance(1).GetBaseUri(), "proj0_copy"); - MakeOplog(Session, Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy"); - std::string LoadOplogRequest = - fmt::format("{}/prj/{}/oplog/{}/rpc", Servers.GetInstance(1).GetBaseUri(), "proj0_copy", "oplog0_copy"); - Session.SetUrl({LoadOplogRequest}); - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { - Writer << "method"sv - << "import"sv; - Writer << "params" << BeginObject; - { - Writer << "force"sv << false; - Writer << "file"sv << BeginObject; - { - Writer << "path"sv << TempDir.Path().string(); - Writer << "name"sv - << "proj0_oplog0"sv; - } - Writer << EndObject; // "file" - } - Writer << EndObject; // "params" - }); - Session.SetBody(AsBody(Payload)); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - cpr::Response Response = Session.Post(); - WaitForCompletion(Servers.GetInstance(1), Response); - } - ValidateAttachments(1, "proj0_copy", "oplog0_copy"); - ValidateOplog(1, "proj0_copy", "oplog0_copy"); - } - - SUBCASE("Zen") - { - ScopedTemporaryDirectory TempDir; - { - std::string ExportSourceUri = Servers.GetInstance(0).GetBaseUri(); - std::string ExportTargetUri = Servers.GetInstance(1).GetBaseUri(); - MakeProject(Session, ExportTargetUri, "proj0_copy"); - MakeOplog(Session, ExportTargetUri, "proj0_copy", "oplog0_copy"); - - std::string SaveOplogRequest = fmt::format("{}/prj/{}/oplog/{}/rpc", ExportSourceUri, "proj0", "oplog0"); - Session.SetUrl({SaveOplogRequest}); - - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { - Writer << "method"sv - << "export"sv; - Writer << "params" << BeginObject; - { - Writer << "maxblocksize"sv << 3072u; - Writer << "maxchunkembedsize"sv << 1296u; - Writer << "chunkfilesizelimit"sv << 5u * 1024u; - Writer << "force"sv << false; - Writer << "zen"sv << BeginObject; - { - Writer << "url"sv << ExportTargetUri.substr(7); - Writer << "project" - << "proj0_copy"; - Writer << "oplog" - << "oplog0_copy"; - } - Writer << EndObject; // "file" - } - Writer << EndObject; // "params" - }); - Session.SetBody(AsBody(Payload)); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - cpr::Response Response = Session.Post(); - WaitForCompletion(Servers.GetInstance(0), Response); - } - ValidateAttachments(1, "proj0_copy", "oplog0_copy"); - ValidateOplog(1, "proj0_copy", "oplog0_copy"); - - { - std::string ImportSourceUri = Servers.GetInstance(1).GetBaseUri(); - std::string ImportTargetUri = Servers.GetInstance(2).GetBaseUri(); - MakeProject(Session, ImportTargetUri, "proj1"); - MakeOplog(Session, ImportTargetUri, "proj1", "oplog1"); - std::string LoadOplogRequest = fmt::format("{}/prj/{}/oplog/{}/rpc", ImportTargetUri, "proj1", "oplog1"); - Session.SetUrl({LoadOplogRequest}); - - IoBuffer Payload = MakeCbObjectPayload([&](CbObjectWriter& Writer) { - Writer << "method"sv - << "import"sv; - Writer << "params" << BeginObject; - { - Writer << "force"sv << false; - Writer << "zen"sv << BeginObject; - { - Writer << "url"sv << ImportSourceUri.substr(7); - Writer << "project" - << "proj0_copy"; - Writer << "oplog" - << "oplog0_copy"; - } - Writer << EndObject; // "file" - } - Writer << EndObject; // "params" - }); - Session.SetBody(AsBody(Payload)); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}}); - cpr::Response Response = Session.Post(); - WaitForCompletion(Servers.GetInstance(2), Response); - } - ValidateAttachments(2, "proj1", "oplog1"); - ValidateOplog(2, "proj1", "oplog1"); - } -} - -std::vector<std::pair<std::filesystem::path, IoBuffer>> -GenerateFolderContent(const std::filesystem::path& RootPath) -{ - CreateDirectories(RootPath); - std::vector<std::pair<std::filesystem::path, IoBuffer>> Result; - Result.push_back(std::make_pair(RootPath / "root_blob_1.bin", CreateRandomBlob(4122))); - Result.push_back(std::make_pair(RootPath / "root_blob_2.bin", CreateRandomBlob(2122))); - - std::filesystem::path EmptyFolder(RootPath / "empty_folder"); - - std::filesystem::path FirstFolder(RootPath / "first_folder"); - CreateDirectories(FirstFolder); - Result.push_back(std::make_pair(FirstFolder / "first_folder_blob1.bin", CreateRandomBlob(22))); - Result.push_back(std::make_pair(FirstFolder / "first_folder_blob2.bin", CreateRandomBlob(122))); - - std::filesystem::path SecondFolder(RootPath / "second_folder"); - CreateDirectories(SecondFolder); - Result.push_back(std::make_pair(SecondFolder / "second_folder_blob1.bin", CreateRandomBlob(522))); - Result.push_back(std::make_pair(SecondFolder / "second_folder_blob2.bin", CreateRandomBlob(122))); - Result.push_back(std::make_pair(SecondFolder / "second_folder_blob3.bin", CreateRandomBlob(225))); - - std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second"); - CreateDirectories(SecondFolderChild); - Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob1.bin", CreateRandomBlob(622))); - - for (const auto& It : Result) - { - WriteFile(It.first, It.second); - } - - return Result; -} - -std::vector<std::pair<std::filesystem::path, IoBuffer>> -GenerateFolderContent2(const std::filesystem::path& RootPath) -{ - std::vector<std::pair<std::filesystem::path, IoBuffer>> Result; - Result.push_back(std::make_pair(RootPath / "root_blob_3.bin", CreateRandomBlob(312))); - std::filesystem::path FirstFolder(RootPath / "first_folder"); - Result.push_back(std::make_pair(FirstFolder / "first_folder_blob3.bin", CreateRandomBlob(722))); - std::filesystem::path SecondFolder(RootPath / "second_folder"); - std::filesystem::path SecondFolderChild(SecondFolder / "child_in_second"); - Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob2.bin", CreateRandomBlob(962))); - Result.push_back(std::make_pair(SecondFolderChild / "second_child_folder_blob3.bin", CreateRandomBlob(561))); - - for (const auto& It : Result) - { - WriteFile(It.first, It.second); - } - - return Result; -} - -TEST_CASE("workspaces.create") -{ - using namespace std::literals; - - std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); - - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - ZenServerInstance Instance(TestEnv); - Instance.SetTestDir(TestDir); - const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( - fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - ScopedTemporaryDirectory TempDir; - std::filesystem::path Root1Path = TempDir.Path() / "root1"; - std::filesystem::path Root2Path = TempDir.Path() / "root2"; - DeleteDirectories(Root1Path); - DeleteDirectories(Root2Path); - - std::filesystem::path Share1Path = "shared_1"; - std::filesystem::path Share2Path = "shared_2"; - CreateDirectories(Root1Path / Share1Path); - CreateDirectories(Root1Path / Share2Path); - CreateDirectories(Root2Path / Share1Path); - CreateDirectories(Root2Path / Share2Path); - - Oid Root1Id = Oid::Zero; - Oid Root2Id = Oid::NewOid(); - - HttpClient Client(Instance.GetBaseUri()); - - CHECK(Client.Put(fmt::format("/ws/{}", Root1Id)).StatusCode == HttpResponseCode::BadRequest); - - if (HttpClient::Response Root1Response = - Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}); - Root1Response.StatusCode == HttpResponseCode::Created) - { - Root1Id = Oid::TryFromHexString(Root1Response.AsText()); - CHECK(Root1Id != Oid::Zero); - } - else - { - CHECK(false); - } - if (HttpClient::Response Root1Response = - Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}); - Root1Response.StatusCode == HttpResponseCode::OK) - { - CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText())); - } - else - { - CHECK(false); - } - if (HttpClient::Response Root1Response = - Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}); - Root1Response.StatusCode == HttpResponseCode::OK) - { - CHECK(Root1Id == Oid::TryFromHexString(Root1Response.AsText())); - } - else - { - CHECK(false); - } - CHECK(Client.Put(fmt::format("/ws/{}", Root1Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}}).StatusCode == - HttpResponseCode::Conflict); - - CHECK( - Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == - HttpResponseCode::Created); - - CHECK( - Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == - HttpResponseCode::NotFound); - - CHECK(Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode == - HttpResponseCode::Conflict); - - if (HttpClient::Response Root2Response = - Client.Put(fmt::format("/ws/{}", Root2Id), HttpClient::KeyValueMap{{"root_path", Root2Path.string()}}); - Root2Response.StatusCode == HttpResponseCode::Created) - { - CHECK(Root2Id == Oid::TryFromHexString(Root2Response.AsText())); - } - else - { - CHECK(false); - } - - CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero)).StatusCode == HttpResponseCode::BadRequest); - - Oid Share2Id = Oid::Zero; - if (HttpClient::Response Share2Response = - Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}); - Share2Response.StatusCode == HttpResponseCode::Created) - { - Share2Id = Oid::TryFromHexString(Share2Response.AsText()); - CHECK(Share2Id != Oid::Zero); - } - else - { - CHECK(false); - } - - CHECK( - Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == - HttpResponseCode::OK); - - CHECK( - Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == - HttpResponseCode::OK); - - CHECK( - Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode == - HttpResponseCode::Conflict); - - CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::NewOid()), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}) - .StatusCode == HttpResponseCode::Conflict); - - CHECK(Client.Put(fmt::format("/ws/{}/{}", Root2Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", "idonotexist"}}).StatusCode != - HttpResponseCode::OK); - - while (true) - { - std::error_code Ec; - DeleteDirectories(Root2Path / Share2Path, Ec); - if (!Ec) - break; - } - - CHECK(Client.Get(fmt::format("/ws/{}/{}/files", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound); -} - -TEST_CASE("workspaces.restricted") -{ - using namespace std::literals; - - std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); - - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - ZenServerInstance Instance(TestEnv); - Instance.SetTestDir(TestDir); - const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - ScopedTemporaryDirectory TempDir; - std::filesystem::path Root1Path = TempDir.Path() / "root1"; - std::filesystem::path Root2Path = TempDir.Path() / "root2"; - DeleteDirectories(Root1Path); - DeleteDirectories(Root2Path); - - std::filesystem::path Share1Path = "shared_1"; - std::filesystem::path Share2Path = "shared_2"; - CreateDirectories(Root1Path / Share1Path); - CreateDirectories(Root1Path / Share2Path); - CreateDirectories(Root2Path / Share1Path); - CreateDirectories(Root2Path / Share2Path); - - Oid Root1Id = Oid::NewOid(); - Oid Root2Id = Oid::NewOid(); - Oid Share1Id = Oid::NewOid(); - Oid Share2Id = Oid::NewOid(); - - HttpClient Client(Instance.GetBaseUri()); - CHECK(Client.Put(fmt::format("/ws/{}", Oid::Zero), HttpClient::KeyValueMap{{"root_path", Root1Path.string()}}).StatusCode == - HttpResponseCode::Unauthorized); - - CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::NotFound); - - std::string Config1; - { - CbObjectWriter Config; - Config.BeginArray("workspaces"); - Config.BeginObject(); - Config << "id"sv << Root1Id.ToString(); - Config << "root_path"sv << Root1Path.string(); - Config << "allow_share_creation_from_http"sv << false; - Config.EndObject(); - Config.EndArray(); - ExtendableStringBuilder<256> SB; - CompactBinaryToJson(Config.Save(), SB); - Config1 = SB.ToString(); - } - WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config1.data(), Config1.size())); - - CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode)); - - CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root1Id)).StatusCode, HttpResponseCode::OK); - - CHECK(Client.Get(fmt::format("/ws/{}/{}", Root1Id, Share1Id)).StatusCode == HttpResponseCode::NotFound); - CHECK( - Client.Put(fmt::format("/ws/{}/{}", Root1Id, Oid::Zero), HttpClient::KeyValueMap{{"share_path", Share1Path.string()}}).StatusCode == - HttpResponseCode::Unauthorized); - - std::string Config2; - { - CbObjectWriter Config; - Config.BeginArray("workspaces"); - Config.BeginObject(); - Config << "id"sv << Root1Id.ToString(); - Config << "root_path"sv << Root1Path.string(); - Config << "allow_share_creation_from_http"sv << false; - Config.EndObject(); - Config.BeginObject(); - Config << "id"sv << Root2Id.ToString(); - Config << "root_path"sv << Root2Path.string(); - Config << "allow_share_creation_from_http"sv << true; - Config.EndObject(); - Config.EndArray(); - ExtendableStringBuilder<256> SB; - CompactBinaryToJson(Config.Save(), SB); - Config2 = SB.ToString(); - } - WriteFile(SystemRootPath / "workspaces" / "config.json", IoBuffer(IoBuffer::Wrap, Config2.data(), Config2.size())); - - CHECK(IsHttpSuccessCode(Client.Get("/ws/refresh").StatusCode)); - - CHECK_EQ(Client.Get(fmt::format("/ws/{}", Root2Id)).StatusCode, HttpResponseCode::OK); - - CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::NotFound); - CHECK( - Client.Put(fmt::format("/ws/{}/{}", Root2Id, Share2Id), HttpClient::KeyValueMap{{"share_path", Share2Path.string()}}).StatusCode == - HttpResponseCode::Created); - CHECK(Client.Get(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode == HttpResponseCode::OK); - - CHECK(IsHttpSuccessCode(Client.Delete(fmt::format("/ws/{}/{}", Root2Id, Share2Id)).StatusCode)); -} - -TEST_CASE("workspaces.lifetimes") -{ - using namespace std::literals; - - std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); - - Oid WorkspaceId = Oid::NewOid(); - Oid ShareId = Oid::NewOid(); - - ScopedTemporaryDirectory TempDir; - std::filesystem::path RootPath = TempDir.Path(); - DeleteDirectories(RootPath); - std::filesystem::path SharePath = RootPath / "shared_folder"; - CreateDirectories(SharePath); - - { - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - ZenServerInstance Instance(TestEnv); - Instance.SetTestDir(TestDir); - const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( - fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri()); - CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode == - HttpResponseCode::Created); - CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId); - CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode == - HttpResponseCode::OK); - - CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}}) - .StatusCode == HttpResponseCode::Created); - CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId); - CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}}) - .StatusCode == HttpResponseCode::OK); - } - - // Restart - - { - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - ZenServerInstance Instance(TestEnv); - Instance.SetTestDir(TestDir); - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri()); - CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId); - - CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId); - } - - // Wipe system config - DeleteDirectories(SystemRootPath); - - // Restart - - { - std::filesystem::path TestDir = TestEnv.CreateNewTestDir(); - ZenServerInstance Instance(TestEnv); - Instance.SetTestDir(TestDir); - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--workspaces-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri()); - CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound); - CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound); - } -} - -TEST_CASE("workspaces.share") -{ - std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); - - ZenServerInstance Instance(TestEnv); - - const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( - fmt::format("--workspaces-enabled --workspaces-allow-changes --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - ScopedTemporaryDirectory TempDir; - std::filesystem::path RootPath = TempDir.Path(); - DeleteDirectories(RootPath); - std::filesystem::path SharePath = RootPath / "shared_folder"; - GenerateFolderContent(SharePath); - - HttpClient Client(Instance.GetBaseUri()); - - Oid WorkspaceId = Oid::NewOid(); - CHECK(Client.Put(fmt::format("/ws/{}", WorkspaceId), HttpClient::KeyValueMap{{"root_path", RootPath.string()}}).StatusCode == - HttpResponseCode::Created); - CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).AsObject()["id"sv].AsObjectId() == WorkspaceId); - - Oid ShareId = Oid::NewOid(); - CHECK(Client.Put(fmt::format("/ws/{}/{}", WorkspaceId, ShareId), HttpClient::KeyValueMap{{"share_path", "shared_folder"}}).StatusCode == - HttpResponseCode::Created); - CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).AsObject()["id"sv].AsObjectId() == ShareId); - - CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8); - GenerateFolderContent2(SharePath); - CHECK(Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId)).AsObject()["files"sv].AsArrayView().Num() == 8); - HttpClient::Response FilesResponse = - Client.Get(fmt::format("/ws/{}/{}/files", WorkspaceId, ShareId), - {}, - HttpClient::KeyValueMap{{"refresh", ToString(true)}, {"fieldnames", "id,clientpath,size"}}); - CHECK(FilesResponse); - std::unordered_map<Oid, std::pair<std::filesystem::path, uint64_t>, Oid::Hasher> Files; - { - CbArrayView FilesArray = FilesResponse.AsObject()["files"sv].AsArrayView(); - CHECK(FilesArray.Num() == 12); - for (CbFieldView Field : FilesArray) - { - CbObjectView FileObject = Field.AsObjectView(); - Oid ChunkId = FileObject["id"sv].AsObjectId(); - CHECK(ChunkId != Oid::Zero); - uint64_t Size = FileObject["size"sv].AsUInt64(); - std::u8string_view Path = FileObject["clientpath"sv].AsU8String(); - std::filesystem::path AbsFilePath = SharePath / Path; - CHECK(IsFile(AbsFilePath)); - CHECK(FileSizeFromPath(AbsFilePath) == Size); - Files.insert_or_assign(ChunkId, std::make_pair(AbsFilePath, Size)); - } - } - - HttpClient::Response EntriesResponse = - Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId), {}, HttpClient::KeyValueMap{{"fieldfilter", "id,clientpath"}}); - CHECK(EntriesResponse); - { - CbArrayView EntriesArray = EntriesResponse.AsObject()["entries"sv].AsArrayView(); - CHECK(EntriesArray.Num() == 1); - for (CbFieldView EntryField : EntriesArray) - { - CbObjectView EntryObject = EntryField.AsObjectView(); - CbArrayView FilesArray = EntryObject["files"sv].AsArrayView(); - CHECK(FilesArray.Num() == 12); - for (CbFieldView FileField : FilesArray) - { - CbObjectView FileObject = FileField.AsObjectView(); - Oid ChunkId = FileObject["id"sv].AsObjectId(); - CHECK(ChunkId != Oid::Zero); - std::u8string_view Path = FileObject["clientpath"sv].AsU8String(); - std::filesystem::path AbsFilePath = SharePath / Path; - CHECK(IsFile(AbsFilePath)); - } - } - } - - HttpClient::Response FileManifestResponse = - Client.Get(fmt::format("/ws/{}/{}/entries", WorkspaceId, ShareId), - {}, - HttpClient::KeyValueMap{{"opkey", "file_manifest"}, {"fieldfilter", "id,clientpath"}}); - CHECK(FileManifestResponse); - { - CbArrayView EntriesArray = FileManifestResponse.AsObject()["entry"sv].AsObjectView()["files"sv].AsArrayView(); - CHECK(EntriesArray.Num() == 12); - for (CbFieldView Field : EntriesArray) - { - CbObjectView FileObject = Field.AsObjectView(); - Oid ChunkId = FileObject["id"sv].AsObjectId(); - CHECK(ChunkId != Oid::Zero); - std::u8string_view Path = FileObject["clientpath"sv].AsU8String(); - std::filesystem::path AbsFilePath = SharePath / Path; - CHECK(IsFile(AbsFilePath)); - } - } - - for (auto It : Files) - { - const Oid& ChunkId = It.first; - const std::filesystem::path& Path = It.second.first; - const uint64_t Size = It.second.second; - - CHECK(Client.Get(fmt::format("/ws/{}/{}/{}/info", WorkspaceId, ShareId, ChunkId)).AsObject()["size"sv].AsUInt64() == Size); - - { - IoBuffer Payload = Client.Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId)).ResponsePayload; - CHECK(Payload); - CHECK(Payload.GetSize() == Size); - IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path); - CHECK(FileContent); - CHECK(FileContent.GetView().EqualBytes(Payload.GetView())); - } - - { - IoBuffer Payload = - Client - .Get(fmt::format("/ws/{}/{}/{}", WorkspaceId, ShareId, ChunkId), - {}, - HttpClient::KeyValueMap{{"offset", fmt::format("{}", Size / 4)}, {"size", fmt::format("{}", Size / 2)}}) - .ResponsePayload; - CHECK(Payload); - CHECK(Payload.GetSize() == Size / 2); - IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Size / 4, Size / 2); - CHECK(FileContent); - CHECK(FileContent.GetView().EqualBytes(Payload.GetView())); - } - } - - { - uint32_t CorrelationId = gsl::narrow<uint32_t>(Files.size()); - std::vector<RequestChunkEntry> BatchEntries; - for (auto It : Files) - { - const Oid& ChunkId = It.first; - const uint64_t Size = It.second.second; - - BatchEntries.push_back( - RequestChunkEntry{.ChunkId = ChunkId, .CorrelationId = --CorrelationId, .Offset = Size / 4, .RequestBytes = Size / 2}); - } - IoBuffer BatchResponse = - Client.Post(fmt::format("/ws/{}/{}/batch", WorkspaceId, ShareId), BuildChunkBatchRequest(BatchEntries)).ResponsePayload; - CHECK(BatchResponse); - std::vector<IoBuffer> BatchResult = ParseChunkBatchResponse(BatchResponse); - CHECK(BatchResult.size() == Files.size()); - for (const RequestChunkEntry& Request : BatchEntries) - { - IoBuffer Result = BatchResult[Request.CorrelationId]; - auto It = Files.find(Request.ChunkId); - const std::filesystem::path& Path = It->second.first; - CHECK(Result.GetSize() == Request.RequestBytes); - IoBuffer FileContent = IoBufferBuilder::MakeFromFile(Path, Request.Offset, Request.RequestBytes); - CHECK(FileContent); - CHECK(FileContent.GetView().EqualBytes(Result.GetView())); - } - } - - CHECK(Client.Delete(fmt::format("/ws/{}/{}", WorkspaceId, ShareId))); - CHECK(Client.Get(fmt::format("/ws/{}/{}", WorkspaceId, ShareId)).StatusCode == HttpResponseCode::NotFound); - CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId))); - - CHECK(Client.Delete(fmt::format("/ws/{}", WorkspaceId))); - CHECK(Client.Get(fmt::format("/ws/{}", WorkspaceId)).StatusCode == HttpResponseCode::NotFound); -} - -TEST_CASE("buildstore.blobs") -{ - std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); - auto _ = MakeGuard([&SystemRootPath]() { DeleteDirectories(SystemRootPath); }); - - std::string_view Namespace = "ns"sv; - std::string_view Bucket = "bkt"sv; - Oid BuildId = Oid::NewOid(); - - std::vector<IoHash> CompressedBlobsHashes; - { - ZenServerInstance Instance(TestEnv); - - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri() + "/builds/"); - - for (size_t I = 0; I < 5; I++) - { - IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7); - CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob))); - CompressedBlobsHashes.push_back(CompressedBlob.DecodeRawHash()); - IoBuffer Payload = std::move(CompressedBlob).GetCompressed().Flatten().AsIoBuffer(); - Payload.SetContentType(ZenContentType::kCompressedBinary); - - HttpClient::Response Result = - Client.Put(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, CompressedBlobsHashes.back()), Payload); - CHECK(Result); - } - - for (const IoHash& RawHash : CompressedBlobsHashes) - { - HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash), - HttpClient::Accept(ZenContentType::kCompressedBinary)); - CHECK(Result); - IoBuffer Payload = Result.ResponsePayload; - CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary); - IoHash VerifyRawHash; - uint64_t VerifyRawSize; - CompressedBuffer CompressedBlob = - CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize); - CHECK(CompressedBlob); - CHECK(VerifyRawHash == RawHash); - IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer(); - CHECK(IoHash::HashBuffer(Decompressed) == RawHash); - } - } - { - ZenServerInstance Instance(TestEnv); - - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri() + "/builds/"); - - for (const IoHash& RawHash : CompressedBlobsHashes) - { - HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash), - HttpClient::Accept(ZenContentType::kCompressedBinary)); - CHECK(Result); - IoBuffer Payload = Result.ResponsePayload; - CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary); - IoHash VerifyRawHash; - uint64_t VerifyRawSize; - CompressedBuffer CompressedBlob = - CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize); - CHECK(CompressedBlob); - CHECK(VerifyRawHash == RawHash); - IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer(); - CHECK(IoHash::HashBuffer(Decompressed) == RawHash); - } - - for (size_t I = 0; I < 5; I++) - { - IoBuffer Blob = CreateSemiRandomBlob(5713 + I * 7); - CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob))); - CompressedBlobsHashes.push_back(CompressedBlob.DecodeRawHash()); - IoBuffer Payload = std::move(CompressedBlob).GetCompressed().Flatten().AsIoBuffer(); - Payload.SetContentType(ZenContentType::kCompressedBinary); - - HttpClient::Response Result = - Client.Put(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, CompressedBlobsHashes.back()), Payload); - CHECK(Result); - } - } - { - ZenServerInstance Instance(TestEnv); - - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri() + "/builds/"); - - for (const IoHash& RawHash : CompressedBlobsHashes) - { - HttpClient::Response Result = Client.Get(fmt::format("{}/{}/{}/blobs/{}", Namespace, Bucket, BuildId, RawHash), - HttpClient::Accept(ZenContentType::kCompressedBinary)); - CHECK(Result); - IoBuffer Payload = Result.ResponsePayload; - CHECK(Payload.GetContentType() == ZenContentType::kCompressedBinary); - IoHash VerifyRawHash; - uint64_t VerifyRawSize; - CompressedBuffer CompressedBlob = - CompressedBuffer::FromCompressed(SharedBuffer(std::move(Payload)), VerifyRawHash, VerifyRawSize); - CHECK(CompressedBlob); - CHECK(VerifyRawHash == RawHash); - IoBuffer Decompressed = CompressedBlob.Decompress().AsIoBuffer(); - CHECK(IoHash::HashBuffer(Decompressed) == RawHash); - } - } -} - -namespace { - CbObject MakeMetadata(const IoHash& BlobHash, const std::vector<std::pair<std::string, std::string>>& KeyValues) - { - CbObjectWriter Writer; - Writer.AddHash("rawHash"sv, BlobHash); - Writer.BeginObject("values"); - { - for (const auto& V : KeyValues) - { - Writer.AddString(V.first, V.second); - } - } - Writer.EndObject(); // values - return Writer.Save(); - }; - -} // namespace - -TEST_CASE("buildstore.metadata") -{ - std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); - auto _ = MakeGuard([&SystemRootPath]() { DeleteDirectories(SystemRootPath); }); - - std::string_view Namespace = "ns"sv; - std::string_view Bucket = "bkt"sv; - Oid BuildId = Oid::NewOid(); - - std::vector<IoHash> BlobHashes; - std::vector<CbObject> Metadatas; - std::vector<IoHash> MetadataHashes; - - auto GetMetadatas = - [](HttpClient& Client, std::string_view Namespace, std::string_view Bucket, const Oid& BuildId, std::vector<IoHash> BlobHashes) { - CbObjectWriter Request; - - Request.BeginArray("blobHashes"sv); - for (const IoHash& BlobHash : BlobHashes) - { - Request.AddHash(BlobHash); - } - Request.EndArray(); - - IoBuffer Payload = Request.Save().GetBuffer().AsIoBuffer(); - Payload.SetContentType(ZenContentType::kCbObject); - - HttpClient::Response Result = Client.Post(fmt::format("{}/{}/{}/blobs/getBlobMetadata", Namespace, Bucket, BuildId), - Payload, - HttpClient::Accept(ZenContentType::kCbObject)); - CHECK(Result); - - std::vector<CbObject> ResultMetadatas; - - CbPackage ResponsePackage = ParsePackageMessage(Result.ResponsePayload); - CbObject ResponseObject = ResponsePackage.GetObject(); - - CbArrayView BlobHashArray = ResponseObject["blobHashes"sv].AsArrayView(); - CbArrayView MetadatasArray = ResponseObject["metadatas"sv].AsArrayView(); - ResultMetadatas.reserve(MetadatasArray.Num()); - auto BlobHashesIt = BlobHashes.begin(); - auto BlobHashArrayIt = begin(BlobHashArray); - auto MetadataArrayIt = begin(MetadatasArray); - while (MetadataArrayIt != end(MetadatasArray)) - { - const IoHash BlobHash = (*BlobHashArrayIt).AsHash(); - while (BlobHash != *BlobHashesIt) - { - ZEN_ASSERT(BlobHashesIt != BlobHashes.end()); - BlobHashesIt++; - } - - ZEN_ASSERT(BlobHash == *BlobHashesIt); - - const IoHash MetaHash = (*MetadataArrayIt).AsAttachment(); - const CbAttachment* MetaAttachment = ResponsePackage.FindAttachment(MetaHash); - ZEN_ASSERT(MetaAttachment); - - CbObject Metadata = MetaAttachment->AsObject(); - ResultMetadatas.emplace_back(std::move(Metadata)); - - BlobHashArrayIt++; - MetadataArrayIt++; - BlobHashesIt++; - } - return ResultMetadatas; - }; - - { - ZenServerInstance Instance(TestEnv); - - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri() + "/builds/"); - - const size_t BlobCount = 5; - - for (size_t I = 0; I < BlobCount; I++) - { - BlobHashes.push_back(IoHash::HashBuffer(&I, sizeof(I))); - Metadatas.push_back(MakeMetadata(BlobHashes.back(), {{"index", fmt::format("{}", I)}})); - MetadataHashes.push_back(IoHash::HashBuffer(Metadatas.back().GetBuffer().AsIoBuffer())); - } - - { - CbPackage RequestPackage; - std::vector<CbAttachment> Attachments; - tsl::robin_set<IoHash, IoHash::Hasher> AttachmentHashes; - Attachments.reserve(BlobCount); - AttachmentHashes.reserve(BlobCount); - { - CbObjectWriter RequestWriter; - RequestWriter.BeginArray("blobHashes"); - for (size_t BlockHashIndex = 0; BlockHashIndex < BlobHashes.size(); BlockHashIndex++) - { - RequestWriter.AddHash(BlobHashes[BlockHashIndex]); - } - RequestWriter.EndArray(); // blobHashes - - RequestWriter.BeginArray("metadatas"); - for (size_t BlockHashIndex = 0; BlockHashIndex < BlobHashes.size(); BlockHashIndex++) - { - const IoHash ObjectHash = Metadatas[BlockHashIndex].GetHash(); - RequestWriter.AddBinaryAttachment(ObjectHash); - if (!AttachmentHashes.contains(ObjectHash)) - { - Attachments.push_back(CbAttachment(Metadatas[BlockHashIndex], ObjectHash)); - AttachmentHashes.insert(ObjectHash); - } - } - - RequestWriter.EndArray(); // metadatas - - RequestPackage.SetObject(RequestWriter.Save()); - } - RequestPackage.AddAttachments(Attachments); - - CompositeBuffer RpcRequestBuffer = FormatPackageMessageBuffer(RequestPackage); - - HttpClient::Response Result = Client.Post(fmt::format("{}/{}/{}/blobs/putBlobMetadata", Namespace, Bucket, BuildId), - RpcRequestBuffer, - ZenContentType::kCbPackage); - CHECK(Result); - } - - { - std::vector<CbObject> ResultMetadatas = GetMetadatas(Client, Namespace, Bucket, BuildId, BlobHashes); - - for (size_t Index = 0; Index < MetadataHashes.size(); Index++) - { - const IoHash& ExpectedHash = MetadataHashes[Index]; - IoHash Hash = IoHash::HashBuffer(ResultMetadatas[Index].GetBuffer().AsIoBuffer()); - CHECK_EQ(ExpectedHash, Hash); - } - } - } - { - ZenServerInstance Instance(TestEnv); - - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri() + "/builds/"); - - std::vector<CbObject> ResultMetadatas = GetMetadatas(Client, Namespace, Bucket, BuildId, BlobHashes); - - for (size_t Index = 0; Index < MetadataHashes.size(); Index++) - { - const IoHash& ExpectedHash = MetadataHashes[Index]; - IoHash Hash = IoHash::HashBuffer(ResultMetadatas[Index].GetBuffer().AsIoBuffer()); - CHECK_EQ(ExpectedHash, Hash); - } - } -} - -TEST_CASE("buildstore.cache") -{ - std::filesystem::path SystemRootPath = TestEnv.CreateNewTestDir(); - std::filesystem::path TempDir = TestEnv.CreateNewTestDir(); - auto _ = MakeGuard([&SystemRootPath, &TempDir]() { - DeleteDirectories(SystemRootPath); - DeleteDirectories(TempDir); - }); - - std::string_view Namespace = "ns"sv; - std::string_view Bucket = "bkt"sv; - Oid BuildId = Oid::NewOid(); - - std::vector<IoHash> BlobHashes; - std::vector<CbObject> Metadatas; - std::vector<IoHash> MetadataHashes; - - const size_t BlobCount = 5; - { - ZenServerInstance Instance(TestEnv); - - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri()); - - BuildStorageCache::Statistics Stats; - std::unique_ptr<BuildStorageCache> Cache(CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false)); - - { - IoHash NoneBlob = IoHash::HashBuffer("data", 4); - std::vector<BuildStorageCache::BlobExistsResult> NoneExists = Cache->BlobsExists(BuildId, std::vector<IoHash>{NoneBlob}); - CHECK(NoneExists.size() == 1); - CHECK(!NoneExists[0].HasBody); - CHECK(!NoneExists[0].HasMetadata); - } - - for (size_t I = 0; I < BlobCount; I++) - { - IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7); - CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob))); - BlobHashes.push_back(CompressedBlob.DecodeRawHash()); - Cache->PutBuildBlob(BuildId, BlobHashes.back(), ZenContentType::kCompressedBinary, CompressedBlob.GetCompressed()); - } - - Cache->Flush(500); - Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false); - - { - std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes); - CHECK(Exists.size() == BlobHashes.size()); - for (size_t I = 0; I < BlobCount; I++) - { - CHECK(Exists[I].HasBody); - CHECK(!Exists[I].HasMetadata); - } - - std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); - CHECK_EQ(0, FetchedMetadatas.size()); - } - - { - for (size_t I = 0; I < BlobCount; I++) - { - IoBuffer BuildBlob = Cache->GetBuildBlob(BuildId, BlobHashes[I]); - CHECK(BuildBlob); - CHECK_EQ(BlobHashes[I], - IoHash::HashBuffer(CompressedBuffer::FromCompressedNoValidate(std::move(BuildBlob)).Decompress().AsIoBuffer())); - } - } - - { - for (size_t I = 0; I < BlobCount; I++) - { - CbObject Metadata = MakeMetadata(BlobHashes[I], - {{"key", fmt::format("{}", I)}, - {"key_plus_one", fmt::format("{}", I + 1)}, - {"block_hash", fmt::format("{}", BlobHashes[I])}}); - Metadatas.push_back(Metadata); - MetadataHashes.push_back(IoHash::HashBuffer(Metadata.GetBuffer().AsIoBuffer())); - } - Cache->PutBlobMetadatas(BuildId, BlobHashes, Metadatas); - } - - Cache->Flush(500); - Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false); - - { - std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes); - CHECK(Exists.size() == BlobHashes.size()); - for (size_t I = 0; I < BlobCount; I++) - { - CHECK(Exists[I].HasBody); - CHECK(Exists[I].HasMetadata); - } - - std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); - CHECK_EQ(BlobCount, FetchedMetadatas.size()); - - for (size_t I = 0; I < BlobCount; I++) - { - CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer())); - } - } - - for (size_t I = 0; I < BlobCount; I++) - { - IoBuffer Blob = CreateSemiRandomBlob(4711 + I * 7); - CompressedBuffer CompressedBlob = CompressedBuffer::Compress(SharedBuffer(std::move(Blob))); - BlobHashes.push_back(CompressedBlob.DecodeRawHash()); - Cache->PutBuildBlob(BuildId, BlobHashes.back(), ZenContentType::kCompressedBinary, CompressedBlob.GetCompressed()); - } - - Cache->Flush(500); - Cache = CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false); - - { - std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes); - CHECK(Exists.size() == BlobHashes.size()); - for (size_t I = 0; I < BlobCount * 2; I++) - { - CHECK(Exists[I].HasBody); - CHECK_EQ(I < BlobCount, Exists[I].HasMetadata); - } - - std::vector<CbObject> MetaDatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); - CHECK_EQ(BlobCount, MetaDatas.size()); - - std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); - CHECK_EQ(BlobCount, FetchedMetadatas.size()); - - for (size_t I = 0; I < BlobCount; I++) - { - CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer())); - } - } - } - - { - ZenServerInstance Instance(TestEnv); - - const uint16_t PortNumber = - Instance.SpawnServerAndWaitUntilReady(fmt::format("--buildstore-enabled --system-dir {}", SystemRootPath)); - CHECK(PortNumber != 0); - - HttpClient Client(Instance.GetBaseUri()); - - BuildStorageCache::Statistics Stats; - std::unique_ptr<BuildStorageCache> Cache(CreateZenBuildStorageCache(Client, Stats, Namespace, Bucket, TempDir, false)); - - std::vector<BuildStorageCache::BlobExistsResult> Exists = Cache->BlobsExists(BuildId, BlobHashes); - CHECK(Exists.size() == BlobHashes.size()); - for (size_t I = 0; I < BlobCount * 2; I++) - { - CHECK(Exists[I].HasBody); - CHECK_EQ(I < BlobCount, Exists[I].HasMetadata); - } - - for (size_t I = 0; I < BlobCount * 2; I++) - { - IoBuffer BuildBlob = Cache->GetBuildBlob(BuildId, BlobHashes[I]); - CHECK(BuildBlob); - CHECK_EQ(BlobHashes[I], - IoHash::HashBuffer(CompressedBuffer::FromCompressedNoValidate(std::move(BuildBlob)).Decompress().AsIoBuffer())); - } - - std::vector<CbObject> MetaDatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); - CHECK_EQ(BlobCount, MetaDatas.size()); - - std::vector<CbObject> FetchedMetadatas = Cache->GetBlobMetadatas(BuildId, BlobHashes); - CHECK_EQ(BlobCount, FetchedMetadatas.size()); - - for (size_t I = 0; I < BlobCount; I++) - { - CHECK_EQ(MetadataHashes[I], IoHash::HashBuffer(FetchedMetadatas[I].GetBuffer().AsIoBuffer())); - } - } -} - # if 0 TEST_CASE("lifetime.owner") { diff --git a/src/zenserver-test/zenserver-test.h b/src/zenserver-test/zenserver-test.h new file mode 100644 index 000000000..e7cee3f94 --- /dev/null +++ b/src/zenserver-test/zenserver-test.h @@ -0,0 +1,207 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#if ZEN_WITH_TESTS + +# include <zencore/compactbinarybuilder.h> +# include <zencore/compactbinarypackage.h> +# include <zencore/iobuffer.h> +# include <zencore/stream.h> +# include <zencore/string.h> +# include <zencore/testing.h> +# include <zencore/testutils.h> +# include <zenhttp/httpcommon.h> +# include <zenutil/zenserverprocess.h> + +# include <functional> + +namespace zen::tests { + +extern zen::ZenServerEnvironment TestEnv; + +inline IoBuffer +MakeCbObjectPayload(std::function<void(CbObjectWriter& Writer)> WriteCB) +{ + CbObjectWriter Writer; + WriteCB(Writer); + IoBuffer Payload = Writer.Save().GetBuffer().AsIoBuffer(); + Payload.SetContentType(ZenContentType::kCbObject); + return Payload; +}; + +inline IoBuffer +SerializeToBuffer(const zen::CbPackage& Package) +{ + BinaryWriter MemStream; + + Package.Save(MemStream); + + IoBuffer Buffer = zen::IoBuffer(zen::IoBuffer::Clone, MemStream.Data(), MemStream.Size()); + Buffer.SetContentType(HttpContentType::kCbPackage); + return Buffer; +}; + +namespace utils { + + struct ZenConfig + { + std::filesystem::path DataDir; + uint16_t Port; + std::string BaseUri; + std::string Args; + + static ZenConfig New(std::string Args = "") + { + return ZenConfig{.DataDir = TestEnv.CreateNewTestDir(), .Port = TestEnv.GetNewPortNumber(), .Args = std::move(Args)}; + } + + static ZenConfig New(uint16_t Port, std::string Args = "") + { + return ZenConfig{.DataDir = TestEnv.CreateNewTestDir(), .Port = Port, .Args = std::move(Args)}; + } + + static ZenConfig NewWithUpstream(uint16_t Port, uint16_t UpstreamPort, std::string Args = "") + { + return New(Port, + fmt::format("{}{}--debug --upstream-thread-count=0 --upstream-zen-url=http://localhost:{}", + Args, + Args.length() > 0 ? " " : "", + UpstreamPort)); + } + + static ZenConfig NewWithThreadedUpstreams(uint16_t NewPort, std::span<uint16_t> UpstreamPorts, bool Debug) + { + std::string Args = Debug ? "--debug" : ""; + for (uint16_t Port : UpstreamPorts) + { + Args = fmt::format("{}{}--upstream-zen-url=http://localhost:{}", Args, Args.length() > 0 ? " " : "", Port); + } + return New(NewPort, Args); + } + + void Spawn(ZenServerInstance& Inst) + { + Inst.SetTestDir(DataDir); + Inst.SpawnServer(Port, Args); + const uint16_t InstancePort = Inst.WaitUntilReady(); + CHECK_MESSAGE(InstancePort != 0, Inst.GetLogOutput()); + + if (Port != InstancePort) + ZEN_DEBUG("relocation detected from {} to {}", Port, InstancePort); + + Port = InstancePort; + BaseUri = fmt::format("http://localhost:{}/z$", Port); + } + }; + + inline void SpawnServer(ZenServerInstance& Server, ZenConfig& Cfg) { Cfg.Spawn(Server); } + + inline CompressedBuffer CreateSemiRandomBlob(size_t AttachmentSize, + OodleCompressionLevel CompressionLevel = OodleCompressionLevel::VeryFast) + { + // Convoluted way to get a compressed buffer whose result it large enough to be a separate file + // but also does actually compress + const size_t PartCount = (AttachmentSize / (1u * 1024u * 64)) + 1; + const size_t PartSize = AttachmentSize / PartCount; + auto Part = SharedBuffer(CreateRandomBlob(PartSize)); + std::vector<SharedBuffer> Parts(PartCount, Part); + size_t RemainPartSize = AttachmentSize - (PartSize * PartCount); + if (RemainPartSize > 0) + { + Parts.push_back(SharedBuffer(CreateRandomBlob(RemainPartSize))); + } + CompressedBuffer Value = CompressedBuffer::Compress(CompositeBuffer(std::move(Parts)), OodleCompressor::Mermaid, CompressionLevel); + return Value; + }; + + inline std::vector<std::pair<Oid, CompressedBuffer>> CreateAttachments(const std::span<const size_t>& Sizes) + { + std::vector<std::pair<Oid, CompressedBuffer>> Result; + Result.reserve(Sizes.size()); + for (size_t Size : Sizes) + { + CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(CreateRandomBlob(Size))); + Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed)); + } + return Result; + } + + inline std::vector<std::pair<Oid, CompressedBuffer>> CreateSemiRandomAttachments(const std::span<const size_t>& Sizes) + { + std::vector<std::pair<Oid, CompressedBuffer>> Result; + Result.reserve(Sizes.size()); + for (size_t Size : Sizes) + { + CompressedBuffer Compressed = + CreateSemiRandomBlob(Size, Size > 1024u * 1024u ? OodleCompressionLevel::None : OodleCompressionLevel::VeryFast); + Result.emplace_back(std::pair<Oid, CompressedBuffer>(Oid::NewOid(), Compressed)); + } + return Result; + } + +} // namespace utils + +class ZenServerTestHelper +{ +public: + ZenServerTestHelper(std::string_view HelperId, int ServerCount) : m_HelperId{HelperId}, m_ServerCount{ServerCount} {} + ~ZenServerTestHelper() {} + + void SpawnServers(std::string_view AdditionalServerArgs = std::string_view()) + { + SpawnServers([](ZenServerInstance&) {}, AdditionalServerArgs); + } + + void SpawnServers(auto&& Callback, std::string_view AdditionalServerArgs) + { + ZEN_INFO("{}: spawning {} server instances", m_HelperId, m_ServerCount); + + m_Instances.resize(m_ServerCount); + + for (int i = 0; i < m_ServerCount; ++i) + { + auto& Instance = m_Instances[i]; + Instance = std::make_unique<ZenServerInstance>(TestEnv); + Instance->SetTestDir(TestEnv.CreateNewTestDir()); + } + + for (int i = 0; i < m_ServerCount; ++i) + { + auto& Instance = m_Instances[i]; + Callback(*Instance); + } + + for (int i = 0; i < m_ServerCount; ++i) + { + auto& Instance = m_Instances[i]; + Instance->SpawnServer(TestEnv.GetNewPortNumber(), AdditionalServerArgs); + } + + for (int i = 0; i < m_ServerCount; ++i) + { + auto& Instance = m_Instances[i]; + uint16_t PortNumber = Instance->WaitUntilReady(); + CHECK_MESSAGE(PortNumber != 0, Instance->GetLogOutput()); + } + } + + ZenServerInstance& GetInstance(int Index) { return *m_Instances[Index]; } + +private: + std::string m_HelperId; + int m_ServerCount = 0; + std::vector<std::unique_ptr<ZenServerInstance>> m_Instances; +}; + +inline std::string +OidAsString(const Oid& Id) +{ + StringBuilder<25> OidStringBuilder; + Id.ToString(OidStringBuilder); + return OidStringBuilder.ToString(); +} + +} // namespace zen::tests + +#endif diff --git a/src/zenserver/admin/admin.cpp b/src/zenserver/admin/admin.cpp index f6ad14422..97522e892 100644 --- a/src/zenserver/admin/admin.cpp +++ b/src/zenserver/admin/admin.cpp @@ -73,7 +73,7 @@ GetStatsForDirectory(std::filesystem::path Dir) GetDirectoryContent(Dir, DirectoryContentFlags::IncludeAllEntries | DirectoryContentFlags::IncludeFileSizes, DirTraverser, - GetSmallWorkerPool(EWorkloadType::Burst), + GetSmallWorkerPool(EWorkloadType::Background), PendingWorkCount); PendingWorkCount.CountDown(); PendingWorkCount.Wait(); diff --git a/src/zenserver/buildstore/httpbuildstore.cpp b/src/zenserver/buildstore/httpbuildstore.cpp index 2a3ce41b7..bce993f17 100644 --- a/src/zenserver/buildstore/httpbuildstore.cpp +++ b/src/zenserver/buildstore/httpbuildstore.cpp @@ -48,8 +48,8 @@ HttpBuildStoreService::Initialize() { ZEN_LOG_INFO(LogBuilds, "Initializing Builds Service"); - m_Router.AddPattern("namespace", "([[:alnum:]-_.]+)"); - m_Router.AddPattern("bucket", "([[:alnum:]-_.]+)"); + m_Router.AddPattern("namespace", "([[:alnum:]\\-_.]+)"); + m_Router.AddPattern("bucket", "([[:alnum:]\\-_.]+)"); m_Router.AddPattern("buildid", "([[:xdigit:]]{24})"); m_Router.AddPattern("hash", "([[:xdigit:]]{40})"); diff --git a/src/zenserver/cache/httpstructuredcache.cpp b/src/zenserver/cache/httpstructuredcache.cpp index 68f1c602e..dd5bf05cb 100644 --- a/src/zenserver/cache/httpstructuredcache.cpp +++ b/src/zenserver/cache/httpstructuredcache.cpp @@ -5,12 +5,14 @@ #include <zencore/compactbinary.h> #include <zencore/compactbinarybuilder.h> #include <zencore/compactbinarypackage.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compactbinaryvalidation.h> #include <zencore/compress.h> #include <zencore/enumflags.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zencore/memory/llm.h> +#include <zencore/parallelwork.h> #include <zencore/scopeguard.h> #include <zencore/stream.h> #include <zencore/timer.h> @@ -19,13 +21,11 @@ #include <zenhttp/httpserver.h> #include <zenhttp/httpstats.h> #include <zenhttp/packageformat.h> +#include <zenremotestore/jupiter/jupiterclient.h> +#include <zenstore/cache/cache.h> #include <zenstore/cache/structuredcachestore.h> #include <zenstore/gc.h> -#include <zenutil/cache/cache.h> -#include <zenutil/cache/cacherequests.h> -#include <zenutil/cache/rpcrecording.h> -#include <zenutil/jupiter/jupiterclient.h> -#include <zenutil/parallelwork.h> +#include <zenutil/rpcrecording.h> #include <zenutil/workerpools.h> #include "upstream/upstreamcache.h" @@ -39,7 +39,6 @@ #include <queue> #include <thread> -#include <cpr/cpr.h> #include <gsl/gsl-lite.hpp> namespace zen { @@ -71,15 +70,6 @@ namespace { static constinit std::string_view HttpZCacheUtilStopRecording = "exec$/stop-recording"sv; static constinit std::string_view HttpZCacheUtilReplayRecording = "exec$/replay-recording"sv; static constinit std::string_view HttpZCacheDetailsPrefix = "details$"sv; - - struct HttpRequestData - { - std::optional<std::string> Namespace; - std::optional<std::string> Bucket; - std::optional<IoHash> HashKey; - std::optional<IoHash> ValueContentId; - }; - } // namespace ////////////////////////////////////////////////////////////////////////// @@ -390,8 +380,9 @@ HttpStructuredCacheService::HandleRequest(HttpServerRequest& Request) if (Key == HttpZCacheUtilStartRecording) { - HttpServerRequest::QueryParams Params = Request.GetQueryParams(); - std::string RecordPath = cpr::util::urlDecode(std::string(Params.GetValue("path"))); + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + + std::string RecordPath = UrlDecode(Params.GetValue("path")); { RwLock::ExclusiveLockScope _(m_RequestRecordingLock); @@ -428,9 +419,11 @@ HttpStructuredCacheService::HandleRequest(HttpServerRequest& Request) m_RequestRecorder.reset(); } - HttpServerRequest::QueryParams Params = Request.GetQueryParams(); - std::string RecordPath = cpr::util::urlDecode(std::string(Params.GetValue("path"))); - uint32_t ThreadCount = std::thread::hardware_concurrency(); + HttpServerRequest::QueryParams Params = Request.GetQueryParams(); + + std::string RecordPath = UrlDecode(Params.GetValue("path")); + + uint32_t ThreadCount = std::thread::hardware_concurrency(); if (auto Param = Params.GetValue("thread_count"); Param.empty() == false) { if (auto Value = ParseInt<uint64_t>(Param)) @@ -444,7 +437,7 @@ HttpStructuredCacheService::HandleRequest(HttpServerRequest& Request) std::unique_ptr<cache::IRpcRequestReplayer> Replayer(cache::MakeDiskRequestReplayer(RecordPath, false)); ReplayRequestRecorder(RequestContext, *Replayer, ThreadCount < 1 ? 1 : ThreadCount); - ZEN_INFO("cache RPC replay STARTED"); + ZEN_INFO("cache RPC replay COMPLETED"); Request.WriteResponse(HttpResponseCode::OK); return; @@ -456,8 +449,8 @@ HttpStructuredCacheService::HandleRequest(HttpServerRequest& Request) return; } - cacherequests::HttpRequestData RequestData; - if (!cacherequests::HttpRequestParseRelativeUri(Key, ZenCacheStore::DefaultNamespace, RequestData)) + HttpCacheRequestData RequestData; + if (!HttpCacheRequestParseRelativeUri(Key, ZenCacheStore::DefaultNamespace, RequestData)) { m_CacheStats.BadRequestCount++; return Request.WriteResponse(HttpResponseCode::BadRequest); // invalid URL @@ -820,7 +813,8 @@ HttpStructuredCacheService::HandleGetCacheRecord(HttpServerRequest& Request, con const bool SkipData = EnumHasAllFlags(PolicyFromUrl, CachePolicy::SkipData); const bool PartialRecord = EnumHasAllFlags(PolicyFromUrl, CachePolicy::PartialRecord); - bool Success = false; + bool Success = false; + uint32_t MissingCount = 0; ZenCacheValue ClientResultValue; if (!EnumHasAnyFlags(PolicyFromUrl, CachePolicy::Query)) { @@ -842,45 +836,60 @@ HttpStructuredCacheService::HandleGetCacheRecord(HttpServerRequest& Request, con { if (ContentType == ZenContentType::kCbObject) { - CbPackage Package; - uint32_t MissingCount = 0; - - CbObjectView CacheRecord(ClientResultValue.Value.Data()); - CacheRecord.IterateAttachments([this, &MissingCount, &Package, SkipData](CbFieldView AttachmentHash) { - if (SkipData) - { - if (!m_CidStore.ContainsChunk(AttachmentHash.AsHash())) + CbPackage Package; + CbValidateError ValidateError = CbValidateError::None; + if (CbObject PackageObject = ValidateAndReadCompactBinaryObject(std::move(ClientResultValue.Value), ValidateError); + ValidateError == CbValidateError::None) + { + CbObjectView CacheRecord(ClientResultValue.Value.Data()); + CacheRecord.IterateAttachments([this, &MissingCount, &Package, SkipData](CbFieldView AttachmentHash) { + if (SkipData) { - MissingCount++; + if (!m_CidStore.ContainsChunk(AttachmentHash.AsHash())) + { + MissingCount++; + } } - } - else - { - if (IoBuffer Chunk = m_CidStore.FindChunkByCid(AttachmentHash.AsHash())) + else { - CompressedBuffer Compressed = CompressedBuffer::FromCompressedNoValidate(std::move(Chunk)); - if (Compressed) + if (IoBuffer Chunk = m_CidStore.FindChunkByCid(AttachmentHash.AsHash())) { - Package.AddAttachment(CbAttachment(Compressed, AttachmentHash.AsHash())); + CompressedBuffer Compressed = CompressedBuffer::FromCompressedNoValidate(std::move(Chunk)); + if (Compressed) + { + Package.AddAttachment(CbAttachment(Compressed, AttachmentHash.AsHash())); + } + else + { + ZEN_WARN("invalid compressed binary returned for {}", AttachmentHash.AsHash()); + MissingCount++; + } } else { - ZEN_WARN("invalid compressed binary returned for {}", AttachmentHash.AsHash()); MissingCount++; } } - else - { - MissingCount++; - } - } - }); + }); - Success = MissingCount == 0 || PartialRecord; + Success = MissingCount == 0 || PartialRecord; + } + else + { + ZEN_WARN("Invalid compact binary payload returned for {}/{}/{} ({}). Reason: '{}'", + Ref.Namespace, + Ref.BucketSegment, + Ref.HashKey, + Ref.ValueContentId, + ToString(ValidateError)); + Success = false; + } if (Success) { - Package.SetObject(LoadCompactBinaryObject(ClientResultValue.Value)); + CbObject PackageObject = LoadCompactBinaryObject(std::move(ClientResultValue.Value)); + + Package.SetObject(std::move(PackageObject)); BinaryWriter MemStream; Package.Save(MemStream); @@ -919,7 +928,9 @@ HttpStructuredCacheService::HandleGetCacheRecord(HttpServerRequest& Request, con else { // kCbPackage handled SkipData when constructing the ClientResultValue, kcbObject ignores SkipData - return Request.WriteResponse(HttpResponseCode::OK, ClientResultValue.Value.GetContentType(), ClientResultValue.Value); + return Request.WriteResponse((MissingCount == 0) ? HttpResponseCode::OK : HttpResponseCode::PartialContent, + ClientResultValue.Value.GetContentType(), + ClientResultValue.Value); } } else if (!HasUpstream || !EnumHasAllFlags(PolicyFromUrl, CachePolicy::QueryRemote)) @@ -1204,8 +1215,11 @@ HttpStructuredCacheService::HandlePutCacheRecord(HttpServerRequest& Request, con break; } - return PutResult.Message.empty() ? Request.WriteResponse(ResponseCode) - : Request.WriteResponse(ResponseCode, zen::HttpContentType::kText, PutResult.Message); + if (PutResult.Details) + { + Request.WriteResponse(ResponseCode, PutResult.Details); + } + return Request.WriteResponse(ResponseCode); }; const HttpContentType ContentType = Request.RequestContentType(); @@ -1629,7 +1643,7 @@ HttpStructuredCacheService::ReplayRequestRecorder(const CacheRequestContext& Co auto _ = MakeGuard([&]() { ZEN_INFO("Replayed {} requests in {}", RequestCount, NiceLatencyNs(Timer.GetElapsedTimeUs() * 1000)); }); std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::EnableBacklog); ZEN_INFO("Replaying {} requests", RequestCount); for (uint64_t RequestIndex = 0; RequestIndex < RequestCount; ++RequestIndex) { diff --git a/src/zenserver/cache/httpstructuredcache.h b/src/zenserver/cache/httpstructuredcache.h index cb822f117..a157148c9 100644 --- a/src/zenserver/cache/httpstructuredcache.h +++ b/src/zenserver/cache/httpstructuredcache.h @@ -6,8 +6,8 @@ #include <zenhttp/httpserver.h> #include <zenhttp/httpstats.h> #include <zenhttp/httpstatus.h> +#include <zenstore/cache/cache.h> #include <zenstore/cache/cacherpc.h> -#include <zenutil/cache/cache.h> #include <zenutil/openprocesscache.h> #include <memory> diff --git a/src/zenserver/config.cpp b/src/zenserver/config.cpp index fb2d9b7f4..0cf5a9ca3 100644 --- a/src/zenserver/config.cpp +++ b/src/zenserver/config.cpp @@ -7,6 +7,7 @@ #include <zencore/basicfile.h> #include <zencore/compactbinarybuilder.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compactbinaryvalidation.h> #include <zencore/crypto.h> #include <zencore/except.h> @@ -72,13 +73,12 @@ ReadAllCentralManifests(const std::filesystem::path& SystemRoot) { try { - FileContents FileData = ReadFile(File); - IoBuffer DataBuffer = FileData.Flatten(); - CbValidateError ValidateError = ValidateCompactBinary(DataBuffer, CbValidateMode::All); - - if (ValidateError == CbValidateError::None) + FileContents FileData = ReadFile(File); + CbValidateError ValidateError; + if (CbObject Manifest = ValidateAndReadCompactBinaryObject(FileData.Flatten(), ValidateError); + ValidateError == CbValidateError::None) { - Manifests.push_back(LoadCompactBinaryObject(DataBuffer)); + Manifests.emplace_back(std::move(Manifest)); } else { @@ -99,35 +99,37 @@ ValidateOptions(ZenServerOptions& ServerOptions) { if (ServerOptions.EncryptionKey.empty() == false) { - const auto Key = zen::AesKey256Bit::FromString(ServerOptions.EncryptionKey); + const auto Key = AesKey256Bit::FromString(ServerOptions.EncryptionKey); if (Key.IsValid() == false) { - throw zen::OptionParseException("Invalid AES encryption key"); + throw OptionParseException(fmt::format("'--encryption-aes-key' ('{}') is malformed", ServerOptions.EncryptionKey), {}); } } if (ServerOptions.EncryptionIV.empty() == false) { - const auto IV = zen::AesIV128Bit::FromString(ServerOptions.EncryptionIV); + const auto IV = AesIV128Bit::FromString(ServerOptions.EncryptionIV); if (IV.IsValid() == false) { - throw zen::OptionParseException("Invalid AES initialization vector"); + throw OptionParseException(fmt::format("'--encryption-aes-iv' ('{}') is malformed", ServerOptions.EncryptionIV), {}); } } if (ServerOptions.HttpServerConfig.ForceLoopback && ServerOptions.IsDedicated) { - throw zen::OptionParseException("Dedicated server can not be used with forced local server address"); + throw OptionParseException("'--dedicated' conflicts with '--http-forceloopback'", {}); } if (ServerOptions.GcConfig.AttachmentPassCount > ZenGcConfig::GcMaxAttachmentPassCount) { - throw zen::OptionParseException( - fmt::format("GC attachment pass count can not be larger than {}", ZenGcConfig::GcMaxAttachmentPassCount)); + throw OptionParseException(fmt::format("'--gc-attachment-passes' ('{}') is invalid, maximum is {}.", + ServerOptions.GcConfig.AttachmentPassCount, + ZenGcConfig::GcMaxAttachmentPassCount), + {}); } if (ServerOptions.GcConfig.UseGCV2 == false) { - ZEN_WARN("--gc-v2=false is deprecated, reverting to --gc-v2=true"); + ZEN_WARN("'--gc-v2=false' is deprecated, reverting to '--gc-v2=true'"); ServerOptions.GcConfig.UseGCV2 = true; } } @@ -185,7 +187,7 @@ class CachePolicyOption : public LuaConfig::OptionValue { public: CachePolicyOption(UpstreamCachePolicy& Value) : Value(Value) {} - virtual void Print(std::string_view, zen::StringBuilderBase& StringBuilder) override + virtual void Print(std::string_view, StringBuilderBase& StringBuilder) override { switch (Value) { @@ -232,7 +234,7 @@ class ZenAuthConfigOption : public LuaConfig::OptionValue { public: ZenAuthConfigOption(ZenAuthConfig& Value) : Value(Value) {} - virtual void Print(std::string_view Indent, zen::StringBuilderBase& StringBuilder) override + virtual void Print(std::string_view Indent, StringBuilderBase& StringBuilder) override { if (Value.OpenIdProviders.empty()) { @@ -275,7 +277,7 @@ class ZenObjectStoreConfigOption : public LuaConfig::OptionValue { public: ZenObjectStoreConfigOption(ZenObjectStoreConfig& Value) : Value(Value) {} - virtual void Print(std::string_view Indent, zen::StringBuilderBase& StringBuilder) override + virtual void Print(std::string_view Indent, StringBuilderBase& StringBuilder) override { if (Value.Buckets.empty()) { @@ -318,7 +320,7 @@ class ZenStructuredCacheBucketsConfigOption : public LuaConfig::OptionValue { public: ZenStructuredCacheBucketsConfigOption(std::vector<std::pair<std::string, ZenStructuredCacheBucketConfig>>& Value) : Value(Value) {} - virtual void Print(std::string_view Indent, zen::StringBuilderBase& StringBuilder) override + virtual void Print(std::string_view Indent, StringBuilderBase& StringBuilder) override { if (Value.empty()) { @@ -359,14 +361,15 @@ public: std::string Name = Kv.first.as<std::string>(); if (Name.empty()) { - throw zen::OptionParseException(fmt::format("cache bucket option must have a name.")); + throw OptionParseException("Cache bucket option must have a name.", {}); } const uint64_t MaxBlockSize = Bucket.value().get_or("maxblocksize", BucketConfig.MaxBlockSize); if (MaxBlockSize == 0) { - throw zen::OptionParseException( - fmt::format("maxblocksize option for cache bucket '{}' is invalid. It must be non-zero.", Name)); + throw OptionParseException( + fmt::format("'maxblocksize' option for cache bucket '{}' is invalid. It must be non-zero.", Name), + {}); } BucketConfig.MaxBlockSize = MaxBlockSize; @@ -375,8 +378,9 @@ public: const uint64_t MemCacheSizeThreshold = Bucket.value().get_or("sizethreshold", BucketConfig.MemCacheSizeThreshold); if (MemCacheSizeThreshold == 0) { - throw zen::OptionParseException( - fmt::format("memlayer.sizethreshold option for cache bucket '{}' is invalid. It must be non-zero.", Name)); + throw OptionParseException( + fmt::format("'memlayer.sizethreshold' option for cache bucket '{}' is invalid. It must be non-zero.", Name), + {}); } BucketConfig.MemCacheSizeThreshold = Bucket.value().get_or("sizethreshold", BucketConfig.MemCacheSizeThreshold); } @@ -384,17 +388,20 @@ public: const uint32_t PayloadAlignment = Bucket.value().get_or("payloadalignment", BucketConfig.PayloadAlignment); if (PayloadAlignment == 0 || !IsPow2(PayloadAlignment)) { - throw zen::OptionParseException(fmt::format( - "payloadalignment option for cache bucket '{}' is invalid. It needs to be non-zero and a power of two.", - Name)); + throw OptionParseException( + fmt::format( + "'payloadalignment' option for cache bucket '{}' is invalid. It needs to be non-zero and a power of two.", + Name), + {}); } BucketConfig.PayloadAlignment = PayloadAlignment; const uint64_t LargeObjectThreshold = Bucket.value().get_or("largeobjectthreshold", BucketConfig.LargeObjectThreshold); if (LargeObjectThreshold == 0) { - throw zen::OptionParseException( - fmt::format("largeobjectthreshold option for cache bucket '{}' is invalid. It must be non-zero.", Name)); + throw OptionParseException( + fmt::format("'largeobjectthreshold' option for cache bucket '{}' is invalid. It must be non-zero.", Name), + {}); } BucketConfig.LargeObjectThreshold = LargeObjectThreshold; @@ -409,19 +416,19 @@ public: }; std::shared_ptr<LuaConfig::OptionValue> -MakeOption(zen::UpstreamCachePolicy& Value) +MakeOption(UpstreamCachePolicy& Value) { return std::make_shared<CachePolicyOption>(Value); }; std::shared_ptr<LuaConfig::OptionValue> -MakeOption(zen::ZenAuthConfig& Value) +MakeOption(ZenAuthConfig& Value) { return std::make_shared<ZenAuthConfigOption>(Value); }; std::shared_ptr<LuaConfig::OptionValue> -MakeOption(zen::ZenObjectStoreConfig& Value) +MakeOption(ZenObjectStoreConfig& Value) { return std::make_shared<ZenObjectStoreConfigOption>(Value); }; @@ -660,11 +667,11 @@ ParseConfigFile(const std::filesystem::path& Path, if (!OutputConfigFile.empty()) { - std::filesystem::path WritePath(MakeSafeAbsolutePath(OutputConfigFile)); - zen::ExtendableStringBuilder<512> ConfigStringBuilder; + std::filesystem::path WritePath(MakeSafeAbsolutePath(OutputConfigFile)); + ExtendableStringBuilder<512> ConfigStringBuilder; LuaOptions.Print(ConfigStringBuilder, CmdLineResult); - zen::BasicFile Output; - Output.Open(WritePath, zen::BasicFile::Mode::kTruncate); + BasicFile Output; + Output.Open(WritePath, BasicFile::Mode::kTruncate); Output.Write(ConfigStringBuilder.Data(), ConfigStringBuilder.Size(), 0); } } @@ -737,7 +744,7 @@ ParseCliOptions(int argc, char* argv[], ZenServerOptions& ServerOptions) const char* DefaultHttp = "asio"; #if ZEN_WITH_HTTPSYS - if (!zen::windows::IsRunningOnWine()) + if (!windows::IsRunningOnWine()) { DefaultHttp = "httpsys"; } @@ -1334,7 +1341,7 @@ ParseCliOptions(int argc, char* argv[], ZenServerOptions& ServerOptions) } catch (const std::exception& Ex) { - throw zen::OptionParseException(Ex.what()); + throw OptionParseException(Ex.what(), options.help()); } if (Result.count("help")) @@ -1399,17 +1406,17 @@ ParseCliOptions(int argc, char* argv[], ZenServerOptions& ServerOptions) if (!BaseSnapshotDir.empty()) { if (DataDir.empty()) - throw zen::OptionParseException("You must explicitly specify a data directory when specifying a base snapshot"); + throw OptionParseException("'--snapshot-dir' requires '--data-dir'", options.help()); if (!IsDir(ServerOptions.BaseSnapshotDir)) - throw OptionParseException(fmt::format("Snapshot directory must be a directory: '{}", BaseSnapshotDir)); + throw std::runtime_error(fmt::format("'--snapshot-dir' ('{}') must be a directory", BaseSnapshotDir)); } if (OpenIdProviderUrl.empty() == false) { if (OpenIdClientId.empty()) { - throw zen::OptionParseException("Invalid OpenID client ID"); + throw OptionParseException("'--openid-provider-url' requires '--openid-client-id'", options.help()); } ServerOptions.AuthConfig.OpenIdProviders.push_back( @@ -1436,10 +1443,10 @@ ParseCliOptions(int argc, char* argv[], ZenServerOptions& ServerOptions) ValidateOptions(ServerOptions); } - catch (const zen::OptionParseException& e) + catch (const OptionParseException& e) { - ZEN_CONSOLE_ERROR("Invalid zenserver arguments: {}\n\n{}", e.what(), options.help()); - + ZEN_CONSOLE("{}\n", options.help()); + ZEN_CONSOLE_ERROR("Invalid zenserver arguments: {}", e.what()); throw; } diff --git a/src/zenserver/frontend/html.zip b/src/zenserver/frontend/html.zip Binary files differindex 1de4c3d74..bb3d61198 100644 --- a/src/zenserver/frontend/html.zip +++ b/src/zenserver/frontend/html.zip diff --git a/src/zenserver/frontend/html/pages/tree.js b/src/zenserver/frontend/html/pages/tree.js index 23ff3e819..08a578492 100644 --- a/src/zenserver/frontend/html/pages/tree.js +++ b/src/zenserver/frontend/html/pages/tree.js @@ -93,7 +93,7 @@ export class Page extends ZenPage const is_node_l = l.endsWith("/"); const any_nodes = is_node_l + r.endsWith("/"); if (any_nodes == 1) return is_node_l ? -1 : 1; - if (sort_by >= 0) return (new_nodes[r][sort_by] - new_nodes[l][sort_by]); + if (sort_by >= 0) return Number(new_nodes[r][sort_by] - new_nodes[l][sort_by]); return r < l; }) diff --git a/src/zenserver/main.cpp b/src/zenserver/main.cpp index d512d4894..a91c95ffb 100644 --- a/src/zenserver/main.cpp +++ b/src/zenserver/main.cpp @@ -31,7 +31,7 @@ #if ZEN_PLATFORM_WINDOWS # include <zencore/windows.h> -# include <zenutil/windows/service.h> +# include <zenutil/windows/windowsservice.h> #endif ////////////////////////////////////////////////////////////////////////// diff --git a/src/zenserver/projectstore/httpprojectstore.cpp b/src/zenserver/projectstore/httpprojectstore.cpp index 317a419eb..1c6b5d6b0 100644 --- a/src/zenserver/projectstore/httpprojectstore.cpp +++ b/src/zenserver/projectstore/httpprojectstore.cpp @@ -2,9 +2,6 @@ #include "httpprojectstore.h" -#include "oplogreferencedset.h" -#include "projectstore.h" - #include <zencore/compactbinarybuilder.h> #include <zencore/compactbinarypackage.h> #include <zencore/compactbinaryutil.h> @@ -13,9 +10,19 @@ #include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zencore/memory/llm.h> +#include <zencore/scopeguard.h> #include <zencore/stream.h> #include <zencore/trace.h> +#include <zenhttp/packageformat.h> +#include <zenremotestore/projectstore/buildsremoteprojectstore.h> +#include <zenremotestore/projectstore/fileremoteprojectstore.h> +#include <zenremotestore/projectstore/jupiterremoteprojectstore.h> +#include <zenremotestore/projectstore/remoteprojectstore.h> +#include <zenremotestore/projectstore/zenremoteprojectstore.h> +#include <zenstore/oplogreferencedset.h> +#include <zenstore/projectstore.h> #include <zenstore/zenstore.h> +#include <zenutil/openprocesscache.h> #include <zenutil/workerpools.h> namespace zen { @@ -46,15 +53,15 @@ CSVHeader(bool Details, bool AttachmentDetails, StringBuilderBase& CSVWriter) } void -CSVWriteOp(CidStore& CidStore, - std::string_view ProjectId, - std::string_view OplogId, - bool Details, - bool AttachmentDetails, - uint32_t LSN, - const Oid& Key, - CbObjectView Op, - StringBuilderBase& CSVWriter) +CSVWriteOp(CidStore& CidStore, + std::string_view ProjectId, + std::string_view OplogId, + bool Details, + bool AttachmentDetails, + ProjectStore::LogSequenceNumber LSN, + const Oid& Key, + CbObjectView Op, + StringBuilderBase& CSVWriter) { StringBuilder<32> KeyStringBuilder; Key.ToString(KeyStringBuilder); @@ -66,8 +73,8 @@ CSVWriteOp(CidStore& CidStore, const IoHash AttachmentHash = FieldView.AsAttachment(); IoBuffer Attachment = CidStore.FindChunkByCid(AttachmentHash); CSVWriter << "\r\n" - << ProjectId << ", " << OplogId << ", " << LSN << ", " << KeyString << ", " << AttachmentHash.ToHexString() << ", " - << gsl::narrow<uint64_t>(Attachment.GetSize()); + << ProjectId << ", " << OplogId << ", " << LSN.Number << ", " << KeyString << ", " << AttachmentHash.ToHexString() + << ", " << gsl::narrow<uint64_t>(Attachment.GetSize()); }); } else if (Details) @@ -81,8 +88,8 @@ CSVWriteOp(CidStore& CidStore, AttachmentsSize += Attachment.GetSize(); }); CSVWriter << "\r\n" - << ProjectId << ", " << OplogId << ", " << LSN << ", " << KeyString << ", " << gsl::narrow<uint64_t>(Op.GetSize()) << ", " - << AttachmentCount << ", " << gsl::narrow<uint64_t>(AttachmentsSize); + << ProjectId << ", " << OplogId << ", " << LSN.Number << ", " << KeyString << ", " << gsl::narrow<uint64_t>(Op.GetSize()) + << ", " << AttachmentCount << ", " << gsl::narrow<uint64_t>(AttachmentsSize); } else { @@ -94,21 +101,21 @@ CSVWriteOp(CidStore& CidStore, namespace { - void CbWriteOp(CidStore& CidStore, - bool Details, - bool OpDetails, - bool AttachmentDetails, - uint32_t LSN, - const Oid& Key, - CbObjectView Op, - CbObjectWriter& CbWriter) + void CbWriteOp(CidStore& CidStore, + bool Details, + bool OpDetails, + bool AttachmentDetails, + ProjectStore::LogSequenceNumber LSN, + const Oid& Key, + CbObjectView Op, + CbObjectWriter& CbWriter) { CbWriter.BeginObject(); { CbWriter.AddObjectId("key", Key); if (Details) { - CbWriter.AddInteger("lsn", LSN); + CbWriter.AddInteger("lsn", LSN.Number); CbWriter.AddInteger("size", gsl::narrow<uint64_t>(Op.GetSize())); } if (AttachmentDetails) @@ -170,10 +177,11 @@ namespace { { Cbo.BeginArray("ops"); { - Oplog.IterateOplogWithKey( - [&Cbo, &CidStore, Details, OpDetails, AttachmentDetails](uint32_t LSN, const Oid& Key, CbObjectView Op) { - CbWriteOp(CidStore, Details, OpDetails, AttachmentDetails, LSN, Key, Op, Cbo); - }); + Oplog.IterateOplogWithKey([&Cbo, &CidStore, Details, OpDetails, AttachmentDetails](ProjectStore::LogSequenceNumber LSN, + const Oid& Key, + CbObjectView Op) { + CbWriteOp(CidStore, Details, OpDetails, AttachmentDetails, LSN, Key, Op, Cbo); + }); } Cbo.EndArray(); } @@ -205,8 +213,8 @@ namespace { { for (const std::string& OpLogId : OpLogs) { - ProjectStore::Oplog* Oplog = Project.OpenOplog(OpLogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); - if (Oplog != nullptr) + Ref<ProjectStore::Oplog> Oplog = Project.OpenOplog(OpLogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); + if (Oplog) { CbWriteOplog(CidStore, *Oplog, Details, OpDetails, AttachmentDetails, Cbo); } @@ -231,6 +239,264 @@ namespace { Cbo.EndObject(); } + struct CreateRemoteStoreResult + { + std::shared_ptr<RemoteProjectStore> Store; + std::string Description; + }; + + CreateRemoteStoreResult CreateRemoteStore(CbObjectView Params, + AuthMgr& AuthManager, + size_t MaxBlockSize, + size_t MaxChunkEmbedSize, + const std::filesystem::path& TempFilePath) + { + ZEN_MEMSCOPE(GetProjectHttpTag()); + + using namespace std::literals; + + std::shared_ptr<RemoteProjectStore> RemoteStore; + + if (CbObjectView File = Params["file"sv].AsObjectView(); File) + { + std::filesystem::path FolderPath(File["path"sv].AsString()); + if (FolderPath.empty()) + { + return {nullptr, "Missing file path"}; + } + std::string_view Name(File["name"sv].AsString()); + if (Name.empty()) + { + return {nullptr, "Missing file name"}; + } + std::string_view OptionalBaseName(File["basename"sv].AsString()); + bool ForceDisableBlocks = File["disableblocks"sv].AsBool(false); + bool ForceEnableTempBlocks = File["enabletempblocks"sv].AsBool(false); + + FileRemoteStoreOptions Options = { + RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunksPerBlock = 1000, .MaxChunkEmbedSize = MaxChunkEmbedSize}, + FolderPath, + std::string(Name), + std::string(OptionalBaseName), + ForceDisableBlocks, + ForceEnableTempBlocks}; + RemoteStore = CreateFileRemoteStore(Options); + } + + if (CbObjectView Cloud = Params["cloud"sv].AsObjectView(); Cloud) + { + std::string_view CloudServiceUrl = Cloud["url"sv].AsString(); + if (CloudServiceUrl.empty()) + { + return {nullptr, "Missing service url"}; + } + + std::string Url = UrlDecode(CloudServiceUrl); + std::string_view Namespace = Cloud["namespace"sv].AsString(); + if (Namespace.empty()) + { + return {nullptr, "Missing namespace"}; + } + std::string_view Bucket = Cloud["bucket"sv].AsString(); + if (Bucket.empty()) + { + return {nullptr, "Missing bucket"}; + } + std::string_view OpenIdProvider = Cloud["openid-provider"sv].AsString(); + std::string AccessToken = std::string(Cloud["access-token"sv].AsString()); + if (AccessToken.empty()) + { + std::string_view AccessTokenEnvVariable = Cloud["access-token-env"].AsString(); + if (!AccessTokenEnvVariable.empty()) + { + AccessToken = GetEnvVariable(AccessTokenEnvVariable); + } + } + std::filesystem::path OidcExePath; + if (std::string_view OidcExePathString = Cloud["oidc-exe-path"].AsString(); !OidcExePathString.empty()) + { + std::filesystem::path OidcExePathMaybe(OidcExePathString); + if (IsFile(OidcExePathMaybe)) + { + OidcExePath = std::move(OidcExePathMaybe); + } + else + { + ZEN_WARN("Path to OidcToken executable '{}' can not be reached by server", OidcExePathString); + } + } + std::string_view KeyParam = Cloud["key"sv].AsString(); + if (KeyParam.empty()) + { + return {nullptr, "Missing key"}; + } + if (KeyParam.length() != IoHash::StringLength) + { + return {nullptr, "Invalid key"}; + } + IoHash Key = IoHash::FromHexString(KeyParam); + if (Key == IoHash::Zero) + { + return {nullptr, "Invalid key string"}; + } + IoHash BaseKey = IoHash::Zero; + std::string_view BaseKeyParam = Cloud["basekey"sv].AsString(); + if (!BaseKeyParam.empty()) + { + if (BaseKeyParam.length() != IoHash::StringLength) + { + return {nullptr, "Invalid base key"}; + } + BaseKey = IoHash::FromHexString(BaseKeyParam); + if (BaseKey == IoHash::Zero) + { + return {nullptr, "Invalid base key string"}; + } + } + + bool ForceDisableBlocks = Cloud["disableblocks"sv].AsBool(false); + bool ForceDisableTempBlocks = Cloud["disabletempblocks"sv].AsBool(false); + bool AssumeHttp2 = Cloud["assumehttp2"sv].AsBool(false); + + JupiterRemoteStoreOptions Options = { + RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunksPerBlock = 1000, .MaxChunkEmbedSize = MaxChunkEmbedSize}, + Url, + std::string(Namespace), + std::string(Bucket), + Key, + BaseKey, + std::string(OpenIdProvider), + AccessToken, + AuthManager, + OidcExePath, + ForceDisableBlocks, + ForceDisableTempBlocks, + AssumeHttp2}; + RemoteStore = CreateJupiterRemoteStore(Options, TempFilePath, /*Quiet*/ false, /*Unattended*/ false, /*Hidden*/ true); + } + + if (CbObjectView Zen = Params["zen"sv].AsObjectView(); Zen) + { + std::string_view Url = Zen["url"sv].AsString(); + std::string_view Project = Zen["project"sv].AsString(); + if (Project.empty()) + { + return {nullptr, "Missing project"}; + } + std::string_view Oplog = Zen["oplog"sv].AsString(); + if (Oplog.empty()) + { + return {nullptr, "Missing oplog"}; + } + ZenRemoteStoreOptions Options = { + RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunksPerBlock = 1000, .MaxChunkEmbedSize = MaxChunkEmbedSize}, + std::string(Url), + std::string(Project), + std::string(Oplog)}; + RemoteStore = CreateZenRemoteStore(Options, TempFilePath); + } + + if (CbObjectView Builds = Params["builds"sv].AsObjectView(); Builds) + { + std::string_view BuildsServiceUrl = Builds["url"sv].AsString(); + if (BuildsServiceUrl.empty()) + { + return {nullptr, "Missing service url"}; + } + + std::string Url = UrlDecode(BuildsServiceUrl); + std::string_view Namespace = Builds["namespace"sv].AsString(); + if (Namespace.empty()) + { + return {nullptr, "Missing namespace"}; + } + std::string_view Bucket = Builds["bucket"sv].AsString(); + if (Bucket.empty()) + { + return {nullptr, "Missing bucket"}; + } + std::string_view OpenIdProvider = Builds["openid-provider"sv].AsString(); + std::string AccessToken = std::string(Builds["access-token"sv].AsString()); + if (AccessToken.empty()) + { + std::string_view AccessTokenEnvVariable = Builds["access-token-env"].AsString(); + if (!AccessTokenEnvVariable.empty()) + { + AccessToken = GetEnvVariable(AccessTokenEnvVariable); + } + } + std::filesystem::path OidcExePath; + if (std::string_view OidcExePathString = Builds["oidc-exe-path"].AsString(); !OidcExePathString.empty()) + { + std::filesystem::path OidcExePathMaybe(OidcExePathString); + if (IsFile(OidcExePathMaybe)) + { + OidcExePath = std::move(OidcExePathMaybe); + } + else + { + ZEN_WARN("Path to OidcToken executable '{}' can not be reached by server", OidcExePathString); + } + } + std::string_view BuildIdParam = Builds["buildsid"sv].AsString(); + if (BuildIdParam.empty()) + { + return {nullptr, "Missing build id"}; + } + if (BuildIdParam.length() != Oid::StringLength) + { + return {nullptr, "Invalid build id"}; + } + Oid BuildId = Oid::FromHexString(BuildIdParam); + if (BuildId == Oid::Zero) + { + return {nullptr, "Invalid build id string"}; + } + + bool ForceDisableBlocks = Builds["disableblocks"sv].AsBool(false); + bool ForceDisableTempBlocks = Builds["disabletempblocks"sv].AsBool(false); + bool AssumeHttp2 = Builds["assumehttp2"sv].AsBool(false); + + MemoryView MetaDataSection = Builds["metadata"sv].AsBinaryView(); + IoBuffer MetaData(IoBuffer::Wrap, MetaDataSection.GetData(), MetaDataSection.GetSize()); + + BuildsRemoteStoreOptions Options = { + RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunksPerBlock = 1000, .MaxChunkEmbedSize = MaxChunkEmbedSize}, + Url, + std::string(Namespace), + std::string(Bucket), + BuildId, + std::string(OpenIdProvider), + AccessToken, + AuthManager, + OidcExePath, + ForceDisableBlocks, + ForceDisableTempBlocks, + AssumeHttp2, + MetaData}; + RemoteStore = CreateJupiterBuildsRemoteStore(Options, TempFilePath, /*Quiet*/ false, /*Unattended*/ false, /*Hidden*/ true); + } + + if (!RemoteStore) + { + return {nullptr, "Unknown remote store type"}; + } + + return {std::move(RemoteStore), ""}; + } + + std::pair<HttpResponseCode, std::string> ConvertResult(const RemoteProjectStore::Result& Result) + { + if (Result.ErrorCode == 0) + { + return {HttpResponseCode::OK, Result.Text}; + } + return {static_cast<HttpResponseCode>(Result.ErrorCode), + Result.Reason.empty() ? Result.Text + : Result.Text.empty() ? Result.Reason + : fmt::format("{}: {}", Result.Reason, Result.Text)}; + } + } // namespace ////////////////////////////////////////////////////////////////////////// @@ -239,13 +505,17 @@ HttpProjectService::HttpProjectService(CidStore& Store, ProjectStore* Projects, HttpStatusService& StatusService, HttpStatsService& StatsService, - AuthMgr& AuthMgr) + AuthMgr& AuthMgr, + OpenProcessCache& InOpenProcessCache, + JobQueue& InJobQueue) : m_Log(logging::Get("project")) , m_CidStore(Store) , m_ProjectStore(Projects) , m_StatusService(StatusService) , m_StatsService(StatsService) , m_AuthMgr(AuthMgr) +, m_OpenProcessCache(InOpenProcessCache) +, m_JobQueue(InJobQueue) { ZEN_MEMSCOPE(GetProjectHttpTag()); @@ -507,7 +777,7 @@ HttpProjectService::HandleChunkBatchRequest(HttpRouterRequest& Req) } Project->TouchProject(); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); @@ -580,7 +850,7 @@ HttpProjectService::HandleChunkBatchRequest(HttpRouterRequest& Req) for (uint32_t ChunkIndex = 0; ChunkIndex < RequestHdr.ChunkCount; ++ChunkIndex) { const RequestChunkEntry& RequestedChunk = RequestedChunks[ChunkIndex]; - IoBuffer FoundChunk = FoundLog->FindChunk(RequestedChunk.ChunkId, nullptr); + IoBuffer FoundChunk = FoundLog->FindChunk(Project->RootDir, RequestedChunk.ChunkId, nullptr); if (FoundChunk) { if (RequestedChunk.Offset > 0 || RequestedChunk.RequestBytes < uint64_t(-1)) @@ -666,38 +936,35 @@ HttpProjectService::HandleFilesRequest(HttpRouterRequest& Req) } } - CbObject ResponsePayload; - std::pair<HttpResponseCode, std::string> Result = - m_ProjectStore->GetProjectFiles(ProjectId, OplogId, WantedFieldNames, ResponsePayload); - if (Result.first == HttpResponseCode::OK) + Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); + if (!Project) { - if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary) - { - CompositeBuffer Payload = CompressedBuffer::Compress(ResponsePayload.GetBuffer()).GetCompressed(); - return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload); - } - else - { - return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); - } + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Project files request for unknown project '{}'", ProjectId)); } - else + Project->TouchProject(); + + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); + if (!FoundLog) { - if (Result.first == HttpResponseCode::BadRequest) - { - m_ProjectStats.BadRequestCount++; - } - ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", - ToString(HttpReq.RequestVerb()), - HttpReq.QueryString(), - static_cast<int>(Result.first), - Result.second); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Project files for unknown oplog '{}/{}'", ProjectId, OplogId)); } - if (Result.second.empty()) + Project->TouchOplog(OplogId); + + CbObject ResponsePayload = ProjectStore::GetProjectFiles(Log(), *Project, *FoundLog, WantedFieldNames); + + if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary) { - return HttpReq.WriteResponse(Result.first); + CompositeBuffer Payload = CompressedBuffer::Compress(ResponsePayload.GetBuffer()).GetCompressed(); + return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload); + } + else + { + return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); } - return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void @@ -730,38 +997,34 @@ HttpProjectService::HandleChunkInfosRequest(HttpRouterRequest& Req) WantedFieldNames.insert("rawsize"); } - CbObject ResponsePayload; - std::pair<HttpResponseCode, std::string> Result = - m_ProjectStore->GetProjectChunkInfos(ProjectId, OplogId, WantedFieldNames, ResponsePayload); - if (Result.first == HttpResponseCode::OK) + Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); + if (!Project) { - if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary) - { - CompositeBuffer Payload = CompressedBuffer::Compress(ResponsePayload.GetBuffer()).GetCompressed(); - return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload); - } - else - { - return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); - } + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Chunk infos request for unknown project '{}'", ProjectId)); } - else + Project->TouchProject(); + + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); + if (!FoundLog) { - if (Result.first == HttpResponseCode::BadRequest) - { - m_ProjectStats.BadRequestCount++; - } - ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", - ToString(HttpReq.RequestVerb()), - HttpReq.QueryString(), - static_cast<int>(Result.first), - Result.second); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Chunk infos for unknown oplog '{}/{}'", ProjectId, OplogId)); + } + Project->TouchOplog(OplogId); + + CbObject ResponsePayload = ProjectStore::GetProjectChunkInfos(Log(), *Project, *FoundLog, WantedFieldNames); + if (HttpReq.AcceptContentType() == HttpContentType::kCompressedBinary) + { + CompositeBuffer Payload = CompressedBuffer::Compress(ResponsePayload.GetBuffer()).GetCompressed(); + return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCompressedBinary, Payload); } - if (Result.second.empty()) + else { - return HttpReq.WriteResponse(Result.first); + return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); } - return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void @@ -775,35 +1038,48 @@ HttpProjectService::HandleChunkInfoRequest(HttpRouterRequest& Req) const auto& OplogId = Req.GetCapture(2); const auto& ChunkId = Req.GetCapture(3); - CbObject ResponsePayload; - std::pair<HttpResponseCode, std::string> Result = m_ProjectStore->GetChunkInfo(ProjectId, OplogId, ChunkId, ResponsePayload); - if (Result.first == HttpResponseCode::OK) + Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); + if (!Project) { - m_ProjectStats.ChunkHitCount++; - return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Chunk info request for unknown project '{}'", ProjectId)); } - else if (Result.first == HttpResponseCode::NotFound) + Project->TouchProject(); + + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); + if (!FoundLog) { - m_ProjectStats.ChunkMissCount++; - ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, ChunkId); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Chunk info for unknown oplog '{}/{}'", ProjectId, OplogId)); } - else + Project->TouchOplog(OplogId); + + if (ChunkId.size() != 2 * sizeof(Oid::OidBits)) { - if (Result.first == HttpResponseCode::BadRequest) - { - m_ProjectStats.BadRequestCount++; - } - ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", - ToString(HttpReq.RequestVerb()), - HttpReq.QueryString(), - static_cast<int>(Result.first), - Result.second); + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Chunk info request for invalid chunk id '{}/{}'/'{}'", ProjectId, OplogId, ChunkId)); + } + + const Oid Obj = Oid::FromHexString(ChunkId); + + CbObject ResponsePayload = ProjectStore::GetChunkInfo(Log(), *Project, *FoundLog, Obj); + if (ResponsePayload) + { + m_ProjectStats.ChunkHitCount++; + return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); } - if (Result.second.empty()) + else { - return HttpReq.WriteResponse(Result.first); + m_ProjectStats.ChunkMissCount++; + ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, ChunkId); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Chunk info for unknown chunk '{}/{}/{}'", ProjectId, OplogId, ChunkId)); } - return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void @@ -848,40 +1124,65 @@ HttpProjectService::HandleChunkByIdRequest(HttpRouterRequest& Req) } } - HttpContentType AcceptType = HttpReq.AcceptContentType(); - - CompositeBuffer Chunk; - HttpContentType ContentType; - std::pair<HttpResponseCode, std::string> Result = - m_ProjectStore->GetChunkRange(ProjectId, OplogId, ChunkId, Offset, Size, AcceptType, Chunk, ContentType, nullptr); - if (Result.first == HttpResponseCode::OK) + Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); + if (!Project) { - m_ProjectStats.ChunkHitCount++; - ZEN_DEBUG("chunk - '{}/{}/{}' '{}'", ProjectId, OplogId, ChunkId, ToString(ContentType)); - return HttpReq.WriteResponse(HttpResponseCode::OK, ContentType, Chunk); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Chunk request for unknown project '{}'", ProjectId)); } - else if (Result.first == HttpResponseCode::NotFound) + Project->TouchProject(); + + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); + if (!FoundLog) { - m_ProjectStats.ChunkMissCount++; - ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, ChunkId); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Chunk request for unknown oplog '{}/{}'", ProjectId, OplogId)); } - else + Project->TouchOplog(OplogId); + + if (ChunkId.size() != 2 * sizeof(Oid::OidBits)) { - if (Result.first == HttpResponseCode::BadRequest) - { - m_ProjectStats.BadRequestCount++; - } - ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", - ToString(HttpReq.RequestVerb()), - HttpReq.QueryString(), - static_cast<int>(Result.first), - Result.second); + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Chunk request for invalid chunk id '{}/{}/{}'", ProjectId, OplogId, ChunkId)); } - if (Result.second.empty()) + + const Oid Obj = Oid::FromHexString(ChunkId); + + HttpContentType AcceptType = HttpReq.AcceptContentType(); + + ProjectStore::GetChunkRangeResult Result = + ProjectStore::GetChunkRange(Log(), *Project, *FoundLog, Obj, Offset, Size, AcceptType, /*OptionalInOutModificationTag*/ nullptr); + + switch (Result.Error) { - return HttpReq.WriteResponse(Result.first); + case ProjectStore::GetChunkRangeResult::EError::Ok: + m_ProjectStats.ChunkHitCount++; + ZEN_DEBUG("chunk - '{}/{}/{}' '{}'", ProjectId, OplogId, ChunkId, ToString(Result.ContentType)); + return HttpReq.WriteResponse(HttpResponseCode::OK, Result.ContentType, Result.Chunk); + case ProjectStore::GetChunkRangeResult::EError::NotFound: + m_ProjectStats.ChunkMissCount++; + ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, ChunkId); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, Result.ContentType, Result.Chunk); + case ProjectStore::GetChunkRangeResult::EError::MalformedContent: + return HttpReq.WriteResponse( + HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Get chunk {}/{}/{} failed. Reason: {}", ProjectId, OplogId, ChunkId, Result.ErrorDescription)); + case ProjectStore::GetChunkRangeResult::EError::OutOfRange: + m_ProjectStats.ChunkMissCount++; + ZEN_DEBUG("chunk - '{}/{}/{}' OUT OF RANGE", ProjectId, OplogId, ChunkId); + return HttpReq.WriteResponse( + HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Get chunk {}/{}/{} failed. Reason: {}", ProjectId, OplogId, ChunkId, Result.ErrorDescription)); + default: + ZEN_ASSERT(false); + break; } - return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void @@ -897,13 +1198,40 @@ HttpProjectService::HandleChunkByCidRequest(HttpRouterRequest& Req) HttpContentType AcceptType = HttpReq.AcceptContentType(); HttpContentType RequestType = HttpReq.RequestContentType(); + Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); + if (!Project) + { + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Chunk request for unknown project '{}'", ProjectId)); + } + Project->TouchProject(); + + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); + if (!FoundLog) + { + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Chunk request for unknown oplog '{}/{}'", ProjectId, OplogId)); + } + Project->TouchOplog(OplogId); + + if (Cid.length() != IoHash::StringLength) + { + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Chunk request for invalid chunk id '{}/{}/{}'", ProjectId, OplogId, Cid)); + } + + const IoHash Hash = IoHash::FromHexString(Cid); + switch (HttpReq.RequestVerb()) { case HttpVerb::kGet: { - IoBuffer Value; - std::pair<HttpResponseCode, std::string> Result = m_ProjectStore->GetChunk(ProjectId, OplogId, Cid, Value, nullptr); - if (Result.first == HttpResponseCode::OK) + IoBuffer Value = m_ProjectStore->GetChunk(*Project, *FoundLog, Hash); + if (Value) { if (AcceptType == ZenContentType::kUnknownContentType || AcceptType == ZenContentType::kBinary || AcceptType == ZenContentType::kJSON || AcceptType == ZenContentType::kYAML || @@ -962,28 +1290,12 @@ HttpProjectService::HandleChunkByCidRequest(HttpRouterRequest& Req) m_ProjectStats.ChunkHitCount++; return HttpReq.WriteResponse(HttpResponseCode::OK, Value.GetContentType(), Value); } - else if (Result.first == HttpResponseCode::NotFound) + else { m_ProjectStats.ChunkMissCount++; ZEN_DEBUG("chunk - '{}/{}/{}' MISSING", ProjectId, OplogId, Cid); + return HttpReq.WriteResponse(HttpResponseCode::NotFound); } - else - { - if (Result.first == HttpResponseCode::BadRequest) - { - m_ProjectStats.BadRequestCount++; - } - ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", - ToString(HttpReq.RequestVerb()), - HttpReq.QueryString(), - static_cast<int>(Result.first), - Result.second); - } - if (Result.second.empty()) - { - return HttpReq.WriteResponse(Result.first); - } - return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } case HttpVerb::kPost: { @@ -991,30 +1303,23 @@ HttpProjectService::HandleChunkByCidRequest(HttpRouterRequest& Req) { return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } - std::pair<HttpResponseCode, std::string> Result = - m_ProjectStore->PutChunk(ProjectId, OplogId, Cid, RequestType, HttpReq.ReadPayload()); - if (Result.first == HttpResponseCode::OK || Result.first == HttpResponseCode::Created) - { - m_ProjectStats.ChunkWriteCount++; - return HttpReq.WriteResponse(Result.first); - } - else + if (RequestType != HttpContentType::kCompressedBinary) { - if (Result.first == HttpResponseCode::BadRequest) - { - m_ProjectStats.BadRequestCount++; - } - ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", - ToString(HttpReq.RequestVerb()), - HttpReq.QueryString(), - static_cast<int>(Result.first), - Result.second); - } - if (Result.second.empty()) - { - return HttpReq.WriteResponse(Result.first); + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Chunk request for chunk id '{}/{}'/'{}' as unexpected content type: '{}'", + ProjectId, + OplogId, + Cid, + ToString(RequestType))); } - return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); + IoBuffer Payload = HttpReq.ReadPayload(); + Payload.SetContentType(RequestType); + bool IsNew = m_ProjectStore->PutChunk(*Project, *FoundLog, Hash, std::move(Payload)); + + m_ProjectStats.ChunkWriteCount++; + return HttpReq.WriteResponse(IsNew ? HttpResponseCode::Created : HttpResponseCode::OK); } break; } @@ -1039,7 +1344,7 @@ HttpProjectService::HandleOplogOpPrepRequest(HttpRouterRequest& Req) } Project->TouchProject(); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); @@ -1050,36 +1355,44 @@ HttpProjectService::HandleOplogOpPrepRequest(HttpRouterRequest& Req) // chunks are not present on this server. This list is then returned in // the "need" list in the response - IoBuffer Payload = HttpReq.ReadPayload(); - CbObject RequestObject = LoadCompactBinaryObject(Payload); + CbValidateError ValidateResult; + if (CbObject RequestObject = ValidateAndReadCompactBinaryObject(HttpReq.ReadPayload(), ValidateResult); + ValidateResult == CbValidateError::None) + { + std::vector<IoHash> NeedList; - std::vector<IoHash> NeedList; + { + eastl::fixed_vector<IoHash, 16> ChunkList; + CbArrayView HaveList = RequestObject["have"sv].AsArrayView(); + ChunkList.reserve(HaveList.Num()); + for (auto& Entry : HaveList) + { + ChunkList.push_back(Entry.AsHash()); + } - { - eastl::fixed_vector<IoHash, 16> ChunkList; - CbArrayView HaveList = RequestObject["have"sv].AsArrayView(); - ChunkList.reserve(HaveList.Num()); - for (auto& Entry : HaveList) + NeedList = FoundLog->CheckPendingChunkReferences(std::span(begin(ChunkList), end(ChunkList)), std::chrono::minutes(2)); + } + + CbObjectWriter Cbo(1 + 1 + 5 + NeedList.size() * (1 + sizeof(IoHash::Hash)) + 1); + Cbo.BeginArray("need"); { - ChunkList.push_back(Entry.AsHash()); + for (const IoHash& Hash : NeedList) + { + ZEN_DEBUG("prep - NEED: {}", Hash); + Cbo << Hash; + } } + Cbo.EndArray(); + CbObject Response = Cbo.Save(); - NeedList = FoundLog->CheckPendingChunkReferences(std::span(begin(ChunkList), end(ChunkList)), std::chrono::minutes(2)); + return HttpReq.WriteResponse(HttpResponseCode::OK, Response); } - - CbObjectWriter Cbo(1 + 1 + 5 + NeedList.size() * (1 + sizeof(IoHash::Hash)) + 1); - Cbo.BeginArray("need"); + else { - for (const IoHash& Hash : NeedList) - { - ZEN_DEBUG("prep - NEED: {}", Hash); - Cbo << Hash; - } + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Invalid compact binary format: '{}'", ToString(ValidateResult))); } - Cbo.EndArray(); - CbObject Response = Cbo.Save(); - - return HttpReq.WriteResponse(HttpResponseCode::OK, Response); } void @@ -1118,7 +1431,7 @@ HttpProjectService::HandleOplogOpNewRequest(HttpRouterRequest& Req) } Project->TouchProject(); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); @@ -1173,7 +1486,9 @@ HttpProjectService::HandleOplogOpNewRequest(HttpRouterRequest& Req) if (!legacy::TryLoadCbPackage(Package, Payload, &UniqueBuffer::Alloc, &Resolver)) { - if (CbObject Core = LoadCompactBinaryObject(Payload)) + CbValidateError ValidateResult; + if (CbObject Core = ValidateAndReadCompactBinaryObject(IoBuffer(Payload), ValidateResult); + ValidateResult == CbValidateError::None && Core) { Package.SetObject(Core); } @@ -1182,7 +1497,7 @@ HttpProjectService::HandleOplogOpNewRequest(HttpRouterRequest& Req) std::filesystem::path BadPackagePath = Oplog.TempPath() / "bad_packages"sv / fmt::format("session{}_request{}"sv, HttpReq.SessionId(), HttpReq.RequestId()); - ZEN_WARN("Received malformed package! Saving payload to '{}'", BadPackagePath); + ZEN_WARN("Received malformed package ('{}')! Saving payload to '{}'", ToString(ValidateResult), BadPackagePath); WriteFile(BadPackagePath, Payload); @@ -1230,10 +1545,9 @@ HttpProjectService::HandleOplogOpNewRequest(HttpRouterRequest& Req) // Write core to oplog - size_t AttachmentCount = Package.GetAttachments().size(); - const uint32_t OpLsn = Oplog.AppendNewOplogEntry(Package); - - if (OpLsn == ProjectStore::Oplog::kInvalidOp) + size_t AttachmentCount = Package.GetAttachments().size(); + const ProjectStore::LogSequenceNumber OpLsn = Oplog.AppendNewOplogEntry(Package); + if (!OpLsn) { m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest); @@ -1247,7 +1561,7 @@ HttpProjectService::HandleOplogOpNewRequest(HttpRouterRequest& Req) } m_ProjectStats.OpWriteCount++; - ZEN_DEBUG("'{}/{}' op #{} ({}) - '{}'", ProjectId, OplogId, OpLsn, NiceBytes(Payload.Size()), Core["key"sv].AsString()); + ZEN_DEBUG("'{}/{}' op #{} ({}) - '{}'", ProjectId, OplogId, OpLsn.Number, NiceBytes(Payload.Size()), Core["key"sv].AsString()); HttpReq.WriteResponse(HttpResponseCode::Created); } @@ -1275,7 +1589,7 @@ HttpProjectService::HandleOplogValidateRequest(HttpRouterRequest& Req) } Project->TouchProject(); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound, @@ -1286,8 +1600,8 @@ HttpProjectService::HandleOplogValidateRequest(HttpRouterRequest& Req) ProjectStore::Oplog& Oplog = *FoundLog; - std::atomic_bool CancelFlag = false; - ProjectStore::Oplog::ValidationResult Result = Oplog.Validate(CancelFlag, &GetSmallWorkerPool(EWorkloadType::Burst)); + std::atomic_bool CancelFlag = false; + ProjectStore::Oplog::ValidationResult Result = Oplog.Validate(Project->RootDir, CancelFlag, &GetSmallWorkerPool(EWorkloadType::Burst)); tsl::robin_map<Oid, std::string, Oid::Hasher> KeyNameLookup; KeyNameLookup.reserve(Result.OpKeys.size()); for (const auto& It : Result.OpKeys) @@ -1297,8 +1611,8 @@ HttpProjectService::HandleOplogValidateRequest(HttpRouterRequest& Req) CbObjectWriter Writer; Writer << "HasMissingData" << !Result.IsEmpty(); Writer << "OpCount" << Result.OpCount; - Writer << "LSNLow" << Result.LSNLow; - Writer << "LSNHigh" << Result.LSNHigh; + Writer << "LSNLow" << Result.LSNLow.Number; + Writer << "LSNHigh" << Result.LSNHigh.Number; if (!Result.MissingFiles.empty()) { Writer.BeginArray("MissingFiles"); @@ -1386,7 +1700,7 @@ HttpProjectService::HandleOpLogOpRequest(HttpRouterRequest& Req) } Project->TouchProject(); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); @@ -1397,7 +1711,7 @@ HttpProjectService::HandleOpLogOpRequest(HttpRouterRequest& Req) if (const std::optional<int32_t> OpId = ParseInt<uint32_t>(OpIdString)) { - if (std::optional<CbObject> MaybeOp = Oplog.GetOpByIndex(OpId.value())) + if (std::optional<CbObject> MaybeOp = Oplog.GetOpByIndex(ProjectStore::LogSequenceNumber(OpId.value()))) { CbObject& Op = MaybeOp.value(); if (HttpReq.AcceptContentType() == ZenContentType::kCbPackage) @@ -1413,15 +1727,18 @@ HttpProjectService::HandleOpLogOpRequest(HttpRouterRequest& Req) switch (Payload.GetContentType()) { case ZenContentType::kCbObject: - if (CbObject Object = LoadCompactBinaryObject(Payload)) { - Package.AddAttachment(CbAttachment(Object)); - } - else - { - // Error - malformed object - - ZEN_WARN("malformed object returned for {}", AttachmentHash); + CbValidateError ValidateResult; + if (CbObject Object = ValidateAndReadCompactBinaryObject(std::move(Payload), ValidateResult); + ValidateResult == CbValidateError::None && Object) + { + Package.AddAttachment(CbAttachment(Object)); + } + else + { + // Error - malformed object + ZEN_WARN("malformed object returned for {} ('{}')", AttachmentHash, ToString(ValidateResult)); + } } break; @@ -1483,7 +1800,7 @@ HttpProjectService::HandleOpLogRequest(HttpRouterRequest& Req) { case HttpVerb::kGet: { - ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); + Ref<ProjectStore::Oplog> OplogIt = Project->ReadOplog(OplogId); if (!OplogIt) { return HttpReq.WriteResponse(HttpResponseCode::NotFound, @@ -1491,8 +1808,6 @@ HttpProjectService::HandleOpLogRequest(HttpRouterRequest& Req) fmt::format("oplog {} not found in project {}", OplogId, ProjectId)); } - Project->TouchOplog(OplogId); - ProjectStore::Oplog& Log = *OplogIt; CbObjectWriter Cb; @@ -1517,7 +1832,7 @@ HttpProjectService::HandleOpLogRequest(HttpRouterRequest& Req) OplogMarkerPath = Params["gcpath"sv].AsString(); } - ProjectStore::Oplog* OplogIt = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); + Ref<ProjectStore::Oplog> OplogIt = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!OplogIt) { if (!Project->NewOplog(OplogId, OplogMarkerPath)) @@ -1554,7 +1869,7 @@ HttpProjectService::HandleOpLogRequest(HttpRouterRequest& Req) OplogMarkerPath = Params["gcpath"sv].AsString(); } - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!FoundLog) { if (!Project->NewOplog(OplogId, OplogMarkerPath)) @@ -1604,7 +1919,7 @@ HttpProjectService::HandleOpLogRequest(HttpRouterRequest& Req) } std::optional<OplogReferencedSet> -LoadReferencedSet(ProjectStore::Oplog& Log) +LoadReferencedSet(ProjectStore::Project& Project, ProjectStore::Oplog& Log) { using namespace std::literals; @@ -1622,7 +1937,7 @@ LoadReferencedSet(ProjectStore::Oplog& Log) return std::optional<OplogReferencedSet>(); } - return OplogReferencedSet::LoadFromChunk(Log.FindChunk(ChunkId, nullptr)); + return OplogReferencedSet::LoadFromChunk(Log.FindChunk(Project.RootDir, ChunkId, nullptr)); } void @@ -1644,7 +1959,7 @@ HttpProjectService::HandleOpLogEntriesRequest(HttpRouterRequest& Req) } Project->TouchProject(); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); + Ref<ProjectStore::Oplog> FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); @@ -1701,13 +2016,6 @@ HttpProjectService::HandleOpLogEntriesRequest(HttpRouterRequest& Req) } else { - std::optional<OplogReferencedSet> ReferencedSet; - if (auto TrimString = Params.GetValue("trim_by_referencedset"); TrimString == "true") - { - ReferencedSet = LoadReferencedSet(*FoundLog); - } - Response.BeginArray("entries"sv); - ProjectStore::Oplog::Paging EntryPaging; if (std::string_view Param = Params.GetValue("start"); !Param.empty()) { @@ -1724,24 +2032,56 @@ HttpProjectService::HandleOpLogEntriesRequest(HttpRouterRequest& Req) } } + std::optional<OplogReferencedSet> MaybeReferencedSet; + if (auto TrimString = Params.GetValue("trim_by_referencedset"); TrimString == "true") + { + MaybeReferencedSet = LoadReferencedSet(*Project, *FoundLog); + } + Response.BeginArray("entries"sv); + bool ShouldFilterFields = !FieldNamesFilter.empty(); - FoundLog->IterateOplogWithKey( - [this, &Response, &FilterObject, ShouldFilterFields, &ReferencedSet](uint32_t /* LSN */, const Oid& Key, CbObjectView Op) { - if (ReferencedSet && !ReferencedSet->Contains(Key, Op["key"].AsString())) - { - return; - } - if (ShouldFilterFields) - { - Response << FilterObject(Op); - } - else - { - Response << Op; - } - }, - EntryPaging); + if (MaybeReferencedSet) + { + const OplogReferencedSet& ReferencedSet = MaybeReferencedSet.value(); + FoundLog->IterateOplogWithKey( + [this, &Response, &FilterObject, ShouldFilterFields, &ReferencedSet](ProjectStore::LogSequenceNumber /* LSN */, + const Oid& Key, + CbObjectView Op) { + if (!ReferencedSet.Contains(Key)) + { + if (!OplogReferencedSet::IsNonPackage(Op["key"].AsString())) + { + return; + } + } + + if (ShouldFilterFields) + { + Response << FilterObject(Op); + } + else + { + Response << Op; + } + }, + EntryPaging); + } + else + { + FoundLog->IterateOplog( + [this, &Response, &FilterObject, ShouldFilterFields](CbObjectView Op) { + if (ShouldFilterFields) + { + Response << FilterObject(Op); + } + else + { + Response << Op; + } + }, + EntryPaging); + } Response.EndArray(); } @@ -1776,66 +2116,20 @@ HttpProjectService::HandleProjectRequest(HttpRouterRequest& Req) return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); } - IoBuffer Payload = HttpReq.ReadPayload(); - CbObject Params = LoadCompactBinaryObject(Payload); - std::filesystem::path Root = Params["root"sv].AsU8String(); // Workspace root (i.e `D:/UE5/`) - std::filesystem::path EngineRoot = Params["engine"sv].AsU8String(); // Engine root (i.e `D:/UE5/Engine`) - std::filesystem::path ProjectRoot = - Params["project"sv].AsU8String(); // Project root directory (i.e `D:/UE5/Samples/Games/Lyra`) - std::filesystem::path ProjectFilePath = - Params["projectfile"sv].AsU8String(); // Project file path (i.e `D:/UE5/Samples/Games/Lyra/Lyra.uproject`) - - const std::filesystem::path BasePath = m_ProjectStore->BasePath() / ProjectId; - m_ProjectStore->NewProject(BasePath, ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath); - - ZEN_INFO("established project (id: '{}', roots: '{}', '{}', '{}', '{}'{})", - ProjectId, - Root, - EngineRoot, - ProjectRoot, - ProjectFilePath, - ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : ""); - - m_ProjectStats.ProjectWriteCount++; - HttpReq.WriteResponse(HttpResponseCode::Created); - } - break; - - case HttpVerb::kPut: - { - if (!m_ProjectStore->AreDiskWritesAllowed()) + CbValidateError ValidateResult; + if (CbObject Params = ValidateAndReadCompactBinaryObject(HttpReq.ReadPayload(), ValidateResult); + ValidateResult == CbValidateError::None) { - return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); - } - - IoBuffer Payload = HttpReq.ReadPayload(); - CbObject Params = LoadCompactBinaryObject(Payload); - std::filesystem::path Root = Params["root"sv].AsU8String(); // Workspace root (i.e `D:/UE5/`) - std::filesystem::path EngineRoot = Params["engine"sv].AsU8String(); // Engine root (i.e `D:/UE5/Engine`) - std::filesystem::path ProjectRoot = - Params["project"sv].AsU8String(); // Project root directory (i.e `D:/UE5/Samples/Games/Lyra`) - std::filesystem::path ProjectFilePath = - Params["projectfile"sv].AsU8String(); // Project file path (i.e `D:/UE5/Samples/Games/Lyra/Lyra.uproject`) + std::filesystem::path Root = Params["root"sv].AsU8String(); // Workspace root (i.e `D:/UE5/`) + std::filesystem::path EngineRoot = Params["engine"sv].AsU8String(); // Engine root (i.e `D:/UE5/Engine`) + std::filesystem::path ProjectRoot = + Params["project"sv].AsU8String(); // Project root directory (i.e `D:/UE5/Samples/Games/Lyra`) + std::filesystem::path ProjectFilePath = + Params["projectfile"sv].AsU8String(); // Project file path (i.e `D:/UE5/Samples/Games/Lyra/Lyra.uproject`) - if (m_ProjectStore->UpdateProject(ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath)) - { - m_ProjectStats.ProjectWriteCount++; - ZEN_INFO("updated project (id: '{}', roots: '{}', '{}', '{}', '{}'{})", - ProjectId, - Root, - EngineRoot, - ProjectRoot, - ProjectFilePath, - ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : ""); - - HttpReq.WriteResponse(HttpResponseCode::OK); - } - else - { const std::filesystem::path BasePath = m_ProjectStore->BasePath() / ProjectId; m_ProjectStore->NewProject(BasePath, ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath); - m_ProjectStats.ProjectWriteCount++; ZEN_INFO("established project (id: '{}', roots: '{}', '{}', '{}', '{}'{})", ProjectId, Root, @@ -1844,8 +2138,71 @@ HttpProjectService::HandleProjectRequest(HttpRouterRequest& Req) ProjectFilePath, ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : ""); + m_ProjectStats.ProjectWriteCount++; HttpReq.WriteResponse(HttpResponseCode::Created); } + else + { + HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Malformed compact binary object: '{}'", ToString(ValidateResult))); + } + } + break; + + case HttpVerb::kPut: + { + if (!m_ProjectStore->AreDiskWritesAllowed()) + { + return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); + } + CbValidateError ValidateResult; + if (CbObject Params = ValidateAndReadCompactBinaryObject(HttpReq.ReadPayload(), ValidateResult); + ValidateResult == CbValidateError::None) + { + std::filesystem::path Root = Params["root"sv].AsU8String(); // Workspace root (i.e `D:/UE5/`) + std::filesystem::path EngineRoot = Params["engine"sv].AsU8String(); // Engine root (i.e `D:/UE5/Engine`) + std::filesystem::path ProjectRoot = + Params["project"sv].AsU8String(); // Project root directory (i.e `D:/UE5/Samples/Games/Lyra`) + std::filesystem::path ProjectFilePath = + Params["projectfile"sv].AsU8String(); // Project file path (i.e `D:/UE5/Samples/Games/Lyra/Lyra.uproject`) + + if (m_ProjectStore->UpdateProject(ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath)) + { + m_ProjectStats.ProjectWriteCount++; + ZEN_INFO("updated project (id: '{}', roots: '{}', '{}', '{}', '{}'{})", + ProjectId, + Root, + EngineRoot, + ProjectRoot, + ProjectFilePath, + ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : ""); + + HttpReq.WriteResponse(HttpResponseCode::OK); + } + else + { + const std::filesystem::path BasePath = m_ProjectStore->BasePath() / ProjectId; + m_ProjectStore->NewProject(BasePath, ProjectId, Root, EngineRoot, ProjectRoot, ProjectFilePath); + + m_ProjectStats.ProjectWriteCount++; + ZEN_INFO("established project (id: '{}', roots: '{}', '{}', '{}', '{}'{})", + ProjectId, + Root, + EngineRoot, + ProjectRoot, + ProjectFilePath, + ProjectFilePath.empty() ? ", project will not be GCd due to empty project file path" : ""); + + HttpReq.WriteResponse(HttpResponseCode::Created); + } + } + else + { + HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Malformed compact binary object: '{}'", ToString(ValidateResult))); + } } break; @@ -1933,29 +2290,113 @@ HttpProjectService::HandleOplogSaveRequest(HttpRouterRequest& Req) } IoBuffer Payload = HttpReq.ReadPayload(); - CbObject Response; - std::pair<HttpResponseCode, std::string> Result = m_ProjectStore->WriteOplog(ProjectId, OplogId, std::move(Payload), Response); - if (Result.first == HttpResponseCode::OK) + Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); + if (!Project) { - return HttpReq.WriteResponse(HttpResponseCode::OK, Response); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Write oplog request for unknown project '{}'", ProjectId)); } - else + Project->TouchProject(); + + Ref<ProjectStore::Oplog> Oplog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ false); + if (!Oplog) { - if (Result.first == HttpResponseCode::BadRequest) + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Write oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)); + } + Project->TouchOplog(OplogId); + + CbValidateError ValidateResult; + if (CbObject ContainerObject = ValidateAndReadCompactBinaryObject(std::move(Payload), ValidateResult); + ValidateResult == CbValidateError::None && ContainerObject) + { + RwLock AttachmentsLock; + tsl::robin_set<IoHash, IoHash::Hasher> Attachments; + + auto HasAttachment = [this](const IoHash& RawHash) { return m_CidStore.ContainsChunk(RawHash); }; + auto OnNeedBlock = [&AttachmentsLock, &Attachments](const IoHash& BlockHash, const std::vector<IoHash>&& ChunkHashes) { + RwLock::ExclusiveLockScope _(AttachmentsLock); + if (BlockHash != IoHash::Zero) + { + Attachments.insert(BlockHash); + } + else + { + Attachments.insert(ChunkHashes.begin(), ChunkHashes.end()); + } + }; + auto OnNeedAttachment = [&AttachmentsLock, &Attachments](const IoHash& RawHash) { + RwLock::ExclusiveLockScope _(AttachmentsLock); + Attachments.insert(RawHash); + }; + + auto OnChunkedAttachment = [](const ChunkedInfo&) {}; + + auto OnReferencedAttachments = [&Oplog](std::span<IoHash> RawHashes) { Oplog->CaptureAddedAttachments(RawHashes); }; + + // Make sure we retain any attachments we download before writing the oplog + Oplog->EnableUpdateCapture(); + auto _ = MakeGuard([&Oplog]() { Oplog->DisableUpdateCapture(); }); + + RemoteProjectStore::Result Result = SaveOplogContainer(*Oplog, + ContainerObject, + OnReferencedAttachments, + HasAttachment, + OnNeedBlock, + OnNeedAttachment, + OnChunkedAttachment, + nullptr); + + if (Result.ErrorCode == 0) { - m_ProjectStats.BadRequestCount++; + if (Attachments.empty()) + { + HttpReq.WriteResponse(HttpResponseCode::OK); + } + else + { + CbObjectWriter Cbo(1 + 1 + 5 + Attachments.size() * (1 + sizeof(IoHash::Hash)) + 1); + Cbo.BeginArray("need"); + { + for (const IoHash& Hash : Attachments) + { + ZEN_DEBUG("Need attachment {}", Hash); + Cbo << Hash; + } + } + Cbo.EndArray(); // "need" + + CbObject ResponsePayload = Cbo.Save(); + return HttpReq.WriteResponse(HttpResponseCode::OK, ResponsePayload); + } + } + else + { + ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", + ToString(HttpReq.RequestVerb()), + HttpReq.QueryString(), + Result.ErrorCode, + Result.Reason); + + if (Result.Reason.empty()) + { + return HttpReq.WriteResponse(HttpResponseCode(Result.ErrorCode)); + } + else + { + return HttpReq.WriteResponse(HttpResponseCode(Result.ErrorCode), HttpContentType::kText, Result.Reason); + } } - ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", - ToString(HttpReq.RequestVerb()), - HttpReq.QueryString(), - static_cast<int>(Result.first), - Result.second); } - if (Result.second.empty()) + else { - return HttpReq.WriteResponse(Result.first); + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Invalid payload: '{}'", ToString(ValidateResult))); } - return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void @@ -1966,42 +2407,114 @@ HttpProjectService::HandleOplogLoadRequest(HttpRouterRequest& Req) HttpServerRequest& HttpReq = Req.ServerRequest(); const auto& ProjectId = Req.GetCapture(1); const auto& OplogId = Req.GetCapture(2); + + const HttpServerRequest::QueryParams Params = HttpReq.GetQueryParams(); + if (HttpReq.AcceptContentType() != HttpContentType::kCbObject) { m_ProjectStats.BadRequestCount++; return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid accept content type"); } - IoBuffer Payload = HttpReq.ReadPayload(); - CbObject Response; - std::pair<HttpResponseCode, std::string> Result = m_ProjectStore->ReadOplog(ProjectId, OplogId, HttpReq.GetQueryParams(), Response); - if (Result.first == HttpResponseCode::OK) + Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); + if (!Project) { - return HttpReq.WriteResponse(HttpResponseCode::OK, Response); + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Read oplog request for unknown project '{}'", ProjectId)); } - else + Project->TouchProject(); + + Ref<ProjectStore::Oplog> Oplog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); + if (!Oplog) { - if (Result.first == HttpResponseCode::BadRequest) + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Read oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)); + } + Project->TouchOplog(OplogId); + + size_t MaxBlockSize = RemoteStoreOptions::DefaultMaxBlockSize; + if (auto Param = Params.GetValue("maxblocksize"); Param.empty() == false) + { + if (auto Value = ParseInt<size_t>(Param)) { - m_ProjectStats.BadRequestCount++; + MaxBlockSize = Value.value(); + } + } + size_t MaxChunkEmbedSize = RemoteStoreOptions::DefaultMaxChunkEmbedSize; + if (auto Param = Params.GetValue("maxchunkembedsize"); Param.empty() == false) + { + if (auto Value = ParseInt<size_t>(Param)) + { + MaxChunkEmbedSize = Value.value(); + } + } + size_t MaxChunksPerBlock = RemoteStoreOptions::DefaultMaxChunksPerBlock; + if (auto Param = Params.GetValue("maxchunksperblock"); Param.empty() == false) + { + if (auto Value = ParseInt<size_t>(Param)) + { + MaxChunksPerBlock = Value.value(); + } + } + + size_t ChunkFileSizeLimit = RemoteStoreOptions::DefaultChunkFileSizeLimit; + if (auto Param = Params.GetValue("chunkfilesizelimit"); Param.empty() == false) + { + if (auto Value = ParseInt<size_t>(Param)) + { + ChunkFileSizeLimit = Value.value(); } + } + + WorkerThreadPool& WorkerPool = GetLargeWorkerPool(EWorkloadType::Background); + + RemoteProjectStore::LoadContainerResult ContainerResult = BuildContainer( + m_CidStore, + *Project, + *Oplog, + WorkerPool, + MaxBlockSize, + MaxChunkEmbedSize, + MaxChunksPerBlock, + ChunkFileSizeLimit, + /* BuildBlocks */ false, + /* IgnoreMissingAttachments */ false, + /* AllowChunking*/ false, + [](CompressedBuffer&&, ChunkBlockDescription&&) {}, + [](const IoHash&, TGetAttachmentBufferFunc&&) {}, + [](std::vector<std::pair<IoHash, FetchChunkFunc>>&&) {}, + /* EmbedLooseFiles*/ false); + + if (ContainerResult.ErrorCode == 0) + { + return HttpReq.WriteResponse(HttpResponseCode::OK, ContainerResult.ContainerObject); + } + else + { ZEN_DEBUG("Request {}: '{}' failed with {}. Reason: `{}`", ToString(HttpReq.RequestVerb()), HttpReq.QueryString(), - static_cast<int>(Result.first), - Result.second); - } - if (Result.second.empty()) - { - return HttpReq.WriteResponse(Result.first); + ContainerResult.ErrorCode, + ContainerResult.Reason); + + if (ContainerResult.Reason.empty()) + { + return HttpReq.WriteResponse(HttpResponseCode(ContainerResult.ErrorCode)); + } + else + { + return HttpReq.WriteResponse(HttpResponseCode(ContainerResult.ErrorCode), HttpContentType::kText, ContainerResult.Reason); + } } - return HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); } void HttpProjectService::HandleRpcRequest(HttpRouterRequest& Req) { ZEN_TRACE_CPU("ProjectService::Rpc"); + using namespace std::literals; HttpServerRequest& HttpReq = Req.ServerRequest(); @@ -2009,10 +2522,558 @@ HttpProjectService::HandleRpcRequest(HttpRouterRequest& Req) const auto& OplogId = Req.GetCapture(2); IoBuffer Payload = HttpReq.ReadPayload(); - bool OkRequest = m_ProjectStore->Rpc(HttpReq, ProjectId, OplogId, std::move(Payload), m_AuthMgr); - if (!OkRequest) + HttpContentType PayloadContentType = HttpReq.RequestContentType(); + CbPackage Package; + CbObject Cb; + switch (PayloadContentType) { - m_ProjectStats.BadRequestCount++; + case HttpContentType::kJSON: + case HttpContentType::kUnknownContentType: + case HttpContentType::kText: + { + std::string JsonText(reinterpret_cast<const char*>(Payload.GetData()), Payload.GetSize()); + Cb = LoadCompactBinaryFromJson(JsonText).AsObject(); + if (!Cb) + { + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + "Content format not supported, expected JSON format"); + } + } + break; + case HttpContentType::kCbObject: + { + CbValidateError ValidateResult; + if (Cb = ValidateAndReadCompactBinaryObject(std::move(Payload), ValidateResult); + ValidateResult != CbValidateError::None || !Cb) + { + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse( + HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Content format not supported, expected compact binary format ('{}')", ToString(ValidateResult))); + } + break; + } + case HttpContentType::kCbPackage: + try + { + Package = ParsePackageMessage(Payload); + Cb = Package.GetObject(); + } + catch (const std::invalid_argument& ex) + { + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Failed to parse package request, reason: '{}'", ex.what())); + } + if (!Cb) + { + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + "Content format not supported, expected package message format"); + } + break; + default: + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid request content type"); + } + + Ref<ProjectStore::Project> Project = m_ProjectStore->OpenProject(ProjectId); + if (!Project) + { + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Rpc oplog request for unknown project '{}'", ProjectId)); + } + Project->TouchProject(); + + std::string_view Method = Cb["method"sv].AsString(); + + bool VerifyPathOnDisk = Method != "getchunks"sv; + + Ref<ProjectStore::Oplog> Oplog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, VerifyPathOnDisk); + if (!Oplog) + { + return HttpReq.WriteResponse(HttpResponseCode::NotFound, + HttpContentType::kText, + fmt::format("Rpc oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)); + } + Project->TouchOplog(OplogId); + + uint32_t MethodHash = HashStringDjb2(Method); + + switch (MethodHash) + { + case HashStringDjb2("import"sv): + { + if (!m_ProjectStore->AreDiskWritesAllowed()) + { + return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); + } + + CbObjectView Params = Cb["params"sv].AsObjectView(); + size_t MaxBlockSize = Params["maxblocksize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxBlockSize); + size_t MaxChunkEmbedSize = Params["maxchunkembedsize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxChunkEmbedSize); + bool Force = Params["force"sv].AsBool(false); + bool IgnoreMissingAttachments = Params["ignoremissingattachments"sv].AsBool(false); + bool CleanOplog = Params["clean"].AsBool(false); + + CreateRemoteStoreResult RemoteStoreResult = + CreateRemoteStore(Params, m_AuthMgr, MaxBlockSize, MaxChunkEmbedSize, Oplog->TempPath()); + + if (RemoteStoreResult.Store == nullptr) + { + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, RemoteStoreResult.Description); + } + std::shared_ptr<RemoteProjectStore> RemoteStore = std::move(RemoteStoreResult.Store); + RemoteProjectStore::RemoteStoreInfo StoreInfo = RemoteStore->GetInfo(); + + JobId JobId = m_JobQueue.QueueJob( + fmt::format("Import oplog '{}/{}'", Project->Identifier, Oplog->OplogId()), + [this, + ChunkStore = &m_CidStore, + ActualRemoteStore = std::move(RemoteStore), + Oplog, + Force, + IgnoreMissingAttachments, + CleanOplog](JobContext& Context) { + Context.ReportMessage(fmt::format("Loading oplog '{}/{}' from {}", + Oplog->GetOuterProjectIdentifier(), + Oplog->OplogId(), + ActualRemoteStore->GetInfo().Description)); + + WorkerThreadPool& WorkerPool = GetLargeWorkerPool(EWorkloadType::Background); + WorkerThreadPool& NetworkWorkerPool = GetMediumWorkerPool(EWorkloadType::Background); + + RemoteProjectStore::Result Result = LoadOplog(m_CidStore, + *ActualRemoteStore, + *Oplog, + NetworkWorkerPool, + WorkerPool, + Force, + IgnoreMissingAttachments, + CleanOplog, + &Context); + auto Response = ConvertResult(Result); + ZEN_INFO("LoadOplog: Status: {} '{}'", ToString(Response.first), Response.second); + if (!IsHttpSuccessCode(Response.first)) + { + throw JobError(Response.second.empty() ? fmt::format("Status: {}", ToString(Response.first)) : Response.second, + (int)Response.first); + } + }); + + return HttpReq.WriteResponse(HttpResponseCode::Accepted, HttpContentType::kText, fmt::format("{}", JobId.Id)); + } + case HashStringDjb2("export"sv): + { + CbObjectView Params = Cb["params"sv].AsObjectView(); + size_t MaxBlockSize = Params["maxblocksize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxBlockSize); + size_t MaxChunkEmbedSize = Params["maxchunkembedsize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxChunkEmbedSize); + size_t MaxChunksPerBlock = Params["maxchunksperblock"sv].AsUInt64(RemoteStoreOptions::DefaultMaxChunksPerBlock); + size_t ChunkFileSizeLimit = Params["chunkfilesizelimit"sv].AsUInt64(RemoteStoreOptions::DefaultChunkFileSizeLimit); + bool Force = Params["force"sv].AsBool(false); + bool IgnoreMissingAttachments = Params["ignoremissingattachments"sv].AsBool(false); + bool EmbedLooseFile = Params["embedloosefiles"sv].AsBool(false); + + CreateRemoteStoreResult RemoteStoreResult = + CreateRemoteStore(Params, m_AuthMgr, MaxBlockSize, MaxChunkEmbedSize, Oplog->TempPath()); + + if (RemoteStoreResult.Store == nullptr) + { + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, RemoteStoreResult.Description); + } + std::shared_ptr<RemoteProjectStore> RemoteStore = std::move(RemoteStoreResult.Store); + RemoteProjectStore::RemoteStoreInfo StoreInfo = RemoteStore->GetInfo(); + + JobId JobId = m_JobQueue.QueueJob( + fmt::format("Export oplog '{}/{}'", Project->Identifier, Oplog->OplogId()), + [this, + ActualRemoteStore = std::move(RemoteStore), + Project, + Oplog, + MaxBlockSize, + MaxChunksPerBlock, + MaxChunkEmbedSize, + ChunkFileSizeLimit, + EmbedLooseFile, + Force, + IgnoreMissingAttachments](JobContext& Context) { + Context.ReportMessage(fmt::format("Saving oplog '{}/{}' to {}, maxblocksize {}, maxchunkembedsize {}", + Project->Identifier, + Oplog->OplogId(), + ActualRemoteStore->GetInfo().Description, + NiceBytes(MaxBlockSize), + NiceBytes(MaxChunkEmbedSize))); + + WorkerThreadPool& WorkerPool = GetLargeWorkerPool(EWorkloadType::Background); + WorkerThreadPool& NetworkWorkerPool = GetMediumWorkerPool(EWorkloadType::Background); + + RemoteProjectStore::Result Result = SaveOplog(m_CidStore, + *ActualRemoteStore, + *Project, + *Oplog, + NetworkWorkerPool, + WorkerPool, + MaxBlockSize, + MaxChunksPerBlock, + MaxChunkEmbedSize, + ChunkFileSizeLimit, + EmbedLooseFile, + Force, + IgnoreMissingAttachments, + &Context); + auto Response = ConvertResult(Result); + ZEN_INFO("SaveOplog: Status: {} '{}'", ToString(Response.first), Response.second); + if (!IsHttpSuccessCode(Response.first)) + { + throw JobError(Response.second.empty() ? fmt::format("Status: {}", ToString(Response.first)) : Response.second, + (int)Response.first); + } + }); + + return HttpReq.WriteResponse(HttpResponseCode::Accepted, HttpContentType::kText, fmt::format("{}", JobId.Id)); + } + case HashStringDjb2("getchunks"sv): + { + RpcAcceptOptions AcceptFlags = static_cast<RpcAcceptOptions>(Cb["AcceptFlags"sv].AsUInt16(0u)); + int32_t TargetProcessId = Cb["Pid"sv].AsInt32(0); + + std::vector<ProjectStore::ChunkRequest> Requests = m_ProjectStore->ParseChunksRequests(*Project, *Oplog, Cb); + std::vector<ProjectStore::ChunkResult> Results = + Requests.empty() ? std::vector<ProjectStore::ChunkResult>{} : m_ProjectStore->GetChunks(*Project, *Oplog, Requests); + CbPackage Response = m_ProjectStore->WriteChunksRequestResponse(*Project, *Oplog, std::move(Requests), std::move(Results)); + + void* TargetProcessHandle = nullptr; + FormatFlags Flags = FormatFlags::kDefault; + if (EnumHasAllFlags(AcceptFlags, RpcAcceptOptions::kAllowLocalReferences)) + { + Flags |= FormatFlags::kAllowLocalReferences; + if (!EnumHasAnyFlags(AcceptFlags, RpcAcceptOptions::kAllowPartialLocalReferences)) + { + Flags |= FormatFlags::kDenyPartialLocalReferences; + } + TargetProcessHandle = m_OpenProcessCache.GetProcessHandle(HttpReq.SessionId(), TargetProcessId); + } + + CompositeBuffer RpcResponseBuffer = FormatPackageMessageBuffer(Response, Flags, TargetProcessHandle); + return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCbPackage, RpcResponseBuffer); + } + case HashStringDjb2("putchunks"sv): + { + ZEN_TRACE_CPU("Store::Rpc::putchunks"); + if (!m_ProjectStore->AreDiskWritesAllowed()) + { + return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); + } + + CbObject Object = Package.GetObject(); + const bool UsingTempFiles = Object["usingtmpfiles"].AsBool(false); + + std::span<const CbAttachment> Attachments = Package.GetAttachments(); + if (!Attachments.empty()) + { + std::vector<IoBuffer> WriteAttachmentBuffers; + std::vector<IoHash> WriteRawHashes; + + WriteAttachmentBuffers.reserve(Attachments.size()); + WriteRawHashes.reserve(Attachments.size()); + + for (const CbAttachment& Attachment : Attachments) + { + IoHash RawHash = Attachment.GetHash(); + const CompressedBuffer& Compressed = Attachment.AsCompressedBinary(); + IoBuffer AttachmentPayload = Compressed.GetCompressed().Flatten().AsIoBuffer(); + if (UsingTempFiles) + { + AttachmentPayload.SetDeleteOnClose(true); + } + WriteAttachmentBuffers.push_back(std::move(AttachmentPayload)); + WriteRawHashes.push_back(RawHash); + } + + Oplog->CaptureAddedAttachments(WriteRawHashes); + m_CidStore.AddChunks(WriteAttachmentBuffers, + WriteRawHashes, + UsingTempFiles ? CidStore::InsertMode::kMayBeMovedInPlace : CidStore::InsertMode::kCopyOnly); + } + return HttpReq.WriteResponse(HttpResponseCode::OK); + } + case HashStringDjb2("snapshot"sv): + { + ZEN_TRACE_CPU("Store::Rpc::snapshot"); + if (!m_ProjectStore->AreDiskWritesAllowed()) + { + return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); + } + + // Snapshot all referenced files. This brings the content of all + // files into the CID store + + uint32_t OpCount = 0; + uint64_t InlinedBytes = 0; + uint64_t InlinedFiles = 0; + uint64_t TotalBytes = 0; + uint64_t TotalFiles = 0; + + std::vector<CbObject> NewOps; + struct AddedChunk + { + IoBuffer Buffer; + uint64_t RawSize = 0; + }; + tsl::robin_map<IoHash, AddedChunk, IoHash::Hasher> AddedChunks; + + Oplog->IterateOplog( + [&](CbObjectView Op) { + bool OpRewritten = false; + bool AllOk = true; + + CbWriter FilesArrayWriter; + FilesArrayWriter.BeginArray("files"sv); + + for (CbFieldView& Field : Op["files"sv]) + { + bool CopyField = true; + + if (CbObjectView View = Field.AsObjectView()) + { + const IoHash DataHash = View["data"sv].AsHash(); + + if (DataHash == IoHash::Zero) + { + std::string_view ServerPath = View["serverpath"sv].AsString(); + std::filesystem::path FilePath = Project->RootDir / ServerPath; + BasicFile DataFile; + std::error_code Ec; + DataFile.Open(FilePath, BasicFile::Mode::kRead, Ec); + + if (Ec) + { + // Error... + + ZEN_ERROR("unable to read data from file '{}': {}", FilePath, Ec.message()); + + AllOk = false; + } + else + { + // Read file contents into memory, compress and keep in map of chunks to add to Cid store + IoBuffer FileIoBuffer = DataFile.ReadAll(); + CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(std::move(FileIoBuffer))); + const uint64_t RawSize = Compressed.DecodeRawSize(); + const IoHash RawHash = Compressed.DecodeRawHash(); + if (!AddedChunks.contains(RawHash)) + { + const std::filesystem::path TempChunkPath = Oplog->TempPath() / RawHash.ToHexString(); + BasicFile ChunkTempFile; + ChunkTempFile.Open(TempChunkPath, BasicFile::Mode::kTruncateDelete); + ChunkTempFile.Write(Compressed.GetCompressed(), 0, Ec); + if (Ec) + { + Oid ChunkId = View["id"sv].AsObjectId(); + ZEN_ERROR("unable to write external file as compressed chunk '{}', id {}: {}", + FilePath, + ChunkId, + Ec.message()); + AllOk = false; + } + else + { + void* FileHandle = ChunkTempFile.Detach(); + IoBuffer ChunkBuffer(IoBuffer::File, + FileHandle, + 0, + Compressed.GetCompressed().GetSize(), + /*IsWholeFile*/ true); + ChunkBuffer.SetDeleteOnClose(true); + AddedChunks.insert_or_assign( + RawHash, + AddedChunk{.Buffer = std::move(ChunkBuffer), .RawSize = RawSize}); + } + } + + TotalBytes += RawSize; + ++TotalFiles; + + // Rewrite file array entry with new data reference + CbObjectWriter Writer(View.GetSize()); + RewriteCbObject(Writer, View, [&](CbObjectWriter&, CbFieldView Field) -> bool { + if (Field.GetName() == "data"sv) + { + // omit this field as we will write it explicitly ourselves + return true; + } + return false; + }); + Writer.AddBinaryAttachment("data"sv, RawHash); + + CbObject RewrittenOp = Writer.Save(); + FilesArrayWriter.AddObject(std::move(RewrittenOp)); + CopyField = false; + } + } + } + + if (CopyField) + { + FilesArrayWriter.AddField(Field); + } + else + { + OpRewritten = true; + } + } + + if (OpRewritten && AllOk) + { + FilesArrayWriter.EndArray(); + CbArray FilesArray = FilesArrayWriter.Save().AsArray(); + + CbObject RewrittenOp = RewriteCbObject(Op, [&](CbObjectWriter& NewWriter, CbFieldView Field) -> bool { + if (Field.GetName() == "files"sv) + { + NewWriter.AddArray("files"sv, FilesArray); + + return true; + } + + return false; + }); + + NewOps.push_back(std::move(RewrittenOp)); + } + + OpCount++; + }, + ProjectStore::Oplog::Paging{}); + + CbObjectWriter ResponseObj; + + // Persist rewritten oplog entries + if (!NewOps.empty()) + { + ResponseObj.BeginArray("rewritten_ops"); + + for (CbObject& NewOp : NewOps) + { + ProjectStore::LogSequenceNumber NewLsn = Oplog->AppendNewOplogEntry(std::move(NewOp)); + + ZEN_DEBUG("appended rewritten op at LSN: {}", NewLsn.Number); + + ResponseObj.AddInteger(NewLsn.Number); + } + + ResponseObj.EndArray(); + } + + // Ops that have moved chunks to a compressed buffer for storage in m_CidStore have been rewritten with references to the + // new chunk(s). Make sure we add the chunks to m_CidStore, and do it after we update the oplog so GC doesn't think we have + // unreferenced chunks. + for (auto It : AddedChunks) + { + const IoHash& RawHash = It.first; + AddedChunk& Chunk = It.second; + CidStore::InsertResult Result = m_CidStore.AddChunk(Chunk.Buffer, RawHash); + if (Result.New) + { + InlinedBytes += Chunk.RawSize; + ++InlinedFiles; + } + } + + ResponseObj << "inlined_bytes" << InlinedBytes << "inlined_files" << InlinedFiles; + ResponseObj << "total_bytes" << TotalBytes << "total_files" << TotalFiles; + + ZEN_INFO("oplog '{}/{}': rewrote {} oplog entries (out of {})", ProjectId, OplogId, NewOps.size(), OpCount); + + return HttpReq.WriteResponse(HttpResponseCode::OK, ResponseObj.Save()); + } + case HashStringDjb2("appendops"sv): + { + ZEN_TRACE_CPU("Store::Rpc::appendops"); + if (!m_ProjectStore->AreDiskWritesAllowed()) + { + return HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); + } + + CbArrayView OpsArray = Cb["ops"sv].AsArrayView(); + + size_t OpsBufferSize = 0; + for (CbFieldView OpView : OpsArray) + { + OpsBufferSize += OpView.GetSize(); + } + UniqueBuffer OpsBuffers = UniqueBuffer::Alloc(OpsBufferSize); + MutableMemoryView OpsBuffersMemory = OpsBuffers.GetMutableView(); + + std::vector<CbObjectView> Ops; + Ops.reserve(OpsArray.Num()); + for (CbFieldView OpView : OpsArray) + { + OpView.CopyTo(OpsBuffersMemory); + Ops.push_back(CbObjectView(OpsBuffersMemory.GetData())); + OpsBuffersMemory.MidInline(OpView.GetSize()); + } + + std::vector<ProjectStore::LogSequenceNumber> LSNs = Oplog->AppendNewOplogEntries(Ops); + ZEN_ASSERT(LSNs.size() == Ops.size()); + + std::vector<IoHash> MissingAttachments; + for (size_t OpIndex = 0; OpIndex < Ops.size(); OpIndex++) + { + if (LSNs[OpIndex]) + { + CbObjectView Op = Ops[OpIndex]; + Op.IterateAttachments([this, &MissingAttachments](CbFieldView AttachmentView) { + const IoHash Cid = AttachmentView.AsAttachment(); + if (!m_CidStore.ContainsChunk(Cid)) + { + MissingAttachments.push_back(Cid); + } + }); + } + } + + CbPackage ResponsePackage; + + { + CbObjectWriter ResponseObj; + ResponseObj.BeginArray("written_ops"); + + for (ProjectStore::LogSequenceNumber NewLsn : LSNs) + { + ZEN_DEBUG("appended written op at LSN: {}", NewLsn.Number); + ResponseObj.AddInteger(NewLsn.Number); + } + ResponseObj.EndArray(); + + if (!MissingAttachments.empty()) + { + ResponseObj.BeginArray("need"); + + for (const IoHash& Cid : MissingAttachments) + { + ResponseObj.AddHash(Cid); + } + ResponseObj.EndArray(); + } + ResponsePackage.SetObject(ResponseObj.Save()); + } + + std::vector<IoBuffer> ResponseBuffers = FormatPackageMessage(ResponsePackage); + + return HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCbPackage, ResponseBuffers); + } + default: + m_ProjectStats.BadRequestCount++; + return HttpReq.WriteResponse(HttpResponseCode::BadRequest, + HttpContentType::kText, + fmt::format("Unknown rpc method '{}'", Method)); } } void @@ -2038,7 +3099,9 @@ HttpProjectService::HandleDetailsRequest(HttpRouterRequest& Req) m_ProjectStore->IterateProjects([&](ProjectStore::Project& Project) { Project.IterateOplogs([&](const RwLock::SharedLockScope&, ProjectStore::Oplog& Oplog) { Oplog.IterateOplogWithKey( - [this, &Project, &Oplog, &CSVWriter, Details, AttachmentDetails](uint32_t LSN, const Oid& Key, CbObjectView Op) { + [this, &Project, &Oplog, &CSVWriter, Details, AttachmentDetails](ProjectStore::LogSequenceNumber LSN, + const Oid& Key, + CbObjectView Op) { CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter); }); }); @@ -2092,10 +3155,11 @@ HttpProjectService::HandleProjectDetailsRequest(HttpRouterRequest& Req) CSVHeader(Details, AttachmentDetails, CSVWriter); FoundProject->IterateOplogs([&](const RwLock::SharedLockScope&, ProjectStore::Oplog& Oplog) { - Oplog.IterateOplogWithKey( - [this, &Project, &Oplog, &CSVWriter, Details, AttachmentDetails](uint32_t LSN, const Oid& Key, CbObjectView Op) { - CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter); - }); + Oplog.IterateOplogWithKey([this, &Project, &Oplog, &CSVWriter, Details, AttachmentDetails](ProjectStore::LogSequenceNumber LSN, + const Oid& Key, + CbObjectView Op) { + CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter); + }); }); HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView()); } @@ -2135,7 +3199,7 @@ HttpProjectService::HandleOplogDetailsRequest(HttpRouterRequest& Req) return HttpReq.WriteResponse(HttpResponseCode::NotFound); } - ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); + Ref<ProjectStore::Oplog> FoundLog = FoundProject->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); @@ -2148,10 +3212,11 @@ HttpProjectService::HandleOplogDetailsRequest(HttpRouterRequest& Req) ExtendableStringBuilder<4096> CSVWriter; CSVHeader(Details, AttachmentDetails, CSVWriter); - Oplog.IterateOplogWithKey( - [this, &Project, &Oplog, &CSVWriter, Details, AttachmentDetails](uint32_t LSN, const Oid& Key, CbObjectView Op) { - CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter); - }); + Oplog.IterateOplogWithKey([this, &Project, &Oplog, &CSVWriter, Details, AttachmentDetails](ProjectStore::LogSequenceNumber LSN, + const Oid& Key, + CbObjectView Op) { + CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, Key, Op, CSVWriter); + }); HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView()); } else @@ -2190,7 +3255,7 @@ HttpProjectService::HandleOplogOpDetailsRequest(HttpRouterRequest& Req) return HttpReq.WriteResponse(HttpResponseCode::NotFound); } - ProjectStore::Oplog* FoundLog = FoundProject->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); + Ref<ProjectStore::Oplog> FoundLog = FoundProject->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true); if (!FoundLog) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); @@ -2213,8 +3278,8 @@ HttpProjectService::HandleOplogOpDetailsRequest(HttpRouterRequest& Req) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } - std::optional<uint32_t> LSN = Oplog.GetOpIndexByKey(ObjId); - if (!LSN.has_value()) + ProjectStore::LogSequenceNumber LSN = Oplog.GetOpIndexByKey(ObjId); + if (!LSN) { return HttpReq.WriteResponse(HttpResponseCode::NotFound); } @@ -2224,7 +3289,7 @@ HttpProjectService::HandleOplogOpDetailsRequest(HttpRouterRequest& Req) ExtendableStringBuilder<4096> CSVWriter; CSVHeader(Details, AttachmentDetails, CSVWriter); - CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN.value(), ObjId, Op.value(), CSVWriter); + CSVWriteOp(m_CidStore, Project.Identifier, Oplog.OplogId(), Details, AttachmentDetails, LSN, ObjId, Op.value(), CSVWriter); HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, CSVWriter.ToView()); } else @@ -2232,7 +3297,7 @@ HttpProjectService::HandleOplogOpDetailsRequest(HttpRouterRequest& Req) CbObjectWriter Cbo; Cbo.BeginArray("ops"); { - CbWriteOp(m_CidStore, Details, OpDetails, AttachmentDetails, LSN.value(), ObjId, Op.value(), Cbo); + CbWriteOp(m_CidStore, Details, OpDetails, AttachmentDetails, LSN, ObjId, Op.value(), Cbo); } Cbo.EndArray(); HttpReq.WriteResponse(HttpResponseCode::OK, Cbo.Save()); diff --git a/src/zenserver/projectstore/httpprojectstore.h b/src/zenserver/projectstore/httpprojectstore.h index 295defa5c..f0a0bcfa1 100644 --- a/src/zenserver/projectstore/httpprojectstore.h +++ b/src/zenserver/projectstore/httpprojectstore.h @@ -11,6 +11,8 @@ namespace zen { class AuthMgr; +class JobQueue; +class OpenProcessCache; class ProjectStore; ////////////////////////////////////////////////////////////////////////// @@ -39,7 +41,9 @@ public: ProjectStore* InProjectStore, HttpStatusService& StatusService, HttpStatsService& StatsService, - AuthMgr& AuthMgr); + AuthMgr& AuthMgr, + OpenProcessCache& InOpenProcessCache, + JobQueue& InJobQueue); ~HttpProjectService(); virtual const char* BaseUri() const override; @@ -98,6 +102,8 @@ private: HttpStatusService& m_StatusService; HttpStatsService& m_StatsService; AuthMgr& m_AuthMgr; + OpenProcessCache& m_OpenProcessCache; + JobQueue& m_JobQueue; ProjectStats m_ProjectStats; metrics::OperationTiming m_HttpRequests; }; diff --git a/src/zenserver/upstream/upstreamcache.cpp b/src/zenserver/upstream/upstreamcache.cpp index 744b861dd..8558e2a10 100644 --- a/src/zenserver/upstream/upstreamcache.cpp +++ b/src/zenserver/upstream/upstreamcache.cpp @@ -20,8 +20,8 @@ #include <zenstore/cache/structuredcachestore.h> #include <zenstore/cidstore.h> -#include <zenutil/jupiter/jupiterclient.h> -#include <zenutil/jupiter/jupitersession.h> +#include <zenremotestore/jupiter/jupiterclient.h> +#include <zenremotestore/jupiter/jupitersession.h> #include "cache/httpstructuredcache.h" #include "diag/logging.h" diff --git a/src/zenserver/upstream/upstreamcache.h b/src/zenserver/upstream/upstreamcache.h index 26e5decac..d5d61c8d9 100644 --- a/src/zenserver/upstream/upstreamcache.h +++ b/src/zenserver/upstream/upstreamcache.h @@ -8,8 +8,8 @@ #include <zencore/iohash.h> #include <zencore/stats.h> #include <zencore/zencore.h> +#include <zenstore/cache/cache.h> #include <zenstore/cache/upstreamcacheclient.h> -#include <zenutil/cache/cache.h> #include <atomic> #include <chrono> diff --git a/src/zenserver/upstream/zen.cpp b/src/zenserver/upstream/zen.cpp index 7494ae379..25fd3a3bb 100644 --- a/src/zenserver/upstream/zen.cpp +++ b/src/zenserver/upstream/zen.cpp @@ -9,44 +9,18 @@ #include <zencore/session.h> #include <zencore/stream.h> #include <zenhttp/formatters.h> +#include <zenhttp/httpclient.h> #include <zenhttp/httpcommon.h> #include <zenhttp/packageformat.h> #include <zenstore/cache/structuredcachestore.h> #include "diag/logging.h" -ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> -ZEN_THIRD_PARTY_INCLUDES_END - #include <xxhash.h> #include <gsl/gsl-lite.hpp> namespace zen { -namespace detail { - struct ZenCacheSessionState - { - ZenCacheSessionState(ZenStructuredCacheClient& Client) : OwnerClient(Client) {} - ~ZenCacheSessionState() {} - - void Reset(std::chrono::milliseconds ConnectTimeout, std::chrono::milliseconds Timeout) - { - Session.SetBody({}); - Session.SetHeader({}); - Session.SetConnectTimeout(ConnectTimeout); - Session.SetTimeout(Timeout); - } - - cpr::Session& GetSession() { return Session; } - - private: - ZenStructuredCacheClient& OwnerClient; - cpr::Session Session; - }; - -} // namespace detail - ////////////////////////////////////////////////////////////////////////// ZenStructuredCacheClient::ZenStructuredCacheClient(const ZenStructuredCacheClientOptions& Options) @@ -59,39 +33,6 @@ ZenStructuredCacheClient::ZenStructuredCacheClient(const ZenStructuredCacheClien ZenStructuredCacheClient::~ZenStructuredCacheClient() { - RwLock::ExclusiveLockScope _(m_SessionStateLock); - for (auto& CacheEntry : m_SessionStateCache) - { - delete CacheEntry; - } -} - -detail::ZenCacheSessionState* -ZenStructuredCacheClient::AllocSessionState() -{ - detail::ZenCacheSessionState* State = nullptr; - - if (RwLock::ExclusiveLockScope _(m_SessionStateLock); !m_SessionStateCache.empty()) - { - State = m_SessionStateCache.front(); - m_SessionStateCache.pop_front(); - } - - if (State == nullptr) - { - State = new detail::ZenCacheSessionState(*this); - } - - State->Reset(m_ConnectTimeout, m_Timeout); - - return State; -} - -void -ZenStructuredCacheClient::FreeSessionState(detail::ZenCacheSessionState* State) -{ - RwLock::ExclusiveLockScope _(m_SessionStateLock); - m_SessionStateCache.push_front(State); } ////////////////////////////////////////////////////////////////////////// @@ -102,59 +43,54 @@ ZenStructuredCacheSession::ZenStructuredCacheSession(Ref<ZenStructuredCacheClien : m_Log(OuterClient->Log()) , m_Client(std::move(OuterClient)) { - m_SessionState = m_Client->AllocSessionState(); } ZenStructuredCacheSession::~ZenStructuredCacheSession() { - m_Client->FreeSessionState(m_SessionState); } ZenCacheResult ZenStructuredCacheSession::CheckHealth() { - ExtendableStringBuilder<256> Uri; - Uri << m_Client->ServiceUrl() << "/health/check"; + HttpClient Http{m_Client->ServiceUrl()}; - cpr::Session& Session = m_SessionState->GetSession(); - Session.SetOption(cpr::Url{Uri.c_str()}); - cpr::Response Response = Session.Get(); + HttpClient::Response Response = Http.Get("/health/check"sv); - if (Response.error) + if (auto& Error = Response.Error; Error) { - return {.ErrorCode = static_cast<int32_t>(Response.error.code), .Reason = std::move(Response.error.message)}; + return {.ErrorCode = static_cast<int32_t>(Error->ErrorCode), .Reason = std::move(Error->ErrorMessage)}; } - return {.Bytes = Response.downloaded_bytes, .ElapsedSeconds = Response.elapsed, .Success = Response.status_code == 200}; + return {.Bytes = Response.DownloadedBytes, + .ElapsedSeconds = Response.ElapsedSeconds, + .Success = Response.StatusCode == HttpResponseCode::OK}; } ZenCacheResult ZenStructuredCacheSession::GetCacheRecord(std::string_view Namespace, std::string_view BucketId, const IoHash& Key, ZenContentType Type) { + HttpClient Http{m_Client->ServiceUrl()}; + ExtendableStringBuilder<256> Uri; - Uri << m_Client->ServiceUrl() << "/z$/"; + Uri << "/z$/"; if (Namespace != ZenCacheStore::DefaultNamespace) { Uri << Namespace << "/"; } Uri << BucketId << "/" << Key.ToHexString(); - cpr::Session& Session = m_SessionState->GetSession(); - - Session.SetOption(cpr::Url{Uri.c_str()}); - Session.SetHeader(cpr::Header{{"Accept", std::string{MapContentTypeToString(Type)}}}); - cpr::Response Response = Session.Get(); + HttpClient::Response Response = Http.Get(Uri, {{"Accept", std::string{MapContentTypeToString(Type)}}}); ZEN_DEBUG("GET {}", Response); - if (Response.error) + if (auto& Error = Response.Error; Error) { - return {.ErrorCode = static_cast<int32_t>(Response.error.code), .Reason = std::move(Response.error.message)}; + return {.ErrorCode = static_cast<int32_t>(Error->ErrorCode), .Reason = std::move(Error->ErrorMessage)}; } - const bool Success = Response.status_code == 200; - const IoBuffer Buffer = Success ? IoBufferBuilder::MakeCloneFromMemory(Response.text.data(), Response.text.size()) : IoBuffer(); + const bool Success = Response.StatusCode == HttpResponseCode::OK; + const IoBuffer Buffer = Success ? Response.ResponsePayload : IoBuffer{}; - return {.Response = Buffer, .Bytes = Response.downloaded_bytes, .ElapsedSeconds = Response.elapsed, .Success = Success}; + return {.Response = Buffer, .Bytes = Response.DownloadedBytes, .ElapsedSeconds = Response.ElapsedSeconds, .Success = Success}; } ZenCacheResult @@ -163,35 +99,28 @@ ZenStructuredCacheSession::GetCacheChunk(std::string_view Namespace, const IoHash& Key, const IoHash& ValueContentId) { + HttpClient Http{m_Client->ServiceUrl()}; + ExtendableStringBuilder<256> Uri; - Uri << m_Client->ServiceUrl() << "/z$/"; + Uri << "/z$/"; if (Namespace != ZenCacheStore::DefaultNamespace) { Uri << Namespace << "/"; } Uri << BucketId << "/" << Key.ToHexString() << "/" << ValueContentId.ToHexString(); - cpr::Session& Session = m_SessionState->GetSession(); - - Session.SetOption(cpr::Url{Uri.c_str()}); - Session.SetHeader(cpr::Header{{"Accept", "application/x-ue-comp"}}); - - cpr::Response Response = Session.Get(); + HttpClient::Response Response = Http.Get(Uri, {{"Accept", "application/x-ue-comp"}}); ZEN_DEBUG("GET {}", Response); - if (Response.error) + if (auto& Error = Response.Error; Error) { - return {.ErrorCode = static_cast<int32_t>(Response.error.code), .Reason = std::move(Response.error.message)}; + return {.ErrorCode = static_cast<int32_t>(Error->ErrorCode), .Reason = std::move(Error->ErrorMessage)}; } - const bool Success = Response.status_code == 200; - const IoBuffer Buffer = Success ? IoBufferBuilder::MakeCloneFromMemory(Response.text.data(), Response.text.size()) : IoBuffer(); + const bool Success = Response.StatusCode == HttpResponseCode::OK; + const IoBuffer Buffer = Success ? Response.ResponsePayload : IoBuffer{}; - return {.Response = Buffer, - .Bytes = Response.downloaded_bytes, - .ElapsedSeconds = Response.elapsed, - .Reason = Response.reason, - .Success = Success}; + return {.Response = Buffer, .Bytes = Response.DownloadedBytes, .ElapsedSeconds = Response.ElapsedSeconds, .Success = Success}; } ZenCacheResult @@ -201,33 +130,29 @@ ZenStructuredCacheSession::PutCacheRecord(std::string_view Namespace, IoBuffer Value, ZenContentType Type) { + HttpClient Http{m_Client->ServiceUrl()}; + ExtendableStringBuilder<256> Uri; - Uri << m_Client->ServiceUrl() << "/z$/"; + Uri << "/z$/"; if (Namespace != ZenCacheStore::DefaultNamespace) { Uri << Namespace << "/"; } Uri << BucketId << "/" << Key.ToHexString(); - cpr::Session& Session = m_SessionState->GetSession(); - - Session.SetOption(cpr::Url{Uri.c_str()}); - Session.SetHeader(cpr::Header{{"Content-Type", - Type == ZenContentType::kCbPackage ? "application/x-ue-cbpkg" - : Type == ZenContentType::kCbObject ? "application/x-ue-cb" - : "application/octet-stream"}}); - Session.SetBody(cpr::Body{static_cast<const char*>(Value.Data()), Value.Size()}); + Value.SetContentType(Type); - cpr::Response Response = Session.Put(); + HttpClient::Response Response = Http.Put(Uri, Value); ZEN_DEBUG("PUT {}", Response); - if (Response.error) + if (auto& Error = Response.Error; Error) { - return {.ErrorCode = static_cast<int32_t>(Response.error.code), .Reason = std::move(Response.error.message)}; + return {.ErrorCode = static_cast<int32_t>(Error->ErrorCode), .Reason = std::move(Error->ErrorMessage)}; } - const bool Success = Response.status_code == 200 || Response.status_code == 201; - return {.Bytes = Response.uploaded_bytes, .ElapsedSeconds = Response.elapsed, .Reason = Response.reason, .Success = Success}; + const bool Success = Response.StatusCode == HttpResponseCode::OK || Response.StatusCode == HttpResponseCode::Created; + + return {.Bytes = Response.DownloadedBytes, .ElapsedSeconds = Response.ElapsedSeconds, .Success = Success}; } ZenCacheResult @@ -237,94 +162,89 @@ ZenStructuredCacheSession::PutCacheValue(std::string_view Namespace, const IoHash& ValueContentId, IoBuffer Payload) { + HttpClient Http{m_Client->ServiceUrl()}; + ExtendableStringBuilder<256> Uri; - Uri << m_Client->ServiceUrl() << "/z$/"; + Uri << "/z$/"; if (Namespace != ZenCacheStore::DefaultNamespace) { Uri << Namespace << "/"; } Uri << BucketId << "/" << Key.ToHexString() << "/" << ValueContentId.ToHexString(); - cpr::Session& Session = m_SessionState->GetSession(); - - Session.SetOption(cpr::Url{Uri.c_str()}); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-comp"}}); - Session.SetBody(cpr::Body{static_cast<const char*>(Payload.Data()), Payload.Size()}); + Payload.SetContentType(HttpContentType::kCompressedBinary); - cpr::Response Response = Session.Put(); + HttpClient::Response Response = Http.Put(Uri, Payload); ZEN_DEBUG("PUT {}", Response); - if (Response.error) + if (auto& Error = Response.Error; Error) { - return {.ErrorCode = static_cast<int32_t>(Response.error.code), .Reason = std::move(Response.error.message)}; + return {.ErrorCode = static_cast<int32_t>(Error->ErrorCode), .Reason = std::move(Error->ErrorMessage)}; } - const bool Success = Response.status_code == 200 || Response.status_code == 201; - return {.Bytes = Response.uploaded_bytes, .ElapsedSeconds = Response.elapsed, .Reason = Response.reason, .Success = Success}; + const bool Success = Response.StatusCode == HttpResponseCode::OK || Response.StatusCode == HttpResponseCode::Created; + + return {.Bytes = Response.DownloadedBytes, .ElapsedSeconds = Response.ElapsedSeconds, .Success = Success}; } ZenCacheResult ZenStructuredCacheSession::InvokeRpc(const CbObjectView& Request) { + HttpClient Http{m_Client->ServiceUrl()}; + ExtendableStringBuilder<256> Uri; - Uri << m_Client->ServiceUrl() << "/z$/$rpc"; + Uri << "/z$/$rpc"; - BinaryWriter Body; - Request.CopyTo(Body); + // TODO: this seems redundant, we should be able to send the data more directly, without the BinaryWriter - cpr::Session& Session = m_SessionState->GetSession(); + BinaryWriter BodyWriter; + Request.CopyTo(BodyWriter); - Session.SetOption(cpr::Url{Uri.c_str()}); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cb"}, {"Accept", "application/x-ue-cbpkg"}}); - Session.SetBody(cpr::Body{reinterpret_cast<const char*>(Body.GetData()), Body.GetSize()}); + IoBuffer Body{IoBuffer::Wrap, BodyWriter.GetData(), BodyWriter.GetSize()}; + Body.SetContentType(HttpContentType::kCbObject); - cpr::Response Response = Session.Post(); + HttpClient::Response Response = Http.Post(Uri, Body, {{"Accept", "application/x-ue-cbpkg"}}); ZEN_DEBUG("POST {}", Response); - if (Response.error) + if (auto& Error = Response.Error; Error) { - return {.ErrorCode = static_cast<int32_t>(Response.error.code), .Reason = std::move(Response.error.message)}; + return {.ErrorCode = static_cast<int32_t>(Error->ErrorCode), .Reason = std::move(Error->ErrorMessage)}; } - const bool Success = Response.status_code == 200; - const IoBuffer Buffer = Success ? IoBufferBuilder::MakeCloneFromMemory(Response.text.data(), Response.text.size()) : IoBuffer(); + const bool Success = Response.StatusCode == HttpResponseCode::OK; + const IoBuffer Buffer = Success ? Response.ResponsePayload : IoBuffer{}; return {.Response = std::move(Buffer), - .Bytes = Response.uploaded_bytes, - .ElapsedSeconds = Response.elapsed, - .Reason = Response.reason, + .Bytes = Response.DownloadedBytes, + .ElapsedSeconds = Response.ElapsedSeconds, .Success = Success}; } ZenCacheResult ZenStructuredCacheSession::InvokeRpc(const CbPackage& Request) { - ExtendableStringBuilder<256> Uri; - Uri << m_Client->ServiceUrl() << "/z$/$rpc"; + HttpClient Http{m_Client->ServiceUrl()}; - SharedBuffer Message = FormatPackageMessageBuffer(Request).Flatten(); - - cpr::Session& Session = m_SessionState->GetSession(); + ExtendableStringBuilder<256> Uri; + Uri << "/z$/$rpc"; - Session.SetOption(cpr::Url{Uri.c_str()}); - Session.SetHeader(cpr::Header{{"Content-Type", "application/x-ue-cbpkg"}, {"Accept", "application/x-ue-cbpkg"}}); - Session.SetBody(cpr::Body{reinterpret_cast<const char*>(Message.GetData()), Message.GetSize()}); + IoBuffer Message = FormatPackageMessageBuffer(Request).Flatten().AsIoBuffer(); + Message.SetContentType(HttpContentType::kCbPackage); - cpr::Response Response = Session.Post(); + HttpClient::Response Response = Http.Post(Uri, Message, {{"Accept", "application/x-ue-cbpkg"}}); ZEN_DEBUG("POST {}", Response); - if (Response.error) + if (auto& Error = Response.Error; Error) { - return {.ErrorCode = static_cast<int32_t>(Response.error.code), .Reason = std::move(Response.error.message)}; + return {.ErrorCode = static_cast<int32_t>(Error->ErrorCode), .Reason = std::move(Error->ErrorMessage)}; } - const bool Success = Response.status_code == 200; - const IoBuffer Buffer = Success ? IoBufferBuilder::MakeCloneFromMemory(Response.text.data(), Response.text.size()) : IoBuffer(); + const bool Success = Response.StatusCode == HttpResponseCode::OK; + const IoBuffer Buffer = Success ? Response.ResponsePayload : IoBuffer{}; return {.Response = std::move(Buffer), - .Bytes = Response.uploaded_bytes, - .ElapsedSeconds = Response.elapsed, - .Reason = Response.reason, + .Bytes = Response.DownloadedBytes, + .ElapsedSeconds = Response.ElapsedSeconds, .Success = Success}; } diff --git a/src/zenserver/upstream/zen.h b/src/zenserver/upstream/zen.h index 78b6bc589..6321b46b1 100644 --- a/src/zenserver/upstream/zen.h +++ b/src/zenserver/upstream/zen.h @@ -6,17 +6,10 @@ #include <zencore/iohash.h> #include <zencore/logging.h> #include <zencore/memoryview.h> -#include <zencore/thread.h> #include <zencore/uid.h> #include <zencore/zencore.h> -ZEN_THIRD_PARTY_INCLUDES_START -#include <tsl/robin_map.h> -#include <asio.hpp> -ZEN_THIRD_PARTY_INCLUDES_END - #include <chrono> -#include <list> struct ZenCacheValue; @@ -29,10 +22,6 @@ class ZenStructuredCacheClient; ////////////////////////////////////////////////////////////////////////// -namespace detail { - struct ZenCacheSessionState; -} - struct ZenCacheResult { IoBuffer Response; @@ -85,7 +74,6 @@ private: LoggerRef m_Log; Ref<ZenStructuredCacheClient> m_Client; - detail::ZenCacheSessionState* m_SessionState; }; /** Zen Structured Cache client @@ -109,12 +97,6 @@ private: std::chrono::milliseconds m_ConnectTimeout; std::chrono::milliseconds m_Timeout; - RwLock m_SessionStateLock; - std::list<detail::ZenCacheSessionState*> m_SessionStateCache; - - detail::ZenCacheSessionState* AllocSessionState(); - void FreeSessionState(detail::ZenCacheSessionState*); - friend class ZenStructuredCacheSession; }; diff --git a/src/zenserver/vfs/vfsservice.cpp b/src/zenserver/vfs/vfsservice.cpp index bf761f8d1..863ec348a 100644 --- a/src/zenserver/vfs/vfsservice.cpp +++ b/src/zenserver/vfs/vfsservice.cpp @@ -1,7 +1,8 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "vfsservice.h" -#include "vfsimpl.h" + +#include <zenstore/vfsimpl.h> #include <zencore/compactbinarybuilder.h> @@ -61,10 +62,8 @@ GetContentAsCbObject(HttpServerRequest& HttpReq, CbObject& Cb) // echo {"method": "mount", "params": {"path": "d:\\VFS_ROOT"}} | curl.exe http://localhost:8558/vfs --data-binary @- // echo {"method": "unmount"} | curl.exe http://localhost:8558/vfs --data-binary @- -VfsService::VfsService(HttpStatusService& StatusService) : m_StatusService(StatusService) +VfsService::VfsService(HttpStatusService& StatusService, VfsServiceImpl* ServiceImpl) : m_StatusService(StatusService), m_Impl(ServiceImpl) { - m_Impl = new Impl; - m_Router.RegisterRoute( "info", [&](HttpRouterRequest& Request) { @@ -142,67 +141,19 @@ VfsService::VfsService(HttpStatusService& StatusService) : m_StatusService(Statu VfsService::~VfsService() { m_StatusService.UnregisterHandler("vfs", *this); - delete m_Impl; -} - -void -VfsService::Mount(std::string_view MountPoint) -{ - m_Impl->Mount(MountPoint); -} - -void -VfsService::Unmount() -{ - m_Impl->Unmount(); -} - -void -VfsService::AddService(Ref<ProjectStore>&& Ps) -{ - m_Impl->AddService(std::move(Ps)); -} - -void -VfsService::AddService(Ref<ZenCacheStore>&& Z$) -{ - m_Impl->AddService(std::move(Z$)); } #else -VfsService::VfsService(HttpStatusService& StatusService) : m_StatusService(StatusService) +VfsService::VfsService(HttpStatusService& StatusService, VfsServiceImpl* ServiceImpl) : m_StatusService(StatusService) { - ZEN_UNUSED(StatusService); + ZEN_UNUSED(ServiceImpl); } VfsService::~VfsService() { } -void -VfsService::Mount(std::string_view MountPoint) -{ - ZEN_UNUSED(MountPoint); -} - -void -VfsService::Unmount() -{ -} - -void -VfsService::AddService(Ref<ProjectStore>&& Ps) -{ - ZEN_UNUSED(Ps); -} - -void -VfsService::AddService(Ref<ZenCacheStore>&& Z$) -{ - ZEN_UNUSED(Z$); -} - #endif const char* diff --git a/src/zenserver/vfs/vfsservice.h b/src/zenserver/vfs/vfsservice.h index 0d0168e23..4e06da878 100644 --- a/src/zenserver/vfs/vfsservice.h +++ b/src/zenserver/vfs/vfsservice.h @@ -5,7 +5,6 @@ #include <zenbase/refcount.h> #include <zenhttp/httpserver.h> #include <zenhttp/httpstatus.h> -#include <zenvfs/vfs.h> #include <memory> @@ -13,6 +12,7 @@ namespace zen { class ProjectStore; class ZenCacheStore; +struct VfsServiceImpl; /** Virtual File System service @@ -28,23 +28,16 @@ class ZenCacheStore; class VfsService : public HttpService, public IHttpStatusProvider { public: - explicit VfsService(HttpStatusService& StatusService); + explicit VfsService(HttpStatusService& StatusService, VfsServiceImpl* ServiceImpl); ~VfsService(); - void Mount(std::string_view MountPoint); - void Unmount(); - - void AddService(Ref<ProjectStore>&&); - void AddService(Ref<ZenCacheStore>&&); - protected: virtual const char* BaseUri() const override; virtual void HandleRequest(HttpServerRequest& HttpServiceRequest) override; virtual void HandleStatusRequest(HttpServerRequest& Request) override; private: - struct Impl; - Impl* m_Impl = nullptr; + VfsServiceImpl* m_Impl = nullptr; HttpStatusService& m_StatusService; HttpRequestRouter m_Router; diff --git a/src/zenserver/xmake.lua b/src/zenserver/xmake.lua index 470fbd24e..57105045d 100644 --- a/src/zenserver/xmake.lua +++ b/src/zenserver/xmake.lua @@ -5,6 +5,7 @@ target("zenserver") add_deps("zencore", "zenhttp", "zennet", + "zenremotestore", "zenstore", "zenutil", "zenvfs") diff --git a/src/zenserver/zenserver.cpp b/src/zenserver/zenserver.cpp index 44c25368c..cab234165 100644 --- a/src/zenserver/zenserver.cpp +++ b/src/zenserver/zenserver.cpp @@ -22,11 +22,12 @@ #include <zencore/trace.h> #include <zencore/workthreadpool.h> #include <zenhttp/httpserver.h> +#include <zenremotestore/jupiter/jupiterclient.h> #include <zenstore/buildstore/buildstore.h> #include <zenstore/cidstore.h> #include <zenstore/scrubcontext.h> +#include <zenstore/vfsimpl.h> #include <zenstore/workspaces.h> -#include <zenutil/jupiter/jupiterclient.h> #include <zenutil/workerpools.h> #include <zenutil/zenserverprocess.h> @@ -176,7 +177,7 @@ ZenServer::Initialize(const ZenServerOptions& ServerOptions, ZenServerState::Zen InitializeState(ServerOptions); - m_JobQueue = MakeJobQueue(8, "backgroundjobs"); + m_JobQueue = MakeJobQueue(8, "bgjobs"); m_HealthService.SetHealthInfo({.DataRoot = m_DataRoot, .AbsLogPath = ServerOptions.AbsLogFile, @@ -234,18 +235,13 @@ ZenServer::Initialize(const ZenServerOptions& ServerOptions, ZenServerState::Zen } m_AuthService = std::make_unique<HttpAuthService>(*m_AuthMgr); - m_Http->RegisterService(*m_AuthService); - - m_Http->RegisterService(m_HealthService); - m_Http->RegisterService(m_StatsService); m_StatsReporter.Initialize(ServerOptions.StatsConfig); if (ServerOptions.StatsConfig.Enabled) { EnqueueStatsReportingTimer(); } - m_Http->RegisterService(m_StatusService); m_StatusService.RegisterHandler("status", *this); // Initialize storage and services @@ -260,13 +256,9 @@ ZenServer::Initialize(const ZenServerOptions& ServerOptions, ZenServerState::Zen ZEN_INFO("instantiating project service"); - m_ProjectStore = new ProjectStore(*m_CidStore, - m_DataRoot / "projects", - m_GcManager, - *m_JobQueue, - *m_OpenProcessCache, - ProjectStore::Configuration{}); - m_HttpProjectService.reset(new HttpProjectService{*m_CidStore, m_ProjectStore, m_StatusService, m_StatsService, *m_AuthMgr}); + m_ProjectStore = new ProjectStore(*m_CidStore, m_DataRoot / "projects", m_GcManager, ProjectStore::Configuration{}); + m_HttpProjectService.reset( + new HttpProjectService{*m_CidStore, m_ProjectStore, m_StatusService, m_StatsService, *m_AuthMgr, *m_OpenProcessCache, *m_JobQueue}); if (ServerOptions.WorksSpacesConfig.Enabled) { @@ -301,29 +293,6 @@ ZenServer::Initialize(const ZenServerOptions& ServerOptions, ZenServerState::Zen ZEN_INFO("NOT instantiating structured cache service"); } - m_Http->RegisterService(m_TestService); // NOTE: this is intentionally not limited to test mode as it's useful for diagnostics - -#if ZEN_WITH_TESTS - m_Http->RegisterService(m_TestingService); -#endif - - if (m_HttpProjectService) - { - m_Http->RegisterService(*m_HttpProjectService); - } - - if (m_HttpWorkspacesService) - { - m_Http->RegisterService(*m_HttpWorkspacesService); - } - - m_FrontendService = std::make_unique<HttpFrontendService>(m_ContentRoot, m_StatusService); - - if (m_FrontendService) - { - m_Http->RegisterService(*m_FrontendService); - } - if (ServerOptions.ObjectStoreEnabled) { ObjectStoreConfig ObjCfg; @@ -337,21 +306,20 @@ ZenServer::Initialize(const ZenServerOptions& ServerOptions, ZenServerState::Zen } m_ObjStoreService = std::make_unique<HttpObjectStoreService>(m_StatusService, std::move(ObjCfg)); - m_Http->RegisterService(*m_ObjStoreService); } if (ServerOptions.BuildStoreConfig.Enabled) { m_BuildStoreService = std::make_unique<HttpBuildStoreService>(m_StatusService, m_StatsService, *m_BuildStore); - m_Http->RegisterService(*m_BuildStoreService); } #if ZEN_WITH_VFS - m_VfsService = std::make_unique<VfsService>(m_StatusService); - m_VfsService->AddService(Ref<ProjectStore>(m_ProjectStore)); - m_VfsService->AddService(Ref<ZenCacheStore>(m_CacheStore)); - m_Http->RegisterService(*m_VfsService); -#endif + m_VfsServiceImpl = std::make_unique<VfsServiceImpl>(); + m_VfsServiceImpl->AddService(Ref<ProjectStore>(m_ProjectStore)); + m_VfsServiceImpl->AddService(Ref<ZenCacheStore>(m_CacheStore)); + + m_VfsService = std::make_unique<VfsService>(m_StatusService, m_VfsServiceImpl.get()); +#endif // ZEN_WITH_VFS ZEN_INFO("initializing GC, enabled '{}', interval {}, lightweight interval {}", ServerOptions.GcConfig.Enabled, @@ -387,8 +355,65 @@ ZenServer::Initialize(const ZenServerOptions& ServerOptions, ZenServerState::Zen .HttpLogPath = ServerOptions.DataDir / "logs" / "http.log", .CacheLogPath = ServerOptions.DataDir / "logs" / "z$.log"}, ServerOptions); + + // Register all services when all initialization for all services are done + + m_Http->RegisterService(*m_AuthService); + + m_Http->RegisterService(m_StatsService); + m_Http->RegisterService(m_StatusService); + m_Http->RegisterService(m_TestService); // NOTE: this is intentionally not limited to test mode as it's useful for diagnostics + +#if ZEN_WITH_TESTS + m_Http->RegisterService(m_TestingService); +#endif + + if (m_StructuredCacheService) + { + m_Http->RegisterService(*m_StructuredCacheService); + } + + if (m_UpstreamService) + { + m_Http->RegisterService(*m_UpstreamService); + } + + if (m_HttpProjectService) + { + m_Http->RegisterService(*m_HttpProjectService); + } + + if (m_HttpWorkspacesService) + { + m_Http->RegisterService(*m_HttpWorkspacesService); + } + + m_FrontendService = std::make_unique<HttpFrontendService>(m_ContentRoot, m_StatusService); + + if (m_FrontendService) + { + m_Http->RegisterService(*m_FrontendService); + } + + if (m_ObjStoreService) + { + m_Http->RegisterService(*m_ObjStoreService); + } + + if (m_BuildStoreService) + { + m_Http->RegisterService(*m_BuildStoreService); + } +#if ZEN_WITH_VFS + m_Http->RegisterService(*m_VfsService); +#endif // ZEN_WITH_VFS + m_Http->RegisterService(*m_AdminService); + // Register health service last so if we return "OK" for health it means all services have been properly initialized + + m_Http->RegisterService(m_HealthService); + return EffectiveBasePort; } @@ -685,9 +710,6 @@ ZenServer::InitializeStructuredCache(const ZenServerOptions& ServerOptions) m_GcManager.GetDiskWriteBlocker(), *m_OpenProcessCache); - m_Http->RegisterService(*m_StructuredCacheService); - m_Http->RegisterService(*m_UpstreamService); - m_StatsReporter.AddProvider(m_CacheStore.Get()); m_StatsReporter.AddProvider(m_CidStore.get()); m_StatsReporter.AddProvider(m_BuildCidStore.get()); @@ -857,6 +879,7 @@ ZenServer::Cleanup() m_AdminService.reset(); m_VfsService.reset(); + m_VfsServiceImpl.reset(); m_ObjStoreService.reset(); m_FrontendService.reset(); @@ -1105,7 +1128,6 @@ ZenServer::ToString(ServerState Value) void zenserver_forcelinktests() { - zen::prj_forcelink(); } #endif diff --git a/src/zenserver/zenserver.h b/src/zenserver/zenserver.h index bcb02d336..ba76c5fff 100644 --- a/src/zenserver/zenserver.h +++ b/src/zenserver/zenserver.h @@ -24,6 +24,7 @@ ZEN_THIRD_PARTY_INCLUDES_END #include <zenhttp/httptest.h> #include <zenstore/cache/structuredcachestore.h> #include <zenstore/gc.h> +#include <zenstore/projectstore.h> #include "admin/admin.h" #include "buildstore/httpbuildstore.h" #include "cache/httpstructuredcache.h" @@ -31,7 +32,6 @@ ZEN_THIRD_PARTY_INCLUDES_END #include "frontend/frontend.h" #include "objectstore/objectstore.h" #include "projectstore/httpprojectstore.h" -#include "projectstore/projectstore.h" #include "stats/statsreporter.h" #include "upstream/upstream.h" #include "vfs/vfsservice.h" @@ -134,6 +134,7 @@ private: HttpTestingService m_TestingService; #endif RefPtr<ProjectStore> m_ProjectStore; + std::unique_ptr<VfsServiceImpl> m_VfsServiceImpl; std::unique_ptr<HttpProjectService> m_HttpProjectService; std::unique_ptr<Workspaces> m_Workspaces; std::unique_ptr<HttpWorkspacesService> m_HttpWorkspacesService; diff --git a/src/zenstore-test/xmake.lua b/src/zenstore-test/xmake.lua index 6e614459c..ca260ee52 100644 --- a/src/zenstore-test/xmake.lua +++ b/src/zenstore-test/xmake.lua @@ -5,5 +5,5 @@ target("zenstore-test") set_group("tests") add_headerfiles("**.h") add_files("*.cpp") - add_deps("zenstore", "zencore") + add_deps("zenstore") add_packages("vcpkg::doctest") diff --git a/src/zenstore-test/zenstore-test.cpp b/src/zenstore-test/zenstore-test.cpp index 6ae0b84b0..6df7162fd 100644 --- a/src/zenstore-test/zenstore-test.cpp +++ b/src/zenstore-test/zenstore-test.cpp @@ -23,7 +23,15 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) zen::IgnoreChildSignals(); # endif +# if ZEN_WITH_TRACE zen::TraceInit("zenstore-test"); + zen::TraceOptions TraceCommandlineOptions; + if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) + { + TraceConfigure(TraceCommandlineOptions); + } +# endif // ZEN_WITH_TRACE + zen::logging::InitializeLogging(); zen::MaximizeOpenFileCount(); diff --git a/src/zenstore/blockstore.cpp b/src/zenstore/blockstore.cpp index 7b56c64bd..0861baaf8 100644 --- a/src/zenstore/blockstore.cpp +++ b/src/zenstore/blockstore.cpp @@ -113,17 +113,25 @@ BlockStoreFile::Create(uint64_t InitialSize) uint64_t BlockStoreFile::FileSize() const { - if (m_CachedFileSize == 0) + uint64_t CachedSize = m_CachedFileSize; + if (CachedSize == 0) { std::error_code Ec; uint64_t Size = m_File.FileSize(Ec); if (Ec) { + ZEN_WARN("Failed to get file size of block {}. Reason: {}", m_Path, Ec.message()); return 0; } + uint64_t Expected = 0; + if (!m_CachedFileSize.compare_exchange_strong(Expected, Size)) + { + // Force a new check next time file size is fetched + m_CachedFileSize.store(0); + } return Size; } - return m_CachedFileSize; + return CachedSize; } void @@ -153,13 +161,9 @@ void BlockStoreFile::Write(const void* Data, uint64_t Size, uint64_t FileOffset) { ZEN_TRACE_CPU("BlockStoreFile::Write"); -#if ZEN_BUILD_DEBUG - if (uint64_t CachedFileSize = m_CachedFileSize.load(); CachedFileSize > 0) - { - ZEN_ASSERT(FileOffset + Size <= CachedFileSize); - } -#endif // ZEN_BUILD_DEBUG + ZEN_ASSERT(Size + FileOffset <= m_IoBuffer.GetSize()); m_File.Write(Data, Size, FileOffset); + m_CachedFileSize.store(0); } void @@ -169,10 +173,11 @@ BlockStoreFile::Flush(uint64_t FinalSize) m_File.Flush(); if (FinalSize != (uint64_t)-1) { + ZEN_ASSERT(FinalSize <= m_IoBuffer.GetSize()); uint64_t ExpectedSize = 0; - if (!m_CachedFileSize.compare_exchange_weak(ExpectedSize, FinalSize)) + while (!m_CachedFileSize.compare_exchange_weak(ExpectedSize, FinalSize)) { - ZEN_ASSERT(m_CachedFileSize.load() == FinalSize); + ZEN_ASSERT(ExpectedSize <= FinalSize); } } } @@ -724,6 +729,14 @@ BlockStore::HasChunk(const BlockStoreLocation& Location) const { return true; } + else + { + ZEN_WARN("BlockLocation: Block {}, Offset {}, Size {} is outside block size {}", + Location.BlockIndex, + Location.Offset, + Location.Size, + BlockSize); + } } } return false; @@ -1152,7 +1165,7 @@ BlockStore::CompactBlocks(const BlockStoreCompactState& CompactState, ChunkView = MemoryView(ChunkBuffer.data(), ChunkLocation.Size); } - if ((WriteOffset + ChunkView.GetSize()) > m_MaxBlockSize) + if ((RoundUp(WriteOffset, PayloadAlignment) + ChunkView.GetSize()) > m_MaxBlockSize) { if (TargetFileBuffer) { @@ -1675,7 +1688,7 @@ TEST_CASE("blockstore.iterate.chunks") .Size = DefaultIterateSmallChunkWindowSize * 2}; BlockStoreLocation BadBlockIndex = {.BlockIndex = 0xfffff, .Offset = 1024, .Size = 1024}; - WorkerThreadPool WorkerPool(4); + WorkerThreadPool WorkerPool(Max(std::thread::hardware_concurrency() - 1u, 4u)); std::vector<BlockStoreLocation> Locations{FirstChunkLocation, SecondChunkLocation, @@ -1686,80 +1699,82 @@ TEST_CASE("blockstore.iterate.chunks") Latch WorkLatch(1); Store.IterateChunks(Locations, [&](uint32_t, std::span<const size_t> ChunkIndexes) -> bool { WorkLatch.AddCount(1); - WorkerPool.ScheduleWork([&, ChunkIndexes = std::vector<size_t>(ChunkIndexes.begin(), ChunkIndexes.end())]() { - auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); }); - bool Continue = Store.IterateBlock( - Locations, - ChunkIndexes, - [&](size_t ChunkIndex, const void* Data, uint64_t Size) -> bool { - switch (ChunkIndex) - { - case 0: - CHECK(Data); - CHECK(Size == FirstChunkData.size()); - CHECK(std::string((const char*)Data, Size) == FirstChunkData); - break; - case 1: - CHECK(Data); - CHECK(Size == SecondChunkData.size()); - CHECK(std::string((const char*)Data, Size) == SecondChunkData); - break; - case 2: - CHECK(false); - break; - case 3: - CHECK(!Data); - break; - case 4: - CHECK(!Data); - break; - case 5: - CHECK(!Data); - break; - default: - CHECK(false); - break; - } - return true; - }, - [&](size_t ChunkIndex, BlockStoreFile& File, uint64_t Offset, uint64_t Size) -> bool { - switch (ChunkIndex) - { - case 0: - case 1: - CHECK(false); - break; - case 2: - { - CHECK(Size == VeryLargeChunk.size()); - char* Buffer = new char[Size]; - size_t HashOffset = 0; - File.StreamByteRange(Offset, Size, [&](const void* Data, uint64_t Size) { - memcpy(&Buffer[HashOffset], Data, Size); - HashOffset += Size; - }); - CHECK(memcmp(Buffer, VeryLargeChunk.data(), Size) == 0); - delete[] Buffer; - } - break; - case 3: - CHECK(false); - break; - case 4: - CHECK(false); - break; - case 5: - CHECK(false); - break; - default: - CHECK(false); - break; - } - return true; - }, - 0); - CHECK(Continue); - }); + WorkerPool.ScheduleWork( + [&, ChunkIndexes = std::vector<size_t>(ChunkIndexes.begin(), ChunkIndexes.end())]() { + auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); }); + bool Continue = Store.IterateBlock( + Locations, + ChunkIndexes, + [&](size_t ChunkIndex, const void* Data, uint64_t Size) -> bool { + switch (ChunkIndex) + { + case 0: + CHECK(Data); + CHECK(Size == FirstChunkData.size()); + CHECK(std::string((const char*)Data, Size) == FirstChunkData); + break; + case 1: + CHECK(Data); + CHECK(Size == SecondChunkData.size()); + CHECK(std::string((const char*)Data, Size) == SecondChunkData); + break; + case 2: + CHECK(false); + break; + case 3: + CHECK(!Data); + break; + case 4: + CHECK(!Data); + break; + case 5: + CHECK(!Data); + break; + default: + CHECK(false); + break; + } + return true; + }, + [&](size_t ChunkIndex, BlockStoreFile& File, uint64_t Offset, uint64_t Size) -> bool { + switch (ChunkIndex) + { + case 0: + case 1: + CHECK(false); + break; + case 2: + { + CHECK(Size == VeryLargeChunk.size()); + char* Buffer = new char[Size]; + size_t HashOffset = 0; + File.StreamByteRange(Offset, Size, [&](const void* Data, uint64_t Size) { + memcpy(&Buffer[HashOffset], Data, Size); + HashOffset += Size; + }); + CHECK(memcmp(Buffer, VeryLargeChunk.data(), Size) == 0); + delete[] Buffer; + } + break; + case 3: + CHECK(false); + break; + case 4: + CHECK(false); + break; + case 5: + CHECK(false); + break; + default: + CHECK(false); + break; + } + return true; + }, + 0); + CHECK(Continue); + }, + WorkerThreadPool::EMode::DisableBacklog); return true; }); WorkLatch.CountDown(); @@ -1776,7 +1791,7 @@ TEST_CASE("blockstore.thread.read.write") BlockStore Store; Store.Initialize(RootDirectory / "store", 1088, 1024); - constexpr size_t ChunkCount = 1000; + constexpr size_t ChunkCount = 500; constexpr size_t Alignment = 8; std::vector<IoBuffer> Chunks; std::vector<IoHash> ChunkHashes; @@ -1792,60 +1807,84 @@ TEST_CASE("blockstore.thread.read.write") std::vector<BlockStoreLocation> ChunkLocations; ChunkLocations.resize(ChunkCount); - WorkerThreadPool WorkerPool(8); - std::atomic<size_t> WorkCompleted = 0; - for (size_t ChunkIndex = 0; ChunkIndex < ChunkCount; ++ChunkIndex) + WorkerThreadPool WorkerPool(Max(std::thread::hardware_concurrency() - 1u, 8u)); { - WorkerPool.ScheduleWork([&Store, ChunkIndex, &Chunks, &ChunkLocations, &WorkCompleted]() { - IoBuffer& Chunk = Chunks[ChunkIndex]; - Store.WriteChunk(Chunk.Data(), Chunk.Size(), Alignment, [&](const BlockStoreLocation& L) { ChunkLocations[ChunkIndex] = L; }); - WorkCompleted.fetch_add(1); - }); - } - while (WorkCompleted < Chunks.size()) - { - Sleep(1); + std::atomic<size_t> WorkCompleted = 0; + Latch L(1); + for (size_t ChunkIndex = 0; ChunkIndex < ChunkCount; ++ChunkIndex) + { + L.AddCount(1); + WorkerPool.ScheduleWork( + [&Store, ChunkIndex, &Chunks, &ChunkLocations, &WorkCompleted, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + IoBuffer& Chunk = Chunks[ChunkIndex]; + Store.WriteChunk(Chunk.Data(), Chunk.Size(), Alignment, [&](const BlockStoreLocation& L) { + ChunkLocations[ChunkIndex] = L; + }); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); + } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == Chunks.size()); } - WorkCompleted = 0; - for (size_t ChunkIndex = 0; ChunkIndex < ChunkCount; ++ChunkIndex) { - WorkerPool.ScheduleWork([&Store, ChunkIndex, &ChunkLocations, &ChunkHashes, &WorkCompleted]() { - IoBuffer VerifyChunk = Store.TryGetChunk(ChunkLocations[ChunkIndex]); - CHECK(VerifyChunk); - IoHash VerifyHash = IoHash::HashBuffer(VerifyChunk.Data(), VerifyChunk.Size()); - CHECK(VerifyHash == ChunkHashes[ChunkIndex]); - WorkCompleted.fetch_add(1); - }); - } - while (WorkCompleted < Chunks.size()) - { - Sleep(1); + std::atomic<size_t> WorkCompleted = 0; + Latch L(1); + for (size_t ChunkIndex = 0; ChunkIndex < ChunkCount; ++ChunkIndex) + { + L.AddCount(1); + WorkerPool.ScheduleWork( + [&Store, ChunkIndex, &ChunkLocations, &ChunkHashes, &WorkCompleted, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + IoBuffer VerifyChunk = Store.TryGetChunk(ChunkLocations[ChunkIndex]); + CHECK(VerifyChunk); + IoHash VerifyHash = IoHash::HashBuffer(VerifyChunk.Data(), VerifyChunk.Size()); + CHECK(VerifyHash == ChunkHashes[ChunkIndex]); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); + } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == Chunks.size()); } std::vector<BlockStoreLocation> SecondChunkLocations; SecondChunkLocations.resize(ChunkCount); - WorkCompleted = 0; - for (size_t ChunkIndex = 0; ChunkIndex < ChunkCount; ++ChunkIndex) { - WorkerPool.ScheduleWork([&Store, ChunkIndex, &Chunks, &SecondChunkLocations, &WorkCompleted]() { - IoBuffer& Chunk = Chunks[ChunkIndex]; - Store.WriteChunk(Chunk.Data(), Chunk.Size(), Alignment, [&](const BlockStoreLocation& L) { - SecondChunkLocations[ChunkIndex] = L; - }); - WorkCompleted.fetch_add(1); - }); - WorkerPool.ScheduleWork([&Store, ChunkIndex, &ChunkLocations, &ChunkHashes, &WorkCompleted]() { - IoBuffer VerifyChunk = Store.TryGetChunk(ChunkLocations[ChunkIndex]); - CHECK(VerifyChunk); - IoHash VerifyHash = IoHash::HashBuffer(VerifyChunk.Data(), VerifyChunk.Size()); - CHECK(VerifyHash == ChunkHashes[ChunkIndex]); - WorkCompleted.fetch_add(1); - }); - } - while (WorkCompleted < Chunks.size() * 2) - { - Sleep(1); + std::atomic<size_t> WorkCompleted = 0; + Latch L(1); + for (size_t ChunkIndex = 0; ChunkIndex < ChunkCount; ++ChunkIndex) + { + L.AddCount(1); + WorkerPool.ScheduleWork( + [&Store, ChunkIndex, &Chunks, &SecondChunkLocations, &WorkCompleted, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + IoBuffer& Chunk = Chunks[ChunkIndex]; + Store.WriteChunk(Chunk.Data(), Chunk.Size(), Alignment, [&](const BlockStoreLocation& L) { + SecondChunkLocations[ChunkIndex] = L; + }); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); + L.AddCount(1); + WorkerPool.ScheduleWork( + [&Store, ChunkIndex, &ChunkLocations, &ChunkHashes, &WorkCompleted, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + IoBuffer VerifyChunk = Store.TryGetChunk(ChunkLocations[ChunkIndex]); + CHECK(VerifyChunk); + IoHash VerifyHash = IoHash::HashBuffer(VerifyChunk.Data(), VerifyChunk.Size()); + CHECK(VerifyHash == ChunkHashes[ChunkIndex]); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); + } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == Chunks.size() * 2); } } diff --git a/src/zenstore/buildstore/buildstore.cpp b/src/zenstore/buildstore/buildstore.cpp index 1b2cf036b..5c6aa099c 100644 --- a/src/zenstore/buildstore/buildstore.cpp +++ b/src/zenstore/buildstore/buildstore.cpp @@ -3,14 +3,15 @@ #include <zenstore/buildstore/buildstore.h> #include <zencore/compactbinarybuilder.h> +#include <zencore/compactbinaryutil.h> #include <zencore/compress.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zencore/memory/llm.h> +#include <zencore/parallelwork.h> #include <zencore/scopeguard.h> #include <zencore/trace.h> #include <zencore/workthreadpool.h> -#include <zenutil/parallelwork.h> #include <zencore/uid.h> #include <zencore/xxhash.h> @@ -138,24 +139,33 @@ BuildStore::BuildStore(const BuildStoreConfig& Config, GcManager& Gc, CidStore& { RwLock::ExclusiveLockScope Lock(m_Lock); - CbObject ManifestReader = LoadCompactBinaryObject(ReadFile(ManifestPath).Flatten()); - Oid ManifestId = ManifestReader["id"].AsObjectId(); - uint32_t Version = ManifestReader["version"].AsUInt32(); - DateTime CreationDate = ManifestReader["createdAt"].AsDateTime(); - ZEN_UNUSED(CreationDate); - if (ManifestId == Oid::Zero || Version != blobstore::impl::ManifestVersion) + CbValidateError ValidateResult = CbValidateError::None; + if (CbObject ManifestReader = ValidateAndReadCompactBinaryObject(ReadFile(ManifestPath).Flatten(), ValidateResult); + ValidateResult == CbValidateError::None && ManifestReader) { - ZEN_WARN("Invalid manifest at {}, wiping state", ManifestPath); - IsNew = true; + Oid ManifestId = ManifestReader["id"].AsObjectId(); + uint32_t Version = ManifestReader["version"].AsUInt32(); + DateTime CreationDate = ManifestReader["createdAt"].AsDateTime(); + ZEN_UNUSED(CreationDate); + if (ManifestId == Oid::Zero || Version != blobstore::impl::ManifestVersion) + { + ZEN_WARN("Invalid manifest at {}, wiping state", ManifestPath); + IsNew = true; + } + else + { + m_BlobLogFlushPosition = ReadPayloadLog(Lock, BlobLogPath, 0); + m_MetaLogFlushPosition = ReadMetadataLog(Lock, MetaLogPath, 0); + if (IsFile(AccessTimesPath)) + { + ReadAccessTimes(Lock, AccessTimesPath); + } + } } else { - m_BlobLogFlushPosition = ReadPayloadLog(Lock, BlobLogPath, 0); - m_MetaLogFlushPosition = ReadMetadataLog(Lock, MetaLogPath, 0); - if (IsFile(AccessTimesPath)) - { - ReadAccessTimes(Lock, AccessTimesPath); - } + ZEN_WARN("Invalid manifest at {} ('{}'), wiping state", ManifestPath, ToString(ValidateResult)); + IsNew = true; } } @@ -366,7 +376,7 @@ BuildStore::PutMetadatas(std::span<const IoHash> BlobHashes, std::span<const IoB { std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::DisableBacklog); for (size_t Index = 0; Index < Metadatas.size(); Index++) { Work.ScheduleWork( 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/cachedisklayer.cpp b/src/zenstore/cache/cachedisklayer.cpp index cacbbd966..c2e811003 100644 --- a/src/zenstore/cache/cachedisklayer.cpp +++ b/src/zenstore/cache/cachedisklayer.cpp @@ -10,14 +10,15 @@ #include <zencore/fmtutils.h> #include <zencore/jobqueue.h> #include <zencore/logging.h> +#include <zencore/parallelwork.h> #include <zencore/scopeguard.h> #include <zencore/trace.h> #include <zencore/workthreadpool.h> #include <zencore/xxhash.h> -#include <zenutil/parallelwork.h> -#include <zenutil/referencemetadata.h> #include <zenutil/workerpools.h> +#include "../referencemetadata.h" + #include <future> #include <limits> @@ -1968,17 +1969,19 @@ ZenCacheDiskLayer::CacheBucket::ShouldRejectPut(const IoHash& HashKey, IndexLock.ReleaseNow(); if (!cache::impl::UpdateValueWithRawSizeAndHash(InOutValue)) { - OutPutResult = PutResult{zen::PutStatus::Fail, "Value provided is of bad format"}; + CbObjectWriter DetailWriter; + DetailWriter.AddString("Message", "Value provided is of bad format"); + OutPutResult = PutResult{zen::PutStatus::Fail, DetailWriter.Save()}; return true; } else if (MetaData.RawSize != InOutValue.RawSize || MetaData.RawHash != InOutValue.RawHash) { - OutPutResult = PutResult{ - zen::PutStatus::Conflict, - fmt::format("Value exists with different size '{}' or hash '{}'", MetaData.RawSize, MetaData.RawHash)}; - return true; + // Deliberate fall through without return so that we load the value and include it in the result + } + else + { + return false; } - return false; } } @@ -2008,16 +2011,22 @@ ZenCacheDiskLayer::CacheBucket::ShouldRejectPut(const IoHash& HashKey, { if (!cache::impl::UpdateValueWithRawSizeAndHash(InOutValue)) { - OutPutResult = PutResult{zen::PutStatus::Fail, "Value provided is of bad format"}; + CbObjectWriter DetailWriter; + DetailWriter.AddString("Message", "Value provided is of bad format"); + OutPutResult = PutResult{zen::PutStatus::Fail, DetailWriter.Save()}; return true; } if (ExistingValue.RawSize != InOutValue.RawSize || ExistingValue.RawHash != InOutValue.RawHash) { - OutPutResult = PutResult{zen::PutStatus::Conflict, - fmt::format("Value exists with different size '{}' or hash '{}'", - ExistingValue.RawSize, - ExistingValue.RawHash)}; + CbObjectWriter DetailWriter; + DetailWriter.AddInteger("RawSize", ExistingValue.RawSize); + DetailWriter.AddHash("RawHash", ExistingValue.RawHash); + if (Location.IsFlagSet(DiskLocation::kStructured)) + { + DetailWriter.AddObject("Record", CbObject(SharedBuffer(ExistingValue.Value))); + } + OutPutResult = PutResult{zen::PutStatus::Conflict, DetailWriter.Save()}; return true; } } @@ -3442,13 +3451,27 @@ ZenCacheDiskLayer::CacheBucket::GetReferences(const LoggerRef& Logger, auto Log = [&Logger]() { return Logger; }; - auto GetAttachments = [&](MemoryView Data) -> bool { - if (ValidateCompactBinary(Data, CbValidateMode::Default) == CbValidateError::None) + auto GetAttachments = [&](const IoHash& RawHash, MemoryView Data) -> bool { + if (CbValidateError Error = ValidateCompactBinary(Data, CbValidateMode::Default); Error == CbValidateError::None) { CbObjectView Obj(Data.GetData()); - Obj.IterateAttachments([&](CbFieldView Field) { OutReferences.emplace_back(Field.AsAttachment()); }); + if (Obj.GetSize() == Data.GetSize()) + { + Obj.IterateAttachments([&](CbFieldView Field) { OutReferences.emplace_back(Field.AsAttachment()); }); + } + else + { + ZEN_WARN("Cache record {} payload is malformed. Payload size is {}, but compact binary object size is {}", + RawHash, + Data.GetSize(), + Obj.GetSize()); + } return true; } + else + { + ZEN_WARN("Cache record {} payload is malformed. Reason: ", RawHash, ToString(Error)); + } return false; }; @@ -3540,7 +3563,7 @@ ZenCacheDiskLayer::CacheBucket::GetReferences(const LoggerRef& Logger, } auto CaptureAttachments = [&](size_t ChunkIndex, MemoryView Data) { - if (GetAttachments(Data)) + if (GetAttachments(InlineKeys[ChunkIndex], Data)) { if (WriteMetaData) { @@ -3609,7 +3632,7 @@ ZenCacheDiskLayer::CacheBucket::GetReferences(const LoggerRef& Logger, IoBuffer Buffer = GetStandaloneCacheValue(It.second, It.first); if (Buffer) { - GetAttachments(Buffer.GetView()); + GetAttachments(It.first, Buffer.GetView()); } } return true; @@ -4215,7 +4238,7 @@ ZenCacheDiskLayer::DiscoverBuckets() WorkerThreadPool& Pool = GetLargeWorkerPool(EWorkloadType::Burst); std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::DisableBacklog); try { for (auto& BucketPath : FoundBucketDirectories) @@ -4387,7 +4410,7 @@ ZenCacheDiskLayer::Flush() WorkerThreadPool& Pool = GetMediumWorkerPool(EWorkloadType::Burst); std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::DisableBacklog); try { for (auto& Bucket : Buckets) @@ -4434,7 +4457,8 @@ ZenCacheDiskLayer::Scrub(ScrubContext& Ctx) { # if 1 Results.push_back(Ctx.ThreadPool().EnqueueTask( - std::packaged_task<void()>{[this, Bucket = Kv.second.get(), &Ctx] { Bucket->ScrubStorage(Ctx); }})); + std::packaged_task<void()>{[this, Bucket = Kv.second.get(), &Ctx] { Bucket->ScrubStorage(Ctx); }}, + WorkerThreadPool::EMode::EnableBacklog)); # else CacheBucket& Bucket = *Kv.second; Bucket.ScrubStorage(Ctx); diff --git a/src/zenutil/cache/cachekey.cpp b/src/zenstore/cache/cachekey.cpp index 545b47f11..e5a5d0334 100644 --- a/src/zenutil/cache/cachekey.cpp +++ b/src/zenstore/cache/cachekey.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/cache/cachekey.h> +#include <zenstore/cache/cachekey.h> namespace zen { diff --git a/src/zenutil/cache/cachepolicy.cpp b/src/zenstore/cache/cachepolicy.cpp index 0bdfd87ce..ca8a95ca1 100644 --- a/src/zenutil/cache/cachepolicy.cpp +++ b/src/zenstore/cache/cachepolicy.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/cache/cachepolicy.h> +#include <zenstore/cache/cachepolicy.h> #include <zencore/compactbinary.h> #include <zencore/compactbinarybuilder.h> diff --git a/src/zenstore/cache/cacherpc.cpp b/src/zenstore/cache/cacherpc.cpp index 5d9a68919..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; @@ -190,9 +189,8 @@ CacheRpcHandler::HandleRpcRequest(const CacheRequestContext& Context, m_CacheStats.RpcRequests.fetch_add(1); - CbPackage Package; - CbObjectView Object; - CbObject ObjectBuffer; + CbPackage Package; + CbObject Object; try { if (ContentType == ZenContentType::kCbObject) @@ -203,8 +201,7 @@ CacheRpcHandler::HandleRpcRequest(const CacheRequestContext& Context, return RpcResponseCode::BadRequest; } - ObjectBuffer = LoadCompactBinaryObject(std::move(Body)); - Object = ObjectBuffer; + Object = LoadCompactBinaryObject(std::move(Body)); if (!Object) { ZEN_WARN("Content format not supported, expected compact binary format"); @@ -312,7 +309,7 @@ CacheRpcHandler::HandleRpcPutCacheRecords(const CacheRequestContext& Context, co } DefaultPolicy = !PolicyText.empty() ? ParseCachePolicy(PolicyText) : CachePolicy::Default; - eastl::fixed_vector<bool, 32> Results; + eastl::fixed_vector<ZenCacheStore::PutResult, 32> Results; CbArrayView RequestsArray = Params["Requests"sv].AsArrayView(); for (CbFieldView RequestField : RequestsArray) @@ -334,33 +331,50 @@ CacheRpcHandler::HandleRpcPutCacheRecords(const CacheRequestContext& Context, co .Policy = std::move(Policy), .Context = Context}; - PutStatus Result = PutCacheRecord(PutRequest, &BatchRequest); + ZenCacheStore::PutResult Result = PutCacheRecord(PutRequest, &BatchRequest); - if (Result == PutStatus::Invalid) + if (Result.Status == PutStatus::Invalid) { return CbPackage{}; } - Results.push_back(Result == PutStatus::Success); + Results.push_back(Result); } if (Results.empty()) { return CbPackage{}; } + bool bWriteAllDetails = false; CbObjectWriter ResponseObject{256}; ResponseObject.BeginArray("Result"sv); - for (bool Value : Results) + for (const ZenCacheStore::PutResult& Result : Results) { - ResponseObject.AddBool(Value); + // Conflicts will be treated for response purposes + // as successful puts because the key is present in the cache. + ResponseObject.AddBool((Result.Status == PutStatus::Success) || (Result.Status == PutStatus::Conflict)); + if (Result.Details) + { + bWriteAllDetails = true; + } } ResponseObject.EndArray(); + if (bWriteAllDetails) + { + ResponseObject.BeginArray("Details"sv); + for (const ZenCacheStore::PutResult& Result : Results) + { + ResponseObject.AddObject(Result.Details); + } + ResponseObject.EndArray(); + } + CbPackage RpcResponse; RpcResponse.SetObject(ResponseObject.Save()); return RpcResponse; } -PutStatus +ZenCacheStore::PutResult CacheRpcHandler::PutCacheRecord(PutRequestData& Request, const CbPackage* Package) { CbObjectView Record = Request.RecordObject; @@ -422,7 +436,9 @@ CacheRpcHandler::PutCacheRecord(PutRequestData& Request, const CbPackage* Packag if (Count.Invalid > 0) { - return PutStatus::Invalid; + CbObjectWriter DetailWriter; + DetailWriter.AddString("Message", fmt::format("Found {}/{} invalid attachments", Count.Invalid, Count.Total)); + return ZenCacheStore::PutResult{PutStatus::Invalid, DetailWriter.Save()}; } ZenCacheValue CacheValue; @@ -442,7 +458,7 @@ CacheRpcHandler::PutCacheRecord(PutRequestData& Request, const CbPackage* Packag nullptr); if (PutResult.Status != zen::PutStatus::Success) { - return PutResult.Status; + return PutResult; } m_CacheStats.WriteCount++; @@ -480,7 +496,7 @@ CacheRpcHandler::PutCacheRecord(PutRequestData& Request, const CbPackage* Packag .Key = Request.Key, .ValueContentIds = std::move(ValidAttachments)}); } - return PutStatus::Success; + return PutResult; } CbPackage @@ -843,14 +859,10 @@ CacheRpcHandler::HandleRpcGetCacheRecords(const CacheRequestContext& Context, Cb Key.Hash); } } - if (!Value.Exists && !EnumHasAllFlags(ValuePolicy, CachePolicy::SkipData)) + if (!Value.Exists) { Request.Complete = false; } - // Request.Complete does not need to be set to false for upstream SkipData attachments. - // In the PartialRecord==false case, the upstream will have failed the entire record if any SkipData attachment - // didn't exist and we will not get here. In the PartialRecord==true case, we do not need to inform the client of - // any missing SkipData attachments. } Request.ElapsedTimeUs += Timer.GetElapsedTimeUs(); } @@ -866,7 +878,9 @@ CacheRpcHandler::HandleRpcGetCacheRecords(const CacheRequestContext& Context, Cb ResponsePackage.ReserveAttachments(Requests.size()); + eastl::fixed_vector<size_t, 4> IncompleteResultIndexes; ResponseObject.BeginArray("Result"sv); + size_t ResultIndex = 0; for (RecordRequestData& Request : Requests) { const CacheKey& Key = Request.Key; @@ -882,6 +896,12 @@ CacheRpcHandler::HandleRpcGetCacheRecords(const CacheRequestContext& Context, Cb } } + if (!Request.Complete) + { + // If requesting a partial record, report back that the record overall is incomplete to the client + IncompleteResultIndexes.push_back(ResultIndex); + } + ZEN_DEBUG("GETCACHERECORD HIT - '{}/{}/{}' {}{} ({}) in {}", *Namespace, Key.Bucket, @@ -918,8 +938,37 @@ CacheRpcHandler::HandleRpcGetCacheRecords(const CacheRequestContext& Context, Cb m_CacheStats.MissCount++; } } + ++ResultIndex; } ResponseObject.EndArray(); + + if (!IncompleteResultIndexes.empty()) + { + size_t IndexIntoIncompleteResultArray = 0; + size_t IncompleteResultIndex = IncompleteResultIndexes[IndexIntoIncompleteResultArray]; + ResultIndex = 0; + ResponseObject.BeginArray("Incomplete"sv); + for (ResultIndex = 0; ResultIndex < Requests.size(); ++ResultIndex) + { + if (IncompleteResultIndex == ResultIndex) + { + ResponseObject.AddBool(true); + if (++IndexIntoIncompleteResultArray >= IncompleteResultIndexes.size()) + { + IncompleteResultIndex = (size_t)-1; + } + else + { + IncompleteResultIndex = IncompleteResultIndexes[IndexIntoIncompleteResultArray]; + } + } + else + { + ResponseObject.AddBool(true); + } + } + ResponseObject.EndArray(); + } ResponsePackage.SetObject(ResponseObject.Save()); return ResponsePackage; } @@ -1050,7 +1099,9 @@ CacheRpcHandler::HandleRpcPutCacheValues(const CacheRequestContext& Context, con } else { - Results.push_back({zen::PutStatus::Fail, fmt::format("Missing attachment with raw hash {}", RawHash)}); + CbObjectWriter DetailWriter; + DetailWriter.AddString("Message", fmt::format("Missing attachment with raw hash {}", RawHash)); + Results.push_back({zen::PutStatus::Fail, DetailWriter.Save()}); } } // We do not search the Upstream. No data in a put means the caller is probing for whether they need to do a heavy put. @@ -1101,29 +1152,32 @@ CacheRpcHandler::HandleRpcPutCacheValues(const CacheRequestContext& Context, con ZEN_TRACE_CPU("Z$::RpcPutCacheValues::Response"); CbObjectWriter ResponseObject{1024}; ResponseObject.BeginArray("Result"sv); - bool bAnyErrors = false; + bool bWriteAllDetails = false; for (const ZenCacheStore::PutResult& Value : Results) { - if (Value.Status == zen::PutStatus::Success) + // Conflicts will be treated for response purposes + // as successful puts because the key is present in the cache. + if ((Value.Status == zen::PutStatus::Success) || (Value.Status == zen::PutStatus::Conflict)) { ResponseObject.AddBool(true); } else { - bAnyErrors = true; ResponseObject.AddBool(false); } + // If one result has details, we must write all details + if (Value.Details) + { + bWriteAllDetails = true; + } } ResponseObject.EndArray(); - if (bAnyErrors) + if (bWriteAllDetails) { - ResponseObject.BeginArray("ErrorMessages"sv); + ResponseObject.BeginArray("Details"sv); for (const ZenCacheStore::PutResult& Value : Results) { - if (Value.Status != zen::PutStatus::Success) - { - ResponseObject.AddString(Value.Message); - } + ResponseObject.AddObject(Value.Details); } ResponseObject.EndArray(); } @@ -1768,7 +1822,9 @@ CacheRpcHandler::GetLocalCacheValues(const CacheRequestContext& Context, ChunkRequest* Request = ValueRequests[RequestIndex]; if (Chunks[RequestIndex].Value) { - if (IsCompressedBinary(Chunks[RequestIndex].Value.GetContentType())) + // If the request included a raw hash, only offer a result if the RawHash matched what we found in storage + if (IsCompressedBinary(Chunks[RequestIndex].Value.GetContentType()) && + ((Request->Key->ChunkId == IoHash::Zero) || (Request->Key->ChunkId == Chunks[RequestIndex].RawHash))) { Request->Key->ChunkId = Chunks[RequestIndex].RawHash; Request->Exists = true; diff --git a/src/zenstore/cache/structuredcachestore.cpp b/src/zenstore/cache/structuredcachestore.cpp index 1f2d6c37f..da6acbde4 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> @@ -730,7 +729,9 @@ ZenCacheStore::Put(const CacheRequestContext& Context, if (IsKnownBadBucketName(Bucket)) { m_RejectedWriteCount++; - return PutResult{zen::PutStatus::Invalid, "Bad bucket name"}; + CbObjectWriter DetailWriter; + DetailWriter.AddString("Message", "Bad bucket name"); + return PutResult{zen::PutStatus::Invalid, DetailWriter.Save()}; } ZEN_MEMSCOPE(GetCacheStoreTag()); @@ -777,7 +778,10 @@ ZenCacheStore::Put(const CacheRequestContext& Context, Namespace, Bucket, HashKey.ToHexString()); - return PutResult{zen::PutStatus::Fail, fmt::format("Unknown namespace '{}'", Namespace)}; + + CbObjectWriter DetailWriter; + DetailWriter.AddString("Message", fmt::format("Unknown namespace '{}'", Namespace)); + return PutResult{zen::PutStatus::Fail, DetailWriter.Save()}; } bool @@ -1328,17 +1332,30 @@ namespace testutils { std::pair<Oid, IoBuffer> CreateBinaryBlob(size_t Size) { return {Oid::NewOid(), CreateRandomBlob(Size)}; } - std::vector<std::pair<Oid, CompressedBuffer>> CreateCompressedAttachment(CidStore& Store, const std::span<const size_t>& Sizes) + std::vector<std::pair<Oid, CompressedBuffer>> CreateCompressedAttachment(CidStore& Store, + WorkerThreadPool& ThreadPool, + const std::span<const size_t>& Sizes) { std::vector<std::pair<Oid, CompressedBuffer>> Result; - Result.reserve(Sizes.size()); - for (size_t Size : Sizes) + Result.resize(Sizes.size()); + Latch L(1); + for (size_t Index = 0; Index < Sizes.size(); Index++) { - auto Blob = CreateBinaryBlob(Size); - CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(Blob.second.Data(), Blob.second.Size())); - CHECK(!Store.ContainsChunk(Compressed.DecodeRawHash())); - Result.emplace_back(std::pair<Oid, CompressedBuffer>(Blob.first, Compressed)); + L.AddCount(1); + ThreadPool.ScheduleWork( + [&, Index]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + size_t Size = Sizes[Index]; + auto Blob = CreateBinaryBlob(Size); + CompressedBuffer Compressed = + CompressedBuffer::Compress(SharedBuffer::MakeView(Blob.second.Data(), Blob.second.Size())); + CHECK(!Store.ContainsChunk(Compressed.DecodeRawHash())); + Result[Index] = std::pair<Oid, CompressedBuffer>(Blob.first, Compressed); + }, + WorkerThreadPool::EMode::DisableBacklog); } + L.CountDown(); + L.Wait(); return Result; } @@ -1524,7 +1541,7 @@ TEST_CASE("cachestore.threadedinsert") // * doctest::skip(true)) ScopedTemporaryDirectory TempDir; const uint64_t kChunkSize = 1048; - const int32_t kChunkCount = 8192; + const int32_t kChunkCount = 4096; struct Chunk { @@ -1565,24 +1582,28 @@ TEST_CASE("cachestore.threadedinsert") // * doctest::skip(true)) CreateDirectories(TempDir.Path()); - WorkerThreadPool ThreadPool(4); + WorkerThreadPool ThreadPool(Max(std::thread::hardware_concurrency(), 8u)); GcManager Gc; auto JobQueue = MakeJobQueue(1, "testqueue"); ZenCacheNamespace Zcs(Gc, *JobQueue, TempDir.Path(), {}); { + Latch L(1); std::atomic<size_t> WorkCompleted = 0; for (const auto& Chunk : Chunks) { - ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, &Chunk]() { - Zcs.Put(Chunk.second.Bucket, Chunk.first, {.Value = Chunk.second.Buffer}, {}, false); - WorkCompleted.fetch_add(1); - }); - } - while (WorkCompleted < Chunks.size()) - { - Sleep(1); + L.AddCount(1); + ThreadPool.ScheduleWork( + [&Zcs, &WorkCompleted, &Chunk, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + Zcs.Put(Chunk.second.Bucket, Chunk.first, {.Value = Chunk.second.Buffer}, {}, false); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::EnableBacklog); } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == Chunks.size()); } auto DoGC = [](GcManager& Gc, ZenCacheNamespace& Zcs, std::unordered_map<IoHash, std::string, IoHash::Hasher>& GcChunkHashes) { @@ -1609,24 +1630,28 @@ TEST_CASE("cachestore.threadedinsert") // * doctest::skip(true)) CHECK_LE(kChunkSize * Chunks.size(), TotalSize); { + Latch L(1); std::atomic<size_t> WorkCompleted = 0; for (const auto& Chunk : Chunks) { - ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, &Chunk]() { - std::string Bucket = Chunk.second.Bucket; - IoHash ChunkHash = Chunk.first; - ZenCacheValue CacheValue; + L.AddCount(1); + ThreadPool.ScheduleWork( + [&Zcs, &WorkCompleted, &Chunk, &L]() { + auto _ = MakeGuard([&]() { L.CountDown(); }); + std::string Bucket = Chunk.second.Bucket; + IoHash ChunkHash = Chunk.first; + ZenCacheValue CacheValue; - CHECK(Zcs.Get(Bucket, ChunkHash, CacheValue)); - IoHash Hash = IoHash::HashBuffer(CacheValue.Value); - CHECK(ChunkHash == Hash); - WorkCompleted.fetch_add(1); - }); - } - while (WorkCompleted < Chunks.size()) - { - Sleep(1); + CHECK(Zcs.Get(Bucket, ChunkHash, CacheValue)); + IoHash Hash = IoHash::HashBuffer(CacheValue.Value); + CHECK(ChunkHash == Hash); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::EnableBacklog); } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == Chunks.size()); } std::unordered_map<IoHash, std::string, IoHash::Hasher> GcChunkHashes; GcChunkHashes.reserve(Chunks.size()); @@ -1655,23 +1680,30 @@ TEST_CASE("cachestore.threadedinsert") // * doctest::skip(true)) std::atomic_uint32_t AddedChunkCount = 0; for (const auto& Chunk : NewChunks) { - ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, Chunk, &AddedChunkCount]() { - Zcs.Put(Chunk.second.Bucket, Chunk.first, {.Value = Chunk.second.Buffer}, {}, false); - AddedChunkCount.fetch_add(1); - WorkCompleted.fetch_add(1); - }); + ThreadPool.ScheduleWork( + [&Zcs, &WorkCompleted, Chunk, &AddedChunkCount]() { + Zcs.Put(Chunk.second.Bucket, Chunk.first, {.Value = Chunk.second.Buffer}, {}, false); + AddedChunkCount.fetch_add(1); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::EnableBacklog); } + Latch L(1); for (const auto& Chunk : Chunks) { - ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, Chunk]() { - ZenCacheValue CacheValue; - if (Zcs.Get(Chunk.second.Bucket, Chunk.first, CacheValue)) - { - CHECK(Chunk.first == IoHash::HashBuffer(CacheValue.Value)); - } - WorkCompleted.fetch_add(1); - }); + L.AddCount(1); + ThreadPool.ScheduleWork( + [&Zcs, &WorkCompleted, Chunk, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + ZenCacheValue CacheValue; + if (Zcs.Get(Chunk.second.Bucket, Chunk.first, CacheValue)) + { + CHECK(Chunk.first == IoHash::HashBuffer(CacheValue.Value)); + } + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::EnableBacklog); } while (AddedChunkCount.load() < NewChunks.size()) { @@ -1687,10 +1719,9 @@ TEST_CASE("cachestore.threadedinsert") // * doctest::skip(true)) DoGC(Gc, Zcs, GcChunkHashes); } - while (WorkCompleted < NewChunks.size() + Chunks.size()) - { - Sleep(1); - } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == NewChunks.size() + Chunks.size()); { // Need to be careful since we might GC blocks we don't know outside of RwLock::ExclusiveLockScope @@ -1706,21 +1737,25 @@ TEST_CASE("cachestore.threadedinsert") // * doctest::skip(true)) } } { + Latch L(1); { std::atomic<size_t> WorkCompleted = 0; for (const auto& Chunk : GcChunkHashes) { - ThreadPool.ScheduleWork([&Zcs, &WorkCompleted, Chunk]() { - ZenCacheValue CacheValue; - CHECK(Zcs.Get(Chunk.second, Chunk.first, CacheValue)); - CHECK(Chunk.first == IoHash::HashBuffer(CacheValue.Value)); - WorkCompleted.fetch_add(1); - }); - } - while (WorkCompleted < GcChunkHashes.size()) - { - Sleep(1); + L.AddCount(1); + ThreadPool.ScheduleWork( + [&Zcs, &WorkCompleted, Chunk, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + ZenCacheValue CacheValue; + CHECK(Zcs.Get(Chunk.second, Chunk.first, CacheValue)); + CHECK(Chunk.first == IoHash::HashBuffer(CacheValue.Value)); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::EnableBacklog); } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == GcChunkHashes.size()); } } } @@ -1848,11 +1883,13 @@ TEST_CASE("cachestore.drop.bucket") CHECK(Value1.Value); std::atomic_bool WorkComplete = false; - Workers.ScheduleWork([&]() { - zen::Sleep(100); - Value1.Value = IoBuffer{}; - WorkComplete = true; - }); + Workers.ScheduleWork( + [&]() { + zen::Sleep(100); + Value1.Value = IoBuffer{}; + WorkComplete = true; + }, + WorkerThreadPool::EMode::EnableBacklog); // On Windows, DropBucket() will be blocked as long as we hold a reference to a buffer in the bucket // Our DropBucket execution blocks any incoming request from completing until we are done with the drop CHECK(Zcs.DropBucket(Namespace, Bucket)); @@ -1931,14 +1968,16 @@ TEST_CASE("cachestore.drop.namespace") CHECK(Value4.Value); std::atomic_bool WorkComplete = false; - Workers.ScheduleWork([&]() { - zen::Sleep(100); - Value1.Value = IoBuffer{}; - Value2.Value = IoBuffer{}; - Value3.Value = IoBuffer{}; - Value4.Value = IoBuffer{}; - WorkComplete = true; - }); + Workers.ScheduleWork( + [&]() { + zen::Sleep(100); + Value1.Value = IoBuffer{}; + Value2.Value = IoBuffer{}; + Value3.Value = IoBuffer{}; + Value4.Value = IoBuffer{}; + WorkComplete = true; + }, + WorkerThreadPool::EMode::EnableBacklog); // On Windows, DropBucket() will be blocked as long as we hold a reference to a buffer in the bucket // Our DropBucket execution blocks any incoming request from completing until we are done with the drop CHECK(Zcs.DropNamespace(Namespace1)); @@ -2218,6 +2257,8 @@ TEST_CASE("cachestore.newgc.basics") std::vector<IoHash> CacheRecords; std::vector<IoHash> UnstructuredCacheValues; + WorkerThreadPool WorkerPool(Max(std::thread::hardware_concurrency() - 1u, 2u)); + const auto TearDrinkerBucket = "teardrinker"sv; { GcManager Gc; @@ -2228,11 +2269,12 @@ TEST_CASE("cachestore.newgc.basics") // Create some basic data { // Structured record with attachments - auto Attachments1 = CreateCompressedAttachment(CidStore, std::vector<size_t>{77, 1024 * 1024 * 2, 99, 1024 * 1024 * 2 + 87}); + auto Attachments1 = + CreateCompressedAttachment(CidStore, WorkerPool, std::vector<size_t>{77, 1024 * 1024 * 2, 99, 1024 * 1024 * 2 + 87}); CacheRecords.emplace_back(CreateCacheRecord(Zcs, CidStore, TearDrinkerBucket, Attachments1)); // Structured record with reuse of attachments - auto Attachments2 = CreateCompressedAttachment(CidStore, std::vector<size_t>{971}); + auto Attachments2 = CreateCompressedAttachment(CidStore, WorkerPool, std::vector<size_t>{971}); Attachments2.push_back(Attachments1[0]); Attachments2.push_back(Attachments1[1]); CacheRecords.emplace_back(CreateCacheRecord(Zcs, CidStore, TearDrinkerBucket, Attachments2)); @@ -2646,7 +2688,7 @@ TEST_CASE("cachestore.newgc.basics") CHECK_EQ(7, Zcs.GetBucketInfo(TearDrinkerBucket).value().DiskLayerInfo.EntryCount); auto Attachments = - CreateCompressedAttachment(CidStore, std::vector<size_t>{177, 1024 * 1024 * 2 + 31, 8999, 1024 * 1024 * 2 + 187}); + CreateCompressedAttachment(CidStore, WorkerPool, std::vector<size_t>{177, 1024 * 1024 * 2 + 31, 8999, 1024 * 1024 * 2 + 187}); IoHash CacheRecord = CreateCacheRecord(Zcs, CidStore, TearDrinkerBucket, Attachments); { // Do get so it ends up in memcache diff --git a/src/zenstore/cas.cpp b/src/zenstore/cas.cpp index 6b89beb3d..49d24c21e 100644 --- a/src/zenstore/cas.cpp +++ b/src/zenstore/cas.cpp @@ -132,13 +132,18 @@ CasImpl::Initialize(const CidStoreConfiguration& InConfig) WorkerThreadPool& WorkerPool = GetMediumWorkerPool(EWorkloadType::Burst); std::vector<std::future<void>> Work; Work.emplace_back( - WorkerPool.EnqueueTask(std::packaged_task<void()>{[&]() { m_LargeStrategy.Initialize(m_Config.RootDirectory, IsNewStore); }})); - Work.emplace_back(WorkerPool.EnqueueTask(std::packaged_task<void()>{[&]() { - m_TinyStrategy.Initialize(m_Config.RootDirectory, "tobs", 1u << 28, 16, IsNewStore); // 256 Mb per block - }})); - Work.emplace_back(WorkerPool.EnqueueTask(std::packaged_task<void()>{[&]() { - m_SmallStrategy.Initialize(m_Config.RootDirectory, "sobs", 1u << 30, 4096, IsNewStore); // 1 Gb per block - }})); + WorkerPool.EnqueueTask(std::packaged_task<void()>{[&]() { m_LargeStrategy.Initialize(m_Config.RootDirectory, IsNewStore); }}, + WorkerThreadPool::EMode::DisableBacklog)); + Work.emplace_back(WorkerPool.EnqueueTask( + std::packaged_task<void()>{[&]() { + m_TinyStrategy.Initialize(m_Config.RootDirectory, "tobs", 1u << 28, 16, IsNewStore); // 256 Mb per block + }}, + WorkerThreadPool::EMode::DisableBacklog)); + Work.emplace_back(WorkerPool.EnqueueTask( + std::packaged_task<void()>{[&]() { + m_SmallStrategy.Initialize(m_Config.RootDirectory, "sobs", 1u << 30, 4096, IsNewStore); // 1 Gb per block + }}, + WorkerThreadPool::EMode::DisableBacklog)); for (std::future<void>& Result : Work) { if (Result.valid()) diff --git a/src/zenstore/compactcas.cpp b/src/zenstore/compactcas.cpp index b00abb2cb..5cc4dad54 100644 --- a/src/zenstore/compactcas.cpp +++ b/src/zenstore/compactcas.cpp @@ -11,11 +11,11 @@ #include <zencore/iobuffer.h> #include <zencore/logging.h> #include <zencore/memory/llm.h> +#include <zencore/parallelwork.h> #include <zencore/scopeguard.h> #include <zencore/trace.h> #include <zencore/workthreadpool.h> #include <zenstore/scrubcontext.h> -#include <zenutil/parallelwork.h> #include <gsl/gsl-lite.hpp> @@ -368,6 +368,14 @@ CasContainerStrategy::IterateChunks(std::span<const IoHash> ChunkHas { IoBuffer Chunk = m_BlockStore.TryGetChunk(FoundChunkLocations[Index]); size_t OuterIndex = FoundChunkIndexes[Index]; + if (!Chunk) + { + ZEN_WARN("Failed to fetch chunk {} from block {}, Offset {}, Size {}", + ChunkHashes[OuterIndex], + FoundChunkLocations[Index].BlockIndex, + FoundChunkLocations[Index].Offset, + FoundChunkLocations[Index].Size); + } if (!AsyncCallback(OuterIndex, Chunk)) { return false; @@ -376,17 +384,26 @@ CasContainerStrategy::IterateChunks(std::span<const IoHash> ChunkHas return true; } - auto DoOneBlock = [this](const std::function<bool(size_t Index, const IoBuffer& Payload)>& AsyncCallback, - uint64_t LargeSizeLimit, - std::span<const size_t> FoundChunkIndexes, - std::span<const BlockStoreLocation> FoundChunkLocations, - std::span<const size_t> ChunkIndexes) { + auto DoOneBlock = [this, &ChunkHashes](const std::function<bool(size_t Index, const IoBuffer& Payload)>& AsyncCallback, + uint64_t LargeSizeLimit, + std::span<const size_t> FoundChunkIndexes, + std::span<const BlockStoreLocation> FoundChunkLocations, + std::span<const size_t> ChunkIndexes) { if (ChunkIndexes.size() < 4) { for (size_t ChunkIndex : ChunkIndexes) { - IoBuffer Chunk = m_BlockStore.TryGetChunk(FoundChunkLocations[ChunkIndex]); - if (!AsyncCallback(FoundChunkIndexes[ChunkIndex], Chunk)) + size_t OuterIndex = FoundChunkIndexes[ChunkIndex]; + IoBuffer Chunk = m_BlockStore.TryGetChunk(FoundChunkLocations[ChunkIndex]); + if (!Chunk) + { + ZEN_WARN("Failed to fetch chunk {} from block {}, Offset {}, Size {}", + ChunkHashes[OuterIndex], + FoundChunkLocations[ChunkIndex].BlockIndex, + FoundChunkLocations[ChunkIndex].Offset, + FoundChunkLocations[ChunkIndex].Size); + } + if (!AsyncCallback(OuterIndex, Chunk)) { return false; } @@ -396,15 +413,30 @@ CasContainerStrategy::IterateChunks(std::span<const IoHash> ChunkHas return m_BlockStore.IterateBlock( FoundChunkLocations, ChunkIndexes, - [AsyncCallback, FoundChunkIndexes](size_t ChunkIndex, const void* Data, uint64_t Size) { + [this, &ChunkHashes, AsyncCallback, FoundChunkIndexes](size_t ChunkIndex, const void* Data, uint64_t Size) { + size_t OuterIndex = FoundChunkIndexes[ChunkIndex]; if (Data == nullptr) { - return AsyncCallback(FoundChunkIndexes[ChunkIndex], IoBuffer()); + ZEN_WARN("Failed to fetch chunk {}, Size {}", ChunkHashes[OuterIndex], Size); + return AsyncCallback(OuterIndex, IoBuffer()); } - return AsyncCallback(FoundChunkIndexes[ChunkIndex], IoBuffer(IoBuffer::Wrap, Data, Size)); + return AsyncCallback(OuterIndex, IoBuffer(IoBuffer::Wrap, Data, Size)); }, - [AsyncCallback, FoundChunkIndexes](size_t ChunkIndex, BlockStoreFile& File, uint64_t Offset, uint64_t Size) { - return AsyncCallback(FoundChunkIndexes[ChunkIndex], File.GetChunk(Offset, Size)); + [this, &ChunkHashes, AsyncCallback, FoundChunkIndexes](size_t ChunkIndex, + BlockStoreFile& File, + uint64_t Offset, + uint64_t Size) { + size_t OuterIndex = FoundChunkIndexes[ChunkIndex]; + IoBuffer Chunk = File.GetChunk(Offset, Size); + if (!Chunk) + { + ZEN_WARN("Failed to fetch chunk {} from '{}', Offset {}, Size {}", + ChunkHashes[OuterIndex], + File.GetPath(), + Offset, + Size); + } + return AsyncCallback(OuterIndex, Chunk); }, LargeSizeLimit); }; @@ -412,7 +444,7 @@ CasContainerStrategy::IterateChunks(std::span<const IoHash> ChunkHas std::atomic<bool> AbortFlag; { std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::DisableBacklog); try { const bool Continue = m_BlockStore.IterateChunks( @@ -1459,7 +1491,7 @@ TEST_CASE("compactcas.threadedinsert") ScopedTemporaryDirectory TempDir; const uint64_t kChunkSize = 1048; - const int32_t kChunkCount = 4096; + const int32_t kChunkCount = 2048; uint64_t ExpectedSize = 0; tsl::robin_map<IoHash, IoBuffer, IoHash::Hasher> Chunks; @@ -1481,48 +1513,58 @@ TEST_CASE("compactcas.threadedinsert") } } - std::atomic<size_t> WorkCompleted = 0; WorkerThreadPool ThreadPool(4); GcManager Gc; CasContainerStrategy Cas(Gc); - Cas.Initialize(TempDir.Path(), "test", 32768, 16, true); { - for (const auto& Chunk : Chunks) - { - const IoHash& Hash = Chunk.first; - const IoBuffer& Buffer = Chunk.second; - ThreadPool.ScheduleWork([&Cas, &WorkCompleted, Buffer, Hash]() { - CasStore::InsertResult InsertResult = Cas.InsertChunk(Buffer, Hash); - ZEN_ASSERT(InsertResult.New); - WorkCompleted.fetch_add(1); - }); - } - while (WorkCompleted < Chunks.size()) + std::atomic<size_t> WorkCompleted = 0; + Latch L(1); + Cas.Initialize(TempDir.Path(), "test", 32768, 16, true); { - Sleep(1); + for (const auto& Chunk : Chunks) + { + const IoHash& Hash = Chunk.first; + const IoBuffer& Buffer = Chunk.second; + L.AddCount(1); + ThreadPool.ScheduleWork( + [&Cas, &WorkCompleted, Buffer, Hash, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + CasStore::InsertResult InsertResult = Cas.InsertChunk(Buffer, Hash); + ZEN_ASSERT(InsertResult.New); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); + } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == Chunks.size()); } } - WorkCompleted = 0; - const uint64_t TotalSize = Cas.StorageSize().DiskSize; - CHECK_LE(ExpectedSize, TotalSize); - CHECK_GE(ExpectedSize + 32768, TotalSize); - { + std::atomic<size_t> WorkCompleted = 0; + Latch L(1); + const uint64_t TotalSize = Cas.StorageSize().DiskSize; + CHECK_LE(ExpectedSize, TotalSize); + CHECK_GE(ExpectedSize + 32768, TotalSize); + for (const auto& Chunk : Chunks) { - ThreadPool.ScheduleWork([&Cas, &WorkCompleted, &Chunk]() { - IoHash ChunkHash = Chunk.first; - IoBuffer Buffer = Cas.FindChunk(ChunkHash); - IoHash Hash = IoHash::HashBuffer(Buffer); - CHECK(ChunkHash == Hash); - WorkCompleted.fetch_add(1); - }); - } - while (WorkCompleted < Chunks.size()) - { - Sleep(1); + L.AddCount(1); + ThreadPool.ScheduleWork( + [&Cas, &WorkCompleted, &Chunk, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + IoHash ChunkHash = Chunk.first; + IoBuffer Buffer = Cas.FindChunk(ChunkHash); + IoHash Hash = IoHash::HashBuffer(Buffer); + CHECK(ChunkHash == Hash); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == Chunks.size()); } tsl::robin_set<IoHash, IoHash::Hasher> GcChunkHashes; @@ -1532,7 +1574,8 @@ TEST_CASE("compactcas.threadedinsert") GcChunkHashes.insert(Chunk.first); } { - WorkCompleted = 0; + std::atomic<size_t> WorkCompleted = 0; + Latch L(1); tsl::robin_map<IoHash, IoBuffer, IoHash::Hasher> NewChunks; NewChunks.reserve(kChunkCount); @@ -1548,23 +1591,31 @@ TEST_CASE("compactcas.threadedinsert") for (const auto& Chunk : NewChunks) { - ThreadPool.ScheduleWork([&Cas, &WorkCompleted, Chunk, &AddedChunkCount]() { - Cas.InsertChunk(Chunk.second, Chunk.first); - AddedChunkCount.fetch_add(1); - WorkCompleted.fetch_add(1); - }); + L.AddCount(1); + ThreadPool.ScheduleWork( + [&Cas, &WorkCompleted, Chunk, &AddedChunkCount, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + Cas.InsertChunk(Chunk.second, Chunk.first); + AddedChunkCount.fetch_add(1); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); } for (const auto& Chunk : Chunks) { - ThreadPool.ScheduleWork([&Cas, &WorkCompleted, Chunk]() { - IoHash ChunkHash = Chunk.first; - IoBuffer Buffer = Cas.FindChunk(ChunkHash); - if (Buffer) - { - CHECK(ChunkHash == IoHash::HashBuffer(Buffer)); - } - WorkCompleted.fetch_add(1); - }); + L.AddCount(1); + ThreadPool.ScheduleWork( + [&Cas, &WorkCompleted, Chunk, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + IoHash ChunkHash = Chunk.first; + IoBuffer Buffer = Cas.FindChunk(ChunkHash); + if (Buffer) + { + CHECK(ChunkHash == IoHash::HashBuffer(Buffer)); + } + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); } tsl::robin_set<IoHash, IoHash::Hasher> ChunksToDelete; @@ -1638,27 +1689,31 @@ TEST_CASE("compactcas.threadedinsert") DoGC(Cas, ChunksToDelete, KeepHashes, GcChunkHashes); } - while (WorkCompleted < NewChunks.size() + Chunks.size()) - { - Sleep(1); - } + L.CountDown(); + L.Wait(); + + CHECK(WorkCompleted == NewChunks.size() + Chunks.size()); DoGC(Cas, ChunksToDelete, KeepHashes, GcChunkHashes); } { - WorkCompleted = 0; + std::atomic<size_t> WorkCompleted = 0; + Latch L(1); for (const IoHash& ChunkHash : GcChunkHashes) { - ThreadPool.ScheduleWork([&Cas, &WorkCompleted, ChunkHash]() { - CHECK(Cas.HaveChunk(ChunkHash)); - CHECK(ChunkHash == IoHash::HashBuffer(Cas.FindChunk(ChunkHash))); - WorkCompleted.fetch_add(1); - }); - } - while (WorkCompleted < GcChunkHashes.size()) - { - Sleep(1); + L.AddCount(1); + ThreadPool.ScheduleWork( + [&Cas, &WorkCompleted, ChunkHash, &L]() { + auto _ = MakeGuard([&L]() { L.CountDown(); }); + CHECK(Cas.HaveChunk(ChunkHash)); + CHECK(ChunkHash == IoHash::HashBuffer(Cas.FindChunk(ChunkHash))); + WorkCompleted.fetch_add(1); + }, + WorkerThreadPool::EMode::DisableBacklog); } + L.CountDown(); + L.Wait(); + CHECK(WorkCompleted == GcChunkHashes.size()); } } } @@ -1667,9 +1722,9 @@ TEST_CASE("compactcas.restart") { uint64_t ExpectedSize = 0; - auto GenerateChunks = [&](CasContainerStrategy& Cas, size_t ChunkCount, uint64_t ChunkSize, std::vector<IoHash>& Hashes) { - WorkerThreadPool ThreadPool(Max(std::thread::hardware_concurrency() - 1u, 2u), "put"); + WorkerThreadPool ThreadPool(Max(std::thread::hardware_concurrency() - 1u, 2u), "put"); + auto GenerateChunks = [&](CasContainerStrategy& Cas, size_t ChunkCount, uint64_t ChunkSize, std::vector<IoHash>& Hashes) { Latch WorkLatch(1); tsl::robin_set<IoHash, IoHash::Hasher> ChunkHashesLookup; ChunkHashesLookup.reserve(ChunkCount); @@ -1711,11 +1766,13 @@ TEST_CASE("compactcas.restart") RwLock::ExclusiveLockScope __(InsertLock); Hashes.insert(Hashes.end(), BatchHashes.begin(), BatchHashes.end()); } - }); + }, + WorkerThreadPool::EMode::DisableBacklog); Offset += BatchCount; } WorkLatch.CountDown(); WorkLatch.Wait(); + CHECK(ChunkHashesLookup.size() == ChunkCount); }; ScopedTemporaryDirectory TempDir; @@ -1737,26 +1794,32 @@ TEST_CASE("compactcas.restart") Hashes.reserve(kChunkCount); auto ValidateChunks = [&](CasContainerStrategy& Cas, std::span<const IoHash> Hashes, bool ShouldExist) { - for (const IoHash& Hash : Hashes) - { - if (ShouldExist) - { - CHECK(Cas.HaveChunk(Hash)); - IoBuffer Buffer = Cas.FindChunk(Hash); - CHECK(Buffer); - IoHash ValidateHash; - uint64_t ValidateRawSize; - CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Buffer), ValidateHash, ValidateRawSize); - CHECK(Compressed); - CHECK(ValidateHash == Hash); - } - else - { - CHECK(!Cas.HaveChunk(Hash)); - IoBuffer Buffer = Cas.FindChunk(Hash); - CHECK(!Buffer); - } - } + // It's important to not use std::vector<bool> here as that is not safe to use from multiple threads + // due to its non-atomic updates using bit masking + std::vector<uint8_t> Exists(Hashes.size(), false); + Cas.IterateChunks( + Hashes, + [&](size_t Index, const IoBuffer& Buffer) -> bool { + Exists[Index] = !!Buffer; + if (ShouldExist) + { + CHECK(Buffer); + IoHash ValidateHash; + uint64_t ValidateRawSize; + CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Buffer), ValidateHash, ValidateRawSize); + CHECK(Compressed); + CHECK(ValidateHash == Hashes[Index]); + } + else + { + CHECK(!Buffer); + } + return true; + }, + &ThreadPool, + 1u * 1248u); + + CHECK_EQ(std::find(Exists.begin(), Exists.end(), !ShouldExist ? 1 : 0), Exists.end()); }; { @@ -1901,7 +1964,7 @@ TEST_CASE("compactcas.iteratechunks") const uint64_t kChunkSize = 1048 + 395; const size_t kChunkCount = 63840; - for (uint32_t N = 0; N < 4; N++) + for (uint32_t N = 0; N < 2; N++) { GcManager Gc; CasContainerStrategy Cas(Gc); @@ -1964,7 +2027,8 @@ TEST_CASE("compactcas.iteratechunks") RwLock::ExclusiveLockScope __(InsertLock); Hashes.insert(Hashes.end(), BatchHashes.begin(), BatchHashes.end()); } - }); + }, + WorkerThreadPool::EMode::DisableBacklog); Offset += BatchCount; } WorkLatch.CountDown(); @@ -1998,82 +2062,84 @@ TEST_CASE("compactcas.iteratechunks") for (size_t I = 0; I < 2; I++) { WorkLatch.AddCount(1); - ThreadPool.ScheduleWork([&Cas, &Hashes, &BatchWorkerPool, &WorkLatch, I]() { - auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); }); - std::vector<IoHash> PartialHashes; - PartialHashes.reserve(Hashes.size() / 4); - for (size_t Index = 0; Index < Hashes.size(); Index++) - { - size_t TestIndex = Index + I; - if ((TestIndex % 7 == 1) || (TestIndex % 13 == 1) || (TestIndex % 17 == 1)) + ThreadPool.ScheduleWork( + [&Cas, &Hashes, &BatchWorkerPool, &WorkLatch, I]() { + auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); }); + std::vector<IoHash> PartialHashes; + PartialHashes.reserve(Hashes.size() / 4); + for (size_t Index = 0; Index < Hashes.size(); Index++) { - PartialHashes.push_back(Hashes[Index]); + size_t TestIndex = Index + I; + if ((TestIndex % 7 == 1) || (TestIndex % 13 == 1) || (TestIndex % 17 == 1)) + { + PartialHashes.push_back(Hashes[Index]); + } } - } - std::reverse(PartialHashes.begin(), PartialHashes.end()); + std::reverse(PartialHashes.begin(), PartialHashes.end()); - std::vector<IoHash> NoFoundHashes; - std::vector<size_t> NoFindIndexes; - - NoFoundHashes.reserve(9); - for (size_t J = 0; J < 9; J++) - { - std::string Data = fmt::format("oh no, we don't exist {}", J + 1); - NoFoundHashes.push_back(IoHash::HashBuffer(Data.data(), Data.length())); - } - - NoFindIndexes.reserve(9); - - // Sprinkle in chunks that are not found! - auto It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 0, NoFoundHashes[0]); - NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); - It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 0 + 1, NoFoundHashes[1]); - NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); - It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 1, NoFoundHashes[2]); - NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); - It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 1 + 1, NoFoundHashes[3]); - NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); - It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 2, NoFoundHashes[4]); - NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); - It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 3, NoFoundHashes[5]); - NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); - It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 3 + 1, NoFoundHashes[6]); - NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); - It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 4, NoFoundHashes[7]); - NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); - It = PartialHashes.insert(PartialHashes.end(), NoFoundHashes[8]); - NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); - - std::vector<std::atomic<bool>> FoundFlags(PartialHashes.size() + NoFoundHashes.size()); - std::vector<std::atomic<uint32_t>> FetchedCounts(PartialHashes.size() + NoFoundHashes.size()); - - CHECK(Cas.IterateChunks( - PartialHashes, - [&PartialHashes, &FoundFlags, &FetchedCounts, &NoFindIndexes](size_t Index, const IoBuffer& Payload) { - CHECK_EQ(NoFindIndexes.end(), std::find(NoFindIndexes.begin(), NoFindIndexes.end(), Index)); - uint32_t PreviousCount = FetchedCounts[Index].fetch_add(1); - CHECK(PreviousCount == 0); - FoundFlags[Index] = !!Payload; - const IoHash& Hash = PartialHashes[Index]; - CHECK(Hash == IoHash::HashBuffer(Payload)); - return true; - }, - &BatchWorkerPool, - 2048u)); + std::vector<IoHash> NoFoundHashes; + std::vector<size_t> NoFindIndexes; - for (size_t FoundIndex = 0; FoundIndex < PartialHashes.size(); FoundIndex++) - { - CHECK(FetchedCounts[FoundIndex].load() <= 1); - if (std::find(NoFindIndexes.begin(), NoFindIndexes.end(), FoundIndex) == NoFindIndexes.end()) + NoFoundHashes.reserve(9); + for (size_t J = 0; J < 9; J++) { - CHECK(FoundFlags[FoundIndex]); + std::string Data = fmt::format("oh no, we don't exist {}", J + 1); + NoFoundHashes.push_back(IoHash::HashBuffer(Data.data(), Data.length())); } - else + + NoFindIndexes.reserve(9); + + // Sprinkle in chunks that are not found! + auto It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 0, NoFoundHashes[0]); + NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); + It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 0 + 1, NoFoundHashes[1]); + NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); + It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 1, NoFoundHashes[2]); + NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); + It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 1 + 1, NoFoundHashes[3]); + NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); + It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 2, NoFoundHashes[4]); + NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); + It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 3, NoFoundHashes[5]); + NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); + It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 3 + 1, NoFoundHashes[6]); + NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); + It = PartialHashes.insert(PartialHashes.begin() + (PartialHashes.size() / 4) * 4, NoFoundHashes[7]); + NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); + It = PartialHashes.insert(PartialHashes.end(), NoFoundHashes[8]); + NoFindIndexes.push_back(std::distance(PartialHashes.begin(), It)); + + std::vector<std::atomic<bool>> FoundFlags(PartialHashes.size() + NoFoundHashes.size()); + std::vector<std::atomic<uint32_t>> FetchedCounts(PartialHashes.size() + NoFoundHashes.size()); + + CHECK(Cas.IterateChunks( + PartialHashes, + [&PartialHashes, &FoundFlags, &FetchedCounts, &NoFindIndexes](size_t Index, const IoBuffer& Payload) { + CHECK_EQ(NoFindIndexes.end(), std::find(NoFindIndexes.begin(), NoFindIndexes.end(), Index)); + uint32_t PreviousCount = FetchedCounts[Index].fetch_add(1); + CHECK(PreviousCount == 0); + FoundFlags[Index] = !!Payload; + const IoHash& Hash = PartialHashes[Index]; + CHECK(Hash == IoHash::HashBuffer(Payload)); + return true; + }, + &BatchWorkerPool, + 2048u)); + + for (size_t FoundIndex = 0; FoundIndex < PartialHashes.size(); FoundIndex++) { - CHECK(!FoundFlags[FoundIndex]); + CHECK(FetchedCounts[FoundIndex].load() <= 1); + if (std::find(NoFindIndexes.begin(), NoFindIndexes.end(), FoundIndex) == NoFindIndexes.end()) + { + CHECK(FoundFlags[FoundIndex]); + } + else + { + CHECK(!FoundFlags[FoundIndex]); + } } - } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); } WorkLatch.CountDown(); WorkLatch.Wait(); diff --git a/src/zenstore/filecas.cpp b/src/zenstore/filecas.cpp index 68644be2d..13437369f 100644 --- a/src/zenstore/filecas.cpp +++ b/src/zenstore/filecas.cpp @@ -9,6 +9,7 @@ #include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zencore/memory/memory.h> +#include <zencore/parallelwork.h> #include <zencore/scopeguard.h> #include <zencore/string.h> #include <zencore/testing.h> @@ -20,7 +21,6 @@ #include <zencore/workthreadpool.h> #include <zenstore/gc.h> #include <zenstore/scrubcontext.h> -#include <zenutil/parallelwork.h> #if ZEN_WITH_TESTS # include <zencore/compactbinarybuilder.h> @@ -656,6 +656,12 @@ FileCasStrategy::IterateChunks(std::span<IoHash> ChunkHashes, ZEN_ASSERT(ChunkIndex < ChunkHashes.size()); const IoHash& ChunkHash = ChunkHashes[ChunkIndex]; IoBuffer Payload = SafeOpenChunk(ChunkHash, ExpectedSize); + if (!Payload) + { + ShardingHelper Name(m_RootDirectory, ChunkHash); + const std::filesystem::path ChunkPath = Name.ShardedPath.ToPath(); + ZEN_WARN("Failed to fetch chunk {} from '{}', Size {}", ChunkHash, ChunkPath, ExpectedSize); + } if (!AsyncCallback(ChunkIndex, std::move(Payload))) { return false; @@ -665,7 +671,7 @@ FileCasStrategy::IterateChunks(std::span<IoHash> ChunkHashes, std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::DisableBacklog); try { for (size_t Index = 0; Index < FoundChunkIndexes.size(); Index++) @@ -754,6 +760,12 @@ FileCasStrategy::IterateChunks(std::function<void(const IoHash& Hash, IoBuffer&& const IoHash& ChunkHash = RawHashes[Index]; const uint64_t ExpectedSize = ExpectedSizes[Index]; IoBuffer Payload = SafeOpenChunk(ChunkHash, ExpectedSize); + if (!Payload) + { + ShardingHelper Name(m_RootDirectory, ChunkHash); + const std::filesystem::path ChunkPath = Name.ShardedPath.ToPath(); + ZEN_WARN("Failed to fetch chunk {} from '{}', Size {}", ChunkHash, ChunkPath, ExpectedSize); + } Callback(ChunkHash, std::move(Payload)); } } diff --git a/src/zenstore/gc.cpp b/src/zenstore/gc.cpp index a7ef401d5..050ee3443 100644 --- a/src/zenstore/gc.cpp +++ b/src/zenstore/gc.cpp @@ -543,6 +543,8 @@ FilterReferences(GcCtx& Ctx, std::string_view Context, std::vector<IoHash>& InOu return false; } + auto Log = [&Ctx]() { return Ctx.Logger; }; + const bool Filter = Ctx.Settings.AttachmentRangeMax != IoHash::Max || Ctx.Settings.AttachmentRangeMin != IoHash::Zero; size_t TotalCount = InOutReferences.size(); @@ -762,7 +764,7 @@ GcManager::CollectGarbage(const GcSettings& Settings) } catch (const std::bad_alloc& Ex) { - ZEN_ERROR("GCV2: Failed removing expired data for {}. Reason: '{}'", Owner->GetGcName(Ctx), Ex.what()); + ZEN_WARN("GCV2: Failed removing expired data for {}. Reason: '{}'", Owner->GetGcName(Ctx), Ex.what()); SetCancelGC(true); } catch (const std::exception& Ex) @@ -855,9 +857,9 @@ GcManager::CollectGarbage(const GcSettings& Settings) } catch (const std::bad_alloc& Ex) { - ZEN_ERROR("GCV2: Failed creating reference pruners for {}. Reason: '{}'", - ReferenceStore->GetGcName(Ctx), - Ex.what()); + ZEN_WARN("GCV2: Failed creating reference pruners for {}. Reason: '{}'", + ReferenceStore->GetGcName(Ctx), + Ex.what()); SetCancelGC(true); } catch (const std::exception& Ex) @@ -867,7 +869,8 @@ GcManager::CollectGarbage(const GcSettings& Settings) Ex.what()); SetCancelGC(true); } - }); + }, + WorkerThreadPool::EMode::DisableBacklog); } WorkLeft.CountDown(); WorkLeft.Wait(); @@ -966,9 +969,9 @@ GcManager::CollectGarbage(const GcSettings& Settings) } catch (const std::bad_alloc& Ex) { - ZEN_ERROR("GCV2: Failed creating reference checkers for {}. Reason: '{}'", - Referencer->GetGcName(Ctx), - Ex.what()); + ZEN_WARN("GCV2: Failed creating reference checkers for {}. Reason: '{}'", + Referencer->GetGcName(Ctx), + Ex.what()); SetCancelGC(true); } catch (const std::exception& Ex) @@ -979,7 +982,8 @@ GcManager::CollectGarbage(const GcSettings& Settings) SetCancelGC(true); } } - }); + }, + WorkerThreadPool::EMode::DisableBacklog); } WorkLeft.CountDown(); WorkLeft.Wait(); @@ -1019,77 +1023,80 @@ GcManager::CollectGarbage(const GcSettings& Settings) GcReferencer* Referencer = m_GcReferencers[Index]; std::pair<std::string, GcReferencerStats>* ReferemcerStats = &Result.ReferencerStats[Index]; WorkLeft.AddCount(1); - ParallelWorkThreadPool.ScheduleWork([this, - &Ctx, - &WorkLeft, - Referencer, - Index, - Result = &Result, - ReferemcerStats, - &ReferenceValidatorsLock, - &ReferenceValidators]() { - ZEN_MEMSCOPE(GetGcTag()); + ParallelWorkThreadPool.ScheduleWork( + [this, + &Ctx, + &WorkLeft, + Referencer, + Index, + Result = &Result, + ReferemcerStats, + &ReferenceValidatorsLock, + &ReferenceValidators]() { + ZEN_MEMSCOPE(GetGcTag()); - auto _ = MakeGuard([&WorkLeft]() { WorkLeft.CountDown(); }); - std::vector<GcReferenceValidator*> Validators; - auto __ = MakeGuard([&Validators]() { - while (!Validators.empty()) - { - delete Validators.back(); - Validators.pop_back(); - } - }); - try - { + auto _ = MakeGuard([&WorkLeft]() { WorkLeft.CountDown(); }); + std::vector<GcReferenceValidator*> Validators; + auto __ = MakeGuard([&Validators]() { + while (!Validators.empty()) + { + delete Validators.back(); + Validators.pop_back(); + } + }); + try { - SCOPED_TIMER(ReferemcerStats->second.CreateReferenceValidatorsMS = - std::chrono::milliseconds(Timer.GetElapsedTimeMs());); - Validators = Referencer->CreateReferenceValidators(Ctx); + { + SCOPED_TIMER(ReferemcerStats->second.CreateReferenceValidatorsMS = + std::chrono::milliseconds(Timer.GetElapsedTimeMs());); + Validators = Referencer->CreateReferenceValidators(Ctx); + } + if (!Validators.empty()) + { + RwLock::ExclusiveLockScope __(ReferenceValidatorsLock); + for (auto& ReferenceValidator : Validators) + { + size_t ReferencesStatsIndex = Result->ReferenceValidatorStats.size(); + Result->ReferenceValidatorStats.push_back({ReferenceValidator->GetGcName(Ctx), {}}); + ReferenceValidators.insert_or_assign( + std::unique_ptr<GcReferenceValidator>(ReferenceValidator), + ReferencesStatsIndex); + ReferenceValidator = nullptr; + } + } } - if (!Validators.empty()) + catch (const std::system_error& Ex) { - RwLock::ExclusiveLockScope __(ReferenceValidatorsLock); - for (auto& ReferenceValidator : Validators) + if (IsOOD(Ex) || IsOOM(Ex)) + { + ZEN_WARN("GCV2: Failed creating reference validators for {}. Reason: '{}'", + Referencer->GetGcName(Ctx), + Ex.what()); + } + else { - size_t ReferencesStatsIndex = Result->ReferenceValidatorStats.size(); - Result->ReferenceValidatorStats.push_back({ReferenceValidator->GetGcName(Ctx), {}}); - ReferenceValidators.insert_or_assign(std::unique_ptr<GcReferenceValidator>(ReferenceValidator), - ReferencesStatsIndex); - ReferenceValidator = nullptr; + ZEN_ERROR("GCV2: Failed creating reference validators for {}. Reason: '{}'", + Referencer->GetGcName(Ctx), + Ex.what()); } + SetCancelGC(true); } - } - catch (const std::system_error& Ex) - { - if (IsOOD(Ex) || IsOOM(Ex)) + catch (const std::bad_alloc& Ex) { ZEN_WARN("GCV2: Failed creating reference validators for {}. Reason: '{}'", Referencer->GetGcName(Ctx), Ex.what()); + SetCancelGC(true); } - else + catch (const std::exception& Ex) { ZEN_ERROR("GCV2: Failed creating reference validators for {}. Reason: '{}'", Referencer->GetGcName(Ctx), Ex.what()); + SetCancelGC(true); } - SetCancelGC(true); - } - catch (const std::bad_alloc& Ex) - { - ZEN_ERROR("GCV2: Failed creating reference validators for {}. Reason: '{}'", - Referencer->GetGcName(Ctx), - Ex.what()); - SetCancelGC(true); - } - catch (const std::exception& Ex) - { - ZEN_ERROR("GCV2: Failed creating reference validators for {}. Reason: '{}'", - Referencer->GetGcName(Ctx), - Ex.what()); - SetCancelGC(true); - } - }); + }, + WorkerThreadPool::EMode::DisableBacklog); } WorkLeft.CountDown(); WorkLeft.Wait(); @@ -1141,7 +1148,7 @@ GcManager::CollectGarbage(const GcSettings& Settings) } catch (const std::bad_alloc& Ex) { - ZEN_ERROR("GCV2: Failed precaching for {}. Reason: '{}'", Checker->GetGcName(Ctx), Ex.what()); + ZEN_WARN("GCV2: Failed precaching for {}. Reason: '{}'", Checker->GetGcName(Ctx), Ex.what()); SetCancelGC(true); } catch (const std::exception& Ex) @@ -1219,47 +1226,49 @@ GcManager::CollectGarbage(const GcSettings& Settings) size_t Index = It.second; std::pair<std::string, GcReferencerStats>* Stats = &Result.ReferencerStats[Index]; WorkLeft.AddCount(1); - LockedPhaseThreadPool.ScheduleWork([this, &Ctx, Checker, Index, Stats, &WorkLeft]() { - ZEN_MEMSCOPE(GetGcTag()); + LockedPhaseThreadPool.ScheduleWork( + [this, &Ctx, Checker, Index, Stats, &WorkLeft]() { + ZEN_MEMSCOPE(GetGcTag()); - auto _ = MakeGuard([&WorkLeft]() { WorkLeft.CountDown(); }); - try - { - SCOPED_TIMER(Stats->second.UpdateLockedStateMS = - std::chrono::milliseconds(Timer.GetElapsedTimeMs());); - Checker->UpdateLockedState(Ctx); - } - catch (const std::system_error& Ex) - { - if (IsOOD(Ex) || IsOOM(Ex)) + auto _ = MakeGuard([&WorkLeft]() { WorkLeft.CountDown(); }); + try + { + SCOPED_TIMER(Stats->second.UpdateLockedStateMS = + std::chrono::milliseconds(Timer.GetElapsedTimeMs());); + Checker->UpdateLockedState(Ctx); + } + catch (const std::system_error& Ex) + { + if (IsOOD(Ex) || IsOOM(Ex)) + { + ZEN_WARN("GCV2: Failed Updating locked state for {}. Reason: '{}'", + Checker->GetGcName(Ctx), + Ex.what()); + } + else + { + ZEN_ERROR("GCV2: Failed Updating locked state for {}. Reason: '{}'", + Checker->GetGcName(Ctx), + Ex.what()); + } + SetCancelGC(true); + } + catch (const std::bad_alloc& Ex) { ZEN_WARN("GCV2: Failed Updating locked state for {}. Reason: '{}'", Checker->GetGcName(Ctx), Ex.what()); + SetCancelGC(true); } - else + catch (const std::exception& Ex) { ZEN_ERROR("GCV2: Failed Updating locked state for {}. Reason: '{}'", Checker->GetGcName(Ctx), Ex.what()); + SetCancelGC(true); } - SetCancelGC(true); - } - catch (const std::bad_alloc& Ex) - { - ZEN_WARN("GCV2: Failed Updating locked state for {}. Reason: '{}'", - Checker->GetGcName(Ctx), - Ex.what()); - SetCancelGC(true); - } - catch (const std::exception& Ex) - { - ZEN_ERROR("GCV2: Failed Updating locked state for {}. Reason: '{}'", - Checker->GetGcName(Ctx), - Ex.what()); - SetCancelGC(true); - } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); } WorkLeft.CountDown(); WorkLeft.Wait(); @@ -1359,9 +1368,9 @@ GcManager::CollectGarbage(const GcSettings& Settings) } catch (const std::bad_alloc& Ex) { - ZEN_ERROR("GCV2: Failed removing unused data for {}. Reason: '{}'", - Pruner->GetGcName(Ctx), - Ex.what()); + ZEN_WARN("GCV2: Failed removing unused data for {}. Reason: '{}'", + Pruner->GetGcName(Ctx), + Ex.what()); SetCancelGC(true); } catch (const std::exception& Ex) @@ -1371,7 +1380,8 @@ GcManager::CollectGarbage(const GcSettings& Settings) Ex.what()); SetCancelGC(true); } - }); + }, + WorkerThreadPool::EMode::EnableBacklog); } WorkLeft.CountDown(); WorkLeft.Wait(); @@ -1441,7 +1451,7 @@ GcManager::CollectGarbage(const GcSettings& Settings) } catch (const std::bad_alloc& Ex) { - ZEN_ERROR("GCV2: Failed compacting store {}. Reason: '{}'", Compactor->GetGcName(Ctx), Ex.what()); + ZEN_WARN("GCV2: Failed compacting store {}. Reason: '{}'", Compactor->GetGcName(Ctx), Ex.what()); SetCancelGC(true); } catch (const std::exception& Ex) @@ -1499,7 +1509,7 @@ GcManager::CollectGarbage(const GcSettings& Settings) } catch (const std::bad_alloc& Ex) { - ZEN_ERROR("GCV2: Failed validating referencer {}. Reason: '{}'", ReferenceValidator->GetGcName(Ctx), Ex.what()); + ZEN_WARN("GCV2: Failed validating referencer {}. Reason: '{}'", ReferenceValidator->GetGcName(Ctx), Ex.what()); SetCancelGC(true); } catch (const std::exception& Ex) @@ -2083,10 +2093,11 @@ GcScheduler::SchedulerThread() { ZEN_ASSERT(WaitTime.count() >= 0); std::unique_lock Lock(m_GcMutex); - while (!Timeout) + while (!Timeout && (Status() != GcSchedulerStatus::kStopped)) { std::chrono::seconds ShortWait = Min(WaitTime, ShortWaitTime); bool ShortTimeout = std::cv_status::timeout == m_GcSignal.wait_for(Lock, ShortWait); + if (ShortTimeout) { if (WaitTime > ShortWaitTime) @@ -2848,7 +2859,7 @@ GcScheduler::CollectGarbage(const GcClock::TimePoint& CacheExpireTime, { ZEN_WARN("writing gc scheduler state ran out of disk space: '{}'", SystemError.what()); } - else if (RetryCount == 0) + else if (RetryCount == 2) { ZEN_ERROR("writing gc scheduler state failed with system error exception: '{}' ({})", SystemError.what(), @@ -2867,7 +2878,7 @@ GcScheduler::CollectGarbage(const GcClock::TimePoint& CacheExpireTime, } catch (const std::exception& Ex) { - if (RetryCount == 0) + if (RetryCount == 2) { ZEN_ERROR("writing gc scheduler state failed with: '{}'", Ex.what()); } diff --git a/src/zenstore/include/zenstore/blockstore.h b/src/zenstore/include/zenstore/blockstore.h index fce05766f..4006f4275 100644 --- a/src/zenstore/include/zenstore/blockstore.h +++ b/src/zenstore/include/zenstore/blockstore.h @@ -102,12 +102,12 @@ struct BlockStoreFile : public RefCounted IoBuffer GetMetaData() const; private: - std::filesystem::path GetMetaPath() const; - void RemoveMeta(); - const std::filesystem::path m_Path; - IoBuffer m_IoBuffer; - BasicFile m_File; - std::atomic<uint64_t> m_CachedFileSize = 0; + std::filesystem::path GetMetaPath() const; + void RemoveMeta(); + const std::filesystem::path m_Path; + IoBuffer m_IoBuffer; + BasicFile m_File; + mutable std::atomic<uint64_t> m_CachedFileSize = 0; }; class BlockStoreCompactState; 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/cachedisklayer.h b/src/zenstore/include/zenstore/cache/cachedisklayer.h index 49c52f847..10c61681b 100644 --- a/src/zenstore/include/zenstore/cache/cachedisklayer.h +++ b/src/zenstore/include/zenstore/cache/cachedisklayer.h @@ -4,6 +4,7 @@ #include "cacheshared.h" +#include <zencore/compactbinary.h> #include <zencore/stats.h> #include <zenstore/accesstime.h> #include <zenstore/blockstore.h> @@ -180,7 +181,7 @@ public: struct PutResult { zen::PutStatus Status; - std::string Message; + CbObject Details; }; explicit ZenCacheDiskLayer(GcManager& Gc, JobQueue& JobQueue, const std::filesystem::path& RootDir, const Configuration& Config); diff --git a/src/zenutil/include/zenutil/cache/cachekey.h b/src/zenstore/include/zenstore/cache/cachekey.h index 0ab05f4f1..ae79f2a3d 100644 --- a/src/zenutil/include/zenutil/cache/cachekey.h +++ b/src/zenstore/include/zenstore/cache/cachekey.h @@ -6,7 +6,7 @@ #include <zencore/string.h> #include <zencore/uid.h> -#include <zenutil/cache/cachepolicy.h> +#include <zenstore/cache/cachepolicy.h> namespace zen { diff --git a/src/zenutil/include/zenutil/cache/cachepolicy.h b/src/zenstore/include/zenstore/cache/cachepolicy.h index 7773cd3d1..7773cd3d1 100644 --- a/src/zenutil/include/zenutil/cache/cachepolicy.h +++ b/src/zenstore/include/zenstore/cache/cachepolicy.h diff --git a/src/zenstore/include/zenstore/cache/cacherpc.h b/src/zenstore/include/zenstore/cache/cacherpc.h index 104746aba..eb40befa0 100644 --- a/src/zenstore/include/zenstore/cache/cacherpc.h +++ b/src/zenstore/include/zenstore/cache/cacherpc.h @@ -4,8 +4,9 @@ #include <zencore/iobuffer.h> #include <zencore/logging.h> +#include <zenstore/cache/cache.h> #include <zenstore/cache/cacheshared.h> -#include <zenutil/cache/cache.h> +#include <zenstore/cache/structuredcachestore.h> #include <atomic> #include <string_view> @@ -28,7 +29,6 @@ class CidStore; class DiskWriteBlocker; class HttpStructuredCacheService; class UpstreamCacheClient; -class ZenCacheStore; enum class CachePolicy : uint32_t; enum class RpcAcceptOptions : uint16_t; @@ -101,7 +101,7 @@ private: CbPackage HandleRpcGetCacheValues(const CacheRequestContext& Context, CbObjectView BatchRequest); CbPackage HandleRpcGetCacheChunks(const CacheRequestContext& Context, RpcAcceptOptions AcceptOptions, CbObjectView BatchRequest); - PutStatus PutCacheRecord(PutRequestData& Request, const CbPackage* Package); + ZenCacheStore::PutResult PutCacheRecord(PutRequestData& Request, const CbPackage* Package); /** HandleRpcGetCacheChunks Helper: Parse the Body object into RecordValue Requests and Value Requests. */ bool ParseGetCacheChunksRequest(std::string& Namespace, 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/zenserver/projectstore/oplogreferencedset.h b/src/zenstore/include/zenstore/oplogreferencedset.h index 297fd29d5..dcc156060 100644 --- a/src/zenserver/projectstore/oplogreferencedset.h +++ b/src/zenstore/include/zenstore/oplogreferencedset.h @@ -6,7 +6,10 @@ #include <optional> #include <string_view> -#include <unordered_set> + +ZEN_THIRD_PARTY_INCLUDES_START +#include <tsl/robin_set.h> +ZEN_THIRD_PARTY_INCLUDES_END namespace zen { @@ -24,8 +27,12 @@ class IoBuffer; class OplogReferencedSet { public: - void Emplace(Oid OplogId); - bool Contains(Oid OplogId, std::string_view OplogKey); + inline bool Contains(const Oid& OplogId) const { return Set.contains(OplogId); } + static inline bool IsNonPackage(std::string_view OplogKey) + { + // A referencedset always includes all non-package keys + return OplogKey.empty() || !OplogKey.starts_with('/'); + } void Clear(); static std::optional<OplogReferencedSet> LoadFromChunk(const IoBuffer& ChunkData); @@ -33,7 +40,7 @@ public: static constexpr std::string_view ReferencedSetOplogKey = "ReferencedSet"; private: - std::unordered_set<Oid> Set; + tsl::robin_set<Oid, Oid::Hasher> Set; }; } // namespace zen diff --git a/src/zenserver/projectstore/projectstore.h b/src/zenstore/include/zenstore/projectstore.h index 2595d7198..258be5930 100644 --- a/src/zenserver/projectstore/projectstore.h +++ b/src/zenstore/include/zenstore/projectstore.h @@ -2,10 +2,11 @@ #pragma once -#include <zencore/compactbinary.h> +#include <zencore/compactbinarybuilder.h> +#include <zencore/compactbinarypackage.h> +#include <zencore/compositebuffer.h> #include <zencore/uid.h> #include <zencore/xxhash.h> -#include <zenhttp/httpserver.h> #include <zenstore/gc.h> ZEN_THIRD_PARTY_INCLUDES_START @@ -13,39 +14,13 @@ ZEN_THIRD_PARTY_INCLUDES_START ZEN_THIRD_PARTY_INCLUDES_END #include <map> -#include <unordered_map> +#include <variant> namespace zen { -class CbPackage; class CidStore; class AuthMgr; class ScrubContext; -class JobQueue; -class OpenProcessCache; - -enum class HttpResponseCode; - -struct OplogEntry -{ - uint32_t OpLsn; - uint32_t OpCoreOffset; // note: Multiple of alignment! - uint32_t OpCoreSize; - uint32_t OpCoreHash; // Used as checksum - Oid OpKeyHash; - uint32_t Reserved; - - inline bool IsTombstone() const { return OpCoreOffset == 0 && OpCoreSize == 0 && OpLsn == 0; } - inline void MakeTombstone() { OpLsn = OpCoreOffset = OpCoreSize = OpCoreHash = Reserved = 0; } -}; - -struct OplogEntryAddress -{ - uint64_t Offset; - uint64_t Size; -}; - -static_assert(IsPow2(sizeof(OplogEntry))); /** Project Store @@ -68,23 +43,67 @@ public: { }; - ProjectStore(CidStore& Store, - std::filesystem::path BasePath, - GcManager& Gc, - JobQueue& JobQueue, - OpenProcessCache& InOpenProcessCache, - const Configuration& Config); + ProjectStore(CidStore& Store, std::filesystem::path BasePath, GcManager& Gc, const Configuration& Config); ~ProjectStore(); - struct Project; + struct LogSequenceNumber + { + uint32_t Number = 0u; - struct Oplog + operator bool() const { return Number != 0u; }; + LogSequenceNumber() = default; + explicit LogSequenceNumber(size_t InNumber) : Number(uint32_t(InNumber)) {} + operator size_t() const { return Number; }; + inline auto operator<=>(const LogSequenceNumber& Other) const = default; + + struct Hasher + { + size_t operator()(const LogSequenceNumber& v) const { return std::hash<uint32_t>()(v.Number); } + }; + }; + + template<class V> + using LsnMap = tsl::robin_map<LogSequenceNumber, V, LogSequenceNumber::Hasher>; + + struct OplogEntryAddress { - Oplog(std::string_view Id, - Project* Project, + uint32_t Offset; // note: Multiple of m_OpsAlign! + uint32_t Size; + }; + + struct OplogEntry + { + LogSequenceNumber OpLsn; + OplogEntryAddress OpCoreAddress; + uint32_t OpCoreHash; // Used as checksum + Oid OpKeyHash; + uint32_t Reserved; + + inline bool IsTombstone() const { return OpCoreAddress.Offset == 0 && OpCoreAddress.Size == 0 && OpLsn.Number; } + inline void MakeTombstone() + { + OpLsn = {}; + OpCoreAddress.Offset = OpCoreAddress.Size = OpCoreHash = Reserved = 0; + } + }; + + static_assert(IsPow2(sizeof(OplogEntry))); + + struct Oplog : public RefCounted + { + enum class EMode + { + kBasicReadOnly, + kFull + }; + + Oplog(const LoggerRef& Log, + std::string_view ProjectIdentifier, + std::string_view Id, CidStore& Store, - std::filesystem::path BasePath, - const std::filesystem::path& MarkerPath); + const std::filesystem::path& BasePath, + const std::filesystem::path& MarkerPath, + EMode State); ~Oplog(); [[nodiscard]] static bool ExistsAt(const std::filesystem::path& BasePath); @@ -94,6 +113,7 @@ public: void Write(); void Update(const std::filesystem::path& MarkerPath); bool Reset(); + bool CanUnload(); struct ChunkInfo { @@ -107,54 +127,47 @@ public: int32_t Count = -1; }; - std::vector<ChunkInfo> GetAllChunksInfo(); + std::vector<ChunkInfo> GetAllChunksInfo(const std::filesystem::path& ProjectRootDir); void IterateChunkMap(std::function<void(const Oid&, const IoHash& Hash)>&& Fn); void IterateFileMap(std::function<void(const Oid&, const std::string_view& ServerPath, const std::string_view& ClientPath)>&& Fn); void IterateOplog(std::function<void(CbObjectView)>&& Fn, const Paging& EntryPaging); - void IterateOplogWithKey(std::function<void(uint32_t, const Oid&, CbObjectView)>&& Fn); - void IterateOplogWithKey(std::function<void(uint32_t, const Oid&, CbObjectView)>&& Fn, const Paging& EntryPaging); + void IterateOplogWithKey(std::function<void(LogSequenceNumber, const Oid&, CbObjectView)>&& Fn); + void IterateOplogWithKey(std::function<void(LogSequenceNumber, const Oid&, CbObjectView)>&& Fn, const Paging& EntryPaging); void IterateOplogLocked(std::function<void(CbObjectView)>&& Fn, const Paging& EntryPaging); size_t GetOplogEntryCount() const; std::optional<CbObject> GetOpByKey(const Oid& Key); - std::optional<CbObject> GetOpByIndex(uint32_t Index); - std::optional<uint32_t> GetOpIndexByKey(const Oid& Key); - - IoBuffer FindChunk(const Oid& ChunkId, uint64_t* OptOutModificationTag); - IoBuffer GetChunkByRawHash(const IoHash& RawHash); - bool IterateChunks(std::span<IoHash> RawHashes, - bool IncludeModTag, - const std::function<bool(size_t Index, const IoBuffer& Payload, uint64_t ModTag)>& AsyncCallback, - WorkerThreadPool* OptionalWorkerPool, - uint64_t LargeSizeLimit); - bool IterateChunks(std::span<Oid> ChunkIds, - bool IncludeModTag, - const std::function<bool(size_t Index, const IoBuffer& Payload, uint64_t ModTag)>& AsyncCallback, - WorkerThreadPool* OptionalWorkerPool, - uint64_t LargeSizeLimit); - inline static const uint32_t kInvalidOp = ~0u; + std::optional<CbObject> GetOpByIndex(LogSequenceNumber Index); + LogSequenceNumber GetOpIndexByKey(const Oid& Key); + + IoBuffer FindChunk(const std::filesystem::path& ProjectRootDir, const Oid& ChunkId, uint64_t* OptOutModificationTag); + IoBuffer GetChunkByRawHash(const IoHash& RawHash); + bool IterateChunks(std::span<IoHash> RawHashes, + bool IncludeModTag, + const std::function<bool(size_t Index, const IoBuffer& Payload, uint64_t ModTag)>& AsyncCallback, + WorkerThreadPool* OptionalWorkerPool, + uint64_t LargeSizeLimit); + bool IterateChunks(const std::filesystem::path& ProjectRootDir, + std::span<Oid> ChunkIds, + bool IncludeModTag, + const std::function<bool(size_t Index, const IoBuffer& Payload, uint64_t ModTag)>& AsyncCallback, + WorkerThreadPool* OptionalWorkerPool, + uint64_t LargeSizeLimit); /** Persist a new oplog entry * - * Returns the oplog LSN assigned to the new entry, or kInvalidOp if the entry is rejected + * Returns the oplog LSN assigned to the new entry, or an invalid number if the entry is rejected */ - uint32_t AppendNewOplogEntry(CbPackage Op); - - uint32_t AppendNewOplogEntry(CbObjectView Core); - std::vector<uint32_t> AppendNewOplogEntries(std::span<CbObjectView> Cores); - - enum UpdateType - { - kUpdateNewEntry, - kUpdateReplay - }; + LogSequenceNumber AppendNewOplogEntry(CbPackage Op); + LogSequenceNumber AppendNewOplogEntry(CbObjectView Core); + std::vector<LogSequenceNumber> AppendNewOplogEntries(std::span<CbObjectView> Cores); const std::string& OplogId() const { return m_OplogId; } const std::filesystem::path& TempPath() const { return m_TempPath; } const std::filesystem::path& MarkerPath() const { return m_MarkerPath; } - LoggerRef Log() { return m_OuterProject->Log(); } + LoggerRef Log() const { return m_Log; } void Flush(); void Scrub(ScrubContext& Ctx); static uint64_t TotalSize(const std::filesystem::path& BasePath); @@ -163,14 +176,12 @@ public: std::size_t OplogCount() const { RwLock::SharedLockScope _(m_OplogLock); - return m_LatestOpMap.size(); + return m_OpToPayloadOffsetMap.size(); } void ResetState(); bool PrepareForDelete(std::filesystem::path& OutRemoveDirectory); - void AddChunkMappings(const std::unordered_map<Oid, IoHash, Oid::Hasher>& ChunkMappings); - void EnableUpdateCapture(); void DisableUpdateCapture(); void CaptureAddedAttachments(std::span<const IoHash> AttachmentHashes); @@ -186,8 +197,8 @@ public: void GetAttachmentsLocked(std::vector<IoHash>& OutAttachments, bool StoreMetaDataOnDisk); - Project* GetOuterProject() const { return m_OuterProject; } - void CompactIfUnusedExceeds(bool DryRun, uint32_t CompactUnusedThreshold, std::string_view LogPrefix); + std::string_view GetOuterProjectIdentifier() const { return m_OuterProjectId; } + void CompactIfUnusedExceeds(bool DryRun, uint32_t CompactUnusedThreshold, std::string_view LogPrefix); static std::optional<CbObject> ReadStateFile(const std::filesystem::path& BasePath, std::function<LoggerRef()>&& Log); @@ -208,8 +219,8 @@ public: struct ValidationResult { uint32_t OpCount = 0; - uint32_t LSNLow = 0; - uint32_t LSNHigh = 0; + LogSequenceNumber LSNLow; + LogSequenceNumber LSNHigh; std::vector<std::pair<Oid, FileMapping>> MissingFiles; std::vector<std::pair<Oid, ChunkMapping>> MissingChunks; std::vector<std::pair<Oid, ChunkMapping>> MissingMetas; @@ -222,7 +233,9 @@ public: } }; - ValidationResult Validate(std::atomic_bool& IsCancelledFlag, WorkerThreadPool* OptionalWorkerPool); + ValidationResult Validate(const std::filesystem::path& ProjectRootDir, + std::atomic_bool& IsCancelledFlag, + WorkerThreadPool* OptionalWorkerPool); private: struct FileMapEntry @@ -234,31 +247,60 @@ public: template<class V> using OidMap = tsl::robin_map<Oid, V, Oid::Hasher>; - Project* m_OuterProject = nullptr; + LoggerRef m_Log; + const std::string m_OuterProjectId; const std::string m_OplogId; CidStore& m_CidStore; const std::filesystem::path m_BasePath; std::filesystem::path m_MarkerPath; - std::filesystem::path m_TempPath; - std::filesystem::path m_MetaPath; - - mutable RwLock m_OplogLock; - OidMap<IoHash> m_ChunkMap; // output data chunk id -> CAS address - OidMap<IoHash> m_MetaMap; // meta chunk id -> CAS address - OidMap<FileMapEntry> m_FileMap; // file id -> file map entry - int32_t m_ManifestVersion; // File system manifest version - tsl::robin_map<uint32_t, OplogEntryAddress> m_OpAddressMap; // Index LSN -> op data in ops blob file - OidMap<uint32_t> m_LatestOpMap; // op key -> latest op LSN for key - std::atomic<bool> m_MetaValid = false; + const std::filesystem::path m_TempPath; + const std::filesystem::path m_MetaPath; + + const EMode m_Mode; + + mutable RwLock m_OplogLock; + OidMap<IoHash> m_ChunkMap; // output data chunk id -> CAS address + OidMap<IoHash> m_MetaMap; // meta chunk id -> CAS address + OidMap<FileMapEntry> m_FileMap; // file id -> file map entry + int32_t m_ManifestVersion; // File system manifest version + + struct PayloadIndex + { + uint32_t Index = std::numeric_limits<uint32_t>::max(); + + operator bool() const { return Index != std::numeric_limits<uint32_t>::max(); }; + PayloadIndex() = default; + explicit PayloadIndex(size_t InIndex) : Index(uint32_t(InIndex)) {} + operator size_t() const { return Index; }; + inline auto operator<=>(const PayloadIndex& Other) const = default; + + struct Hasher + { + size_t operator()(const PayloadIndex& v) const { return std::hash<uint32_t>()(v.Index); } + }; + }; + + struct OplogPayload + { + LogSequenceNumber Lsn; + OplogEntryAddress Address; + }; + + OidMap<PayloadIndex> m_OpToPayloadOffsetMap; + std::vector<OplogPayload> m_OpLogPayloads; + std::unique_ptr<LsnMap<PayloadIndex>> m_LsnToPayloadOffsetMap; + + std::atomic<bool> m_MetaValid = false; uint32_t m_UpdateCaptureRefCounter = 0; - std::unique_ptr<std::vector<uint32_t>> m_CapturedLSNs; + std::unique_ptr<std::vector<Oid>> m_CapturedOps; std::unique_ptr<std::vector<IoHash>> m_CapturedAttachments; std::unordered_set<IoHash, IoHash::Hasher> m_PendingPrepOpAttachments; GcClock::TimePoint m_PendingPrepOpAttachmentsRetainEnd; RefPtr<OplogStorage> m_Storage; uint64_t m_LogFlushPosition = 0; + bool m_IsLegacySnapshot = false; RefPtr<OplogStorage> GetStorage(); @@ -267,6 +309,7 @@ public: uint32_t GetUnusedSpacePercentLocked() const; void WriteIndexSnapshot(); void ReadIndexSnapshot(); + void RefreshLsnToPayloadOffsetMap(RwLock::ExclusiveLockScope&); struct OplogEntryMapping { @@ -283,7 +326,9 @@ public: * * Returns the oplog LSN assigned to the new entry, or kInvalidOp if the entry is rejected */ - uint32_t RegisterOplogEntry(RwLock::ExclusiveLockScope& OplogLock, const OplogEntryMapping& OpMapping, const OplogEntry& OpEntry); + LogSequenceNumber RegisterOplogEntry(RwLock::ExclusiveLockScope& OplogLock, + const OplogEntryMapping& OpMapping, + const OplogEntry& OpEntry); void AddFileMapping(const RwLock::ExclusiveLockScope& OplogLock, const Oid& FileId, @@ -293,11 +338,15 @@ public: void AddChunkMapping(const RwLock::ExclusiveLockScope& OplogLock, const Oid& ChunkId, const IoHash& Hash); void AddMetaMapping(const RwLock::ExclusiveLockScope& OplogLock, const Oid& ChunkId, const IoHash& Hash); void Compact(RwLock::ExclusiveLockScope& Lock, bool DryRun, bool RetainLSNs, std::string_view LogPrefix); - void IterateCapturedLSNsLocked(std::function<bool(const CbObjectView& UpdateOp)>&& Callback); + void IterateCapturedOpsLocked(std::function<bool(const Oid& Key, LogSequenceNumber LSN, const CbObjectView& UpdateOp)>&& Callback); + std::vector<PayloadIndex> GetSortedOpPayloadRangeLocked( + const Paging& EntryPaging, + tsl::robin_map<PayloadIndex, Oid, PayloadIndex::Hasher>* OutOptionalReverseKeyMap); friend class ProjectStoreOplogReferenceChecker; friend class ProjectStoreReferenceChecker; friend class ProjectStoreOplogReferenceValidator; + friend struct OplogStorage; }; struct Project : public RefCounted @@ -308,8 +357,10 @@ public: std::filesystem::path ProjectRootDir; std::filesystem::path ProjectFilePath; - Oplog* NewOplog(std::string_view OplogId, const std::filesystem::path& MarkerPath); - Oplog* OpenOplog(std::string_view OplogId, bool AllowCompact, bool VerifyPathOnDisk); + Ref<Oplog> NewOplog(std::string_view OplogId, const std::filesystem::path& MarkerPath); + Ref<Oplog> OpenOplog(std::string_view OplogId, bool AllowCompact, bool VerifyPathOnDisk); + Ref<Oplog> ReadOplog(std::string_view OplogId); + bool TryUnloadOplog(std::string_view OplogId); bool DeleteOplog(std::string_view OplogId); bool RemoveOplog(std::string_view OplogId, std::filesystem::path& OutDeletePath); void IterateOplogs(std::function<void(const RwLock::SharedLockScope&, const Oplog&)>&& Fn) const; @@ -361,8 +412,7 @@ public: ProjectStore* m_ProjectStore; CidStore& m_CidStore; mutable RwLock m_ProjectLock; - std::map<std::string, std::unique_ptr<Oplog>> m_Oplogs; - std::vector<std::unique_ptr<Oplog>> m_DeletedOplogs; + std::map<std::string, Ref<Oplog>> m_Oplogs; std::filesystem::path m_OplogStoragePath; mutable RwLock m_LastAccessTimesLock; mutable tsl::robin_map<std::string, GcClock::Tick> m_LastAccessTimes; @@ -416,79 +466,71 @@ public: virtual std::vector<RwLock::SharedLockScope> LockState(GcCtx& Ctx) override; - CbArray GetProjectsList(); - std::pair<HttpResponseCode, std::string> GetProjectFiles(const std::string_view ProjectId, - const std::string_view OplogId, - const std::unordered_set<std::string>& WantedFieldNames, - CbObject& OutPayload); - std::pair<HttpResponseCode, std::string> GetProjectChunkInfos(const std::string_view ProjectId, - const std::string_view OplogId, - const std::unordered_set<std::string>& WantedFieldNames, - CbObject& OutPayload); - std::pair<HttpResponseCode, std::string> GetChunkInfo(const std::string_view ProjectId, - const std::string_view OplogId, - const std::string_view ChunkId, - CbObject& OutPayload); - std::pair<HttpResponseCode, std::string> GetChunkRange(const std::string_view ProjectId, - const std::string_view OplogId, - const Oid ChunkId, - uint64_t Offset, - uint64_t Size, - ZenContentType AcceptType, - CompositeBuffer& OutChunk, - ZenContentType& OutContentType, - uint64_t* OptionalInOutModificationTag); - std::pair<HttpResponseCode, std::string> GetChunkRange(const std::string_view ProjectId, - const std::string_view OplogId, - const std::string_view ChunkId, - uint64_t Offset, - uint64_t Size, - ZenContentType AcceptType, - CompositeBuffer& OutChunk, - ZenContentType& OutContentType, - uint64_t* OptionalInOutModificationTag); - std::pair<HttpResponseCode, std::string> GetChunk(const std::string_view ProjectId, - const std::string_view OplogId, - const std::string_view Cid, - IoBuffer& OutChunk, - uint64_t* OptionalInOutModificationTag); - - std::pair<HttpResponseCode, std::string> PutChunk(const std::string_view ProjectId, - const std::string_view OplogId, - const std::string_view Cid, - ZenContentType ContentType, - IoBuffer&& Chunk); - - std::pair<HttpResponseCode, std::string> WriteOplog(const std::string_view ProjectId, - const std::string_view OplogId, - IoBuffer&& Payload, - CbObject& OutResponse); - - std::pair<HttpResponseCode, std::string> ReadOplog(const std::string_view ProjectId, - const std::string_view OplogId, - const HttpServerRequest::QueryParams& Params, - CbObject& OutResponse); - - std::pair<HttpResponseCode, std::string> GetChunks(const std::string_view ProjectId, - const std::string_view OplogId, - const CbObject& RequestObject, - CbPackage& OutResponsePackage); - - bool Rpc(HttpServerRequest& HttpReq, - const std::string_view ProjectId, - const std::string_view OplogId, - IoBuffer&& Payload, - AuthMgr& AuthManager); - - std::pair<HttpResponseCode, std::string> Export(Ref<ProjectStore::Project> Project, - ProjectStore::Oplog& Oplog, - CbObjectView&& Params, - AuthMgr& AuthManager); - - std::pair<HttpResponseCode, std::string> Import(ProjectStore::Project& Project, - ProjectStore::Oplog& Oplog, - CbObjectView&& Params, - AuthMgr& AuthManager); + CbArray GetProjectsList(); + static CbObject GetProjectFiles(LoggerRef InLog, + Project& Project, + Oplog& Oplog, + const std::unordered_set<std::string>& WantedFieldNames); + + static CbObject GetProjectChunkInfos(LoggerRef InLog, + Project& Project, + Oplog& Oplog, + const std::unordered_set<std::string>& WantedFieldNames); + static CbObject GetChunkInfo(LoggerRef InLog, Project& Project, Oplog& Oplog, const Oid& ChunkId); + struct GetChunkRangeResult + { + enum class EError : uint8_t + { + Ok, + NotFound, + NotModified, + MalformedContent, + OutOfRange + }; + EError Error = EError(-1); + std::string ErrorDescription; + CompositeBuffer Chunk = CompositeBuffer(); + IoHash RawHash = IoHash::Zero; + uint64_t RawSize = 0; + ZenContentType ContentType = ZenContentType::kUnknownContentType; + }; + static GetChunkRangeResult GetChunkRange(LoggerRef InLog, + Project& Project, + Oplog& Oplog, + const Oid& ChunkId, + uint64_t Offset, + uint64_t Size, + ZenContentType AcceptType, + uint64_t* OptionalInOutModificationTag); + IoBuffer GetChunk(Project& Project, Oplog& Oplog, const IoHash& ChunkHash); + + IoBuffer GetChunk(const std::string_view ProjectId, const std::string_view OplogId, const Oid& ChunkId); + + IoBuffer GetChunk(const std::string_view ProjectId, const std::string_view OplogId, const IoHash& Cid); + + bool PutChunk(Project& Project, Oplog& Oplog, const IoHash& ChunkHash, IoBuffer&& Chunk); + + struct ChunkRequest + { + uint64_t Offset = 0; + uint64_t Size = (uint64_t)-1; + std::variant<IoHash, Oid> Id; + std::optional<uint64_t> ModTag; + bool SkipData = false; + }; + struct ChunkResult + { + bool Exists = false; + IoBuffer ChunkBuffer; + uint64_t ModTag = 0; + }; + std::vector<ChunkResult> GetChunks(Project& Project, Oplog& Oplog, std::span<const ChunkRequest> Requests); + + std::vector<ProjectStore::ChunkRequest> ParseChunksRequests(Project& Project, Oplog& Oplog, const CbObject& Cb); + CbPackage WriteChunksRequestResponse(Project& Project, + Oplog& Oplog, + std::vector<ChunkRequest>&& Requests, + std::vector<ChunkResult>&& Results); bool AreDiskWritesAllowed() const; @@ -500,8 +542,6 @@ private: LoggerRef m_Log; GcManager& m_Gc; CidStore& m_CidStore; - JobQueue& m_JobQueue; - OpenProcessCache& m_OpenProcessCache; std::filesystem::path m_ProjectBasePath; const Configuration m_Config; mutable RwLock m_ProjectsLock; @@ -517,8 +557,27 @@ private: friend class ProjectStoreReferenceChecker; }; +Oid ComputeOpKey(const CbObjectView& Op); + Oid OpKeyStringAsOid(std::string_view OpKey); +template<typename T> +Oid +OpKeyStringAsOid(std::string_view OpKey, T& TmpBuffer) +{ + using namespace std::literals; + + CbObjectWriter Writer; + Writer << "key"sv << OpKey; + Writer.Finalize(); + TmpBuffer.resize(Writer.GetSaveSize()); + MutableMemoryView SaveBuffer(MutableMemoryView(TmpBuffer.data(), TmpBuffer.size())); + + const Oid OpId = ComputeOpKey(Writer.Save(SaveBuffer).AsObjectView()); + + return OpId; +} + void prj_forcelink(); } // namespace zen diff --git a/src/zenserver/vfs/vfsimpl.h b/src/zenstore/include/zenstore/vfsimpl.h index c33df100b..22ca07a27 100644 --- a/src/zenserver/vfs/vfsimpl.h +++ b/src/zenstore/include/zenstore/vfsimpl.h @@ -2,20 +2,20 @@ #pragma once -#include "vfsservice.h" - -#include "projectstore/projectstore.h" - #include <zencore/logging.h> #include <zenvfs/vfs.h> -#if ZEN_WITH_VFS - -# include <memory> -# include <unordered_map> +#include <memory> +#include <unordered_map> namespace zen { +#if ZEN_WITH_VFS + +class ProjectStore; +class ZenCacheStore; +struct VfsServiceImpl; + struct VfsOplogDataSource : public VfsTreeDataSource { VfsOplogDataSource(std::string_view ProjectId, std::string_view OplogId, Ref<ProjectStore> InProjectStore); @@ -47,14 +47,14 @@ private: struct VfsServiceDataSource : public VfsTreeDataSource { - VfsServiceDataSource(VfsService::Impl* VfsImpl) : m_VfsImpl(VfsImpl) {} + VfsServiceDataSource(VfsServiceImpl* VfsImpl) : m_VfsImpl(VfsImpl) {} virtual void ReadNamedData(std::string_view Path, void* Buffer, uint64_t ByteOffset, uint64_t ByteCount) override; virtual void ReadChunkData(const Oid& ChunkId, void* Buffer, uint64_t ByteOffset, uint64_t ByteCount) override; virtual void PopulateDirectory(std::string NodePath, VfsTreeNode& DirNode) override; private: - VfsService::Impl* m_VfsImpl = nullptr; + VfsServiceImpl* m_VfsImpl = nullptr; RwLock m_Lock; std::unordered_map<std::string, Ref<VfsOplogDataSource>> m_OplogSourceMap; @@ -64,12 +64,15 @@ private: Ref<VfsCacheDataSource> GetCacheDataSource(std::string_view NamespaceId, std::string_view BucketId); }; +#endif // ZEN_WITH_VFS + ////////////////////////////////////////////////////////////////////////// -struct VfsService::Impl +struct VfsServiceImpl { - Impl(); - ~Impl(); +#if ZEN_WITH_VFS + VfsServiceImpl(); + ~VfsServiceImpl(); void Mount(std::string_view MountPoint); void Unmount(); @@ -94,8 +97,7 @@ private: void VfsThread(); friend struct VfsServiceDataSource; +#endif // ZEN_WITH_VFS }; } // namespace zen - -#endif diff --git a/src/zenserver/projectstore/oplogreferencedset.cpp b/src/zenstore/oplogreferencedset.cpp index c6bfa0b98..f34b80b1a 100644 --- a/src/zenserver/projectstore/oplogreferencedset.cpp +++ b/src/zenstore/oplogreferencedset.cpp @@ -1,8 +1,8 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include "oplogreferencedset.h" +#include <zenstore/oplogreferencedset.h> -#include "projectstore.h" +#include <zenstore/projectstore.h> #include <zencore/iobuffer.h> #include <zencore/logging.h> @@ -63,7 +63,7 @@ OplogReferencedSet::LoadFromChunk(const IoBuffer& ChunkData) constexpr uint64_t MinSupportedVersion = 1; if (Version < MinSupportedVersion) { - ZEN_INFO("ReferencedSet is below the minimum supported version, ignoring it. Version: {}, minimum version: {}.", + ZEN_WARN("ReferencedSet is below the minimum supported version, ignoring it. Version: {}, minimum version: {}.", Version, MinSupportedVersion); return std::optional<OplogReferencedSet>(); @@ -72,12 +72,14 @@ OplogReferencedSet::LoadFromChunk(const IoBuffer& ChunkData) // Parse the remaining lines after the leading comment block. ChunkText.remove_prefix(FirstNonComment ? FirstNonComment - ChunkText.data() : ChunkText.length()); + eastl::fixed_vector<uint8_t, 256> TmpBuffer; + OplogReferencedSet Result; - ForEachStrTok(ChunkText, '\n', [&Result, &TrimWhitespace](std::string_view Line) { + ForEachStrTok(ChunkText, '\n', [&Result, &TrimWhitespace, &TmpBuffer](std::string_view Line) { Line = AsciiSet::TrimSuffixWith(AsciiSet::TrimPrefixWith(Line, TrimWhitespace), TrimWhitespace); if (!Line.empty() && !Line.starts_with('#')) { - Result.Emplace(OpKeyStringAsOid(Line)); + Result.Set.emplace(OpKeyStringAsOid(Line, TmpBuffer)); } return true; }); @@ -85,26 +87,9 @@ OplogReferencedSet::LoadFromChunk(const IoBuffer& ChunkData) } void -OplogReferencedSet::Emplace(Oid OplogId) -{ - Set.emplace(OplogId); -} - -void OplogReferencedSet::Clear() { Set.clear(); } -bool -OplogReferencedSet::Contains(Oid OplogId, std::string_view OplogKey) -{ - // A referencedset always includes all non-package keys - if (OplogKey.empty() || !OplogKey.starts_with("/")) - { - return true; - } - return Set.contains(OplogId); -} - } // namespace zen diff --git a/src/zenserver/projectstore/projectstore.cpp b/src/zenstore/projectstore.cpp index a5ab24cfb..8ae74c8cf 100644 --- a/src/zenserver/projectstore/projectstore.cpp +++ b/src/zenstore/projectstore.cpp @@ -1,39 +1,28 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include "projectstore.h" +#include <zenstore/projectstore.h> #include <zencore/assertfmt.h> -#include <zencore/compactbinarybuilder.h> -#include <zencore/compactbinarypackage.h> #include <zencore/compactbinaryutil.h> #include <zencore/compactbinaryvalidation.h> +#include <zencore/except.h> #include <zencore/filesystem.h> #include <zencore/fmtutils.h> -#include <zencore/jobqueue.h> #include <zencore/logging.h> #include <zencore/memory/llm.h> +#include <zencore/parallelwork.h> #include <zencore/scopeguard.h> #include <zencore/stream.h> #include <zencore/timer.h> #include <zencore/trace.h> -#include <zenhttp/packageformat.h> #include <zenstore/caslog.h> #include <zenstore/cidstore.h> #include <zenstore/scrubcontext.h> -#include <zenutil/cache/rpcrecording.h> -#include <zenutil/openprocesscache.h> -#include <zenutil/parallelwork.h> -#include <zenutil/referencemetadata.h> #include <zenutil/workerpools.h> -#include "buildsremoteprojectstore.h" -#include "fileremoteprojectstore.h" -#include "jupiterremoteprojectstore.h" -#include "remoteprojectstore.h" -#include "zenremoteprojectstore.h" +#include "referencemetadata.h" ZEN_THIRD_PARTY_INCLUDES_START -#include <cpr/cpr.h> #include <tsl/robin_set.h> #include <xxh3.h> ZEN_THIRD_PARTY_INCLUDES_END @@ -41,6 +30,8 @@ ZEN_THIRD_PARTY_INCLUDES_END #if ZEN_WITH_TESTS # include <zencore/testing.h> # include <zencore/testutils.h> + +# include <unordered_map> #endif // ZEN_WITH_TESTS namespace zen { @@ -140,278 +131,68 @@ namespace { return CheckWriteTime < ReferenceWriteTime; } - struct CreateRemoteStoreResult - { - std::shared_ptr<RemoteProjectStore> Store; - std::string Description; - }; - CreateRemoteStoreResult CreateRemoteStore(CbObjectView Params, - AuthMgr& AuthManager, - size_t MaxBlockSize, - size_t MaxChunkEmbedSize, - const std::filesystem::path& TempFilePath) - { - ZEN_MEMSCOPE(GetProjectstoreTag()); - - using namespace std::literals; - - std::shared_ptr<RemoteProjectStore> RemoteStore; - - if (CbObjectView File = Params["file"sv].AsObjectView(); File) - { - std::filesystem::path FolderPath(File["path"sv].AsString()); - if (FolderPath.empty()) - { - return {nullptr, "Missing file path"}; - } - std::string_view Name(File["name"sv].AsString()); - if (Name.empty()) - { - return {nullptr, "Missing file name"}; - } - std::string_view OptionalBaseName(File["basename"sv].AsString()); - bool ForceDisableBlocks = File["disableblocks"sv].AsBool(false); - bool ForceEnableTempBlocks = File["enabletempblocks"sv].AsBool(false); - - FileRemoteStoreOptions Options = {RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunkEmbedSize = MaxChunkEmbedSize}, - FolderPath, - std::string(Name), - std::string(OptionalBaseName), - ForceDisableBlocks, - ForceEnableTempBlocks}; - RemoteStore = CreateFileRemoteStore(Options); - } - - if (CbObjectView Cloud = Params["cloud"sv].AsObjectView(); Cloud) - { - std::string_view CloudServiceUrl = Cloud["url"sv].AsString(); - if (CloudServiceUrl.empty()) - { - return {nullptr, "Missing service url"}; - } - - std::string Url = cpr::util::urlDecode(std::string(CloudServiceUrl)); - std::string_view Namespace = Cloud["namespace"sv].AsString(); - if (Namespace.empty()) - { - return {nullptr, "Missing namespace"}; - } - std::string_view Bucket = Cloud["bucket"sv].AsString(); - if (Bucket.empty()) - { - return {nullptr, "Missing bucket"}; - } - std::string_view OpenIdProvider = Cloud["openid-provider"sv].AsString(); - std::string AccessToken = std::string(Cloud["access-token"sv].AsString()); - if (AccessToken.empty()) - { - std::string_view AccessTokenEnvVariable = Cloud["access-token-env"].AsString(); - if (!AccessTokenEnvVariable.empty()) - { - AccessToken = GetEnvVariable(AccessTokenEnvVariable); - } - } - std::filesystem::path OidcExePath; - if (std::string_view OidcExePathString = Cloud["oidc-exe-path"].AsString(); !OidcExePathString.empty()) - { - std::filesystem::path OidcExePathMaybe(OidcExePathString); - if (!IsFile(OidcExePathMaybe)) - { - ZEN_WARN("Path to OidcToken executable '{}' can not be reached by server", OidcExePathString); - OidcExePath = std::move(OidcExePathMaybe); - } - } - std::string_view KeyParam = Cloud["key"sv].AsString(); - if (KeyParam.empty()) - { - return {nullptr, "Missing key"}; - } - if (KeyParam.length() != IoHash::StringLength) - { - return {nullptr, "Invalid key"}; - } - IoHash Key = IoHash::FromHexString(KeyParam); - if (Key == IoHash::Zero) - { - return {nullptr, "Invalid key string"}; - } - IoHash BaseKey = IoHash::Zero; - std::string_view BaseKeyParam = Cloud["basekey"sv].AsString(); - if (!BaseKeyParam.empty()) - { - if (BaseKeyParam.length() != IoHash::StringLength) - { - return {nullptr, "Invalid base key"}; - } - BaseKey = IoHash::FromHexString(BaseKeyParam); - if (BaseKey == IoHash::Zero) - { - return {nullptr, "Invalid base key string"}; - } - } - - bool ForceDisableBlocks = Cloud["disableblocks"sv].AsBool(false); - bool ForceDisableTempBlocks = Cloud["disabletempblocks"sv].AsBool(false); - bool AssumeHttp2 = Cloud["assumehttp2"sv].AsBool(false); - - JupiterRemoteStoreOptions Options = {RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunkEmbedSize = MaxChunkEmbedSize}, - Url, - std::string(Namespace), - std::string(Bucket), - Key, - BaseKey, - std::string(OpenIdProvider), - AccessToken, - AuthManager, - OidcExePath, - ForceDisableBlocks, - ForceDisableTempBlocks, - AssumeHttp2}; - RemoteStore = CreateJupiterRemoteStore(Options, TempFilePath, /*Quiet*/ false); - } - - if (CbObjectView Zen = Params["zen"sv].AsObjectView(); Zen) - { - std::string_view Url = Zen["url"sv].AsString(); - std::string_view Project = Zen["project"sv].AsString(); - if (Project.empty()) - { - return {nullptr, "Missing project"}; - } - std::string_view Oplog = Zen["oplog"sv].AsString(); - if (Oplog.empty()) - { - return {nullptr, "Missing oplog"}; - } - ZenRemoteStoreOptions Options = {RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunkEmbedSize = MaxChunkEmbedSize}, - std::string(Url), - std::string(Project), - std::string(Oplog)}; - RemoteStore = CreateZenRemoteStore(Options, TempFilePath); - } - - if (CbObjectView Builds = Params["builds"sv].AsObjectView(); Builds) - { - std::string_view BuildsServiceUrl = Builds["url"sv].AsString(); - if (BuildsServiceUrl.empty()) - { - return {nullptr, "Missing service url"}; - } - - std::string Url = cpr::util::urlDecode(std::string(BuildsServiceUrl)); - std::string_view Namespace = Builds["namespace"sv].AsString(); - if (Namespace.empty()) - { - return {nullptr, "Missing namespace"}; - } - std::string_view Bucket = Builds["bucket"sv].AsString(); - if (Bucket.empty()) - { - return {nullptr, "Missing bucket"}; - } - std::string_view OpenIdProvider = Builds["openid-provider"sv].AsString(); - std::string AccessToken = std::string(Builds["access-token"sv].AsString()); - if (AccessToken.empty()) - { - std::string_view AccessTokenEnvVariable = Builds["access-token-env"].AsString(); - if (!AccessTokenEnvVariable.empty()) - { - AccessToken = GetEnvVariable(AccessTokenEnvVariable); - } - } - std::filesystem::path OidcExePath; - if (std::string_view OidcExePathString = Builds["oidc-exe-path"].AsString(); !OidcExePathString.empty()) - { - std::filesystem::path OidcExePathMaybe(OidcExePathString); - if (!IsFile(OidcExePathMaybe)) - { - ZEN_WARN("Path to OidcToken executable '{}' can not be reached by server", OidcExePathString); - OidcExePath = std::move(OidcExePathMaybe); - } - } - std::string_view BuildIdParam = Builds["buildsid"sv].AsString(); - if (BuildIdParam.empty()) - { - return {nullptr, "Missing build id"}; - } - if (BuildIdParam.length() != Oid::StringLength) - { - return {nullptr, "Invalid build id"}; - } - Oid BuildId = Oid::FromHexString(BuildIdParam); - if (BuildId == Oid::Zero) - { - return {nullptr, "Invalid build id string"}; - } - - bool ForceDisableBlocks = Builds["disableblocks"sv].AsBool(false); - bool ForceDisableTempBlocks = Builds["disabletempblocks"sv].AsBool(false); - bool AssumeHttp2 = Builds["assumehttp2"sv].AsBool(false); - - MemoryView MetaDataSection = Builds["metadata"sv].AsBinaryView(); - IoBuffer MetaData(IoBuffer::Wrap, MetaDataSection.GetData(), MetaDataSection.GetSize()); - - BuildsRemoteStoreOptions Options = {RemoteStoreOptions{.MaxBlockSize = MaxBlockSize, .MaxChunkEmbedSize = MaxChunkEmbedSize}, - Url, - std::string(Namespace), - std::string(Bucket), - BuildId, - std::string(OpenIdProvider), - AccessToken, - AuthManager, - OidcExePath, - ForceDisableBlocks, - ForceDisableTempBlocks, - AssumeHttp2, - MetaData}; - RemoteStore = CreateJupiterBuildsRemoteStore(Options, TempFilePath, /*Quiet*/ false); - } - - if (!RemoteStore) - { - return {nullptr, "Unknown remote store type"}; - } - - return {std::move(RemoteStore), ""}; - } - - std::pair<HttpResponseCode, std::string> ConvertResult(const RemoteProjectStore::Result& Result) + std::pair<int32_t, int32_t> GetPagedRange(int32_t TotalSize, const ProjectStore::Oplog::Paging& EntryPaging) { - if (Result.ErrorCode == 0) - { - return {HttpResponseCode::OK, Result.Text}; - } - return {static_cast<HttpResponseCode>(Result.ErrorCode), - Result.Reason.empty() ? Result.Text - : Result.Text.empty() ? Result.Reason - : fmt::format("{}: {}", Result.Reason, Result.Text)}; + int32_t Start = std::clamp(EntryPaging.Start, 0, TotalSize); + int32_t End = EntryPaging.Count < 0 ? TotalSize : (Start + std::min<int32_t>(EntryPaging.Count, TotalSize - Start)); + return {Start, End}; } #pragma pack(push) #pragma pack(1) struct OplogIndexHeader { + OplogIndexHeader() {} + + struct BodyV1 + { + uint64_t LogPosition = 0; + uint32_t LSNCount = 0; + uint64_t KeyCount = 0; + uint32_t OpAddressMapCount = 0; + uint32_t LatestOpMapCount = 0; + uint64_t ChunkMapCount = 0; + uint64_t MetaMapCount = 0; + uint64_t FileMapCount = 0; + }; + + struct BodyV2 + { + uint64_t LogPosition = 0; + uint64_t KeyCount = 0; + uint32_t OpPayloadIndexCount = 0; + uint32_t OpPayloadCount = 0; + uint32_t ChunkMapCount = 0; + uint32_t MetaMapCount = 0; + uint32_t FileMapCount = 0; + uint32_t MaxLSN = 0; + uint32_t NextOpCoreOffset = 0; // note: Multiple of oplog data alignment! + uint32_t Reserved[2] = {0, 0}; + }; + static constexpr uint32_t ExpectedMagic = 0x7569647a; // 'zidx'; - static constexpr uint32_t CurrentVersion = 1; + static constexpr uint32_t Version1 = 1; + static constexpr uint32_t Version2 = 2; + static constexpr uint32_t CurrentVersion = Version2; static constexpr uint64_t DataAlignment = 8; - uint32_t Magic = ExpectedMagic; - uint32_t Version = CurrentVersion; - uint64_t LogPosition = 0; - uint32_t LSNCount = 0; - uint64_t KeyCount = 0; - uint32_t OpAddressMapCount = 0; - uint32_t LatestOpMapCount = 0; - uint64_t ChunkMapCount = 0; - uint64_t MetaMapCount = 0; - uint64_t FileMapCount = 0; - uint32_t Checksum = 0; + uint32_t Magic = ExpectedMagic; + uint32_t Version = CurrentVersion; + + union + { + BodyV1 V1; + BodyV2 V2; + }; + + uint32_t Checksum = 0; static uint32_t ComputeChecksum(const OplogIndexHeader& Header) { return XXH32(&Header.Magic, sizeof(OplogIndexHeader) - sizeof(uint32_t), 0xC0C0'BABA); } }; + #pragma pack(pop) static_assert(sizeof(OplogIndexHeader) == 64); @@ -486,20 +267,19 @@ struct ProjectStore::OplogStorage : public RefCounted ~OplogStorage() { - ZEN_INFO("oplog '{}/{}': closing oplog storage at {}", - m_OwnerOplog->GetOuterProject()->Identifier, - m_OwnerOplog->OplogId(), - m_OplogStoragePath); + ZEN_DEBUG("oplog '{}/{}': closing oplog storage at {}", + m_OwnerOplog->GetOuterProjectIdentifier(), + m_OwnerOplog->OplogId(), + m_OplogStoragePath); try { - Flush(); m_Oplog.Close(); m_OpBlobs.Close(); } catch (const std::exception& Ex) { ZEN_WARN("oplog '{}/{}': flushing oplog at '{}' failed. Reason: '{}'", - m_OwnerOplog->GetOuterProject()->Identifier, + m_OwnerOplog->GetOuterProjectIdentifier(), m_OwnerOplog->OplogId(), m_OplogStoragePath, Ex.what()); @@ -545,33 +325,63 @@ struct ProjectStore::OplogStorage : public RefCounted m_NextOpsOffset = RoundUp((NextOpFileOffset.Offset * m_OpsAlign) + NextOpFileOffset.Size, m_OpsAlign); } - void Open(bool IsCreate) + void SetMaxLSNAndNextOpOffset(uint32_t MaxLSN, uint32_t NextOpOffset) + { + m_MaxLsn.store(MaxLSN); + m_NextOpsOffset = NextOpOffset * m_OpsAlign; + } + + uint32_t GetNextOpOffset() const { return gsl::narrow<uint32_t>(m_NextOpsOffset / m_OpsAlign); } + + enum EMode + { + Create, + Read, + Write + }; + + void Open(EMode InMode) { ZEN_MEMSCOPE(GetProjectstoreTag()); ZEN_TRACE_CPU("Store::OplogStorage::Open"); - if (IsCreate) + CasLogFile::Mode LogMode = CasLogFile::Mode::kRead; + BasicFile::Mode BlobsMode = BasicFile::Mode::kRead; + + if (InMode == EMode::Create) { - ZEN_INFO("oplog '{}/{}': initializing storage at '{}'", - m_OwnerOplog->GetOuterProject()->Identifier, - m_OwnerOplog->OplogId(), - m_OplogStoragePath); + ZEN_DEBUG("oplog '{}/{}': initializing storage at '{}'", + m_OwnerOplog->GetOuterProjectIdentifier(), + m_OwnerOplog->OplogId(), + m_OplogStoragePath); DeleteDirectories(m_OplogStoragePath); CreateDirectories(m_OplogStoragePath); + + LogMode = CasLogFile::Mode::kTruncate; + BlobsMode = BasicFile::Mode::kTruncate; } - else + else if (InMode == EMode::Write) { - ZEN_INFO("oplog '{}/{}': opening storage at '{}'", - m_OwnerOplog->GetOuterProject()->Identifier, - m_OwnerOplog->OplogId(), - m_OplogStoragePath); + LogMode = CasLogFile::Mode::kWrite; + BlobsMode = BasicFile::Mode::kWrite; + ZEN_DEBUG("oplog '{}/{}': opening storage at '{}'", + m_OwnerOplog->GetOuterProjectIdentifier(), + m_OwnerOplog->OplogId(), + m_OplogStoragePath); + } + else if (InMode == EMode::Read) + { + ZEN_DEBUG("oplog '{}/{}': opening read only storage at '{}'", + m_OwnerOplog->GetOuterProjectIdentifier(), + m_OwnerOplog->OplogId(), + m_OplogStoragePath); } - m_Oplog.Open(GetLogPath(m_OplogStoragePath), IsCreate ? CasLogFile::Mode::kTruncate : CasLogFile::Mode::kWrite); + m_Oplog.Open(GetLogPath(m_OplogStoragePath), LogMode); m_Oplog.Initialize(); - m_OpBlobs.Open(GetBlobsPath(m_OplogStoragePath), IsCreate ? BasicFile::Mode::kTruncate : BasicFile::Mode::kWrite); + m_OpBlobs.Open(GetBlobsPath(m_OplogStoragePath), BlobsMode); ZEN_ASSERT(IsPow2(m_OpsAlign)); ZEN_ASSERT(!(m_NextOpsOffset & (m_OpsAlign - 1))); @@ -581,41 +391,43 @@ struct ProjectStore::OplogStorage : public RefCounted { ZEN_MEMSCOPE(GetProjectstoreTag()); - const uint64_t OpFileOffset = LogEntry.OpCoreOffset * m_OpsAlign; - const MemoryView OpBufferView = OpBlobsBuffer.MakeView(LogEntry.OpCoreSize, OpFileOffset); - if (OpBufferView.GetSize() == LogEntry.OpCoreSize) + const uint64_t OpFileOffset = LogEntry.OpCoreAddress.Offset * m_OpsAlign; + const MemoryView OpBufferView = OpBlobsBuffer.MakeView(LogEntry.OpCoreAddress.Size, OpFileOffset); + if (OpBufferView.GetSize() == LogEntry.OpCoreAddress.Size) { return IoBuffer(IoBuffer::Wrap, OpBufferView.GetData(), OpBufferView.GetSize()); } else { - IoBuffer OpBuffer(LogEntry.OpCoreSize); - OpBlobsBuffer.Read((void*)OpBuffer.Data(), LogEntry.OpCoreSize, OpFileOffset); + IoBuffer OpBuffer(LogEntry.OpCoreAddress.Size); + OpBlobsBuffer.Read((void*)OpBuffer.Data(), LogEntry.OpCoreAddress.Size, OpFileOffset); return OpBuffer; } } - uint64_t GetEffectiveBlobsSize(std::span<const OplogEntryAddress> Addresses) const + uint64_t GetEffectiveBlobsSize(std::span<const Oplog::OplogPayload> Addresses) const { uint64_t EffectiveSize = 0; - for (const OplogEntryAddress& Address : Addresses) + for (const Oplog::OplogPayload& OpPayload : Addresses) { - EffectiveSize += RoundUp(Address.Size, m_OpsAlign); + EffectiveSize += RoundUp(OpPayload.Address.Size, m_OpsAlign); } return EffectiveSize; } - void Compact( - std::span<const uint32_t> LSNs, - std::function<void(const Oid& OpKeyHash, uint32_t OldLSN, uint32_t NewLSN, const OplogEntryAddress& NewAddress)>&& Callback, - bool RetainLSNs, - bool DryRun) + void Compact(std::span<const ProjectStore::LogSequenceNumber> LSNs, + std::function<void(const Oid& OpKeyHash, + ProjectStore::LogSequenceNumber OldLSN, + ProjectStore::LogSequenceNumber NewLSN, + const OplogEntryAddress& NewAddress)>&& Callback, + bool RetainLSNs, + bool DryRun) { ZEN_MEMSCOPE(GetProjectstoreTag()); ZEN_TRACE_CPU("Store::OplogStorage::Compact"); ZEN_INFO("oplog '{}/{}': compacting at '{}'", - m_OwnerOplog->GetOuterProject()->Identifier, + m_OwnerOplog->GetOuterProjectIdentifier(), m_OwnerOplog->OplogId(), m_OplogStoragePath); @@ -643,9 +455,9 @@ struct ProjectStore::OplogStorage : public RefCounted std::vector<OplogEntry> Ops; Ops.reserve(LSNs.size()); - tsl::robin_map<uint32_t, size_t> LSNToIndex; + ProjectStore::LsnMap<size_t> LSNToIndex; LSNToIndex.reserve(LSNs.size()); - for (uint32_t LSN : LSNs) + for (ProjectStore::LogSequenceNumber LSN : LSNs) { LSNToIndex[LSN] = (size_t)-1; } @@ -671,10 +483,10 @@ struct ProjectStore::OplogStorage : public RefCounted SkipEntryCount); std::sort(Ops.begin(), Ops.end(), [&](const OplogEntry& Lhs, const OplogEntry& Rhs) { - return Lhs.OpCoreOffset < Rhs.OpCoreOffset; + return Lhs.OpCoreAddress.Offset < Rhs.OpCoreAddress.Offset; }); - std::vector<uint32_t> OldLSNs; + std::vector<ProjectStore::LogSequenceNumber> OldLSNs; OldLSNs.reserve(Ops.size()); uint64_t OpWriteOffset = 0; @@ -690,15 +502,15 @@ struct ProjectStore::OplogStorage : public RefCounted IoBuffer OpBuffer = GetOpBuffer(OldBlobsBuffer, LogEntry); if (RetainLSNs) { - MaxLSN = Max(MaxLSN, LogEntry.OpLsn); + MaxLSN = Max(MaxLSN, LogEntry.OpLsn.Number); } else { - LogEntry.OpLsn = ++MaxLSN; + LogEntry.OpLsn = ProjectStore::LogSequenceNumber(++MaxLSN); } - LogEntry.OpCoreOffset = gsl::narrow<uint32_t>(OpWriteOffset / m_OpsAlign); - NewOpBlobsBuffer.Write(OpBuffer.GetData(), LogEntry.OpCoreSize, OpWriteOffset); - OpWriteOffset = RoundUp((LogEntry.OpCoreOffset * m_OpsAlign) + LogEntry.OpCoreSize, m_OpsAlign); + LogEntry.OpCoreAddress.Offset = gsl::narrow<uint32_t>(OpWriteOffset / m_OpsAlign); + NewOpBlobsBuffer.Write(OpBuffer.GetData(), LogEntry.OpCoreAddress.Size, OpWriteOffset); + OpWriteOffset = RoundUp((LogEntry.OpCoreAddress.Offset * m_OpsAlign) + LogEntry.OpCoreAddress.Size, m_OpsAlign); } Oplog.Append(Ops); } @@ -746,14 +558,11 @@ struct ProjectStore::OplogStorage : public RefCounted for (size_t Index = 0; Index < Ops.size(); Index++) { const OplogEntry& LogEntry = Ops[Index]; - Callback(LogEntry.OpKeyHash, - OldLSNs[Index], - LogEntry.OpLsn, - OplogEntryAddress{.Offset = LogEntry.OpCoreOffset, .Size = LogEntry.OpCoreSize}); + Callback(LogEntry.OpKeyHash, OldLSNs[Index], LogEntry.OpLsn, LogEntry.OpCoreAddress); } ZEN_INFO("oplog '{}/{}': compact completed in {} - Max LSN# {}, New size: {}, old size {}.", - m_OwnerOplog->GetOuterProject()->Identifier, + m_OwnerOplog->GetOuterProjectIdentifier(), m_OwnerOplog->OplogId(), NiceTimeSpanMs(Timer.GetElapsedTimeMs()), m_MaxLsn.load(), @@ -783,10 +592,10 @@ struct ProjectStore::OplogStorage : public RefCounted std::filesystem::path GetBlobsPath() const { return GetBlobsPath(m_OplogStoragePath); } - void ReplayLog(std::function<void(CbObjectView, const OplogEntry&)>&& Handler, uint64_t SkipEntryCount = 0) + void ReadOplogEntriesFromLog(std::vector<OplogEntry>& OutOpLogEntries, uint64_t& OutInvalidEntries, uint64_t SkipEntryCount) { ZEN_MEMSCOPE(GetProjectstoreTag()); - ZEN_TRACE_CPU("Store::OplogStorage::ReplayLog"); + ZEN_TRACE_CPU("Store::OplogStorage::ReadOplogEntriesFromLog"); if (m_Oplog.GetLogCount() == SkipEntryCount) { @@ -797,11 +606,12 @@ struct ProjectStore::OplogStorage : public RefCounted uint64_t OpsBlockSize = m_OpBlobs.FileSize(); - std::vector<OplogEntry> OpLogEntries; - uint64_t InvalidEntries = 0; - { tsl::robin_map<Oid, size_t, Oid::Hasher> LatestKeys; + if (m_Oplog.GetLogCount() > SkipEntryCount) + { + LatestKeys.reserve(m_Oplog.GetLogCount() - SkipEntryCount); + } m_Oplog.Replay( [&](const OplogEntry& LogEntry) { @@ -810,34 +620,34 @@ struct ProjectStore::OplogStorage : public RefCounted if (auto It = LatestKeys.find(LogEntry.OpKeyHash); It == LatestKeys.end()) { ZEN_SCOPED_WARN("found tombstone referencing unknown key {}", LogEntry.OpKeyHash); - ++InvalidEntries; + ++OutInvalidEntries; return; } } - else if (LogEntry.OpCoreSize == 0) + else if (LogEntry.OpCoreAddress.Size == 0) { ZEN_SCOPED_WARN("skipping zero size op {}", LogEntry.OpKeyHash); - ++InvalidEntries; + ++OutInvalidEntries; return; } - else if (LogEntry.OpLsn == 0) + else if (LogEntry.OpLsn.Number == 0) { ZEN_SCOPED_WARN("skipping zero lsn op {}", LogEntry.OpKeyHash); - ++InvalidEntries; + ++OutInvalidEntries; return; } - const uint64_t OpFileOffset = LogEntry.OpCoreOffset * m_OpsAlign; - if ((OpFileOffset + LogEntry.OpCoreSize) > OpsBlockSize) + const uint64_t OpFileOffset = LogEntry.OpCoreAddress.Offset * m_OpsAlign; + if ((OpFileOffset + LogEntry.OpCoreAddress.Size) > OpsBlockSize) { ZEN_SCOPED_WARN("skipping out of bounds op {}", LogEntry.OpKeyHash); - ++InvalidEntries; + ++OutInvalidEntries; return; } if (auto It = LatestKeys.find(LogEntry.OpKeyHash); It != LatestKeys.end()) { - OplogEntry& Entry = OpLogEntries[It->second]; + OplogEntry& Entry = OutOpLogEntries[It->second]; if (LogEntry.IsTombstone() && Entry.IsTombstone()) { @@ -848,17 +658,31 @@ struct ProjectStore::OplogStorage : public RefCounted } else { - const size_t OpIndex = OpLogEntries.size(); + const size_t OpIndex = OutOpLogEntries.size(); LatestKeys[LogEntry.OpKeyHash] = OpIndex; - OpLogEntries.push_back(LogEntry); + OutOpLogEntries.push_back(LogEntry); } }, SkipEntryCount); } + } + + void ReplayLog(std::function<void(CbObjectView, const OplogEntry&)>&& Handler, uint64_t SkipEntryCount = 0) + { + ZEN_MEMSCOPE(GetProjectstoreTag()); + ZEN_TRACE_CPU("Store::OplogStorage::ReplayLog"); + + if (m_Oplog.GetLogCount() == SkipEntryCount) + { + return; + } - std::sort(OpLogEntries.begin(), OpLogEntries.end(), [&](const OplogEntry& Lhs, const OplogEntry& Rhs) { - return Lhs.OpCoreOffset < Rhs.OpCoreOffset; - }); + Stopwatch Timer; + + std::vector<OplogEntry> OpLogEntries; + uint64_t InvalidEntries = 0; + + ReadOplogEntriesFromLog(OpLogEntries, InvalidEntries, SkipEntryCount); uint64_t TombstoneEntries = 0; @@ -867,6 +691,10 @@ struct ProjectStore::OplogStorage : public RefCounted uint32_t MaxOpLsn = m_MaxLsn; uint64_t NextOpFileOffset = m_NextOpsOffset; + std::sort(OpLogEntries.begin(), OpLogEntries.end(), [&](const OplogEntry& Lhs, const OplogEntry& Rhs) { + return Lhs.OpCoreAddress.Offset < Rhs.OpCoreAddress.Offset; + }); + for (const OplogEntry& LogEntry : OpLogEntries) { if (LogEntry.IsTombstone()) @@ -880,12 +708,12 @@ struct ProjectStore::OplogStorage : public RefCounted // Verify checksum, ignore op data if incorrect const uint32_t ExpectedOpCoreHash = LogEntry.OpCoreHash; - const uint32_t OpCoreHash = uint32_t(XXH3_64bits(OpBuffer.GetData(), LogEntry.OpCoreSize) & 0xffffFFFF); + const uint32_t OpCoreHash = uint32_t(XXH3_64bits(OpBuffer.GetData(), LogEntry.OpCoreAddress.Size) & 0xffffFFFF); if (OpCoreHash != ExpectedOpCoreHash) { ZEN_WARN("oplog '{}/{}': skipping bad checksum op - {}. Expected: {}, found: {}", - m_OwnerOplog->GetOuterProject()->Identifier, + m_OwnerOplog->GetOuterProjectIdentifier(), m_OwnerOplog->OplogId(), LogEntry.OpKeyHash, ExpectedOpCoreHash, @@ -895,17 +723,31 @@ struct ProjectStore::OplogStorage : public RefCounted Err != CbValidateError::None) { ZEN_WARN("oplog '{}/{}': skipping invalid format op - {}. Error: '{}'", - m_OwnerOplog->GetOuterProject()->Identifier, + m_OwnerOplog->GetOuterProjectIdentifier(), m_OwnerOplog->OplogId(), LogEntry.OpKeyHash, ToString(Err)); } else { - Handler(CbObjectView(OpBuffer.GetData()), LogEntry); - MaxOpLsn = Max(MaxOpLsn, LogEntry.OpLsn); - const uint64_t EntryNextOpFileOffset = RoundUp((LogEntry.OpCoreOffset * m_OpsAlign) + LogEntry.OpCoreSize, m_OpsAlign); - NextOpFileOffset = Max(NextOpFileOffset, EntryNextOpFileOffset); + CbObjectView OpView(OpBuffer.GetData()); + if (OpView.GetSize() != OpBuffer.GetSize()) + { + ZEN_WARN("oplog '{}/{}': skipping invalid format op - {}. Object payload size {} does not match op data size {}", + m_OwnerOplog->GetOuterProjectIdentifier(), + m_OwnerOplog->OplogId(), + LogEntry.OpKeyHash, + OpView.GetSize(), + OpBuffer.GetSize()); + } + else + { + Handler(OpView, LogEntry); + MaxOpLsn = Max(MaxOpLsn, LogEntry.OpLsn.Number); + const uint64_t EntryNextOpFileOffset = + RoundUp((LogEntry.OpCoreAddress.Offset * m_OpsAlign) + LogEntry.OpCoreAddress.Size, m_OpsAlign); + NextOpFileOffset = Max(NextOpFileOffset, EntryNextOpFileOffset); + } } } } @@ -914,7 +756,7 @@ struct ProjectStore::OplogStorage : public RefCounted m_NextOpsOffset = NextOpFileOffset; ZEN_INFO("oplog '{}/{}': replay from '{}' completed in {} - Max LSN# {}, Next offset: {}, {} tombstones, {} invalid entries", - m_OwnerOplog->GetOuterProject()->Identifier, + m_OwnerOplog->GetOuterProjectIdentifier(), m_OwnerOplog->OplogId(), m_OplogStoragePath, NiceTimeSpanMs(Timer.GetElapsedTimeMs()), @@ -924,25 +766,80 @@ struct ProjectStore::OplogStorage : public RefCounted InvalidEntries); } - void ReplayLogEntries(const std::span<OplogEntryAddress> Entries, std::function<void(CbObjectView)>&& Handler) + void ReplayLogEntries(const std::span<const Oplog::OplogPayload> Entries, + const std::span<const Oplog::PayloadIndex> Order, + std::function<void(LogSequenceNumber Lsn, CbObjectView)>&& Handler) { ZEN_MEMSCOPE(GetProjectstoreTag()); ZEN_TRACE_CPU("Store::OplogStorage::ReplayLogEntries"); BasicFileBuffer OpBlobsBuffer(m_OpBlobs, 65536); - for (const OplogEntryAddress& Entry : Entries) + for (ProjectStore::Oplog::PayloadIndex EntryOffset : Order) { - const uint64_t OpFileOffset = Entry.Offset * m_OpsAlign; - MemoryView OpBufferView = OpBlobsBuffer.MakeView(Entry.Size, OpFileOffset); - if (OpBufferView.GetSize() == Entry.Size) + const Oplog::OplogPayload& Entry = Entries[EntryOffset]; + + const uint64_t OpFileOffset = Entry.Address.Offset * m_OpsAlign; + MemoryView OpBufferView = OpBlobsBuffer.MakeView(Entry.Address.Size, OpFileOffset); + if (OpBufferView.GetSize() == Entry.Address.Size) { - Handler(CbObjectView(OpBufferView.GetData())); - continue; + if (CbValidateError Error = ValidateCompactBinary(OpBufferView, CbValidateMode::Default); Error == CbValidateError::None) + { + CbObjectView OpView(OpBufferView.GetData()); + if (OpView.GetSize() != OpBufferView.GetSize()) + { + ZEN_WARN("oplog '{}/{}': skipping invalid format op - {}. Object payload size {} does not match op data size {}", + m_OwnerOplog->GetOuterProjectIdentifier(), + m_OwnerOplog->OplogId(), + Entry.Lsn.Number, + OpView.GetSize(), + OpBufferView.GetSize()); + } + else + { + Handler(Entry.Lsn, OpView); + } + } + else + { + ZEN_WARN("oplog '{}/{}': skipping invalid format op - {}. Validation error: {}", + m_OwnerOplog->GetOuterProjectIdentifier(), + m_OwnerOplog->OplogId(), + Entry.Lsn.Number, + ToString(Error)); + } + } + else + { + IoBuffer OpBuffer(Entry.Address.Size); + OpBlobsBuffer.Read((void*)OpBuffer.Data(), Entry.Address.Size, OpFileOffset); + OpBufferView = OpBuffer.GetView(); + if (CbValidateError Error = ValidateCompactBinary(OpBufferView, CbValidateMode::Default); Error == CbValidateError::None) + { + CbObjectView OpView(OpBuffer.Data()); + if (OpView.GetSize() != OpBuffer.GetSize()) + { + ZEN_WARN("oplog '{}/{}': skipping invalid format op - {}. Object payload size {} does not match op data size {}", + m_OwnerOplog->GetOuterProjectIdentifier(), + m_OwnerOplog->OplogId(), + Entry.Lsn.Number, + OpView.GetSize(), + OpBuffer.GetSize()); + } + else + { + Handler(Entry.Lsn, OpView); + } + } + else + { + ZEN_WARN("oplog '{}/{}': skipping invalid format op - {}. Validation error: {}", + m_OwnerOplog->GetOuterProjectIdentifier(), + m_OwnerOplog->OplogId(), + Entry.Lsn.Number, + ToString(Error)); + } } - IoBuffer OpBuffer(Entry.Size); - OpBlobsBuffer.Read((void*)OpBuffer.Data(), Entry.Size, OpFileOffset); - Handler(CbObjectView(OpBuffer.Data())); } } @@ -993,8 +890,8 @@ struct ProjectStore::OplogStorage : public RefCounted RwLock::ExclusiveLockScope Lock(m_RwLock); const uint64_t WriteOffset = m_NextOpsOffset; - const uint32_t OpLsn = ++m_MaxLsn; - if (OpLsn == std::numeric_limits<uint32_t>::max()) + const LogSequenceNumber OpLsn(++m_MaxLsn); + if (!OpLsn.Number) { ZEN_ERROR("Oplog count has exceeded available range for oplog {}", m_OwnerOplog->OplogId()); throw std::runtime_error(fmt::format("Oplog count has exceeded available range for oplog {}", m_OwnerOplog->OplogId())); @@ -1004,11 +901,10 @@ struct ProjectStore::OplogStorage : public RefCounted ZEN_ASSERT(IsMultipleOf(WriteOffset, m_OpsAlign)); - OplogEntry Entry = {.OpLsn = OpLsn, - .OpCoreOffset = gsl::narrow_cast<uint32_t>(WriteOffset / m_OpsAlign), - .OpCoreSize = uint32_t(WriteSize), - .OpCoreHash = OpData.OpCoreHash, - .OpKeyHash = OpData.KeyHash}; + OplogEntry Entry = {.OpLsn = OpLsn, + .OpCoreAddress = {.Offset = gsl::narrow_cast<uint32_t>(WriteOffset / m_OpsAlign), .Size = uint32_t(WriteSize)}, + .OpCoreHash = OpData.OpCoreHash, + .OpKeyHash = OpData.KeyHash}; m_OpBlobs.Write(OpData.Buffer.GetData(), WriteSize, WriteOffset); m_Oplog.Append(Entry); @@ -1023,7 +919,7 @@ struct ProjectStore::OplogStorage : public RefCounted size_t OpCount = Ops.size(); std::vector<std::pair<uint64_t, uint64_t>> OffsetAndSizes; - std::vector<uint32_t> OpLsns; + std::vector<LogSequenceNumber> OpLsns; OffsetAndSizes.resize(OpCount); OpLsns.resize(OpCount); @@ -1042,8 +938,8 @@ struct ProjectStore::OplogStorage : public RefCounted for (size_t OpIndex = 0; OpIndex < OpCount; OpIndex++) { OffsetAndSizes[OpIndex].first = WriteOffset - WriteStart; - OpLsns[OpIndex] = ++m_MaxLsn; - if (OpLsns[OpIndex] == std::numeric_limits<uint32_t>::max()) + OpLsns[OpIndex] = LogSequenceNumber(++m_MaxLsn); + if (!OpLsns[OpIndex]) { ZEN_ERROR("Oplog count has exceeded available range for oplog {}", m_OwnerOplog->OplogId()); throw std::runtime_error(fmt::format("Oplog count has exceeded available range for oplog {}", m_OwnerOplog->OplogId())); @@ -1062,11 +958,12 @@ struct ProjectStore::OplogStorage : public RefCounted { MutableMemoryView WriteBufferView = WriteBuffer.GetMutableView().RightChop(OffsetAndSizes[OpIndex].first); WriteBufferView.CopyFrom(Ops[OpIndex].Buffer); - Entries[OpIndex] = {.OpLsn = OpLsns[OpIndex], - .OpCoreOffset = gsl::narrow_cast<uint32_t>((WriteStart + OffsetAndSizes[OpIndex].first) / m_OpsAlign), - .OpCoreSize = uint32_t(OffsetAndSizes[OpIndex].second), - .OpCoreHash = Ops[OpIndex].OpCoreHash, - .OpKeyHash = Ops[OpIndex].KeyHash}; + Entries[OpIndex] = { + .OpLsn = OpLsns[OpIndex], + .OpCoreAddress = {.Offset = gsl::narrow_cast<uint32_t>((WriteStart + OffsetAndSizes[OpIndex].first) / m_OpsAlign), + .Size = uint32_t(OffsetAndSizes[OpIndex].second)}, + .OpCoreHash = Ops[OpIndex].OpCoreHash, + .OpKeyHash = Ops[OpIndex].KeyHash}; } m_OpBlobs.Write(WriteBuffer.GetData(), WriteBuffer.GetSize(), WriteStart); @@ -1105,16 +1002,22 @@ private: ////////////////////////////////////////////////////////////////////////// -ProjectStore::Oplog::Oplog(std::string_view Id, - Project* Project, +ProjectStore::Oplog::Oplog(const LoggerRef& InLog, + std::string_view ProjectIdentifier, + std::string_view Id, CidStore& Store, - std::filesystem::path BasePath, - const std::filesystem::path& MarkerPath) -: m_OuterProject(Project) + const std::filesystem::path& BasePath, + const std::filesystem::path& MarkerPath, + EMode State) +: m_Log(InLog) +, m_OuterProjectId(ProjectIdentifier) , m_OplogId(Id) , m_CidStore(Store) , m_BasePath(BasePath) , m_MarkerPath(MarkerPath) +, m_TempPath(m_BasePath / std::string_view("temp")) +, m_MetaPath(m_BasePath / std::string_view("ops.meta")) +, m_Mode(State) , m_MetaValid(false) , m_PendingPrepOpAttachmentsRetainEnd(GcClock::Now()) { @@ -1122,21 +1025,32 @@ ProjectStore::Oplog::Oplog(std::string_view Id, using namespace std::literals; - m_Storage = new OplogStorage(this, m_BasePath); - bool StoreExists = m_Storage->Exists(); - if (StoreExists) + m_Storage = new OplogStorage(this, m_BasePath); + OplogStorage::EMode StorageMode = OplogStorage::EMode::Write; + if (m_Mode == EMode::kBasicReadOnly) + { + StorageMode = OplogStorage::EMode::Read; + } + else { - if (!m_Storage->IsValid()) + bool StoreExists = m_Storage->Exists(); + if (StoreExists) { - ZEN_WARN("Invalid oplog found at '{}'. Wiping state for oplog.", m_BasePath); - m_Storage->WipeState(); - std::error_code DummyEc; - RemoveFile(m_MetaPath, DummyEc); + if (!m_Storage->IsValid()) + { + ZEN_WARN("Invalid oplog found at '{}'. Wiping state for oplog.", m_BasePath); + m_Storage->WipeState(); + std::error_code DummyEc; + RemoveFile(m_MetaPath, DummyEc); + StoreExists = false; + } + } + if (!StoreExists) + { + StorageMode = OplogStorage::EMode::Create; } } - m_Storage->Open(/* IsCreate */ !StoreExists); - m_TempPath = m_BasePath / "temp"sv; - m_MetaPath = m_BasePath / "ops.meta"sv; + m_Storage->Open(StorageMode); m_MetaValid = !IsFileOlderThan(m_MetaPath, m_Storage->GetBlobsPath()); CleanDirectory(m_TempPath, /*ForceRemoveReadOnlyFiles*/ false); @@ -1157,21 +1071,45 @@ ProjectStore::Oplog::Flush() ZEN_TRACE_CPU("Oplog::Flush"); - RwLock::SharedLockScope Lock(m_OplogLock); + if (m_Mode == EMode::kFull) + { + RwLock::SharedLockScope Lock(m_OplogLock); - ZEN_ASSERT(m_Storage); + if (!m_Storage) + { + return; + } - m_Storage->Flush(); - if (!m_MetaValid) - { - std::error_code DummyEc; - RemoveFile(m_MetaPath, DummyEc); + m_Storage->Flush(); + if (!m_MetaValid) + { + std::error_code DummyEc; + RemoveFile(m_MetaPath, DummyEc); + } + + uint64_t LogCount = m_Storage->LogCount(); + if (m_LogFlushPosition != LogCount || m_IsLegacySnapshot) + { + WriteIndexSnapshot(); + } } +} - uint64_t LogCount = m_Storage->LogCount(); - if (m_LogFlushPosition != LogCount) +void +ProjectStore::Oplog::RefreshLsnToPayloadOffsetMap(RwLock::ExclusiveLockScope&) +{ + if (!m_LsnToPayloadOffsetMap) { - WriteIndexSnapshot(); + m_LsnToPayloadOffsetMap = std::make_unique<LsnMap<PayloadIndex>>(); + m_LsnToPayloadOffsetMap->reserve(m_OpLogPayloads.size()); + for (uint32_t PayloadOffset = 0; PayloadOffset < m_OpLogPayloads.size(); PayloadOffset++) + { + const OplogPayload& Payload = m_OpLogPayloads[PayloadOffset]; + if (Payload.Lsn.Number != 0 && Payload.Address.Size != 0) + { + m_LsnToPayloadOffsetMap->insert_or_assign(Payload.Lsn, PayloadIndex(PayloadOffset)); + } + } } } @@ -1180,63 +1118,79 @@ ProjectStore::Oplog::Scrub(ScrubContext& Ctx) { ZEN_MEMSCOPE(GetProjectstoreTag()); - std::vector<Oid> BadEntryKeys; - - using namespace std::literals; + ZEN_ASSERT(m_Mode == EMode::kFull); - IterateOplogWithKey([&](uint32_t Lsn, const Oid& Key, CbObjectView Op) { - ZEN_UNUSED(Lsn); + std::vector<std::pair<LogSequenceNumber, Oid>> BadEntries; - std::vector<IoHash> Cids; - Op.IterateAttachments([&](CbFieldView Visitor) { Cids.emplace_back(Visitor.AsAttachment()); }); + using namespace std::literals; + IterateOplogWithKey([&](LogSequenceNumber Lsn, const Oid& Key, CbObjectView Op) { { const Oid KeyHash = ComputeOpKey(Op); - - ZEN_ASSERT_FORMAT(KeyHash == Key, "oplog data does not match information from index (op:{} != index:{})", KeyHash, Key); + if (KeyHash != Key) + { + BadEntries.push_back({Lsn, Key}); + ZEN_WARN("Scrub: oplog data does not match information from index (op:{} != index:{})", KeyHash, Key); + return; + } } - for (const IoHash& Cid : Cids) - { - if (!m_CidStore.ContainsChunk(Cid)) + // TODO: Should we really delete an Op because it points to a missing or malformed Cid chunk? + + Op.IterateAttachments([&](CbFieldView Visitor) { + const IoHash Cid = Visitor.AsAttachment(); + if (Ctx.IsBadCid(Cid)) { - // oplog entry references a CAS chunk which is not - // present - BadEntryKeys.push_back(Key); + // oplog entry references a CAS chunk which has been flagged as bad + BadEntries.push_back({Lsn, Key}); return; } - if (Ctx.IsBadCid(Cid)) + if (!m_CidStore.ContainsChunk(Cid)) { - // oplog entry references a CAS chunk which has been - // flagged as bad - BadEntryKeys.push_back(Key); + // oplog entry references a CAS chunk which is not present + BadEntries.push_back({Lsn, Key}); return; } - } + }); }); - if (!BadEntryKeys.empty()) + if (!BadEntries.empty()) { if (Ctx.RunRecovery()) { ZEN_WARN("oplog '{}/{}': scrubbing found {} bad ops in oplog @ '{}', these will be removed from the index", - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, - BadEntryKeys.size(), + BadEntries.size(), m_BasePath); // Actually perform some clean-up - RwLock::ExclusiveLockScope _(m_OplogLock); - for (const auto& Key : BadEntryKeys) + RwLock::ExclusiveLockScope Lock(m_OplogLock); + + RefreshLsnToPayloadOffsetMap(Lock); + + for (const auto& BadEntry : BadEntries) { - if (auto It = m_LatestOpMap.find(Key); It != m_LatestOpMap.end()) + const LogSequenceNumber BadLsn = BadEntry.first; + const Oid& BadKey = BadEntry.second; + if (auto It = m_OpToPayloadOffsetMap.find(BadKey); It != m_OpToPayloadOffsetMap.end()) + { + OplogPayload& Payload = m_OpLogPayloads[It->second]; + if (Payload.Lsn == BadLsn) + { + m_OpToPayloadOffsetMap.erase(It); + m_Storage->AppendTombstone(BadKey); + } + } + if (auto LsnIt = m_LsnToPayloadOffsetMap->find(BadLsn); LsnIt != m_LsnToPayloadOffsetMap->end()) { - m_OpAddressMap.erase(It->second); - m_LatestOpMap.erase(It); + const PayloadIndex LsnPayloadOffset = LsnIt->second; + OplogPayload& LsnPayload = m_OpLogPayloads[LsnPayloadOffset]; + LsnPayload = {.Lsn = LogSequenceNumber(0), .Address = {.Offset = 0, .Size = 0}}; } - m_Storage->AppendTombstone(Key); + m_LsnToPayloadOffsetMap->erase(BadLsn); } - if (!BadEntryKeys.empty()) + if (!BadEntries.empty()) { m_MetaValid = false; } @@ -1244,9 +1198,9 @@ ProjectStore::Oplog::Scrub(ScrubContext& Ctx) else { ZEN_WARN("oplog '{}/{}': scrubbing found {} bad ops in oplog @ '{}' but no cleanup will be performed", - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, - BadEntryKeys.size(), + BadEntries.size(), m_BasePath); } } @@ -1292,8 +1246,9 @@ ProjectStore::Oplog::ResetState() m_ChunkMap.clear(); m_MetaMap.clear(); m_FileMap.clear(); - m_OpAddressMap.clear(); - m_LatestOpMap.clear(); + m_OpToPayloadOffsetMap.clear(); + m_OpLogPayloads.clear(); + m_LsnToPayloadOffsetMap.reset(); m_Storage = {}; } @@ -1304,14 +1259,15 @@ ProjectStore::Oplog::PrepareForDelete(std::filesystem::path& OutRemoveDirectory) RwLock::ExclusiveLockScope _(m_OplogLock); m_UpdateCaptureRefCounter = 0; - m_CapturedLSNs.reset(); + m_CapturedOps.reset(); m_CapturedAttachments.reset(); m_PendingPrepOpAttachments.clear(); m_ChunkMap.clear(); m_MetaMap.clear(); m_FileMap.clear(); - m_OpAddressMap.clear(); - m_LatestOpMap.clear(); + m_OpToPayloadOffsetMap.clear(); + m_OpLogPayloads.clear(); + m_LsnToPayloadOffsetMap.reset(); m_Storage = {}; if (PrepareDirectoryDelete(m_BasePath, OutRemoveDirectory)) { @@ -1344,7 +1300,11 @@ ProjectStore::Oplog::Read() ZEN_TRACE_CPU("Oplog::Read"); ZEN_LOG_SCOPE("Oplog::Read '{}'", m_OplogId); - ZEN_DEBUG("oplog '{}': reading config from '{}'", m_OuterProject->Identifier, m_OplogId, m_BasePath); + ZEN_DEBUG("oplog '{}/{}': reading from '{}'. State: {}", + m_OuterProjectId, + m_OplogId, + m_BasePath, + m_Mode == EMode::kFull ? "Full" : "BasicNoLookups"); std::optional<CbObject> Config = ReadStateFile(m_BasePath, [this]() { return Log(); }); if (Config.has_value()) @@ -1363,43 +1323,61 @@ ProjectStore::Oplog::Read() RemoveFile(m_MetaPath, DummyEc); } + ZEN_ASSERT(!m_LsnToPayloadOffsetMap); + ReadIndexSnapshot(); - m_Storage->ReplayLog( - [&](CbObjectView Op, const OplogEntry& OpEntry) { - // MaxLSN = Max(OpEntry.OpLsn, MaxLSN); + if (m_Mode == EMode::kFull) + { + m_Storage->ReplayLog( + [&](CbObjectView Op, const OplogEntry& OpEntry) { + const OplogEntryMapping OpMapping = GetMapping(Op); + // Update chunk id maps + for (const ChunkMapping& Chunk : OpMapping.Chunks) + { + m_ChunkMap.insert_or_assign(Chunk.Id, Chunk.Hash); + } - const OplogEntryMapping OpMapping = GetMapping(Op); - // Update chunk id maps - for (const ChunkMapping& Chunk : OpMapping.Chunks) - { - m_ChunkMap.insert_or_assign(Chunk.Id, Chunk.Hash); - } + for (const FileMapping& File : OpMapping.Files) + { + if (File.Hash != IoHash::Zero) + { + m_ChunkMap.insert_or_assign(File.Id, File.Hash); + } + m_FileMap.insert_or_assign(File.Id, + FileMapEntry{.ServerPath = File.Hash == IoHash::Zero ? File.ServerPath : std::string(), + .ClientPath = File.ClientPath}); + } - for (const FileMapping& File : OpMapping.Files) - { - if (File.Hash != IoHash::Zero) + for (const ChunkMapping& Meta : OpMapping.Meta) { - m_ChunkMap.insert_or_assign(File.Id, File.Hash); + m_MetaMap.insert_or_assign(Meta.Id, Meta.Hash); } - m_FileMap.insert_or_assign( - File.Id, - FileMapEntry{.ServerPath = File.Hash == IoHash::Zero ? File.ServerPath : std::string(), .ClientPath = File.ClientPath}); - } - for (const ChunkMapping& Meta : OpMapping.Meta) - { - m_MetaMap.insert_or_assign(Meta.Id, Meta.Hash); - } + const PayloadIndex PayloadOffset(m_OpLogPayloads.size()); - m_OpAddressMap.emplace(OpEntry.OpLsn, OplogEntryAddress{.Offset = OpEntry.OpCoreOffset, .Size = OpEntry.OpCoreSize}); - m_LatestOpMap[OpEntry.OpKeyHash] = OpEntry.OpLsn; - }, - m_LogFlushPosition); + m_OpToPayloadOffsetMap.insert_or_assign(OpEntry.OpKeyHash, PayloadOffset); + m_OpLogPayloads.push_back({.Lsn = OpEntry.OpLsn, .Address = OpEntry.OpCoreAddress}); + }, + m_LogFlushPosition); - if (m_Storage->LogCount() != m_LogFlushPosition) + if (m_Storage->LogCount() != m_LogFlushPosition || m_IsLegacySnapshot) + { + WriteIndexSnapshot(); + } + } + else { - WriteIndexSnapshot(); + std::vector<OplogEntry> OpLogEntries; + uint64_t InvalidEntries; + m_Storage->ReadOplogEntriesFromLog(OpLogEntries, InvalidEntries, m_LogFlushPosition); + for (const OplogEntry& OpEntry : OpLogEntries) + { + const PayloadIndex PayloadOffset(m_OpLogPayloads.size()); + + m_OpToPayloadOffsetMap.insert_or_assign(OpEntry.OpKeyHash, PayloadOffset); + m_OpLogPayloads.push_back({.Lsn = OpEntry.OpLsn, .Address = OpEntry.OpCoreAddress}); + } } } @@ -1407,6 +1385,7 @@ void ProjectStore::Oplog::Write() { ZEN_MEMSCOPE(GetProjectstoreTag()); + ZEN_ASSERT(m_Mode != EMode::kBasicReadOnly); using namespace std::literals; @@ -1420,7 +1399,7 @@ ProjectStore::Oplog::Write() std::filesystem::path StateFilePath = m_BasePath / "oplog.zcb"sv; - ZEN_INFO("oplog '{}/{}': persisting config to '{}'", m_OuterProject->Identifier, m_OplogId, StateFilePath); + ZEN_DEBUG("oplog '{}/{}': persisting config to '{}'", m_OuterProjectId, m_OplogId, StateFilePath); TemporaryFile::SafeWriteFile(StateFilePath, Mem.GetView()); } @@ -1439,7 +1418,7 @@ bool ProjectStore::Oplog::Reset() { ZEN_MEMSCOPE(GetProjectstoreTag()); - + ZEN_ASSERT(m_Mode == EMode::kFull); std::filesystem::path MovedDir; { @@ -1449,17 +1428,18 @@ ProjectStore::Oplog::Reset() { m_Storage = new OplogStorage(this, m_BasePath); const bool StoreExists = m_Storage->Exists(); - m_Storage->Open(/* IsCreate */ !StoreExists); + m_Storage->Open(StoreExists ? OplogStorage::EMode::Write : OplogStorage::EMode::Create); m_MetaValid = !IsFileOlderThan(m_MetaPath, m_Storage->GetBlobsPath()); return false; } m_ChunkMap.clear(); m_MetaMap.clear(); m_FileMap.clear(); - m_OpAddressMap.clear(); - m_LatestOpMap.clear(); + m_OpToPayloadOffsetMap.clear(); + m_OpLogPayloads.clear(); + m_LsnToPayloadOffsetMap.reset(); m_Storage = new OplogStorage(this, m_BasePath); - m_Storage->Open(true); + m_Storage->Open(OplogStorage::EMode::Create); m_MetaValid = false; CleanDirectory(m_TempPath, /*ForceRemoveReadOnlyFiles*/ false); Write(); @@ -1472,6 +1452,30 @@ ProjectStore::Oplog::Reset() return true; } +bool +ProjectStore::Oplog::CanUnload() +{ + ZEN_MEMSCOPE(GetProjectstoreTag()); + + RwLock::SharedLockScope _(m_OplogLock); + + uint64_t LogCount = m_Storage->LogCount(); + if (m_LogFlushPosition != LogCount) + { + return false; // The oplog is not flushed so likely this is an active oplog + } + + if (!m_PendingPrepOpAttachments.empty()) + { + return false; // We have a pending oplog prep operation in flight + } + if (m_UpdateCaptureRefCounter > 0) + { + return false; // GC capture is enable for the oplog + } + return true; +} + std::optional<CbObject> ProjectStore::Oplog::ReadStateFile(const std::filesystem::path& BasePath, std::function<LoggerRef()>&& Log) { @@ -1482,7 +1486,7 @@ ProjectStore::Oplog::ReadStateFile(const std::filesystem::path& BasePath, std::f std::filesystem::path StateFilePath = BasePath / "oplog.zcb"sv; if (IsFile(StateFilePath)) { - // ZEN_INFO("oplog '{}/{}': config read from '{}'", m_OuterProject->Identifier, m_OplogId, StateFilePath); + // ZEN_INFO("oplog '{}/{}': config read from '{}'", m_OuterProjectId, m_OplogId, StateFilePath); BasicFile Blob; Blob.Open(StateFilePath, BasicFile::Mode::kRead); @@ -1503,7 +1507,9 @@ ProjectStore::Oplog::ReadStateFile(const std::filesystem::path& BasePath, std::f } ProjectStore::Oplog::ValidationResult -ProjectStore::Oplog::Validate(std::atomic_bool& IsCancelledFlag, WorkerThreadPool* OptionalWorkerPool) +ProjectStore::Oplog::Validate(const std::filesystem::path& ProjectRootDir, + std::atomic_bool& IsCancelledFlag, + WorkerThreadPool* OptionalWorkerPool) { ZEN_MEMSCOPE(GetProjectstoreTag()); @@ -1522,7 +1528,7 @@ ProjectStore::Oplog::Validate(std::atomic_bool& IsCancelledFlag, WorkerThreadPoo Keys.reserve(OpCount); Mappings.reserve(OpCount); - IterateOplogWithKey([&](uint32_t LSN, const Oid& Key, CbObjectView OpView) { + IterateOplogWithKey([&](LogSequenceNumber LSN, const Oid& Key, CbObjectView OpView) { Result.LSNLow = Min(Result.LSNLow, LSN); Result.LSNHigh = Max(Result.LSNHigh, LSN); KeyHashes.push_back(Key); @@ -1566,7 +1572,7 @@ ProjectStore::Oplog::Validate(std::atomic_bool& IsCancelledFlag, WorkerThreadPoo { if (File.Hash == IoHash::Zero) { - std::filesystem::path FilePath = m_OuterProject->RootDir / File.ServerPath; + std::filesystem::path FilePath = ProjectRootDir / File.ServerPath; if (!IsFile(FilePath)) { ResultLock.WithExclusiveLock([&]() { Result.MissingFiles.push_back({KeyHash, File}); }); @@ -1598,7 +1604,7 @@ ProjectStore::Oplog::Validate(std::atomic_bool& IsCancelledFlag, WorkerThreadPoo std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::DisableBacklog); try { for (uint32_t OpIndex = 0; !IsCancelledFlag && OpIndex < Result.OpCount; OpIndex++) @@ -1649,12 +1655,14 @@ ProjectStore::Oplog::WriteIndexSnapshot() ZEN_MEMSCOPE(GetProjectstoreTag()); ZEN_TRACE_CPU("Oplog::WriteIndexSnapshot"); - ZEN_DEBUG("oplog '{}/{}': write store snapshot at '{}'", m_OuterProject->Identifier, m_OplogId, m_BasePath); + ZEN_ASSERT(m_Mode == EMode::kFull); + + ZEN_DEBUG("oplog '{}/{}': write store snapshot at '{}'", m_OuterProjectId, m_OplogId, m_BasePath); uint64_t EntryCount = 0; Stopwatch Timer; const auto _ = MakeGuard([&] { ZEN_INFO("oplog '{}/{}': wrote store snapshot for '{}' containing {} entries in {}", - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, m_BasePath, EntryCount, @@ -1666,34 +1674,21 @@ ProjectStore::Oplog::WriteIndexSnapshot() const fs::path IndexPath = m_BasePath / "ops.zidx"; try { - // Write the current state of the location map to a new index state - std::vector<uint32_t> LSNEntries; - std::vector<Oid> Keys; - std::vector<OplogEntryAddress> AddressMapEntries; - std::vector<uint32_t> LatestOpMapEntries; - std::vector<IoHash> ChunkMapEntries; - std::vector<IoHash> MetaMapEntries; - std::vector<uint32_t> FilePathLengths; - std::vector<std::string> FilePaths; - uint64_t IndexLogPosition = 0; + std::vector<Oid> Keys; + std::vector<PayloadIndex> OpPayloadOffsets; + std::vector<IoHash> ChunkMapEntries; + std::vector<IoHash> MetaMapEntries; + std::vector<uint32_t> FilePathLengths; + std::vector<std::string> FilePaths; + uint64_t IndexLogPosition = 0; { IndexLogPosition = m_Storage->LogCount(); - - Keys.reserve(m_LatestOpMap.size() + m_ChunkMap.size() + m_MetaMap.size() + m_FileMap.size()); - - AddressMapEntries.reserve(m_OpAddressMap.size()); - LSNEntries.reserve(m_OpAddressMap.size()); - for (const auto& It : m_OpAddressMap) + Keys.reserve(m_OpToPayloadOffsetMap.size() + m_ChunkMap.size() + m_MetaMap.size() + m_FileMap.size()); + OpPayloadOffsets.reserve(m_OpToPayloadOffsetMap.size()); + for (const auto& Kv : m_OpToPayloadOffsetMap) { - LSNEntries.push_back(It.first); - AddressMapEntries.push_back(It.second); - } - - LatestOpMapEntries.reserve(m_LatestOpMap.size()); - for (const auto& It : m_LatestOpMap) - { - Keys.push_back(It.first); - LatestOpMapEntries.push_back(It.second); + Keys.push_back(Kv.first); + OpPayloadOffsets.push_back(Kv.second); } ChunkMapEntries.reserve(m_ChunkMap.size()); @@ -1733,14 +1728,16 @@ ProjectStore::Oplog::WriteIndexSnapshot() { BasicFileWriter IndexFile(ObjectIndexFile, 65536u); - OplogIndexHeader Header = {.LogPosition = IndexLogPosition, - .LSNCount = gsl::narrow<uint32_t>(LSNEntries.size()), - .KeyCount = gsl::narrow<uint64_t>(Keys.size()), - .OpAddressMapCount = gsl::narrow<uint32_t>(AddressMapEntries.size()), - .LatestOpMapCount = gsl::narrow<uint32_t>(LatestOpMapEntries.size()), - .ChunkMapCount = gsl::narrow<uint64_t>(ChunkMapEntries.size()), - .MetaMapCount = gsl::narrow<uint64_t>(MetaMapEntries.size()), - .FileMapCount = gsl::narrow<uint64_t>(FilePathLengths.size() / 2)}; + OplogIndexHeader Header; + Header.V2 = {.LogPosition = IndexLogPosition, + .KeyCount = gsl::narrow<uint64_t>(Keys.size()), + .OpPayloadIndexCount = gsl::narrow<uint32_t>(OpPayloadOffsets.size()), + .OpPayloadCount = gsl::narrow<uint32_t>(m_OpLogPayloads.size()), + .ChunkMapCount = gsl::narrow<uint32_t>(ChunkMapEntries.size()), + .MetaMapCount = gsl::narrow<uint32_t>(MetaMapEntries.size()), + .FileMapCount = gsl::narrow<uint32_t>(FilePathLengths.size() / 2), + .MaxLSN = m_Storage->MaxLSN(), + .NextOpCoreOffset = m_Storage->GetNextOpOffset()}; Header.Checksum = OplogIndexHeader::ComputeChecksum(Header); @@ -1748,16 +1745,13 @@ ProjectStore::Oplog::WriteIndexSnapshot() IndexFile.Write(&Header, sizeof(OplogIndexHeader), Offset); Offset = IndexFile.AlignTo(OplogIndexHeader::DataAlignment); - IndexFile.Write(LSNEntries.data(), LSNEntries.size() * sizeof(uint32_t), Offset); - Offset = IndexFile.AlignTo(OplogIndexHeader::DataAlignment); - IndexFile.Write(Keys.data(), Keys.size() * sizeof(Oid), Offset); Offset = IndexFile.AlignTo(OplogIndexHeader::DataAlignment); - IndexFile.Write(AddressMapEntries.data(), AddressMapEntries.size() * sizeof(OplogEntryAddress), Offset); + IndexFile.Write(OpPayloadOffsets.data(), OpPayloadOffsets.size() * sizeof(PayloadIndex), Offset); Offset = IndexFile.AlignTo(OplogIndexHeader::DataAlignment); - IndexFile.Write(LatestOpMapEntries.data(), LatestOpMapEntries.size() * sizeof(uint32_t), Offset); + IndexFile.Write(m_OpLogPayloads.data(), m_OpLogPayloads.size() * sizeof(OplogPayload), Offset); Offset = IndexFile.AlignTo(OplogIndexHeader::DataAlignment); IndexFile.Write(ChunkMapEntries.data(), ChunkMapEntries.size() * sizeof(IoHash), Offset); @@ -1775,6 +1769,7 @@ ProjectStore::Oplog::WriteIndexSnapshot() Offset += FilePath.length(); } } + ObjectIndexFile.Flush(); ObjectIndexFile.MoveTemporaryIntoPlace(IndexPath, Ec); if (Ec) @@ -1785,12 +1780,13 @@ ProjectStore::Oplog::WriteIndexSnapshot() IndexPath, Ec.message())); } - EntryCount = LSNEntries.size(); + EntryCount = m_OpLogPayloads.size(); m_LogFlushPosition = IndexLogPosition; + m_IsLegacySnapshot = false; } catch (const std::exception& Err) { - ZEN_WARN("oplog '{}/{}': snapshot FAILED, reason: '{}'", m_OuterProject->Identifier, m_OplogId, Err.what()); + ZEN_WARN("oplog '{}/{}': snapshot FAILED, reason: '{}'", m_OuterProjectId, m_OplogId, Err.what()); } } @@ -1806,12 +1802,12 @@ ProjectStore::Oplog::ReadIndexSnapshot() uint64_t EntryCount = 0; Stopwatch Timer; const auto _ = MakeGuard([&] { - ZEN_INFO("oplog '{}/{}': index read from '{}' containing {} entries in {}", - m_OuterProject->Identifier, - m_OplogId, - IndexPath, - EntryCount, - NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + ZEN_DEBUG("oplog '{}/{}': index read from '{}' containing {} entries in {}", + m_OuterProjectId, + m_OplogId, + IndexPath, + EntryCount, + NiceTimeSpanMs(Timer.GetElapsedTimeMs())); }); try @@ -1827,17 +1823,13 @@ ProjectStore::Oplog::ReadIndexSnapshot() Offset += sizeof(OplogIndexHeader); Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); - if ((Header.Magic == OplogIndexHeader::ExpectedMagic) && (Header.Version == OplogIndexHeader::CurrentVersion) && - (Header.Checksum == OplogIndexHeader::ComputeChecksum(Header))) + if (Header.Magic == OplogIndexHeader::ExpectedMagic) { - uint32_t MaxLSN = 0; - OplogEntryAddress LastOpAddress{.Offset = 0, .Size = 0}; - uint32_t Checksum = OplogIndexHeader::ComputeChecksum(Header); if (Header.Checksum != Checksum) { ZEN_WARN("oplog '{}/{}': skipping invalid index file '{}'. Checksum mismatch. Expected: {}, Found: {}", - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, IndexPath, Header.Checksum, @@ -1845,127 +1837,246 @@ ProjectStore::Oplog::ReadIndexSnapshot() return; } - if (Header.LatestOpMapCount + Header.ChunkMapCount + Header.MetaMapCount + Header.FileMapCount != Header.KeyCount) + if (Header.Version == OplogIndexHeader::Version1) { - ZEN_WARN("oplog '{}/{}': skipping invalid index file '{}'. Key count mismatch. Expected: {}, Found: {}", - m_OuterProject->Identifier, - m_OplogId, - IndexPath, - Header.LatestOpMapCount + Header.ChunkMapCount + Header.MetaMapCount + Header.FileMapCount, - Header.KeyCount); - return; - } + uint32_t MaxLSN = 0; + OplogEntryAddress LastOpAddress{.Offset = 0, .Size = 0}; - std::vector<uint32_t> LSNEntries(Header.LSNCount); - ObjectIndexFile.Read(LSNEntries.data(), LSNEntries.size() * sizeof(uint32_t), Offset); - Offset += LSNEntries.size() * sizeof(uint32_t); - Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); - size_t LSNOffset = 0; + if (Header.V1.LatestOpMapCount + Header.V1.ChunkMapCount + Header.V1.MetaMapCount + Header.V1.FileMapCount != + Header.V1.KeyCount) + { + ZEN_WARN("oplog '{}/{}': skipping invalid index file '{}'. Key count mismatch. Expected: {}, Found: {}", + m_OuterProjectId, + m_OplogId, + IndexPath, + Header.V1.LatestOpMapCount + Header.V1.ChunkMapCount + Header.V1.MetaMapCount + Header.V1.FileMapCount, + Header.V1.KeyCount); + return; + } - std::vector<Oid> Keys(Header.KeyCount); - ObjectIndexFile.Read(Keys.data(), Keys.size() * sizeof(Oid), Offset); - Offset += Keys.size() * sizeof(Oid); - Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); - size_t KeyOffset = 0; + std::vector<uint32_t> LSNEntries(Header.V1.LSNCount); + ObjectIndexFile.Read(LSNEntries.data(), LSNEntries.size() * sizeof(uint32_t), Offset); + Offset += LSNEntries.size() * sizeof(uint32_t); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + size_t LSNOffset = 0; + + std::vector<Oid> Keys(Header.V1.KeyCount); + ObjectIndexFile.Read(Keys.data(), Keys.size() * sizeof(Oid), Offset); + Offset += Keys.size() * sizeof(Oid); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + size_t KeyOffset = 0; - { - std::vector<OplogEntryAddress> AddressMapEntries(Header.OpAddressMapCount); - ObjectIndexFile.Read(AddressMapEntries.data(), AddressMapEntries.size() * sizeof(OplogEntryAddress), Offset); - Offset += AddressMapEntries.size() * sizeof(OplogEntryAddress); - Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); - m_OpAddressMap.reserve(AddressMapEntries.size()); - for (const OplogEntryAddress& Address : AddressMapEntries) { - m_OpAddressMap.insert_or_assign(LSNEntries[LSNOffset++], Address); - if (Address.Offset > LastOpAddress.Offset) + std::vector<OplogEntryAddress> AddressMapEntries(Header.V1.OpAddressMapCount); + ObjectIndexFile.Read(AddressMapEntries.data(), AddressMapEntries.size() * sizeof(OplogEntryAddress), Offset); + Offset += AddressMapEntries.size() * sizeof(OplogEntryAddress); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + + tsl::robin_map<uint32_t, PayloadIndex> LsnToPayloadOffset; + LsnToPayloadOffset.reserve(AddressMapEntries.size()); + + m_OpLogPayloads.reserve(AddressMapEntries.size()); + for (const OplogEntryAddress& Address : AddressMapEntries) { - LastOpAddress = Address; + const uint32_t LSN = LSNEntries[LSNOffset++]; + LsnToPayloadOffset.insert_or_assign(LSN, PayloadIndex(m_OpLogPayloads.size())); + + m_OpLogPayloads.push_back({.Lsn = LogSequenceNumber(LSN), .Address = Address}); + if (Address.Offset > LastOpAddress.Offset) + { + LastOpAddress = Address; + } + MaxLSN = Max(MaxLSN, LSN); + } + + { + std::vector<uint32_t> LatestOpMapEntries(Header.V1.LatestOpMapCount); + ObjectIndexFile.Read(LatestOpMapEntries.data(), LatestOpMapEntries.size() * sizeof(uint32_t), Offset); + Offset += LatestOpMapEntries.size() * sizeof(uint32_t); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + m_OpToPayloadOffsetMap.reserve(LatestOpMapEntries.size()); + for (uint32_t Lsn : LatestOpMapEntries) + { + if (auto It = LsnToPayloadOffset.find(Lsn); It != LsnToPayloadOffset.end()) + { + m_OpToPayloadOffsetMap.insert_or_assign(Keys[KeyOffset++], It->second); + MaxLSN = Max(MaxLSN, Lsn); + } + } } } - } - { - std::vector<uint32_t> LatestOpMapEntries(Header.LatestOpMapCount); - ObjectIndexFile.Read(LatestOpMapEntries.data(), LatestOpMapEntries.size() * sizeof(uint32_t), Offset); - Offset += LatestOpMapEntries.size() * sizeof(uint32_t); - Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); - m_LatestOpMap.reserve(LatestOpMapEntries.size()); - for (uint32_t LSN : LatestOpMapEntries) + if (m_Mode == EMode::kFull) { - m_LatestOpMap.insert_or_assign(Keys[KeyOffset++], LSN); - MaxLSN = Max(MaxLSN, LSN); + { + std::vector<IoHash> ChunkMapEntries(Header.V1.ChunkMapCount); + ObjectIndexFile.Read(ChunkMapEntries.data(), ChunkMapEntries.size() * sizeof(IoHash), Offset); + Offset += ChunkMapEntries.size() * sizeof(IoHash); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + m_ChunkMap.reserve(ChunkMapEntries.size()); + for (const IoHash& ChunkId : ChunkMapEntries) + { + m_ChunkMap.insert_or_assign(Keys[KeyOffset++], ChunkId); + } + } + { + std::vector<IoHash> MetaMapEntries(Header.V1.MetaMapCount); + ObjectIndexFile.Read(MetaMapEntries.data(), MetaMapEntries.size() * sizeof(IoHash), Offset); + Offset += MetaMapEntries.size() * sizeof(IoHash); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + m_MetaMap.reserve(MetaMapEntries.size()); + for (const IoHash& ChunkId : MetaMapEntries) + { + m_MetaMap.insert_or_assign(Keys[KeyOffset++], ChunkId); + } + } + { + m_FileMap.reserve(Header.V1.FileMapCount); + + std::vector<uint32_t> FilePathLengths(Header.V1.FileMapCount * 2); + ObjectIndexFile.Read(FilePathLengths.data(), FilePathLengths.size() * sizeof(uint32_t), Offset); + Offset += FilePathLengths.size() * sizeof(uint32_t); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + + BasicFileBuffer IndexFile(ObjectIndexFile, 65536); + auto ReadString([&IndexFile, &Offset](uint32_t Length) -> std::string { + MemoryView StringData = IndexFile.MakeView(Length, Offset); + if (StringData.GetSize() != Length) + { + throw std::runtime_error(fmt::format("Invalid format. Expected to read {} bytes but got {}", + Length, + uint32_t(StringData.GetSize()))); + } + Offset += Length; + return std::string((const char*)StringData.GetData(), Length); + }); + for (uint64_t FileLengthOffset = 0; FileLengthOffset < FilePathLengths.size();) + { + std::string ServerPath = ReadString(FilePathLengths[FileLengthOffset++]); + std::string ClientPath = ReadString(FilePathLengths[FileLengthOffset++]); + m_FileMap.insert_or_assign( + Keys[KeyOffset++], + FileMapEntry{.ServerPath = std::move(ServerPath), .ClientPath = std::move(ClientPath)}); + } + } } + m_LogFlushPosition = Header.V1.LogPosition; + m_Storage->SetMaxLSNAndNextWriteAddress(MaxLSN, LastOpAddress); + EntryCount = Header.V1.LSNCount; + m_IsLegacySnapshot = true; } + else if (Header.Version == OplogIndexHeader::Version2) { - std::vector<IoHash> ChunkMapEntries(Header.ChunkMapCount); - ObjectIndexFile.Read(ChunkMapEntries.data(), ChunkMapEntries.size() * sizeof(IoHash), Offset); - Offset += ChunkMapEntries.size() * sizeof(IoHash); + std::vector<Oid> Keys(Header.V2.KeyCount); + + ObjectIndexFile.Read(Keys.data(), Keys.size() * sizeof(Oid), Offset); + Offset += Keys.size() * sizeof(Oid); Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); - m_ChunkMap.reserve(ChunkMapEntries.size()); - for (const IoHash& ChunkId : ChunkMapEntries) - { - m_ChunkMap.insert_or_assign(Keys[KeyOffset++], ChunkId); - } - } - { - std::vector<IoHash> MetaMapEntries(Header.MetaMapCount); - ObjectIndexFile.Read(MetaMapEntries.data(), MetaMapEntries.size() * sizeof(IoHash), Offset); - Offset += MetaMapEntries.size() * sizeof(IoHash); + + std::vector<PayloadIndex> OpPayloadOffsets(Header.V2.OpPayloadIndexCount); + ObjectIndexFile.Read(OpPayloadOffsets.data(), OpPayloadOffsets.size() * sizeof(PayloadIndex), Offset); + Offset += OpPayloadOffsets.size() * sizeof(PayloadIndex); Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); - m_MetaMap.reserve(MetaMapEntries.size()); - for (const IoHash& ChunkId : MetaMapEntries) - { - m_MetaMap.insert_or_assign(Keys[KeyOffset++], ChunkId); - } - } - { - m_FileMap.reserve(Header.FileMapCount); - std::vector<uint32_t> FilePathLengths(Header.FileMapCount * 2); - ObjectIndexFile.Read(FilePathLengths.data(), FilePathLengths.size() * sizeof(uint32_t), Offset); - Offset += FilePathLengths.size() * sizeof(uint32_t); + m_OpLogPayloads.resize(Header.V2.OpPayloadCount); + ObjectIndexFile.Read(m_OpLogPayloads.data(), m_OpLogPayloads.size() * sizeof(OplogPayload), Offset); + Offset += m_OpLogPayloads.size() * sizeof(OplogPayload); Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); - BasicFileBuffer IndexFile(ObjectIndexFile, 65536); - auto ReadString([&IndexFile, &Offset](uint32_t Length) -> std::string { - MemoryView StringData = IndexFile.MakeView(Length, Offset); - if (StringData.GetSize() != Length) - { - throw std::runtime_error(fmt::format("Invalid format. Expected to read %u bytes but got %u", - Length, - uint32_t(StringData.GetSize()))); - } - Offset += Length; - return std::string((const char*)StringData.GetData(), Length); - }); - for (uint64_t FileLengthOffset = 0; FileLengthOffset < FilePathLengths.size();) + size_t KeyOffset = 0; + + m_OpToPayloadOffsetMap.reserve(Header.V2.OpPayloadIndexCount); + for (const PayloadIndex PayloadOffset : OpPayloadOffsets) { - std::string ServerPath = ReadString(FilePathLengths[FileLengthOffset++]); - std::string ClientPath = ReadString(FilePathLengths[FileLengthOffset++]); - m_FileMap.insert_or_assign( - Keys[KeyOffset++], - FileMapEntry{.ServerPath = std::move(ServerPath), .ClientPath = std::move(ClientPath)}); + const Oid& Key = Keys[KeyOffset++]; + ZEN_ASSERT_SLOW(PayloadOffset.Index < Header.V2.OpPayloadCount); + m_OpToPayloadOffsetMap.insert({Key, PayloadOffset}); + } + + if (m_Mode == EMode::kFull) + { + { + std::vector<IoHash> ChunkMapEntries(Header.V2.ChunkMapCount); + ObjectIndexFile.Read(ChunkMapEntries.data(), ChunkMapEntries.size() * sizeof(IoHash), Offset); + Offset += ChunkMapEntries.size() * sizeof(IoHash); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + m_ChunkMap.reserve(ChunkMapEntries.size()); + for (const IoHash& ChunkId : ChunkMapEntries) + { + m_ChunkMap.insert_or_assign(Keys[KeyOffset++], ChunkId); + } + } + { + std::vector<IoHash> MetaMapEntries(Header.V2.MetaMapCount); + ObjectIndexFile.Read(MetaMapEntries.data(), MetaMapEntries.size() * sizeof(IoHash), Offset); + Offset += MetaMapEntries.size() * sizeof(IoHash); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + m_MetaMap.reserve(MetaMapEntries.size()); + for (const IoHash& ChunkId : MetaMapEntries) + { + m_MetaMap.insert_or_assign(Keys[KeyOffset++], ChunkId); + } + } + { + m_FileMap.reserve(Header.V2.FileMapCount); + + std::vector<uint32_t> FilePathLengths(Header.V2.FileMapCount * 2); + ObjectIndexFile.Read(FilePathLengths.data(), FilePathLengths.size() * sizeof(uint32_t), Offset); + Offset += FilePathLengths.size() * sizeof(uint32_t); + Offset = RoundUp(Offset, OplogIndexHeader::DataAlignment); + + BasicFileBuffer IndexFile(ObjectIndexFile, 65536); + auto ReadString([&IndexFile, &Offset](uint32_t Length) -> std::string { + MemoryView StringData = IndexFile.MakeView(Length, Offset); + if (StringData.GetSize() != Length) + { + throw std::runtime_error(fmt::format("Invalid format. Expected to read {} bytes but got {}", + Length, + uint32_t(StringData.GetSize()))); + } + Offset += Length; + return std::string((const char*)StringData.GetData(), Length); + }); + for (uint64_t FileLengthOffset = 0; FileLengthOffset < FilePathLengths.size();) + { + std::string ServerPath = ReadString(FilePathLengths[FileLengthOffset++]); + std::string ClientPath = ReadString(FilePathLengths[FileLengthOffset++]); + m_FileMap.insert_or_assign( + Keys[KeyOffset++], + FileMapEntry{.ServerPath = std::move(ServerPath), .ClientPath = std::move(ClientPath)}); + } + } } + m_LogFlushPosition = Header.V2.LogPosition; + m_Storage->SetMaxLSNAndNextOpOffset(Header.V2.MaxLSN, Header.V2.NextOpCoreOffset); + EntryCount = Header.V2.OpPayloadCount; + m_IsLegacySnapshot = false; + } + else + { + ZEN_WARN("oplog '{}/{}': skipping invalid index file '{}'. Unsupported version number {}", + m_OuterProjectId, + m_OplogId, + IndexPath, + Header.Version); + return; } - m_LogFlushPosition = Header.LogPosition; - m_Storage->SetMaxLSNAndNextWriteAddress(MaxLSN, LastOpAddress); - EntryCount = Header.LSNCount; } else { - ZEN_WARN("oplog '{}/{}': skipping invalid index file '{}'", m_OuterProject->Identifier, m_OplogId, IndexPath); + ZEN_WARN("oplog '{}/{}': skipping invalid index file '{}'", m_OuterProjectId, m_OplogId, IndexPath); } } } catch (const std::exception& Ex) { - m_OpAddressMap.clear(); - m_LatestOpMap.clear(); + m_OpToPayloadOffsetMap.clear(); + m_OpLogPayloads.clear(); m_ChunkMap.clear(); m_MetaMap.clear(); m_FileMap.clear(); m_LogFlushPosition = 0; ZEN_ERROR("oplog '{}/{}': failed reading index snapshot from '{}'. Reason: '{}'", - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, IndexPath, Ex.what()); @@ -1991,19 +2102,7 @@ ProjectStore::Oplog::GetUnusedSpacePercentLocked() const return 0; } - std::vector<OplogEntryAddress> Addresses; - { - Addresses.reserve(m_LatestOpMap.size()); - for (auto It : m_LatestOpMap) - { - if (auto AddressIt = m_OpAddressMap.find(It.second); AddressIt != m_OpAddressMap.end()) - { - Addresses.push_back(AddressIt->second); - } - } - } - - const uint64_t EffectiveBlobsSize = m_Storage->GetEffectiveBlobsSize(std::move(Addresses)); + const uint64_t EffectiveBlobsSize = m_Storage->GetEffectiveBlobsSize(m_OpLogPayloads); if (EffectiveBlobsSize < ActualBlobsSize) { @@ -2023,35 +2122,42 @@ ProjectStore::Oplog::Compact(bool DryRun, bool RetainLSNs, std::string_view LogP void ProjectStore::Oplog::Compact(RwLock::ExclusiveLockScope&, bool DryRun, bool RetainLSNs, std::string_view LogPrefix) { + ZEN_ASSERT(m_Mode == EMode::kFull); + ZEN_MEMSCOPE(GetProjectstoreTag()); Stopwatch Timer; - std::vector<uint32_t> LSNs; - LSNs.reserve(m_LatestOpMap.size()); - for (auto It : m_LatestOpMap) + std::vector<LogSequenceNumber> LatestLSNs; + LatestLSNs.reserve(m_OpLogPayloads.size()); + for (const auto& Kv : m_OpToPayloadOffsetMap) { - LSNs.push_back(It.second); + const OplogPayload& OpPayload = m_OpLogPayloads[Kv.second]; + LatestLSNs.push_back(OpPayload.Lsn); } - tsl::robin_map<uint32_t, OplogEntryAddress> OpAddressMap; // Index LSN -> op data in ops blob file - OidMap<uint32_t> LatestOpMap; // op key -> latest op LSN for key + std::vector<OplogPayload> OpPayloads; + OpPayloads.reserve(LatestLSNs.size()); + + OidMap<PayloadIndex> OpToPayloadOffsetMap; + OpToPayloadOffsetMap.reserve(LatestLSNs.size()); uint64_t PreSize = TotalSize(); m_Storage->Compact( - LSNs, - [&](const Oid& OpKeyHash, uint32_t, uint32_t NewLSN, const OplogEntryAddress& NewAddress) { - LatestOpMap.insert_or_assign(OpKeyHash, NewLSN); - OpAddressMap.insert_or_assign(NewLSN, NewAddress); + LatestLSNs, + [&](const Oid& OpKeyHash, LogSequenceNumber, LogSequenceNumber NewLsn, const OplogEntryAddress& NewAddress) { + OpToPayloadOffsetMap.insert({OpKeyHash, PayloadIndex(OpPayloads.size())}); + OpPayloads.push_back({.Lsn = NewLsn, .Address = NewAddress}); }, RetainLSNs, /*DryRun*/ DryRun); if (!DryRun) { - m_OpAddressMap.swap(OpAddressMap); - m_LatestOpMap.swap(LatestOpMap); + m_OpToPayloadOffsetMap.swap(OpToPayloadOffsetMap); + m_OpLogPayloads.swap(OpPayloads); + m_LsnToPayloadOffsetMap.reset(); WriteIndexSnapshot(); } @@ -2060,7 +2166,7 @@ ProjectStore::Oplog::Compact(RwLock::ExclusiveLockScope&, bool DryRun, bool Reta ZEN_INFO("{} oplog '{}/{}': Compacted in {}. New size: {}, freeing: {}", LogPrefix, - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, NiceTimeSpanMs(Timer.GetElapsedTimeMs()), NiceBytes(PostSize), @@ -2070,6 +2176,7 @@ ProjectStore::Oplog::Compact(RwLock::ExclusiveLockScope&, bool DryRun, bool Reta IoBuffer ProjectStore::Oplog::GetChunkByRawHash(const IoHash& RawHash) { + ZEN_ASSERT(m_Mode == EMode::kFull); return m_CidStore.FindChunkByCid(RawHash); } @@ -2080,6 +2187,7 @@ ProjectStore::Oplog::IterateChunks(std::span<IoHash> RawHashes, WorkerThreadPool* OptionalWorkerPool, uint64_t LargeSizeLimit) { + ZEN_ASSERT(m_Mode == EMode::kFull); return m_CidStore.IterateChunks( RawHashes, [&](size_t Index, const IoBuffer& Payload) { @@ -2090,12 +2198,14 @@ ProjectStore::Oplog::IterateChunks(std::span<IoHash> RawHashes, } bool -ProjectStore::Oplog::IterateChunks(std::span<Oid> ChunkIds, +ProjectStore::Oplog::IterateChunks(const std::filesystem::path& ProjectRootDir, + std::span<Oid> ChunkIds, bool IncludeModTag, const std::function<bool(size_t Index, const IoBuffer& Payload, uint64_t ModTag)>& AsyncCallback, WorkerThreadPool* OptionalWorkerPool, uint64_t LargeSizeLimit) { + ZEN_ASSERT(m_Mode == EMode::kFull); ZEN_MEMSCOPE(GetProjectstoreTag()); std::vector<size_t> CidChunkIndexes; @@ -2120,7 +2230,7 @@ ProjectStore::Oplog::IterateChunks(std::span<Oid> ChunkIds, else if (auto FileIt = m_FileMap.find(ChunkId); FileIt != m_FileMap.end()) { FileChunkIndexes.push_back(ChunkIndex); - FileChunkPaths.emplace_back(m_OuterProject->RootDir / FileIt->second.ServerPath); + FileChunkPaths.emplace_back(ProjectRootDir / FileIt->second.ServerPath); } } } @@ -2128,7 +2238,7 @@ ProjectStore::Oplog::IterateChunks(std::span<Oid> ChunkIds, { std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::DisableBacklog); try { for (size_t ChunkIndex = 0; ChunkIndex < FileChunkIndexes.size(); ChunkIndex++) @@ -2165,7 +2275,7 @@ ProjectStore::Oplog::IterateChunks(std::span<Oid> ChunkIds, catch (const std::exception& Ex) { ZEN_WARN("oplog '{}/{}': exception caught when iterating file chunk {}, path '{}'. Reason: '{}'", - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, FileChunkIndex, FilePath, @@ -2235,8 +2345,10 @@ ProjectStore::Oplog::IterateChunks(std::span<Oid> ChunkIds, } IoBuffer -ProjectStore::Oplog::FindChunk(const Oid& ChunkId, uint64_t* OptOutModificationTag) +ProjectStore::Oplog::FindChunk(const std::filesystem::path& ProjectRootDir, const Oid& ChunkId, uint64_t* OptOutModificationTag) { + ZEN_ASSERT(m_Mode == EMode::kFull); + RwLock::SharedLockScope OplogLock(m_OplogLock); if (!m_Storage) { @@ -2258,7 +2370,7 @@ ProjectStore::Oplog::FindChunk(const Oid& ChunkId, uint64_t* OptOutModificationT if (auto FileIt = m_FileMap.find(ChunkId); FileIt != m_FileMap.end()) { - std::filesystem::path FilePath = m_OuterProject->RootDir / FileIt->second.ServerPath; + std::filesystem::path FilePath = ProjectRootDir / FileIt->second.ServerPath; OplogLock.ReleaseNow(); @@ -2291,8 +2403,10 @@ ProjectStore::Oplog::FindChunk(const Oid& ChunkId, uint64_t* OptOutModificationT } std::vector<ProjectStore::Oplog::ChunkInfo> -ProjectStore::Oplog::GetAllChunksInfo() +ProjectStore::Oplog::GetAllChunksInfo(const std::filesystem::path& ProjectRootDir) { + ZEN_ASSERT(m_Mode == EMode::kFull); + ZEN_MEMSCOPE(GetProjectstoreTag()); // First just capture all the chunk ids @@ -2322,7 +2436,7 @@ ProjectStore::Oplog::GetAllChunksInfo() for (ChunkInfo& Info : InfoArray) { - if (IoBuffer Chunk = FindChunk(Info.ChunkId, nullptr)) + if (IoBuffer Chunk = FindChunk(ProjectRootDir, Info.ChunkId, nullptr)) { Info.ChunkSize = Chunk.GetSize(); } @@ -2334,6 +2448,8 @@ ProjectStore::Oplog::GetAllChunksInfo() void ProjectStore::Oplog::IterateChunkMap(std::function<void(const Oid&, const IoHash&)>&& Fn) { + ZEN_ASSERT(m_Mode == EMode::kFull); + RwLock::SharedLockScope _(m_OplogLock); if (!m_Storage) { @@ -2350,6 +2466,8 @@ void ProjectStore::Oplog::IterateFileMap( std::function<void(const Oid&, const std::string_view& ServerPath, const std::string_view& ClientPath)>&& Fn) { + ZEN_ASSERT(m_Mode == EMode::kFull); + RwLock::SharedLockScope _(m_OplogLock); if (!m_Storage) { @@ -2369,15 +2487,44 @@ ProjectStore::Oplog::IterateOplog(std::function<void(CbObjectView)>&& Handler, c IterateOplogLocked(std::move(Handler), EntryPaging); } -template<typename ContainerElement> -std::span<ContainerElement> -CreateSpanFromPaging(std::vector<ContainerElement>& Container, const ProjectStore::Oplog::Paging& Paging) +std::vector<ProjectStore::Oplog::PayloadIndex> +ProjectStore::Oplog::GetSortedOpPayloadRangeLocked(const Paging& EntryPaging, + tsl::robin_map<PayloadIndex, Oid, PayloadIndex::Hasher>* OutOptionalReverseKeyMap) { - std::span<ContainerElement> Span(Container); - int32_t Size = int32_t(Container.size()); - int32_t Start = std::clamp(Paging.Start, 0, Size); - int32_t End = Paging.Count < 0 ? Size : (Start + std::min<int32_t>(Paging.Count, Size - Start)); - return Span.subspan(Start, End - Start); + std::pair<int32_t, int32_t> StartAndEnd = GetPagedRange(int32_t(m_OpToPayloadOffsetMap.size()), EntryPaging); + if (StartAndEnd.first == StartAndEnd.second) + { + return {}; + } + + const int32_t ReplayCount = StartAndEnd.second - StartAndEnd.first; + + auto Start = m_OpToPayloadOffsetMap.cbegin(); + std::advance(Start, StartAndEnd.first); + + auto End = Start; + std::advance(End, ReplayCount); + + std::vector<PayloadIndex> ReplayOrder; + ReplayOrder.reserve(ReplayCount); + + for (auto It = Start; It != End; It++) + { + const PayloadIndex PayloadOffset = It->second; + if (OutOptionalReverseKeyMap != nullptr) + { + OutOptionalReverseKeyMap->insert_or_assign(PayloadOffset, It->first); + } + ReplayOrder.push_back(PayloadOffset); + } + + std::sort(ReplayOrder.begin(), ReplayOrder.end(), [this](const PayloadIndex Lhs, const PayloadIndex Rhs) { + const OplogEntryAddress& LhsEntry = m_OpLogPayloads[Lhs].Address; + const OplogEntryAddress& RhsEntry = m_OpLogPayloads[Rhs].Address; + return LhsEntry.Offset < RhsEntry.Offset; + }); + + return ReplayOrder; } void @@ -2390,23 +2537,44 @@ ProjectStore::Oplog::IterateOplogLocked(std::function<void(CbObjectView)>&& Hand return; } - std::vector<OplogEntryAddress> Entries; - Entries.reserve(m_LatestOpMap.size()); - - for (const auto& Kv : m_LatestOpMap) + std::vector<PayloadIndex> ReplayOrder = GetSortedOpPayloadRangeLocked(EntryPaging, nullptr); + if (!ReplayOrder.empty()) { - const auto AddressEntry = m_OpAddressMap.find(Kv.second); - ZEN_ASSERT(AddressEntry != m_OpAddressMap.end()); - - Entries.push_back(AddressEntry->second); + m_Storage->ReplayLogEntries(m_OpLogPayloads, ReplayOrder, [&](LogSequenceNumber, CbObjectView Op) { Handler(Op); }); } +} - std::sort(Entries.begin(), Entries.end(), [](const OplogEntryAddress& Lhs, const OplogEntryAddress& Rhs) { - return Lhs.Offset < Rhs.Offset; - }); +void +ProjectStore::Oplog::IterateOplogWithKey(std::function<void(LogSequenceNumber, const Oid&, CbObjectView)>&& Handler) +{ + IterateOplogWithKey(std::move(Handler), Paging{}); +} + +void +ProjectStore::Oplog::IterateOplogWithKey(std::function<void(LogSequenceNumber, const Oid&, CbObjectView)>&& Handler, + const Paging& EntryPaging) +{ + ZEN_MEMSCOPE(GetProjectstoreTag()); + + tsl::robin_map<PayloadIndex, Oid, PayloadIndex::Hasher> ReverseKeyMap; + std::vector<PayloadIndex> ReplayOrder; - std::span<OplogEntryAddress> EntrySpan = CreateSpanFromPaging(Entries, EntryPaging); - m_Storage->ReplayLogEntries(EntrySpan, [&](CbObjectView Op) { Handler(Op); }); + { + RwLock::SharedLockScope _(m_OplogLock); + if (m_Storage) + { + ReplayOrder = GetSortedOpPayloadRangeLocked(EntryPaging, &ReverseKeyMap); + if (!ReplayOrder.empty()) + { + uint32_t EntryIndex = 0; + m_Storage->ReplayLogEntries(m_OpLogPayloads, ReplayOrder, [&](LogSequenceNumber Lsn, CbObjectView Op) { + const PayloadIndex PayloadOffset = ReplayOrder[EntryIndex]; + Handler(Lsn, ReverseKeyMap.at(PayloadOffset), Op); + EntryIndex++; + }); + } + } + } } static constexpr uint32_t OplogMetaDataExpectedMagic = 0x6f'74'6d'62; // 'omta'; @@ -2476,7 +2644,7 @@ ProjectStore::Oplog::GetAttachmentsLocked(std::vector<IoHash>& OutAttachments, b { m_MetaValid = false; ZEN_WARN("oplog '{}/{}': unable to set meta data meta path: '{}'. Reason: '{}'", - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, MetaPath, Ec.message()); @@ -2496,80 +2664,10 @@ ProjectStore::Oplog::GetOplogEntryCount() const { return 0; } - return m_LatestOpMap.size(); + return m_OpToPayloadOffsetMap.size(); } -void -ProjectStore::Oplog::IterateOplogWithKey(std::function<void(uint32_t, const Oid&, CbObjectView)>&& Handler) -{ - IterateOplogWithKey(std::move(Handler), Paging{}); -} - -void -ProjectStore::Oplog::IterateOplogWithKey(std::function<void(uint32_t, const Oid&, CbObjectView)>&& Handler, const Paging& EntryPaging) -{ - ZEN_MEMSCOPE(GetProjectstoreTag()); - RwLock::SharedLockScope _(m_OplogLock); - if (!m_Storage) - { - return; - } - - std::vector<OplogEntryAddress> SortedEntries; - std::vector<Oid> SortedKeys; - std::vector<uint32_t> SortedLSNs; - - { - const auto TargetEntryCount = m_LatestOpMap.size(); - - std::vector<size_t> EntryIndexes; - std::vector<OplogEntryAddress> Entries; - std::vector<Oid> Keys; - std::vector<uint32_t> LSNs; - - Entries.reserve(TargetEntryCount); - EntryIndexes.reserve(TargetEntryCount); - Keys.reserve(TargetEntryCount); - LSNs.reserve(TargetEntryCount); - - for (const auto& Kv : m_LatestOpMap) - { - const auto AddressEntry = m_OpAddressMap.find(Kv.second); - ZEN_ASSERT(AddressEntry != m_OpAddressMap.end()); - - Entries.push_back(AddressEntry->second); - Keys.push_back(Kv.first); - LSNs.push_back(Kv.second); - EntryIndexes.push_back(EntryIndexes.size()); - } - - std::sort(EntryIndexes.begin(), EntryIndexes.end(), [&Entries](const size_t& Lhs, const size_t& Rhs) { - const OplogEntryAddress& LhsEntry = Entries[Lhs]; - const OplogEntryAddress& RhsEntry = Entries[Rhs]; - return LhsEntry.Offset < RhsEntry.Offset; - }); - - SortedEntries.reserve(EntryIndexes.size()); - SortedKeys.reserve(EntryIndexes.size()); - SortedLSNs.reserve(EntryIndexes.size()); - - for (size_t Index : EntryIndexes) - { - SortedEntries.push_back(Entries[Index]); - SortedKeys.push_back(Keys[Index]); - SortedLSNs.push_back(LSNs[Index]); - } - } - - std::span<OplogEntryAddress> EntrySpan = CreateSpanFromPaging(SortedEntries, EntryPaging); - size_t EntryIndex = EntrySpan.empty() ? 0 : static_cast<size_t>(&EntrySpan.front() - &SortedEntries.front()); - m_Storage->ReplayLogEntries(EntrySpan, [&](CbObjectView Op) { - Handler(SortedLSNs[EntryIndex], SortedKeys[EntryIndex], Op); - EntryIndex++; - }); -} - -std::optional<uint32_t> +ProjectStore::LogSequenceNumber ProjectStore::Oplog::GetOpIndexByKey(const Oid& Key) { RwLock::SharedLockScope _(m_OplogLock); @@ -2577,9 +2675,9 @@ ProjectStore::Oplog::GetOpIndexByKey(const Oid& Key) { return {}; } - if (const auto LatestOp = m_LatestOpMap.find(Key); LatestOp != m_LatestOpMap.end()) + if (const auto LatestOp = m_OpToPayloadOffsetMap.find(Key); LatestOp != m_OpToPayloadOffsetMap.end()) { - return LatestOp->second; + return m_OpLogPayloads[LatestOp->second].Lsn; } return {}; } @@ -2593,42 +2691,45 @@ ProjectStore::Oplog::GetOpByKey(const Oid& Key) return {}; } - if (const auto LatestOp = m_LatestOpMap.find(Key); LatestOp != m_LatestOpMap.end()) + if (const auto LatestOp = m_OpToPayloadOffsetMap.find(Key); LatestOp != m_OpToPayloadOffsetMap.end()) { - const auto AddressEntry = m_OpAddressMap.find(LatestOp->second); - ZEN_ASSERT(AddressEntry != m_OpAddressMap.end()); - - return m_Storage->GetOp(AddressEntry->second); + return m_Storage->GetOp(m_OpLogPayloads[LatestOp->second].Address); } return {}; } std::optional<CbObject> -ProjectStore::Oplog::GetOpByIndex(uint32_t Index) +ProjectStore::Oplog::GetOpByIndex(ProjectStore::LogSequenceNumber Index) { - RwLock::SharedLockScope _(m_OplogLock); - if (!m_Storage) { - return {}; + RwLock::SharedLockScope _(m_OplogLock); + if (!m_Storage) + { + return {}; + } + + if (m_LsnToPayloadOffsetMap) + { + if (auto It = m_LsnToPayloadOffsetMap->find(Index); It != m_LsnToPayloadOffsetMap->end()) + { + return m_Storage->GetOp(m_OpLogPayloads[It->second].Address); + } + } } - if (const auto AddressEntryIt = m_OpAddressMap.find(Index); AddressEntryIt != m_OpAddressMap.end()) + RwLock::ExclusiveLockScope Lock(m_OplogLock); + if (!m_Storage) { - return m_Storage->GetOp(AddressEntryIt->second); + return {}; } - return {}; -} - -void -ProjectStore::Oplog::AddChunkMappings(const std::unordered_map<Oid, IoHash, Oid::Hasher>& ChunkMappings) -{ - RwLock::ExclusiveLockScope OplogLock(m_OplogLock); - for (const auto& It : ChunkMappings) + RefreshLsnToPayloadOffsetMap(Lock); + if (auto It = m_LsnToPayloadOffsetMap->find(Index); It != m_LsnToPayloadOffsetMap->end()) { - AddChunkMapping(OplogLock, It.first, It.second); + return m_Storage->GetOp(m_OpLogPayloads[It->second].Address); } + return {}; } void @@ -2653,14 +2754,14 @@ ProjectStore::Oplog::EnableUpdateCapture() m_OplogLock.WithExclusiveLock([&]() { if (m_UpdateCaptureRefCounter == 0) { - ZEN_ASSERT(!m_CapturedLSNs); + ZEN_ASSERT(!m_CapturedOps); ZEN_ASSERT(!m_CapturedAttachments); - m_CapturedLSNs = std::make_unique<std::vector<uint32_t>>(); + m_CapturedOps = std::make_unique<std::vector<Oid>>(); m_CapturedAttachments = std::make_unique<std::vector<IoHash>>(); } else { - ZEN_ASSERT(m_CapturedLSNs); + ZEN_ASSERT(m_CapturedOps); ZEN_ASSERT(m_CapturedAttachments); } m_UpdateCaptureRefCounter++; @@ -2673,34 +2774,36 @@ ProjectStore::Oplog::DisableUpdateCapture() ZEN_MEMSCOPE(GetProjectstoreTag()); m_OplogLock.WithExclusiveLock([&]() { - ZEN_ASSERT(m_CapturedLSNs); + ZEN_ASSERT(m_CapturedOps); ZEN_ASSERT(m_CapturedAttachments); ZEN_ASSERT(m_UpdateCaptureRefCounter > 0); m_UpdateCaptureRefCounter--; if (m_UpdateCaptureRefCounter == 0) { - m_CapturedLSNs.reset(); + m_CapturedOps.reset(); m_CapturedAttachments.reset(); } }); } void -ProjectStore::Oplog::IterateCapturedLSNsLocked(std::function<bool(const CbObjectView& UpdateOp)>&& Callback) +ProjectStore::Oplog::IterateCapturedOpsLocked( + std::function<bool(const Oid& Key, ProjectStore::LogSequenceNumber LSN, const CbObjectView& UpdateOp)>&& Callback) { ZEN_MEMSCOPE(GetProjectstoreTag()); - if (m_CapturedLSNs) + if (m_CapturedOps) { if (!m_Storage) { return; } - for (uint32_t UpdatedLSN : *m_CapturedLSNs) + for (const Oid& OpKey : *m_CapturedOps) { - if (const auto AddressEntryIt = m_OpAddressMap.find(UpdatedLSN); AddressEntryIt != m_OpAddressMap.end()) + if (const auto AddressEntryIt = m_OpToPayloadOffsetMap.find(OpKey); AddressEntryIt != m_OpToPayloadOffsetMap.end()) { - Callback(m_Storage->GetOp(AddressEntryIt->second)); + const OplogPayload& OpPayload = m_OpLogPayloads[AddressEntryIt->second]; + Callback(OpKey, OpPayload.Lsn, m_Storage->GetOp(OpPayload.Address)); } } } @@ -2840,7 +2943,7 @@ ProjectStore::Oplog::GetMapping(CbObjectView Core) Oid Id = PackageObj["id"sv].AsObjectId(); IoHash Hash = PackageObj["data"sv].AsBinaryAttachment(); Result.Chunks.emplace_back(ChunkMapping{Id, Hash}); - ZEN_DEBUG("oplog {}/{}: package data {} -> {}", m_OuterProject->Identifier, m_OplogId, Id, Hash); + ZEN_DEBUG("oplog {}/{}: package data {} -> {}", m_OuterProjectId, m_OplogId, Id, Hash); continue; } if (FieldName == "bulkdata"sv) @@ -2852,7 +2955,7 @@ ProjectStore::Oplog::GetMapping(CbObjectView Core) Oid Id = BulkObj["id"sv].AsObjectId(); IoHash Hash = BulkObj["data"sv].AsBinaryAttachment(); Result.Chunks.emplace_back(ChunkMapping{Id, Hash}); - ZEN_DEBUG("oplog {}/{}: bulkdata {} -> {}", m_OuterProject->Identifier, m_OplogId, Id, Hash); + ZEN_DEBUG("oplog {}/{}: bulkdata {} -> {}", m_OuterProjectId, m_OplogId, Id, Hash); } continue; } @@ -2865,7 +2968,7 @@ ProjectStore::Oplog::GetMapping(CbObjectView Core) Oid Id = PackageDataObj["id"sv].AsObjectId(); IoHash Hash = PackageDataObj["data"sv].AsBinaryAttachment(); Result.Chunks.emplace_back(ChunkMapping{Id, Hash}); - ZEN_DEBUG("oplog {}/{}: package {} -> {}", m_OuterProject->Identifier, m_OplogId, Id, Hash); + ZEN_DEBUG("oplog {}/{}: package {} -> {}", m_OuterProjectId, m_OplogId, Id, Hash); } continue; } @@ -2884,23 +2987,20 @@ ProjectStore::Oplog::GetMapping(CbObjectView Core) if (ServerPath.empty() && Hash == IoHash::Zero) { ZEN_WARN("oplog {}/{}: invalid file for entry '{}', missing both 'serverpath' and 'data' fields", - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, Id); continue; } if (ClientPath.empty()) { - ZEN_WARN("oplog {}/{}: invalid file for entry '{}', missing 'clientpath' field", - m_OuterProject->Identifier, - m_OplogId, - Id); + ZEN_WARN("oplog {}/{}: invalid file for entry '{}', missing 'clientpath' field", m_OuterProjectId, m_OplogId, Id); continue; } Result.Files.emplace_back(FileMapping{Id, Hash, std::string(ServerPath), std::string(ClientPath)}); ZEN_DEBUG("oplog {}/{}: file {} -> {}, ServerPath: {}, ClientPath: {}", - m_OuterProject->Identifier, + m_OuterProjectId, m_OplogId, Id, Hash, @@ -2920,7 +3020,7 @@ ProjectStore::Oplog::GetMapping(CbObjectView Core) IoHash Hash = MetaObj["data"sv].AsBinaryAttachment(); Result.Meta.emplace_back(ChunkMapping{Id, Hash}); auto NameString = MetaObj["name"sv].AsString(); - ZEN_DEBUG("oplog {}/{}: meta data ({}) {} -> {}", m_OuterProject->Identifier, m_OplogId, NameString, Id, Hash); + ZEN_DEBUG("oplog {}/{}: meta data ({}) {} -> {}", m_OuterProjectId, m_OplogId, NameString, Id, Hash); } continue; } @@ -2929,7 +3029,7 @@ ProjectStore::Oplog::GetMapping(CbObjectView Core) return Result; } -uint32_t +ProjectStore::LogSequenceNumber ProjectStore::Oplog::RegisterOplogEntry(RwLock::ExclusiveLockScope& OplogLock, const OplogEntryMapping& OpMapping, const OplogEntry& OpEntry) @@ -2956,20 +3056,28 @@ ProjectStore::Oplog::RegisterOplogEntry(RwLock::ExclusiveLockScope& OplogLock, AddMetaMapping(OplogLock, Meta.Id, Meta.Hash); } - m_OpAddressMap.emplace(OpEntry.OpLsn, OplogEntryAddress{.Offset = OpEntry.OpCoreOffset, .Size = OpEntry.OpCoreSize}); - m_LatestOpMap[OpEntry.OpKeyHash] = OpEntry.OpLsn; + const PayloadIndex PayloadOffset(m_OpLogPayloads.size()); + m_OpToPayloadOffsetMap.insert_or_assign(OpEntry.OpKeyHash, PayloadOffset); + m_OpLogPayloads.push_back({.Lsn = OpEntry.OpLsn, .Address = OpEntry.OpCoreAddress}); + + if (m_LsnToPayloadOffsetMap) + { + m_LsnToPayloadOffsetMap->insert_or_assign(OpEntry.OpLsn, PayloadOffset); + } return OpEntry.OpLsn; } -uint32_t +ProjectStore::LogSequenceNumber ProjectStore::Oplog::AppendNewOplogEntry(CbPackage OpPackage) { + ZEN_ASSERT(m_Mode == EMode::kFull); + ZEN_MEMSCOPE(GetProjectstoreTag()); ZEN_TRACE_CPU("Store::Oplog::AppendNewOplogEntry"); - const CbObject& Core = OpPackage.GetObject(); - const uint32_t EntryId = AppendNewOplogEntry(Core); - if (EntryId == 0xffffffffu) + const CbObject& Core = OpPackage.GetObject(); + const ProjectStore::LogSequenceNumber EntryId = AppendNewOplogEntry(Core); + if (!EntryId) { // The oplog has been deleted so just drop this return EntryId; @@ -3014,7 +3122,7 @@ ProjectStore::Oplog::AppendNewOplogEntry(CbPackage OpPackage) } } - ZEN_DEBUG("oplog entry #{} attachments: {} new, {} total", EntryId, NiceBytes(NewAttachmentBytes), NiceBytes(AttachmentBytes)); + ZEN_DEBUG("oplog entry #{} attachments: {} new, {} total", EntryId.Number, NiceBytes(NewAttachmentBytes), NiceBytes(AttachmentBytes)); return EntryId; } @@ -3031,9 +3139,11 @@ ProjectStore::Oplog::GetStorage() return Storage; } -uint32_t +ProjectStore::LogSequenceNumber ProjectStore::Oplog::AppendNewOplogEntry(CbObjectView Core) { + ZEN_ASSERT(m_Mode == EMode::kFull); + ZEN_MEMSCOPE(GetProjectstoreTag()); ZEN_TRACE_CPU("Store::Oplog::AppendNewOplogEntry"); @@ -3042,7 +3152,7 @@ ProjectStore::Oplog::AppendNewOplogEntry(CbObjectView Core) RefPtr<OplogStorage> Storage = GetStorage(); if (!Storage) { - return 0xffffffffu; + return {}; } OplogEntryMapping Mapping = GetMapping(Core); @@ -3050,20 +3160,22 @@ ProjectStore::Oplog::AppendNewOplogEntry(CbObjectView Core) const OplogEntry OpEntry = Storage->AppendOp(OpData); - RwLock::ExclusiveLockScope OplogLock(m_OplogLock); - const uint32_t EntryId = RegisterOplogEntry(OplogLock, Mapping, OpEntry); - if (m_CapturedLSNs) + RwLock::ExclusiveLockScope OplogLock(m_OplogLock); + const ProjectStore::LogSequenceNumber EntryId = RegisterOplogEntry(OplogLock, Mapping, OpEntry); + if (m_CapturedOps) { - m_CapturedLSNs->push_back(EntryId); + m_CapturedOps->push_back(OpData.KeyHash); } m_MetaValid = false; return EntryId; } -std::vector<uint32_t> +std::vector<ProjectStore::LogSequenceNumber> ProjectStore::Oplog::AppendNewOplogEntries(std::span<CbObjectView> Cores) { + ZEN_ASSERT(m_Mode == EMode::kFull); + ZEN_MEMSCOPE(GetProjectstoreTag()); ZEN_TRACE_CPU("Store::Oplog::AppendNewOplogEntries"); @@ -3072,7 +3184,7 @@ ProjectStore::Oplog::AppendNewOplogEntries(std::span<CbObjectView> Cores) RefPtr<OplogStorage> Storage = GetStorage(); if (!Storage) { - return std::vector<uint32_t>(Cores.size(), 0xffffffffu); + return std::vector<ProjectStore::LogSequenceNumber>(Cores.size(), LogSequenceNumber{}); } size_t OpCount = Cores.size(); @@ -3090,21 +3202,21 @@ ProjectStore::Oplog::AppendNewOplogEntries(std::span<CbObjectView> Cores) std::vector<OplogEntry> OpEntries = Storage->AppendOps(OpDatas); - std::vector<uint32_t> EntryIds; + std::vector<ProjectStore::LogSequenceNumber> EntryIds; EntryIds.resize(OpCount); { { RwLock::ExclusiveLockScope OplogLock(m_OplogLock); - if (m_CapturedLSNs) + if (m_CapturedOps) { - m_CapturedLSNs->reserve(m_CapturedLSNs->size() + OpCount); + m_CapturedOps->reserve(m_CapturedOps->size() + OpCount); } for (size_t OpIndex = 0; OpIndex < OpCount; OpIndex++) { EntryIds[OpIndex] = RegisterOplogEntry(OplogLock, Mappings[OpIndex], OpEntries[OpIndex]); - if (m_CapturedLSNs) + if (m_CapturedOps) { - m_CapturedLSNs->push_back(EntryIds[OpIndex]); + m_CapturedOps->push_back(OpDatas[OpIndex].KeyHash); } } } @@ -3321,7 +3433,7 @@ ProjectStore::Project::BasePathForOplog(std::string_view OplogId) const return m_OplogStoragePath / OplogId; } -ProjectStore::Oplog* +Ref<ProjectStore::Oplog> ProjectStore::Project::NewOplog(std::string_view OplogId, const std::filesystem::path& MarkerPath) { ZEN_MEMSCOPE(GetProjectstoreTag()); @@ -3331,21 +3443,25 @@ ProjectStore::Project::NewOplog(std::string_view OplogId, const std::filesystem: try { - ZEN_INFO("oplog '{}/{}': creating oplog at '{}'", Identifier, OplogId, OplogBasePath); + Stopwatch Timer; - Oplog* Log = m_Oplogs - .try_emplace(std::string{OplogId}, - std::make_unique<ProjectStore::Oplog>(OplogId, this, m_CidStore, OplogBasePath, MarkerPath)) - .first->second.get(); + Ref<Oplog> NewLog(new Oplog(Log(), Identifier, OplogId, m_CidStore, OplogBasePath, MarkerPath, Oplog::EMode::kFull)); + m_Oplogs.insert_or_assign(std::string{OplogId}, NewLog); - Log->Write(); + NewLog->Write(); + + ZEN_INFO("oplog '{}/{}': created oplog at '{}' in {}", + Identifier, + OplogId, + OplogBasePath, + NiceTimeSpanMs(Timer.GetElapsedTimeMs())); if (m_CapturedOplogs) { m_CapturedOplogs->push_back(std::string(OplogId)); } - return Log; + return NewLog; } catch (const std::exception&) { @@ -3355,11 +3471,55 @@ ProjectStore::Project::NewOplog(std::string_view OplogId, const std::filesystem: m_Oplogs.erase(std::string{OplogId}); - return nullptr; + return {}; } } -ProjectStore::Oplog* +Ref<ProjectStore::Oplog> +ProjectStore::Project::ReadOplog(std::string_view OplogId) +{ + ZEN_MEMSCOPE(GetProjectstoreTag()); + ZEN_TRACE_CPU("Store::OpenOplog"); + + std::filesystem::path OplogBasePath = BasePathForOplog(OplogId); + + RwLock::SharedLockScope Lock(m_ProjectLock); + + if (auto It = m_Oplogs.find(std::string{OplogId}); It != m_Oplogs.end()) + { + if (Oplog::ExistsAt(OplogBasePath)) + { + return It->second; + } + else + { + return {}; + } + } + + if (Oplog::ExistsAt(OplogBasePath)) + { + Stopwatch Timer; + + Ref<Oplog> ExistingLog(new Oplog(m_ProjectStore->Log(), + Identifier, + OplogId, + m_CidStore, + OplogBasePath, + std::filesystem::path{}, + Oplog::EMode::kBasicReadOnly)); + + ExistingLog->Read(); + Lock.ReleaseNow(); + + ZEN_INFO("oplog '{}/{}': read oplog at '{}' in {}", Identifier, OplogId, OplogBasePath, NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + + return ExistingLog; + } + return {}; +} + +Ref<ProjectStore::Oplog> ProjectStore::Project::OpenOplog(std::string_view OplogId, bool AllowCompact, bool VerifyPathOnDisk) { ZEN_MEMSCOPE(GetProjectstoreTag()); @@ -3394,7 +3554,7 @@ ProjectStore::Project::OpenOplog(std::string_view OplogId, bool AllowCompact, bo if (!ReOpen) { - return OplogIt->second.get(); + return OplogIt->second; } } } @@ -3404,30 +3564,40 @@ ProjectStore::Project::OpenOplog(std::string_view OplogId, bool AllowCompact, bo RwLock::ExclusiveLockScope Lock(m_ProjectLock); if (auto It = m_Oplogs.find(std::string{OplogId}); It != m_Oplogs.end()) { - return It->second.get(); + return It->second; } if (Oplog::ExistsAt(OplogBasePath)) { try { - ZEN_INFO("oplog '{}/{}': opening oplog at '{}'", Identifier, OplogId, OplogBasePath); + Stopwatch Timer; + + Ref<Oplog> ExistingLog(new Oplog(m_ProjectStore->Log(), + Identifier, + OplogId, + m_CidStore, + OplogBasePath, + std::filesystem::path{}, + Oplog::EMode::kFull)); - Oplog* Log = - m_Oplogs - .try_emplace(std::string{OplogId}, - std::make_unique<ProjectStore::Oplog>(OplogId, this, m_CidStore, OplogBasePath, std::filesystem::path{})) - .first->second.get(); - Log->Read(); + m_Oplogs.insert_or_assign(std::string{OplogId}, ExistingLog); + ExistingLog->Read(); Lock.ReleaseNow(); + ZEN_INFO("oplog '{}/{}': opened oplog at '{}' in {}", + Identifier, + OplogId, + OplogBasePath, + NiceTimeSpanMs(Timer.GetElapsedTimeMs())); + if (AllowCompact) { const uint32_t CompactUnusedThreshold = 50; - Log->CompactIfUnusedExceeds(/*DryRun*/ false, - CompactUnusedThreshold, - fmt::format("Compact on initial open of oplog {}/{}: ", Identifier, OplogId)); + ExistingLog->CompactIfUnusedExceeds(/*DryRun*/ false, + CompactUnusedThreshold, + fmt::format("Compact on initial open of oplog {}/{}: ", Identifier, OplogId)); } - return Log; + return ExistingLog; } catch (const std::exception& Ex) { @@ -3436,7 +3606,7 @@ ProjectStore::Project::OpenOplog(std::string_view OplogId, bool AllowCompact, bo } } - return nullptr; + return {}; } void @@ -3460,6 +3630,27 @@ ProjectStore::Oplog::CompactIfUnusedExceeds(bool DryRun, uint32_t CompactUnusedT } bool +ProjectStore::Project::TryUnloadOplog(std::string_view OplogId) +{ + ZEN_MEMSCOPE(GetProjectstoreTag()); + + RwLock::ExclusiveLockScope _(m_ProjectLock); + if (auto OplogIt = m_Oplogs.find(std::string(OplogId)); OplogIt != m_Oplogs.end()) + { + Ref<Oplog>& Oplog = OplogIt->second; + + if (Oplog->CanUnload()) + { + m_Oplogs.erase(OplogIt); + return true; + } + return false; + } + + return false; +} + +bool ProjectStore::Project::RemoveOplog(std::string_view OplogId, std::filesystem::path& OutDeletePath) { ZEN_MEMSCOPE(GetProjectstoreTag()); @@ -3480,12 +3671,11 @@ ProjectStore::Project::RemoveOplog(std::string_view OplogId, std::filesystem::pa } else { - std::unique_ptr<Oplog>& Oplog = OplogIt->second; + Ref<Oplog>& Oplog = OplogIt->second; if (!Oplog->PrepareForDelete(OutDeletePath)) { return false; } - m_DeletedOplogs.emplace_back(std::move(Oplog)); m_Oplogs.erase(OplogIt); } } @@ -3529,6 +3719,11 @@ ProjectStore::Project::ScanForOplogs() const Oplogs.reserve(DirContent.Directories.size()); for (const std::filesystem::path& DirPath : DirContent.Directories) { + std::string DirName = PathToUtf8(DirPath.filename()); + if (DirName.starts_with("[dropped]")) + { + continue; + } if (Oplog::ExistsAt(DirPath)) { Oplogs.push_back(DirPath.filename().string()); @@ -3638,7 +3833,6 @@ ProjectStore::Project::PrepareForDelete(std::filesystem::path& OutDeletePath) for (auto& It : m_Oplogs) { It.second->ResetState(); - m_DeletedOplogs.emplace_back(std::move(It.second)); } m_Oplogs.clear(); @@ -3790,7 +3984,7 @@ ProjectStore::Project::IsExpired(const GcClock::TimePoint ExpireTime, std::strin if (OplogIt != m_Oplogs.end()) { Lock.ReleaseNow(); - return IsExpired(ExpireTime, *OplogIt->second.get()); + return IsExpired(ExpireTime, *OplogIt->second); } } @@ -3832,17 +4026,10 @@ ProjectStore::Project::LastOplogAccessTime(std::string_view Oplog) const ////////////////////////////////////////////////////////////////////////// -ProjectStore::ProjectStore(CidStore& Store, - std::filesystem::path BasePath, - GcManager& Gc, - JobQueue& JobQueue, - OpenProcessCache& InOpenProcessCache, - const Configuration& Config) +ProjectStore::ProjectStore(CidStore& Store, std::filesystem::path BasePath, GcManager& Gc, const Configuration& Config) : m_Log(logging::Get("project")) , m_Gc(Gc) , m_CidStore(Store) -, m_JobQueue(JobQueue) -, m_OpenProcessCache(InOpenProcessCache) , m_ProjectBasePath(BasePath) , m_Config(Config) , m_DiskWriteBlocker(Gc.GetDiskWriteBlocker()) @@ -3923,21 +4110,24 @@ ProjectStore::Flush() WorkerThreadPool& WorkerPool = GetSmallWorkerPool(EWorkloadType::Burst); std::atomic<bool> AbortFlag; std::atomic<bool> PauseFlag; - ParallelWork Work(AbortFlag, PauseFlag); + ParallelWork Work(AbortFlag, PauseFlag, WorkerThreadPool::EMode::DisableBacklog); try { for (const Ref<Project>& Project : Projects) { - Work.ScheduleWork(WorkerPool, [this, Project](std::atomic<bool>&) { - try - { - Project->Flush(); - } - catch (const std::exception& Ex) - { - ZEN_WARN("Exception while flushing project {}: {}", Project->Identifier, Ex.what()); - } - }); + Work.ScheduleWork( + WorkerPool, + [this, Project](std::atomic<bool>&) { + try + { + Project->Flush(); + } + catch (const std::exception& Ex) + { + ZEN_WARN("Exception while flushing project {}: {}", Project->Identifier, Ex.what()); + } + }, + 0); } } catch (const std::exception& Ex) @@ -4189,7 +4379,7 @@ ProjectStore::GetProjectsList() CbWriter Response; Response.BeginArray(); - IterateProjects([&Response](ProjectStore::Project& Prj) { + IterateProjects([&Response](Project& Prj) { Response.BeginObject(); Response << "Id"sv << Prj.Identifier; Response << "RootDir"sv << Prj.RootDir.string(); @@ -4202,31 +4392,13 @@ ProjectStore::GetProjectsList() return Response.Save().AsArray(); } -std::pair<HttpResponseCode, std::string> -ProjectStore::GetProjectFiles(const std::string_view ProjectId, - const std::string_view OplogId, - const std::unordered_set<std::string>& WantedFieldNames, - CbObject& OutPayload) +CbObject +ProjectStore::GetProjectFiles(LoggerRef InLog, Project& Project, Oplog& Oplog, const std::unordered_set<std::string>& WantedFieldNames) { - ZEN_MEMSCOPE(GetProjectstoreTag()); - ZEN_TRACE_CPU("Store::GetProjectFiles"); + auto Log = [&InLog]() { return InLog; }; using namespace std::literals; - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) - { - return {HttpResponseCode::NotFound, fmt::format("Project files request for unknown project '{}'", ProjectId)}; - } - Project->TouchProject(); - - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); - if (!FoundLog) - { - return {HttpResponseCode::NotFound, fmt::format("Project files for unknown oplog '{}/{}'", ProjectId, OplogId)}; - } - Project->TouchOplog(OplogId); - const bool WantsAllFields = WantedFieldNames.empty(); const bool WantsIdField = WantsAllFields || WantedFieldNames.contains("id"); @@ -4242,7 +4414,7 @@ ProjectStore::GetProjectFiles(const std::string_view ProjectId, std::vector<uint64_t> RawSizes; size_t Count = 0; - FoundLog->IterateFileMap([&](const Oid& Id, const std::string_view& ServerPath, const std::string_view& ClientPath) { + Oplog.IterateFileMap([&](const Oid& Id, const std::string_view& ServerPath, const std::string_view& ClientPath) { if (WantsIdField || WantsRawSizeField || WantsSizeField) { Ids.push_back(Id); @@ -4268,7 +4440,8 @@ ProjectStore::GetProjectFiles(const std::string_view ProjectId, RawSizes.resize(Ids.size(), (uint64_t)-1); } - FoundLog->IterateChunks( + Oplog.IterateChunks( + Project.RootDir, Ids, false, [&](size_t Index, const IoBuffer& Payload, uint64_t /*ModTag*/) { @@ -4291,8 +4464,8 @@ ProjectStore::GetProjectFiles(const std::string_view ProjectId, else { ZEN_WARN("oplog '{}/{}': payload for project file info for id {} is not a valid compressed binary.", - ProjectId, - OplogId, + Project.Identifier, + Oplog.OplogId(), Ids[Index]); } } @@ -4315,14 +4488,17 @@ ProjectStore::GetProjectFiles(const std::string_view ProjectId, } else { - ZEN_WARN("oplog '{}/{}': failed getting payload for project file info for id {}.", ProjectId, OplogId, Ids[Index]); + ZEN_WARN("oplog '{}/{}': failed getting payload for project file info for id {}.", + Project.Identifier, + Oplog.OplogId(), + Ids[Index]); } } catch (const std::exception& Ex) { ZEN_WARN("oplog '{}/{}': failed getting project file info for id {}. Reason: '{}'", - ProjectId, - OplogId, + Project.Identifier, + Oplog.OplogId(), Ids[Index], Ex.what()); } @@ -4362,34 +4538,18 @@ ProjectStore::GetProjectFiles(const std::string_view ProjectId, } Response.EndArray(); - OutPayload = Response.Save(); - return {HttpResponseCode::OK, {}}; + return Response.Save(); } -std::pair<HttpResponseCode, std::string> -ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, - const std::string_view OplogId, - const std::unordered_set<std::string>& WantedFieldNames, - CbObject& OutPayload) +CbObject +ProjectStore::GetProjectChunkInfos(LoggerRef InLog, Project& Project, Oplog& Oplog, const std::unordered_set<std::string>& WantedFieldNames) { ZEN_MEMSCOPE(GetProjectstoreTag()); ZEN_TRACE_CPU("ProjectStore::GetProjectChunkInfos"); - using namespace std::literals; - - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) - { - return {HttpResponseCode::NotFound, fmt::format("unknown project '{}'", ProjectId)}; - } - Project->TouchProject(); + auto Log = [&InLog]() { return InLog; }; - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); - if (!FoundLog) - { - return {HttpResponseCode::NotFound, fmt::format("unknown oplog '{}/{}'", ProjectId, OplogId)}; - } - Project->TouchOplog(OplogId); + using namespace std::literals; const bool WantsAllFields = WantedFieldNames.empty(); @@ -4404,7 +4564,7 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, std::vector<uint64_t> Sizes; size_t Count = 0; - size_t EstimatedCount = FoundLog->OplogCount(); + size_t EstimatedCount = Oplog.OplogCount(); if (WantsIdField) { Ids.reserve(EstimatedCount); @@ -4413,7 +4573,7 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, { Hashes.reserve(EstimatedCount); } - FoundLog->IterateChunkMap([&](const Oid& Id, const IoHash& Hash) { + Oplog.IterateChunkMap([&](const Oid& Id, const IoHash& Hash) { if (WantsIdField) { Ids.push_back(Id); @@ -4429,15 +4589,15 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, { if (WantsRawSizeField) { - RawSizes.resize(Hashes.size(), 0u); + RawSizes.resize(Hashes.size(), (uint64_t)-1); } if (WantsSizeField) { - Sizes.resize(Hashes.size(), 0u); + Sizes.resize(Hashes.size(), (uint64_t)-1); } WorkerThreadPool& WorkerPool = GetSmallWorkerPool(EWorkloadType::Burst); // GetSyncWorkerPool(); - (void)FoundLog->IterateChunks( + (void)Oplog.IterateChunks( Hashes, false, [&](size_t Index, const IoBuffer& Payload, uint64_t /*ModTag*/) -> bool { @@ -4449,24 +4609,27 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, { if (WantsRawSizeField) { + ZEN_ASSERT_SLOW(RawSizes[Index] == (uint64_t)-1); IoHash _; if (CompressedBuffer::ValidateCompressedHeader(Payload, _, RawSizes[Index])) { if (WantsSizeField) { + ZEN_ASSERT_SLOW(Sizes[Index] == (uint64_t)-1); Sizes[Index] = Payload.GetSize(); } } else { ZEN_WARN("oplog '{}/{}': payload for project chunk for id {} is not a valid compressed binary.", - ProjectId, - OplogId, + Project.Identifier, + Oplog.OplogId(), Ids[Index]); } } else if (WantsSizeField) { + ZEN_ASSERT_SLOW(Sizes[Index] == (uint64_t)-1); Sizes[Index] = Payload.GetSize(); } } @@ -4474,24 +4637,29 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, { if (WantsSizeField) { + ZEN_ASSERT_SLOW(Sizes[Index] == (uint64_t)-1); Sizes[Index] = Payload.GetSize(); } if (WantsRawSizeField) { + ZEN_ASSERT_SLOW(Sizes[Index] == (uint64_t)-1); RawSizes[Index] = Payload.GetSize(); } } } else { - ZEN_WARN("oplog '{}/{}': failed getting payload for chunk for id {}", ProjectId, OplogId, Ids[Index]); + ZEN_WARN("oplog '{}/{}': failed getting payload for chunk for id {}", + Project.Identifier, + Oplog.OplogId(), + Ids[Index]); } } catch (const std::exception& Ex) { ZEN_WARN("oplog '{}/{}': failed getting project chunk info for id {}. Reason: '{}'", - ProjectId, - OplogId, + Project.Identifier, + Oplog.OplogId(), Ids[Index], Ex.what()); } @@ -4529,110 +4697,63 @@ ProjectStore::GetProjectChunkInfos(const std::string_view ProjectId, } Response.EndArray(); - OutPayload = Response.Save(); - return {HttpResponseCode::OK, {}}; + return Response.Save(); } -std::pair<HttpResponseCode, std::string> -ProjectStore::GetChunkInfo(const std::string_view ProjectId, - const std::string_view OplogId, - const std::string_view ChunkId, - CbObject& OutPayload) +CbObject +ProjectStore::GetChunkInfo(LoggerRef InLog, Project& Project, Oplog& Oplog, const Oid& ChunkId) { ZEN_MEMSCOPE(GetProjectstoreTag()); - using namespace std::literals; - - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) - { - return {HttpResponseCode::NotFound, fmt::format("Chunk info request for unknown project '{}'", ProjectId)}; - } - Project->TouchProject(); + ZEN_TRACE_CPU("ProjectStore::GetChunkInfo"); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); - if (!FoundLog) - { - return {HttpResponseCode::NotFound, fmt::format("Chunk info request for unknown oplog '{}/{}'", ProjectId, OplogId)}; - } - Project->TouchOplog(OplogId); - - if (ChunkId.size() != 2 * sizeof(Oid::OidBits)) - { - return {HttpResponseCode::BadRequest, - fmt::format("Chunk info request for invalid chunk id '{}/{}'/'{}'", ProjectId, OplogId, ChunkId)}; - } + using namespace std::literals; - const Oid Obj = Oid::FromHexString(ChunkId); + auto Log = [&InLog]() { return InLog; }; - IoBuffer Chunk = FoundLog->FindChunk(Obj, nullptr); + IoBuffer Chunk = Oplog.FindChunk(Project.RootDir, ChunkId, nullptr); if (!Chunk) { - return {HttpResponseCode::NotFound, {}}; + return {}; } uint64_t ChunkSize = Chunk.GetSize(); - if (Chunk.GetContentType() == HttpContentType::kCompressedBinary) + if (Chunk.GetContentType() == ZenContentType::kCompressedBinary) { IoHash RawHash; uint64_t RawSize; bool IsCompressed = CompressedBuffer::ValidateCompressedHeader(Chunk, RawHash, RawSize); if (!IsCompressed) { - return {HttpResponseCode::InternalServerError, - fmt::format("Chunk info request for malformed chunk id '{}/{}'/'{}'", ProjectId, OplogId, ChunkId)}; + throw std::runtime_error( + fmt::format("Chunk info request for malformed chunk id '{}/{}'/'{}'", Project.Identifier, Oplog.OplogId(), ChunkId)); } ChunkSize = RawSize; } CbObjectWriter Response; Response << "size"sv << ChunkSize; - OutPayload = Response.Save(); - return {HttpResponseCode::OK, {}}; + return Response.Save(); } -std::pair<HttpResponseCode, std::string> -ProjectStore::GetChunkRange(const std::string_view ProjectId, - const std::string_view OplogId, - const std::string_view ChunkId, - uint64_t Offset, - uint64_t Size, - ZenContentType AcceptType, - CompositeBuffer& OutChunk, - ZenContentType& OutContentType, - uint64_t* OptionalInOutModificationTag) +static ProjectStore::GetChunkRangeResult +ExtractRange(IoBuffer&& Chunk, uint64_t Offset, uint64_t Size, ZenContentType AcceptType) { ZEN_MEMSCOPE(GetProjectstoreTag()); - if (ChunkId.size() != 2 * sizeof(Oid::OidBits)) - { - return {HttpResponseCode::BadRequest, fmt::format("Chunk request for invalid chunk id '{}/{}'/'{}'", ProjectId, OplogId, ChunkId)}; - } - - const Oid Obj = Oid::FromHexString(ChunkId); - return GetChunkRange(ProjectId, OplogId, Obj, Offset, Size, AcceptType, OutChunk, OutContentType, OptionalInOutModificationTag); -} + ProjectStore::GetChunkRangeResult Result; -static std::pair<HttpResponseCode, std::string> -ExtractRange(IoBuffer&& Chunk, - uint64_t Offset, - uint64_t Size, - ZenContentType AcceptType, - ZenContentType& OutContentType, - CompositeBuffer& OutChunk, - IoHash& OutRawHash, - uint64_t& OutRawSize) -{ - ZEN_MEMSCOPE(GetProjectstoreTag()); - OutContentType = Chunk.GetContentType(); + Result.ContentType = Chunk.GetContentType(); - if (OutContentType == ZenContentType::kCompressedBinary) + if (Result.ContentType == ZenContentType::kCompressedBinary) { IoHash RawHash; uint64_t RawSize; CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(std::move(Chunk)), RawHash, RawSize); if (!Compressed) { - return {HttpResponseCode::InternalServerError, "malformed compressed buffer"}; + Result.Error = ProjectStore::GetChunkRangeResult::EError::MalformedContent; + Result.ErrorDescription = fmt::format("Malformed payload, not a compressed buffer"); + return Result; } const bool IsFullRange = (Offset == 0) && ((Size == ~(0ull)) || (Size == RawSize)); @@ -4641,14 +4762,14 @@ ExtractRange(IoBuffer&& Chunk, { if (AcceptType == ZenContentType::kBinary) { - OutChunk = Compressed.DecompressToComposite(); - OutContentType = ZenContentType::kBinary; + Result.Chunk = Compressed.DecompressToComposite(); + Result.ContentType = ZenContentType::kBinary; } else { - OutChunk = Compressed.GetCompressed(); + Result.Chunk = Compressed.GetCompressed(); } - OutRawSize = 0; + Result.RawSize = 0; } else { @@ -4666,27 +4787,29 @@ ExtractRange(IoBuffer&& Chunk, if (Size == 0) { - return {HttpResponseCode::NotFound, - fmt::format("Chunk request for range outside of compressed chunk. Offset: {}, Size: {}, ChunkSize: {}", - Offset, - Size, - RawSize)}; + Result.Error = ProjectStore::GetChunkRangeResult::EError::OutOfRange; + Result.ErrorDescription = + fmt::format("Chunk request for range outside of compressed chunk. Offset: {}, Size: {}, ChunkSize: {}", + Offset, + Size, + RawSize); + return Result; } if (AcceptType == ZenContentType::kBinary) { - OutChunk = CompositeBuffer(Compressed.Decompress(Offset, Size)); - OutContentType = ZenContentType::kBinary; + Result.Chunk = CompositeBuffer(Compressed.Decompress(Offset, Size)); + Result.ContentType = ZenContentType::kBinary; } else { // Value will be a range of compressed blocks that covers the requested range // The client will have to compensate for any offsets that do not land on an even block size multiple - OutChunk = Compressed.GetRange(Offset, Size).GetCompressed(); + Result.Chunk = Compressed.GetRange(Offset, Size).GetCompressed(); } - OutRawSize = RawSize; + Result.RawSize = RawSize; } - OutRawHash = RawHash; + Result.RawHash = RawHash; } else { @@ -4695,8 +4818,8 @@ ExtractRange(IoBuffer&& Chunk, const bool IsFullRange = (Offset == 0) && ((Size == ~(0ull)) || (Size == ChunkSize)); if (IsFullRange) { - OutChunk = CompositeBuffer(SharedBuffer(std::move(Chunk))); - OutRawSize = 0; + Result.Chunk = CompositeBuffer(SharedBuffer(std::move(Chunk))); + Result.RawSize = 0; } else { @@ -4714,1150 +4837,467 @@ ExtractRange(IoBuffer&& Chunk, if (Size == 0) { - return {HttpResponseCode::NotFound, - fmt::format("Chunk request for range outside of chunk. Offset: {}, Size: {}, ChunkSize: {}", Offset, Size, Size)}; + Result.Error = ProjectStore::GetChunkRangeResult::EError::OutOfRange; + Result.ErrorDescription = + fmt::format("Chunk request for range outside of compressed chunk. Offset: {}, Size: {}, ChunkSize: {}", + Offset, + Size, + ChunkSize); + return Result; } - OutChunk = CompositeBuffer(SharedBuffer(IoBuffer(std::move(Chunk), Offset, Size))); - OutRawSize = ChunkSize; + Result.Chunk = CompositeBuffer(SharedBuffer(IoBuffer(std::move(Chunk), Offset, Size))); + Result.RawSize = ChunkSize; } } - return {HttpResponseCode::OK, {}}; + Result.Error = ProjectStore::GetChunkRangeResult::EError::Ok; + return Result; } -std::pair<HttpResponseCode, std::string> -ProjectStore::GetChunkRange(const std::string_view ProjectId, - const std::string_view OplogId, - Oid ChunkId, - uint64_t Offset, - uint64_t Size, - ZenContentType AcceptType, - CompositeBuffer& OutChunk, - ZenContentType& OutContentType, - uint64_t* OptionalInOutModificationTag) +ProjectStore::GetChunkRangeResult +ProjectStore::GetChunkRange(LoggerRef InLog, + Project& Project, + Oplog& Oplog, + const Oid& ChunkId, + uint64_t Offset, + uint64_t Size, + ZenContentType AcceptType, + uint64_t* OptionalInOutModificationTag) { ZEN_MEMSCOPE(GetProjectstoreTag()); - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) - { - return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown project '{}'", ProjectId)}; - } - Project->TouchProject(); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); - if (!FoundLog) - { - return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown oplog '{}/{}'", ProjectId, OplogId)}; - } - Project->TouchOplog(OplogId); + ZEN_TRACE_CPU("ProjectStore::GetChunkRange"); + + auto Log = [&InLog]() { return InLog; }; uint64_t OldTag = OptionalInOutModificationTag == nullptr ? 0 : *OptionalInOutModificationTag; - IoBuffer Chunk = FoundLog->FindChunk(ChunkId, OptionalInOutModificationTag); + IoBuffer Chunk = Oplog.FindChunk(Project.RootDir, ChunkId, OptionalInOutModificationTag); if (!Chunk) { - return {HttpResponseCode::NotFound, {}}; + return GetChunkRangeResult{.Error = GetChunkRangeResult::EError::NotFound, + .ErrorDescription = fmt::format("Chunk range request for chunk {}/{}/{} failed, payload not found", + Project.Identifier, + Oplog.OplogId(), + ChunkId)}; } if (OptionalInOutModificationTag != nullptr && OldTag == *OptionalInOutModificationTag) { - return {HttpResponseCode::NotModified, {}}; + return {.Error = GetChunkRangeResult::EError::NotModified}; } - IoHash _; - uint64_t __; - std::pair<HttpResponseCode, std::string> Result = - ExtractRange(std::move(Chunk), Offset, Size, AcceptType, OutContentType, OutChunk, /*OutRawHash*/ _, /*OutRawSize*/ __); - if (Result.first != HttpResponseCode::OK) - { - return {Result.first, - fmt::format("Chunk request for chunk {} in {}/{} failed. Reason: '{}'", ChunkId, ProjectId, OplogId, Result.second)}; - } - return Result; + return ExtractRange(std::move(Chunk), Offset, Size, AcceptType); } -std::pair<HttpResponseCode, std::string> -ProjectStore::GetChunk(const std::string_view ProjectId, - const std::string_view OplogId, - const std::string_view Cid, - IoBuffer& OutChunk, - uint64_t* OptionalInOutModificationTag) +IoBuffer +ProjectStore::GetChunk(Project& Project, Oplog& Oplog, const IoHash& ChunkHash) { ZEN_MEMSCOPE(GetProjectstoreTag()); - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) - { - return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown project '{}'", ProjectId)}; - } - Project->TouchProject(); + ZEN_TRACE_CPU("ProjectStore::GetChunk"); + ZEN_UNUSED(Project, Oplog); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); - if (!FoundLog) - { - return {HttpResponseCode::NotFound, fmt::format("Chunk request for unknown oplog '{}/{}'", ProjectId, OplogId)}; - } - Project->TouchOplog(OplogId); + IoBuffer Chunk = m_CidStore.FindChunkByCid(ChunkHash); - if (Cid.length() != IoHash::StringLength) + if (!Chunk) { - return {HttpResponseCode::BadRequest, fmt::format("Chunk request for invalid chunk id '{}/{}'/'{}'", ProjectId, OplogId, Cid)}; + return {}; } - const IoHash Hash = IoHash::FromHexString(Cid); - OutChunk = m_CidStore.FindChunkByCid(Hash); + Chunk.SetContentType(ZenContentType::kCompressedBinary); + return Chunk; +} + +IoBuffer +ProjectStore::GetChunk(const std::string_view ProjectId, const std::string_view OplogId, const Oid& ChunkId) +{ + ZEN_MEMSCOPE(GetProjectstoreTag()); + ZEN_TRACE_CPU("ProjectStore::GetChunk"); - if (!OutChunk) + Ref<Project> Project = OpenProject(ProjectId); + if (!Project) { - return {HttpResponseCode::NotFound, fmt::format("chunk - '{}' MISSING", Cid)}; + return {}; } - - if (OptionalInOutModificationTag != nullptr) + Ref<Oplog> Oplog = Project->OpenOplog(OplogId, /*AllowCompact */ false, /*VerifyPathOnDisk*/ false); + if (!Oplog) { - uint64_t OldTag = *OptionalInOutModificationTag; - *OptionalInOutModificationTag = GetModificationTagFromRawHash(Hash); - if (*OptionalInOutModificationTag == OldTag) - { - return {HttpResponseCode::NotModified, {}}; - } + return {}; } - - OutChunk.SetContentType(ZenContentType::kCompressedBinary); - return {HttpResponseCode::OK, {}}; + return Oplog->FindChunk(Project->RootDir, ChunkId, /*OptOutModificationTag*/ nullptr); } -std::pair<HttpResponseCode, std::string> -ProjectStore::PutChunk(const std::string_view ProjectId, - const std::string_view OplogId, - const std::string_view Cid, - ZenContentType ContentType, - IoBuffer&& Chunk) +IoBuffer +ProjectStore::GetChunk(const std::string_view ProjectId, const std::string_view OplogId, const IoHash& Cid) { ZEN_MEMSCOPE(GetProjectstoreTag()); - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) - { - return {HttpResponseCode::NotFound, fmt::format("Chunk put request for unknown project '{}'", ProjectId)}; - } - Project->TouchProject(); + ZEN_TRACE_CPU("ProjectStore::GetChunk"); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ false); - if (!FoundLog) + Ref<Project> Project = OpenProject(ProjectId); + if (!Project) { - return {HttpResponseCode::NotFound, fmt::format("Chunk put request for unknown oplog '{}/{}'", ProjectId, OplogId)}; + return {}; } - Project->TouchOplog(OplogId); - - if (Cid.length() != IoHash::StringLength) + Ref<Oplog> Oplog = Project->OpenOplog(OplogId, /*AllowCompact */ false, /*VerifyPathOnDisk*/ false); + if (!Oplog) { - return {HttpResponseCode::BadRequest, fmt::format("Chunk put request for invalid chunk hash '{}'", Cid)}; + return {}; } + return m_CidStore.FindChunkByCid(Cid); +} - const IoHash Hash = IoHash::FromHexString(Cid); +bool +ProjectStore::PutChunk(Project& Project, Oplog& Oplog, const IoHash& ChunkHash, IoBuffer&& Chunk) +{ + ZEN_MEMSCOPE(GetProjectstoreTag()); + ZEN_TRACE_CPU("ProjectStore::PutChunk"); - if (ContentType != HttpContentType::kCompressedBinary) - { - return {HttpResponseCode::BadRequest, fmt::format("Chunk request for invalid content type for chunk '{}'", Cid)}; - } IoHash RawHash; uint64_t RawSize; CompressedBuffer Compressed = CompressedBuffer::FromCompressed(SharedBuffer(Chunk), RawHash, RawSize); - if (RawHash != Hash) + if (RawHash != ChunkHash) { - return {HttpResponseCode::BadRequest, fmt::format("Chunk request for invalid payload format for chunk '{}'", Cid)}; + throw std::runtime_error( + fmt::format("Chunk request for invalid payload format for chunk {}/{}/{}", Project.Identifier, Oplog.OplogId(), ChunkHash)); } - FoundLog->CaptureAddedAttachments(std::vector<IoHash>{Hash}); - CidStore::InsertResult Result = m_CidStore.AddChunk(Chunk, Hash); - return {Result.New ? HttpResponseCode::Created : HttpResponseCode::OK, {}}; + Oplog.CaptureAddedAttachments(std::vector<IoHash>{ChunkHash}); + CidStore::InsertResult Result = m_CidStore.AddChunk(Chunk, ChunkHash); + return Result.New; } -std::pair<HttpResponseCode, std::string> -ProjectStore::GetChunks(const std::string_view ProjectId, - const std::string_view OplogId, - const CbObject& RequestObject, - CbPackage& OutResponsePackage) +std::vector<ProjectStore::ChunkResult> +ProjectStore::GetChunks(Project& Project, Oplog& Oplog, std::span<const ChunkRequest> Requests) { ZEN_MEMSCOPE(GetProjectstoreTag()); - ZEN_TRACE_CPU("Store::GetChunks"); + ZEN_TRACE_CPU("ProjectStore::GetChunks"); - using namespace std::literals; + ZEN_ASSERT(!Requests.empty()); - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) - { - return {HttpResponseCode::NotFound, fmt::format("getchunks rpc request for unknown project '{}'", ProjectId)}; - } - Project->TouchProject(); + std::vector<ProjectStore::ChunkResult> Results; + size_t RequestCount = Requests.size(); - ProjectStore::Oplog* FoundLog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ false); - if (!FoundLog) - { - return {HttpResponseCode::NotFound, fmt::format("getchunks rpc request for unknown oplog '{}/{}'", ProjectId, OplogId)}; - } - Project->TouchOplog(OplogId); + Results.resize(RequestCount); - if (RequestObject["chunks"sv].IsArray()) + if (RequestCount > 1) { - // Legacy full chunks only by rawhash - - CbArrayView ChunksArray = RequestObject["chunks"sv].AsArrayView(); + std::vector<IoHash> ChunkRawHashes; + std::vector<size_t> ChunkRawHashesRequestIndex; + std::vector<Oid> ChunkIds; + std::vector<size_t> ChunkIdsRequestIndex; - CbObjectWriter ResponseWriter; - ResponseWriter.BeginArray("chunks"sv); - for (CbFieldView FieldView : ChunksArray) - { - IoHash RawHash = FieldView.AsHash(); - IoBuffer ChunkBuffer = m_CidStore.FindChunkByCid(RawHash); - if (ChunkBuffer) - { - CompressedBuffer Compressed = CompressedBuffer::FromCompressedNoValidate(std::move(ChunkBuffer)); - if (Compressed) - { - ResponseWriter.AddHash(RawHash); - OutResponsePackage.AddAttachment(CbAttachment(std::move(Compressed), RawHash)); - } - else - { - ZEN_WARN("oplog '{}/{}': invalid compressed binary in cas store for {}", ProjectId, OplogId, RawHash); - } - } - } - ResponseWriter.EndArray(); - OutResponsePackage.SetObject(ResponseWriter.Save()); - return {HttpResponseCode::OK, {}}; - } - else if (auto RequestFieldView = RequestObject["Request"sv]; RequestFieldView.IsObject()) - { - CbObjectView RequestView = RequestFieldView.AsObjectView(); - bool SkipData = RequestView["SkipData"].AsBool(false); - CbArrayView ChunksArray = RequestView["Chunks"sv].AsArrayView(); + ChunkRawHashes.reserve(RequestCount); + ChunkRawHashesRequestIndex.reserve(RequestCount); + ChunkIds.reserve(RequestCount); + ChunkIdsRequestIndex.reserve(RequestCount); - struct Request + for (size_t RequestIndex = 0; RequestIndex < Requests.size(); RequestIndex++) { - struct InputData - { - uint64_t Offset = 0; - uint64_t Size = (uint64_t)-1; - std::variant<IoHash, Oid> Id; - std::optional<uint64_t> ModTag; - } Input; - struct OutputData + const ChunkRequest& Request = Requests[RequestIndex]; + if (Request.Id.index() == 0) { - bool Exists = false; - IoBuffer ChunkBuffer; - uint64_t ModTag = 0; - } Output; - }; - - std::vector<Request> Requests; - size_t RequestCount = ChunksArray.Num(); - if (RequestCount > 0) - { - Requests.reserve(RequestCount); - std::vector<IoHash> ChunkRawHashes; - std::vector<size_t> ChunkRawHashesRequestIndex; - std::vector<Oid> ChunkIds; - std::vector<size_t> ChunkIdsRequestIndex; - bool DoBatch = RequestCount > 1; - if (DoBatch) - { - ChunkRawHashes.reserve(RequestCount); - ChunkRawHashesRequestIndex.reserve(RequestCount); - ChunkIds.reserve(RequestCount); - ChunkIdsRequestIndex.reserve(RequestCount); + ChunkRawHashes.push_back(std::get<IoHash>(Request.Id)); + ChunkRawHashesRequestIndex.push_back(RequestIndex); } - for (CbFieldView FieldView : ChunksArray) + else { - CbObjectView ChunkObject = FieldView.AsObjectView(); - Request ChunkRequest = { - .Input{.Offset = ChunkObject["Offset"sv].AsUInt64(0), .Size = ChunkObject["Size"sv].AsUInt64((uint64_t)-1)}}; - if (CbFieldView InputModificationTagView = ChunkObject.FindView("ModTag"); InputModificationTagView.IsInteger()) - { - ChunkRequest.Input.ModTag = InputModificationTagView.AsUInt64(); - } - if (CbFieldView RawHashView = ChunkObject.FindView("RawHash"sv); RawHashView.IsHash()) - { - const IoHash ChunkHash = RawHashView.AsHash(); - ChunkRequest.Input.Id = ChunkHash; - if (DoBatch) - { - ChunkRawHashes.push_back(ChunkHash); - ChunkRawHashesRequestIndex.push_back(Requests.size()); - } - } - else if (CbFieldView IdView = ChunkObject.FindView("Oid"sv); IdView.IsObjectId()) - { - const Oid ChunkId = IdView.AsObjectId(); - ChunkRequest.Input.Id = ChunkId; - if (DoBatch) - { - ChunkIds.push_back(ChunkId); - ChunkIdsRequestIndex.push_back(Requests.size()); - } - } - else - { - return {HttpResponseCode::BadRequest, - fmt::format("oplog '{}/{}': malformed getchunks rpc request object, chunk request has no identifier", - ProjectId, - OplogId)}; - } - Requests.emplace_back(std::move(ChunkRequest)); + ChunkIds.push_back(std::get<Oid>(Request.Id)); + ChunkIdsRequestIndex.push_back(RequestIndex); } + } - if (DoBatch) - { - WorkerThreadPool& WorkerPool = GetSmallWorkerPool(EWorkloadType::Burst); // GetSyncWorkerPool(); - if (!ChunkRawHashes.empty()) - { - FoundLog->IterateChunks( - ChunkRawHashes, - true, - [&](size_t Index, const IoBuffer& Payload, uint64_t ModTag) -> bool { - if (Payload) - { - size_t RequestIndex = ChunkRawHashesRequestIndex[Index]; - Requests[RequestIndex].Output.Exists = true; - if (!SkipData) - { - Requests[RequestIndex].Output.ChunkBuffer = Payload; - Requests[RequestIndex].Output.ChunkBuffer.MakeOwned(); - } - Requests[RequestIndex].Output.ModTag = ModTag; - } - return true; - }, - &WorkerPool, - 8u * 1024); - } - if (!ChunkIdsRequestIndex.empty()) - { - FoundLog->IterateChunks( - ChunkIds, - true, - [&](size_t Index, const IoBuffer& Payload, uint64_t ModTag) -> bool { - if (Payload) - { - size_t RequestIndex = ChunkIdsRequestIndex[Index]; - Requests[RequestIndex].Output.Exists = true; - if (!SkipData) - { - Requests[RequestIndex].Output.ChunkBuffer = Payload; - Requests[RequestIndex].Output.ChunkBuffer.MakeOwned(); - } - Requests[RequestIndex].Output.ModTag = ModTag; - } - return true; - }, - &WorkerPool, - 8u * 1024); - } - } - else - { - Request& ChunkRequest = Requests.front(); - if (ChunkRequest.Input.Id.index() == 0) - { - const IoHash& ChunkHash = std::get<IoHash>(ChunkRequest.Input.Id); - IoBuffer Payload = m_CidStore.FindChunkByCid(ChunkHash); + WorkerThreadPool& WorkerPool = GetSmallWorkerPool(EWorkloadType::Burst); // GetSyncWorkerPool(); + if (!ChunkRawHashes.empty()) + { + Oplog.IterateChunks( + ChunkRawHashes, + true, + [&](size_t Index, const IoBuffer& Payload, uint64_t ModTag) -> bool { if (Payload) { - ChunkRequest.Output.Exists = true; - ChunkRequest.Output.ModTag = GetModificationTagFromRawHash(ChunkHash); - if (!SkipData) + size_t RequestIndex = ChunkRawHashesRequestIndex[Index]; + const ChunkRequest& Request = Requests[RequestIndex]; + ChunkResult& Result = Results[RequestIndex]; + Result.Exists = true; + if (!Request.SkipData) { - ChunkRequest.Output.ChunkBuffer = Payload; + Result.ChunkBuffer = std::move(Payload); + Result.ChunkBuffer.MakeOwned(); } + Result.ModTag = ModTag; } - } - else - { - const Oid& ChunkId = std::get<Oid>(ChunkRequest.Input.Id); - uint64_t ModTag = 0; - IoBuffer Payload = FoundLog->FindChunk(ChunkId, &ModTag); + return true; + }, + &WorkerPool, + 8u * 1024); + } + if (!ChunkIdsRequestIndex.empty()) + { + Oplog.IterateChunks( + Project.RootDir, + ChunkIds, + true, + [&](size_t Index, const IoBuffer& Payload, uint64_t ModTag) -> bool { if (Payload) { - ChunkRequest.Output.Exists = true; - ChunkRequest.Output.ModTag = ModTag; - if (!SkipData) + size_t RequestIndex = ChunkIdsRequestIndex[Index]; + const ChunkRequest& Request = Requests[RequestIndex]; + ChunkResult& Result = Results[RequestIndex]; + Result.Exists = true; + if (!Request.SkipData) { - ChunkRequest.Output.ChunkBuffer = Payload; + Result.ChunkBuffer = std::move(Payload); + Result.ChunkBuffer.MakeOwned(); } + Result.ModTag = ModTag; } - } - } + return true; + }, + &WorkerPool, + 8u * 1024); } + } + else + { + const ChunkRequest& Request = Requests.front(); + ChunkResult& Result = Results.front(); - CbObjectWriter ResponseWriter(32 + Requests.size() * 64u); - ResponseWriter.BeginArray("Chunks"sv); + if (Request.Id.index() == 0) { - for (Request& ChunkRequest : Requests) + const IoHash& ChunkHash = std::get<IoHash>(Request.Id); + IoBuffer Payload = m_CidStore.FindChunkByCid(ChunkHash); + if (Payload) { - if (ChunkRequest.Output.Exists) + Result.Exists = true; + Result.ModTag = GetModificationTagFromRawHash(ChunkHash); + if (!Request.SkipData) { - ResponseWriter.BeginObject(); - { - if (ChunkRequest.Input.Id.index() == 0) - { - const IoHash& RawHash = std::get<IoHash>(ChunkRequest.Input.Id); - ResponseWriter.AddHash("Id", RawHash); - } - else - { - const Oid& Id = std::get<Oid>(ChunkRequest.Input.Id); - ResponseWriter.AddObjectId("Id", Id); - } - if (!ChunkRequest.Input.ModTag.has_value() || ChunkRequest.Input.ModTag.value() != ChunkRequest.Output.ModTag) - { - ResponseWriter.AddInteger("ModTag", ChunkRequest.Output.ModTag); - if (!SkipData) - { - CompositeBuffer ChunkRange; - ZenContentType ContentType; - IoHash FullChunkRawHash; - uint64_t FullChunkSize = 0; - auto ExtractRangeResult = ExtractRange(std::move(ChunkRequest.Output.ChunkBuffer), - ChunkRequest.Input.Offset, - ChunkRequest.Input.Size, - ZenContentType::kCompressedBinary, - ContentType, - ChunkRange, - FullChunkRawHash, - FullChunkSize); - if (ExtractRangeResult.first == HttpResponseCode::OK) - { - if (ContentType == ZenContentType::kCompressedBinary) - { - ZEN_ASSERT(FullChunkRawHash != IoHash::Zero); - CompressedBuffer CompressedValue = - CompressedBuffer::FromCompressedNoValidate(std::move(ChunkRange)); - ZEN_ASSERT(CompressedValue); - - if (FullChunkSize != 0) - { - // This really could use some thought so we don't send the same data if we get a request for - // multiple ranges from the same chunk block - - uint64_t FragmentRawOffset = 0; - OodleCompressor Compressor; - OodleCompressionLevel CompressionLevel; - uint64_t BlockSize = 0; - if (CompressedValue.TryGetCompressParameters(Compressor, CompressionLevel, BlockSize)) - { - if (BlockSize > 0) - { - FragmentRawOffset = (ChunkRequest.Input.Offset / BlockSize) * BlockSize; - } - else - { - FragmentRawOffset = ChunkRequest.Input.Offset; - } - uint64_t FragmentRawLength = CompressedValue.DecodeRawSize(); - - IoHashStream FragmentHashStream; - FragmentHashStream.Append(FullChunkRawHash.Hash, sizeof(FullChunkRawHash.Hash)); - FragmentHashStream.Append(&FragmentRawOffset, sizeof(FragmentRawOffset)); - FragmentHashStream.Append(&FragmentRawLength, sizeof(FragmentRawLength)); - IoHash FragmentHash = FragmentHashStream.GetHash(); - - ResponseWriter.AddHash("FragmentHash", FragmentHash); - ResponseWriter.AddInteger("FragmentOffset", FragmentRawOffset); - ResponseWriter.AddInteger("RawSize", FullChunkSize); - OutResponsePackage.AddAttachment(CbAttachment(CompressedValue, FragmentHash)); - } - else - { - std::string ErrorString = - "Failed to get compression parameters from partial compressed buffer"; - ResponseWriter.AddString("Error", ErrorString); - ZEN_WARN("oplog '{}/{}': {}", ProjectId, OplogId, ErrorString); - } - } - else - { - ResponseWriter.AddHash("RawHash"sv, FullChunkRawHash); - OutResponsePackage.AddAttachment(CbAttachment(std::move(CompressedValue), FullChunkRawHash)); - } - } - else - { - IoHashStream HashStream; - ZEN_ASSERT(ChunkRequest.Input.Id.index() == 1); - const Oid& Id = std::get<Oid>(ChunkRequest.Input.Id); - HashStream.Append(Id.OidBits, sizeof(Id.OidBits)); - HashStream.Append(&ChunkRequest.Input.Offset, sizeof(ChunkRequest.Input.Offset)); - HashStream.Append(&ChunkRequest.Input.Size, sizeof(ChunkRequest.Input.Size)); - IoHash Hash = HashStream.GetHash(); - - ResponseWriter.AddHash("Hash"sv, Hash); - if (FullChunkSize != 0) - { - ResponseWriter.AddInteger("Size", FullChunkSize); - } - OutResponsePackage.AddAttachment(CbAttachment(std::move(ChunkRange), Hash)); - } - } - else - { - std::string ErrorString = fmt::format("Failed fetchiong chunk range ({})", ExtractRangeResult.second); - ResponseWriter.AddString("Error", ErrorString); - ZEN_WARN("oplog '{}/{}': {}", ProjectId, OplogId, ErrorString); - } - } - } - } - ResponseWriter.EndObject(); + Result.ChunkBuffer = std::move(Payload); + Result.ChunkBuffer.MakeOwned(); } } } - ResponseWriter.EndArray(); - OutResponsePackage.SetObject(ResponseWriter.Save()); - return {HttpResponseCode::OK, {}}; - } - else - { - return {HttpResponseCode::BadRequest, fmt::format("oplog '{}/{}': malformed getchunks rpc request object", ProjectId, OplogId)}; - } -} - -std::pair<HttpResponseCode, std::string> -ProjectStore::WriteOplog(const std::string_view ProjectId, const std::string_view OplogId, IoBuffer&& Payload, CbObject& OutResponse) -{ - ZEN_MEMSCOPE(GetProjectstoreTag()); - ZEN_TRACE_CPU("Store::WriteOplog"); - - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) - { - return {HttpResponseCode::NotFound, fmt::format("Write oplog request for unknown project '{}'", ProjectId)}; - } - Project->TouchProject(); - - ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ false); - if (!Oplog) - { - return {HttpResponseCode::NotFound, fmt::format("Write oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)}; - } - Project->TouchOplog(OplogId); - - CbObject ContainerObject = LoadCompactBinaryObject(Payload); - if (!ContainerObject) - { - return {HttpResponseCode::BadRequest, "Invalid payload format"}; - } - - CidStore& ChunkStore = m_CidStore; - RwLock AttachmentsLock; - tsl::robin_set<IoHash, IoHash::Hasher> Attachments; - - auto HasAttachment = [&ChunkStore](const IoHash& RawHash) { return ChunkStore.ContainsChunk(RawHash); }; - auto OnNeedBlock = [&AttachmentsLock, &Attachments](const IoHash& BlockHash, const std::vector<IoHash>&& ChunkHashes) { - RwLock::ExclusiveLockScope _(AttachmentsLock); - if (BlockHash != IoHash::Zero) - { - Attachments.insert(BlockHash); - } - else - { - Attachments.insert(ChunkHashes.begin(), ChunkHashes.end()); - } - }; - auto OnNeedAttachment = [&AttachmentsLock, &Attachments](const IoHash& RawHash) { - RwLock::ExclusiveLockScope _(AttachmentsLock); - Attachments.insert(RawHash); - }; - - auto OnChunkedAttachment = [](const ChunkedInfo&) {}; - - auto OnReferencedAttachments = [&Oplog](std::span<IoHash> RawHashes) { Oplog->CaptureAddedAttachments(RawHashes); }; - // Make sure we retain any attachments we download before writing the oplog - Oplog->EnableUpdateCapture(); - auto _ = MakeGuard([&Oplog]() { Oplog->DisableUpdateCapture(); }); - - RemoteProjectStore::Result RemoteResult = SaveOplogContainer(*Oplog, - ContainerObject, - OnReferencedAttachments, - HasAttachment, - OnNeedBlock, - OnNeedAttachment, - OnChunkedAttachment, - nullptr); - - if (RemoteResult.ErrorCode) - { - return ConvertResult(RemoteResult); - } - - CbObjectWriter Cbo(1 + 1 + 5 + Attachments.size() * (1 + sizeof(IoHash::Hash)) + 1); - Cbo.BeginArray("need"); - { - for (const IoHash& Hash : Attachments) + else { - ZEN_DEBUG("Need attachment {}", Hash); - Cbo << Hash; + const Oid& ChunkId = std::get<Oid>(Request.Id); + uint64_t ModTag = 0; + IoBuffer Payload = Oplog.FindChunk(Project.RootDir, ChunkId, &ModTag); + if (Payload) + { + Result.Exists = true; + Result.ModTag = ModTag; + if (!Request.SkipData) + { + Result.ChunkBuffer = std::move(Payload); + Result.ChunkBuffer.MakeOwned(); + } + } } } - Cbo.EndArray(); // "need" - - OutResponse = Cbo.Save(); - return {HttpResponseCode::OK, {}}; + return Results; } -std::pair<HttpResponseCode, std::string> -ProjectStore::ReadOplog(const std::string_view ProjectId, - const std::string_view OplogId, - const HttpServerRequest::QueryParams& Params, - CbObject& OutResponse) +std::vector<ProjectStore::ChunkRequest> +ProjectStore::ParseChunksRequests(Project& Project, Oplog& Oplog, const CbObject& Cb) { - ZEN_MEMSCOPE(GetProjectstoreTag()); - ZEN_TRACE_CPU("Store::ReadOplog"); + ZEN_TRACE_CPU("Store::Rpc::getchunks"); - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) - { - return {HttpResponseCode::NotFound, fmt::format("Read oplog request for unknown project '{}'", ProjectId)}; - } - Project->TouchProject(); + using namespace std::literals; - ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId, /*AllowCompact*/ true, /*VerifyPathOnDisk*/ true); - if (!Oplog) - { - return {HttpResponseCode::NotFound, fmt::format("Read oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)}; - } - Project->TouchOplog(OplogId); + std::vector<ChunkRequest> Requests; - size_t MaxBlockSize = RemoteStoreOptions::DefaultMaxBlockSize; - if (auto Param = Params.GetValue("maxblocksize"); Param.empty() == false) + if (auto RequestFieldView = Cb["Request"sv]; RequestFieldView.IsObject()) { - if (auto Value = ParseInt<size_t>(Param)) - { - MaxBlockSize = Value.value(); - } - } - size_t MaxChunkEmbedSize = RemoteStoreOptions::DefaultMaxChunkEmbedSize; - if (auto Param = Params.GetValue("maxchunkembedsize"); Param.empty() == false) - { - if (auto Value = ParseInt<size_t>(Param)) - { - MaxChunkEmbedSize = Value.value(); - } - } + CbObjectView RequestView = RequestFieldView.AsObjectView(); + bool SkipData = RequestView["SkipData"].AsBool(false); + CbArrayView ChunksArray = RequestView["Chunks"sv].AsArrayView(); - size_t ChunkFileSizeLimit = RemoteStoreOptions::DefaultChunkFileSizeLimit; - if (auto Param = Params.GetValue("chunkfilesizelimit"); Param.empty() == false) - { - if (auto Value = ParseInt<size_t>(Param)) + size_t RequestCount = ChunksArray.Num(); + if (RequestCount > 0) { - ChunkFileSizeLimit = Value.value(); - } - } - - CidStore& ChunkStore = m_CidStore; - - RemoteProjectStore::LoadContainerResult ContainerResult = BuildContainer( - ChunkStore, - *Project.Get(), - *Oplog, - MaxBlockSize, - MaxChunkEmbedSize, - ChunkFileSizeLimit, - /* BuildBlocks */ false, - /* IgnoreMissingAttachments */ false, - /* AllowChunking*/ false, - [](CompressedBuffer&&, ChunkBlockDescription&&) {}, - [](const IoHash&, TGetAttachmentBufferFunc&&) {}, - [](std::vector<std::pair<IoHash, FetchChunkFunc>>&&) {}, - /* EmbedLooseFiles*/ false); - - OutResponse = std::move(ContainerResult.ContainerObject); - return ConvertResult(ContainerResult); -} - -bool -ProjectStore::Rpc(HttpServerRequest& HttpReq, - const std::string_view ProjectId, - const std::string_view OplogId, - IoBuffer&& Payload, - AuthMgr& AuthManager) -{ - ZEN_MEMSCOPE(GetProjectstoreTag()); - ZEN_TRACE_CPU("Store::Rpc"); - - using namespace std::literals; - HttpContentType PayloadContentType = HttpReq.RequestContentType(); - CbPackage Package; - CbObject Cb; - switch (PayloadContentType) - { - case HttpContentType::kJSON: - case HttpContentType::kUnknownContentType: - case HttpContentType::kText: - { - std::string JsonText(reinterpret_cast<const char*>(Payload.GetData()), Payload.GetSize()); - Cb = LoadCompactBinaryFromJson(JsonText).AsObject(); - if (!Cb) + Requests.reserve(RequestCount); + for (CbFieldView FieldView : ChunksArray) + { + CbObjectView ChunkObject = FieldView.AsObjectView(); + ChunkRequest ChunkRequest = {.Offset = ChunkObject["Offset"sv].AsUInt64(0), + .Size = ChunkObject["Size"sv].AsUInt64((uint64_t)-1), + .SkipData = SkipData}; + if (CbFieldView InputModificationTagView = ChunkObject.FindView("ModTag"); InputModificationTagView.IsInteger()) { - HttpReq.WriteResponse(HttpResponseCode::BadRequest, - HttpContentType::kText, - "Content format not supported, expected JSON format"); - return false; + ChunkRequest.ModTag = InputModificationTagView.AsUInt64(); } + if (CbFieldView RawHashView = ChunkObject.FindView("RawHash"sv); RawHashView.IsHash()) + { + const IoHash ChunkHash = RawHashView.AsHash(); + ChunkRequest.Id = ChunkHash; + } + else if (CbFieldView IdView = ChunkObject.FindView("Oid"sv); IdView.IsObjectId()) + { + const Oid ChunkId = IdView.AsObjectId(); + ChunkRequest.Id = ChunkId; + } + else + { + throw std::runtime_error( + fmt::format("oplog '{}/{}': malformed getchunks rpc request object, chunk request has no identifier", + Project.Identifier, + Oplog.OplogId())); + } + Requests.emplace_back(std::move(ChunkRequest)); } - break; - case HttpContentType::kCbObject: - Cb = LoadCompactBinaryObject(Payload); - if (!Cb) - { - HttpReq.WriteResponse(HttpResponseCode::BadRequest, - HttpContentType::kText, - "Content format not supported, expected compact binary format"); - return false; - } - break; - case HttpContentType::kCbPackage: - try - { - Package = ParsePackageMessage(Payload); - Cb = Package.GetObject(); - } - catch (const std::invalid_argument& ex) - { - HttpReq.WriteResponse(HttpResponseCode::BadRequest, - HttpContentType::kText, - fmt::format("Failed to parse package request, reason: '{}'", ex.what())); - return false; - } - if (!Cb) - { - HttpReq.WriteResponse(HttpResponseCode::BadRequest, - HttpContentType::kText, - "Content format not supported, expected package message format"); - return false; - } - break; - default: - HttpReq.WriteResponse(HttpResponseCode::BadRequest, HttpContentType::kText, "Invalid request content type"); - return false; + } } - - Ref<ProjectStore::Project> Project = OpenProject(ProjectId); - if (!Project) + else if (CbArrayView ChunksArray = Cb["chunks"sv].AsArrayView(); ChunksArray) { - HttpReq.WriteResponse(HttpResponseCode::NotFound, - HttpContentType::kText, - fmt::format("Rpc oplog request for unknown project '{}'", ProjectId)); - return true; - } - Project->TouchProject(); + // Legacy full chunks only by rawhash - std::string_view Method = Cb["method"sv].AsString(); + size_t RequestCount = ChunksArray.Num(); - bool VerifyPathOnDisk = Method != "getchunks"sv; + Requests.reserve(RequestCount); - ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, VerifyPathOnDisk); - if (!Oplog) - { - HttpReq.WriteResponse(HttpResponseCode::NotFound, - HttpContentType::kText, - fmt::format("Rpc oplog request for unknown oplog '{}/{}'", ProjectId, OplogId)); - return true; - } - Project->TouchOplog(OplogId); - - if (Method == "import"sv) - { - if (!AreDiskWritesAllowed()) - { - HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); - return true; - } - std::pair<HttpResponseCode, std::string> Result = Import(*Project.Get(), *Oplog, Cb["params"sv].AsObjectView(), AuthManager); - if (Result.second.empty()) + std::vector<IoHash> Cids; + Cids.reserve(ChunksArray.Num()); + for (CbFieldView FieldView : ChunksArray) { - HttpReq.WriteResponse(Result.first); - return Result.first != HttpResponseCode::BadRequest; + Requests.push_back(ProjectStore::ChunkRequest{.Id = FieldView.AsHash()}); } - HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); - return true; } - else if (Method == "export"sv) + else { - std::pair<HttpResponseCode, std::string> Result = Export(Project, *Oplog, Cb["params"sv].AsObjectView(), AuthManager); - HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); - return true; + throw std::runtime_error(fmt::format("oplog '{}/{}': malformed getchunks rpc request object", Project.Identifier, Oplog.OplogId())); } - else if (Method == "getchunks"sv) - { - ZEN_TRACE_CPU("Store::Rpc::getchunks"); + return Requests; +} - RpcAcceptOptions AcceptFlags = static_cast<RpcAcceptOptions>(Cb["AcceptFlags"sv].AsUInt16(0u)); - int32_t TargetProcessId = Cb["Pid"sv].AsInt32(0); +CbPackage +ProjectStore::WriteChunksRequestResponse(Project& Project, + Oplog& Oplog, + std::vector<ChunkRequest>&& Requests, + std::vector<ChunkResult>&& Results) +{ + using namespace std::literals; - CbPackage ResponsePackage; - std::pair<HttpResponseCode, std::string> Result = GetChunks(ProjectId, OplogId, Cb, ResponsePackage); - if (Result.first == HttpResponseCode::OK) - { - void* TargetProcessHandle = nullptr; - FormatFlags Flags = FormatFlags::kDefault; - if (EnumHasAllFlags(AcceptFlags, RpcAcceptOptions::kAllowLocalReferences)) - { - Flags |= FormatFlags::kAllowLocalReferences; - if (!EnumHasAnyFlags(AcceptFlags, RpcAcceptOptions::kAllowPartialLocalReferences)) - { - Flags |= FormatFlags::kDenyPartialLocalReferences; - } - TargetProcessHandle = m_OpenProcessCache.GetProcessHandle(HttpReq.SessionId(), TargetProcessId); - } + CbPackage ResponsePackage; - CompositeBuffer RpcResponseBuffer = FormatPackageMessageBuffer(ResponsePackage, Flags, TargetProcessHandle); - HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kCbPackage, RpcResponseBuffer); - } - else - { - HttpReq.WriteResponse(Result.first, HttpContentType::kText, Result.second); - } - return true; - } - else if (Method == "putchunks"sv) + CbObjectWriter ResponseWriter(32 + Requests.size() * 64u); + ResponseWriter.BeginArray("Chunks"sv); { - ZEN_TRACE_CPU("Store::Rpc::putchunks"); - if (!AreDiskWritesAllowed()) + for (size_t Index = 0; Index < Requests.size(); Index++) { - HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); - return true; - } - - std::span<const CbAttachment> Attachments = Package.GetAttachments(); - if (!Attachments.empty()) - { - std::vector<IoBuffer> WriteAttachmentBuffers; - std::vector<IoHash> WriteRawHashes; - - WriteAttachmentBuffers.reserve(Attachments.size()); - WriteRawHashes.reserve(Attachments.size()); - - for (const CbAttachment& Attachment : Attachments) + const ChunkRequest& Request = Requests[Index]; + ChunkResult& Result = Results[Index]; + if (Result.Exists) { - IoHash RawHash = Attachment.GetHash(); - const CompressedBuffer& Compressed = Attachment.AsCompressedBinary(); - WriteAttachmentBuffers.push_back(Compressed.GetCompressed().Flatten().AsIoBuffer()); - WriteRawHashes.push_back(RawHash); - } - - Oplog->CaptureAddedAttachments(WriteRawHashes); - m_CidStore.AddChunks(WriteAttachmentBuffers, WriteRawHashes, CidStore::InsertMode::kCopyOnly); - } - HttpReq.WriteResponse(HttpResponseCode::OK); - return true; - } - else if (Method == "snapshot"sv) - { - ZEN_TRACE_CPU("Store::Rpc::snapshot"); - if (!AreDiskWritesAllowed()) - { - HttpReq.WriteResponse(HttpResponseCode::InsufficientStorage); - return true; - } - - // Snapshot all referenced files. This brings the content of all - // files into the CID store - - uint32_t OpCount = 0; - uint64_t InlinedBytes = 0; - uint64_t InlinedFiles = 0; - uint64_t TotalBytes = 0; - uint64_t TotalFiles = 0; - - std::vector<CbObject> NewOps; - struct AddedChunk - { - IoBuffer Buffer; - uint64_t RawSize = 0; - }; - tsl::robin_map<IoHash, AddedChunk, IoHash::Hasher> AddedChunks; - - Oplog->IterateOplog( - [&](CbObjectView Op) { - bool OpRewritten = false; - bool AllOk = true; - - CbWriter FilesArrayWriter; - FilesArrayWriter.BeginArray("files"sv); - - for (CbFieldView& Field : Op["files"sv]) + ResponseWriter.BeginObject(); { - bool CopyField = true; - - if (CbObjectView View = Field.AsObjectView()) + if (Request.Id.index() == 0) { - const IoHash DataHash = View["data"sv].AsHash(); - - if (DataHash == IoHash::Zero) + const IoHash& RawHash = std::get<IoHash>(Request.Id); + ResponseWriter.AddHash("Id", RawHash); + } + else + { + const Oid& Id = std::get<Oid>(Request.Id); + ResponseWriter.AddObjectId("Id", Id); + } + if (!Request.ModTag.has_value() || Request.ModTag.value() != Result.ModTag) + { + ResponseWriter.AddInteger("ModTag", Result.ModTag); + if (!Request.SkipData) { - std::string_view ServerPath = View["serverpath"sv].AsString(); - std::filesystem::path FilePath = Project->RootDir / ServerPath; - BasicFile DataFile; - std::error_code Ec; - DataFile.Open(FilePath, BasicFile::Mode::kRead, Ec); - - if (Ec) - { - // Error... - - ZEN_ERROR("unable to read data from file '{}': {}", FilePath, Ec.message()); - - AllOk = false; - } - else + auto ExtractRangeResult = ExtractRange(std::move(Result.ChunkBuffer), + Request.Offset, + Request.Size, + ZenContentType::kCompressedBinary); + if (ExtractRangeResult.Error == GetChunkRangeResult::EError::Ok) { - // Read file contents into memory, compress and keep in map of chunks to add to Cid store - IoBuffer FileIoBuffer = DataFile.ReadAll(); - CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer(std::move(FileIoBuffer))); - const uint64_t RawSize = Compressed.DecodeRawSize(); - const IoHash RawHash = Compressed.DecodeRawHash(); - if (!AddedChunks.contains(RawHash)) + if (ExtractRangeResult.ContentType == ZenContentType::kCompressedBinary) { - const std::filesystem::path TempChunkPath = Oplog->TempPath() / RawHash.ToHexString(); - BasicFile ChunkTempFile; - ChunkTempFile.Open(TempChunkPath, BasicFile::Mode::kTruncateDelete); - ChunkTempFile.Write(Compressed.GetCompressed(), 0, Ec); - if (Ec) + ZEN_ASSERT(ExtractRangeResult.RawHash != IoHash::Zero); + CompressedBuffer CompressedValue = + CompressedBuffer::FromCompressedNoValidate(std::move(ExtractRangeResult.Chunk)); + ZEN_ASSERT(CompressedValue); + + if (ExtractRangeResult.RawSize != 0) { - Oid ChunkId = View["id"sv].AsObjectId(); - ZEN_ERROR("unable to write external file as compressed chunk '{}', id {}: {}", - FilePath, - ChunkId, - Ec.message()); - AllOk = false; + // This really could use some thought so we don't send the same data if we get a request for + // multiple ranges from the same chunk block + + uint64_t FragmentRawOffset = 0; + OodleCompressor Compressor; + OodleCompressionLevel CompressionLevel; + uint64_t BlockSize = 0; + if (CompressedValue.TryGetCompressParameters(Compressor, CompressionLevel, BlockSize)) + { + if (BlockSize > 0) + { + FragmentRawOffset = (Request.Offset / BlockSize) * BlockSize; + } + else + { + FragmentRawOffset = Request.Offset; + } + uint64_t FragmentRawLength = CompressedValue.DecodeRawSize(); + + IoHashStream FragmentHashStream; + FragmentHashStream.Append(ExtractRangeResult.RawHash.Hash, + sizeof(ExtractRangeResult.RawHash.Hash)); + FragmentHashStream.Append(&FragmentRawOffset, sizeof(FragmentRawOffset)); + FragmentHashStream.Append(&FragmentRawLength, sizeof(FragmentRawLength)); + IoHash FragmentHash = FragmentHashStream.GetHash(); + + ResponseWriter.AddHash("FragmentHash", FragmentHash); + ResponseWriter.AddInteger("FragmentOffset", FragmentRawOffset); + ResponseWriter.AddInteger("RawSize", ExtractRangeResult.RawSize); + ResponsePackage.AddAttachment(CbAttachment(CompressedValue, FragmentHash)); + } + else + { + std::string ErrorString = "Failed to get compression parameters from partial compressed buffer"; + ResponseWriter.AddString("Error", ErrorString); + ZEN_WARN("oplog '{}/{}': {}", Project.Identifier, Oplog.OplogId(), ErrorString); + } } else { - void* FileHandle = ChunkTempFile.Detach(); - IoBuffer ChunkBuffer(IoBuffer::File, - FileHandle, - 0, - Compressed.GetCompressed().GetSize(), - /*IsWholeFile*/ true); - ChunkBuffer.SetDeleteOnClose(true); - AddedChunks.insert_or_assign(RawHash, - AddedChunk{.Buffer = std::move(ChunkBuffer), .RawSize = RawSize}); + ResponseWriter.AddHash("RawHash"sv, ExtractRangeResult.RawHash); + ResponsePackage.AddAttachment(CbAttachment(std::move(CompressedValue), ExtractRangeResult.RawHash)); } } - - TotalBytes += RawSize; - ++TotalFiles; - - // Rewrite file array entry with new data reference - CbObjectWriter Writer(View.GetSize()); - RewriteCbObject(Writer, View, [&](CbObjectWriter&, CbFieldView Field) -> bool { - if (Field.GetName() == "data"sv) + else + { + IoHashStream HashStream; + ZEN_ASSERT(Request.Id.index() == 1); + const Oid& Id = std::get<Oid>(Request.Id); + HashStream.Append(Id.OidBits, sizeof(Id.OidBits)); + HashStream.Append(&Request.Offset, sizeof(Request.Offset)); + HashStream.Append(&Request.Size, sizeof(Request.Size)); + IoHash Hash = HashStream.GetHash(); + + ResponseWriter.AddHash("Hash"sv, Hash); + if (ExtractRangeResult.RawSize != 0) { - // omit this field as we will write it explicitly ourselves - return true; + ResponseWriter.AddInteger("Size", ExtractRangeResult.RawSize); } - return false; - }); - Writer.AddBinaryAttachment("data"sv, RawHash); - - CbObject RewrittenOp = Writer.Save(); - FilesArrayWriter.AddObject(std::move(RewrittenOp)); - CopyField = false; + ResponsePackage.AddAttachment(CbAttachment(std::move(ExtractRangeResult.Chunk), Hash)); + } + } + else + { + std::string ErrorString = + fmt::format("Failed fetching chunk range ({})", ExtractRangeResult.ErrorDescription); + ResponseWriter.AddString("Error", ErrorString); + ZEN_WARN("oplog '{}/{}': {}", Project.Identifier, Oplog.OplogId(), ErrorString); } } } - - if (CopyField) - { - FilesArrayWriter.AddField(Field); - } - else - { - OpRewritten = true; - } - } - - if (OpRewritten && AllOk) - { - FilesArrayWriter.EndArray(); - CbArray FilesArray = FilesArrayWriter.Save().AsArray(); - - CbObject RewrittenOp = RewriteCbObject(Op, [&](CbObjectWriter& NewWriter, CbFieldView Field) -> bool { - if (Field.GetName() == "files"sv) - { - NewWriter.AddArray("files"sv, FilesArray); - - return true; - } - - return false; - }); - - NewOps.push_back(std::move(RewrittenOp)); } - - OpCount++; - }, - Oplog::Paging{}); - - CbObjectWriter ResponseObj; - - // Persist rewritten oplog entries - if (!NewOps.empty()) - { - ResponseObj.BeginArray("rewritten_ops"); - - for (CbObject& NewOp : NewOps) - { - uint32_t NewLsn = Oplog->AppendNewOplogEntry(std::move(NewOp)); - - ZEN_DEBUG("appended rewritten op at LSN: {}", NewLsn); - - ResponseObj.AddInteger(NewLsn); + ResponseWriter.EndObject(); } - - ResponseObj.EndArray(); } - - // Ops that have moved chunks to a compressed buffer for storage in m_CidStore have been rewritten with references to the new - // chunk(s). Make sure we add the chunks to m_CidStore, and do it after we update the oplog so GC doesn't think we have - // unreferenced chunks. - for (auto It : AddedChunks) - { - const IoHash& RawHash = It.first; - AddedChunk& Chunk = It.second; - CidStore::InsertResult Result = m_CidStore.AddChunk(Chunk.Buffer, RawHash); - if (Result.New) - { - InlinedBytes += Chunk.RawSize; - ++InlinedFiles; - } - } - - ResponseObj << "inlined_bytes" << InlinedBytes << "inlined_files" << InlinedFiles; - ResponseObj << "total_bytes" << TotalBytes << "total_files" << TotalFiles; - - ZEN_INFO("oplog '{}/{}': rewrote {} oplog entries (out of {})", ProjectId, OplogId, NewOps.size(), OpCount); - - HttpReq.WriteResponse(HttpResponseCode::OK, ResponseObj.Save()); - return true; } - HttpReq.WriteResponse(HttpResponseCode::OK, HttpContentType::kText, fmt::format("Unknown rpc method '{}'", Method)); - return true; -} - -std::pair<HttpResponseCode, std::string> -ProjectStore::Export(Ref<ProjectStore::Project> Project, ProjectStore::Oplog& Oplog, CbObjectView&& Params, AuthMgr& AuthManager) -{ - ZEN_MEMSCOPE(GetProjectstoreTag()); - ZEN_TRACE_CPU("Store::Export"); - - using namespace std::literals; + ResponseWriter.EndArray(); + ResponsePackage.SetObject(ResponseWriter.Save()); - size_t MaxBlockSize = Params["maxblocksize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxBlockSize); - size_t MaxChunkEmbedSize = Params["maxchunkembedsize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxChunkEmbedSize); - size_t ChunkFileSizeLimit = Params["chunkfilesizelimit"sv].AsUInt64(RemoteStoreOptions::DefaultChunkFileSizeLimit); - bool Force = Params["force"sv].AsBool(false); - bool IgnoreMissingAttachments = Params["ignoremissingattachments"sv].AsBool(false); - bool EmbedLooseFile = Params["embedloosefiles"sv].AsBool(false); - - CreateRemoteStoreResult RemoteStoreResult = CreateRemoteStore(Params, AuthManager, MaxBlockSize, MaxChunkEmbedSize, Oplog.TempPath()); - - if (RemoteStoreResult.Store == nullptr) - { - return {HttpResponseCode::BadRequest, RemoteStoreResult.Description}; - } - std::shared_ptr<RemoteProjectStore> RemoteStore = std::move(RemoteStoreResult.Store); - RemoteProjectStore::RemoteStoreInfo StoreInfo = RemoteStore->GetInfo(); - - JobId JobId = m_JobQueue.QueueJob( - fmt::format("Export oplog '{}/{}'", Project->Identifier, Oplog.OplogId()), - [this, - ActualRemoteStore = std::move(RemoteStore), - Project, - OplogPtr = &Oplog, - MaxBlockSize, - MaxChunkEmbedSize, - ChunkFileSizeLimit, - EmbedLooseFile, - Force, - IgnoreMissingAttachments](JobContext& Context) { - Context.ReportMessage(fmt::format("Saving oplog '{}/{}' to {}, maxblocksize {}, maxchunkembedsize {}", - Project->Identifier, - OplogPtr->OplogId(), - ActualRemoteStore->GetInfo().Description, - NiceBytes(MaxBlockSize), - NiceBytes(MaxChunkEmbedSize))); - - RemoteProjectStore::Result Result = SaveOplog(m_CidStore, - *ActualRemoteStore, - *Project.Get(), - *OplogPtr, - MaxBlockSize, - MaxChunkEmbedSize, - ChunkFileSizeLimit, - EmbedLooseFile, - Force, - IgnoreMissingAttachments, - &Context); - auto Response = ConvertResult(Result); - ZEN_INFO("SaveOplog: Status: {} '{}'", ToString(Response.first), Response.second); - if (!IsHttpSuccessCode(Response.first)) - { - throw JobError(Response.second.empty() ? fmt::format("Status: {}", ToString(Response.first)) : Response.second, - (int)Response.first); - } - }); - - return {HttpResponseCode::Accepted, fmt::format("{}", JobId.Id)}; -} - -std::pair<HttpResponseCode, std::string> -ProjectStore::Import(ProjectStore::Project& Project, ProjectStore::Oplog& Oplog, CbObjectView&& Params, AuthMgr& AuthManager) -{ - ZEN_MEMSCOPE(GetProjectstoreTag()); - ZEN_TRACE_CPU("Store::Import"); - - using namespace std::literals; - - size_t MaxBlockSize = Params["maxblocksize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxBlockSize); - size_t MaxChunkEmbedSize = Params["maxchunkembedsize"sv].AsUInt64(RemoteStoreOptions::DefaultMaxChunkEmbedSize); - bool Force = Params["force"sv].AsBool(false); - bool IgnoreMissingAttachments = Params["ignoremissingattachments"sv].AsBool(false); - bool CleanOplog = Params["clean"].AsBool(false); - - CreateRemoteStoreResult RemoteStoreResult = CreateRemoteStore(Params, AuthManager, MaxBlockSize, MaxChunkEmbedSize, Oplog.TempPath()); - - if (RemoteStoreResult.Store == nullptr) - { - return {HttpResponseCode::BadRequest, RemoteStoreResult.Description}; - } - std::shared_ptr<RemoteProjectStore> RemoteStore = std::move(RemoteStoreResult.Store); - RemoteProjectStore::RemoteStoreInfo StoreInfo = RemoteStore->GetInfo(); - - JobId JobId = m_JobQueue.QueueJob( - fmt::format("Import oplog '{}/{}'", Project.Identifier, Oplog.OplogId()), - [this, - ChunkStore = &m_CidStore, - ActualRemoteStore = std::move(RemoteStore), - OplogPtr = &Oplog, - Force, - IgnoreMissingAttachments, - CleanOplog](JobContext& Context) { - Context.ReportMessage(fmt::format("Loading oplog '{}/{}' from {}", - OplogPtr->GetOuterProject()->Identifier, - OplogPtr->OplogId(), - ActualRemoteStore->GetInfo().Description)); - - RemoteProjectStore::Result Result = - LoadOplog(m_CidStore, *ActualRemoteStore, *OplogPtr, Force, IgnoreMissingAttachments, CleanOplog, &Context); - auto Response = ConvertResult(Result); - ZEN_INFO("LoadOplog: Status: {} '{}'", ToString(Response.first), Response.second); - if (!IsHttpSuccessCode(Response.first)) - { - throw JobError(Response.second.empty() ? fmt::format("Status: {}", ToString(Response.first)) : Response.second, - (int)Response.first); - } - }); - - return {HttpResponseCode::Accepted, fmt::format("{}", JobId.Id)}; + return ResponsePackage; } bool @@ -5995,19 +5435,35 @@ public: CompactOplogCount += OplogsToCompact.size(); for (const std::string& OplogId : OplogsToCompact) { - ProjectStore::Oplog* OpLog = nullptr; + Ref<ProjectStore::Oplog> OpLog; { RwLock::SharedLockScope __(Project->m_ProjectLock); if (auto OpIt = Project->m_Oplogs.find(OplogId); OpIt != Project->m_Oplogs.end()) { - OpLog = OpIt->second.get(); + OpLog = OpIt->second; } else { + Stopwatch OplogTimer; std::filesystem::path OplogBasePath = Project->BasePathForOplog(OplogId); - OpLog = - new ProjectStore::Oplog(OplogId, Project.Get(), Project->m_CidStore, OplogBasePath, std::filesystem::path{}); + OpLog = new ProjectStore::Oplog( + Project->Log(), + Project->Identifier, + OplogId, + Project->m_CidStore, + OplogBasePath, + std::filesystem::path{}, + ProjectStore::Oplog::EMode::kFull); // We need it to be a full read so we can write a new index snapshot OpLog->Read(); + if (Ctx.Settings.Verbose) + { + ZEN_INFO("GCV2: projectstore [COMPACT] '{}': read oplog '{}/{}' at '{}' in {}", + m_BasePath, + Project->Identifier, + OplogId, + OplogBasePath, + NiceTimeSpanMs(OplogTimer.GetElapsedTimeMs())); + } } if (OpLog) @@ -6024,11 +5480,6 @@ public: Stats.RemovedDisk += FreedSize; } - - if (auto OpIt = Project->m_Oplogs.find(OplogId); OpIt == Project->m_Oplogs.end()) - { - delete OpLog; - } } } } @@ -6117,6 +5568,16 @@ ProjectStore::RemoveExpiredData(GcCtx& Ctx, GcStats& Stats) { ExpiredOplogs.push_back(OplogId); } + else if (!Project->IsOplogTouchedSince(GcClock::Now() - std::chrono::minutes(15), OplogId)) + { + if (Project->TryUnloadOplog(OplogId)) + { + ZEN_INFO("GCV2: projectstore [REMOVE EXPIRED] '{}': Unloaded oplog {}/{} due to inactivity", + m_ProjectBasePath, + Project->Identifier, + OplogId); + } + } } std::filesystem::path ProjectPath = BasePathForProject(Project->Identifier); @@ -6208,7 +5669,7 @@ public: Stopwatch Timer; - std::vector<ProjectStore::Oplog*> AddedOplogs; + std::vector<Ref<ProjectStore::Oplog>> AddedOplogs; const auto _ = MakeGuard([&] { if (!Ctx.Settings.Verbose) @@ -6230,7 +5691,7 @@ public: ProjectStore::Project& Project = *It->second; for (auto& OplogPair : Project.m_Oplogs) { - ProjectStore::Oplog* Oplog = OplogPair.second.get(); + Ref<ProjectStore::Oplog> Oplog = OplogPair.second; AddedOplogs.push_back(Oplog); } } @@ -6244,13 +5705,13 @@ public: { if (auto It = Project.m_Oplogs.find(OplogName); It != Project.m_Oplogs.end()) { - ProjectStore::Oplog* Oplog = It->second.get(); + Ref<ProjectStore::Oplog> Oplog = It->second; AddedOplogs.push_back(Oplog); } } } - for (ProjectStore::Oplog* Oplog : AddedOplogs) + for (const Ref<ProjectStore::Oplog>& Oplog : AddedOplogs) { size_t BaseReferenceCount = m_References.size(); @@ -6326,13 +5787,14 @@ public: { m_Project->DisableUpdateCapture(); - RwLock::SharedLockScope _(m_Project->m_ProjectLock); - if (auto It = m_Project->m_Oplogs.find(m_OplogId); It != m_Project->m_Oplogs.end()) + if (m_OplogHasUpdateCapture) { - ProjectStore::Oplog* Oplog = It->second.get(); - if (Oplog == m_OplogWithUpdateCapture) + RwLock::SharedLockScope _(m_Project->m_ProjectLock); + if (auto It = m_Project->m_Oplogs.find(m_OplogId); It != m_Project->m_Oplogs.end()) { + Ref<ProjectStore::Oplog> Oplog = It->second; Oplog->DisableUpdateCapture(); + m_OplogHasUpdateCapture = false; } } } @@ -6364,50 +5826,61 @@ public: m_OplogId); }); - ProjectStore::Oplog* Oplog = nullptr; - auto __ = MakeGuard([this, &Oplog]() { - if (Oplog != nullptr && m_OplogWithUpdateCapture == nullptr) - { - delete Oplog; - } - }); - m_OplogBasePath = m_Project->BasePathForOplog(m_OplogId); - - RwLock::SharedLockScope ___(m_Project->m_ProjectLock); - if (auto It = m_Project->m_Oplogs.find(m_OplogId); It != m_Project->m_Oplogs.end()) - { - It->second->EnableUpdateCapture(); - Oplog = It->second.get(); - m_OplogWithUpdateCapture = Oplog; - } - else if (ProjectStore::Oplog::ExistsAt(m_OplogBasePath)) - { - Oplog = new ProjectStore::Oplog(m_OplogId, m_Project.Get(), m_Project->m_CidStore, m_OplogBasePath, std::filesystem::path{}); - Oplog->Read(); - } - else + m_OplogBasePath = m_Project->BasePathForOplog(m_OplogId); { - return; - } + Ref<ProjectStore::Oplog> Oplog; - RwLock::SharedLockScope ____(Oplog->m_OplogLock); - if (Ctx.IsCancelledFlag) - { - return; - } + RwLock::SharedLockScope __(m_Project->m_ProjectLock); + if (auto It = m_Project->m_Oplogs.find(m_OplogId); It != m_Project->m_Oplogs.end()) + { + Oplog = It->second; + Oplog->EnableUpdateCapture(); + m_OplogHasUpdateCapture = true; + } + else if (ProjectStore::Oplog::ExistsAt(m_OplogBasePath)) + { + Stopwatch OplogTimer; + Oplog = new ProjectStore::Oplog(m_Project->Log(), + m_Project->Identifier, + m_OplogId, + m_Project->m_CidStore, + m_OplogBasePath, + std::filesystem::path{}, + ProjectStore::Oplog::EMode::kBasicReadOnly); + Oplog->Read(); + if (Ctx.Settings.Verbose) + { + ZEN_INFO("GCV2: projectstore [PRECACHE] '{}': read oplog '{}/{}' in {}", + m_OplogBasePath, + m_Project->Identifier, + m_OplogId, + NiceTimeSpanMs(OplogTimer.GetElapsedTimeMs())); + } + } + else + { + return; + } - GcClock::TimePoint CompactExpireTime = GcClock::Now() - std::chrono::minutes(30); - if (!m_Project->IsOplogTouchedSince(CompactExpireTime, m_OplogId)) - { - const uint32_t CompactUnusedThreshold = 25; - if (Oplog->GetUnusedSpacePercent() >= CompactUnusedThreshold) + RwLock::SharedLockScope ___(Oplog->m_OplogLock); + if (Ctx.IsCancelledFlag) { - m_Project->AddOplogToCompact(m_OplogId); + return; + } + + GcClock::TimePoint CompactExpireTime = GcClock::Now() - std::chrono::minutes(30); + if (!m_Project->IsOplogTouchedSince(CompactExpireTime, m_OplogId)) + { + const uint32_t CompactUnusedThreshold = 25; + if (Oplog->GetUnusedSpacePercent() >= CompactUnusedThreshold) + { + m_Project->AddOplogToCompact(m_OplogId); + } } - } - Oplog->GetAttachmentsLocked(m_References, Ctx.Settings.StoreProjectAttachmentMetaData); - m_OplogAccessTime = m_Project->LastOplogAccessTime(m_OplogId); + Oplog->GetAttachmentsLocked(m_References, Ctx.Settings.StoreProjectAttachmentMetaData); + m_OplogAccessTime = m_Project->LastOplogAccessTime(m_OplogId); + } FilterReferences(Ctx, fmt::format("projectstore [PRECACHE] '{}'", m_OplogBasePath), m_References); } @@ -6433,8 +5906,9 @@ public: if (auto It = m_Project->m_Oplogs.find(m_OplogId); It != m_Project->m_Oplogs.end()) { - ProjectStore::Oplog* Oplog = It->second.get(); - Oplog->IterateCapturedLSNsLocked([&](const CbObjectView& UpdateOp) -> bool { + Ref<ProjectStore::Oplog> Oplog = It->second; + Oplog->IterateCapturedOpsLocked([&](const Oid& Key, ProjectStore::LogSequenceNumber LSN, const CbObjectView& UpdateOp) -> bool { + ZEN_UNUSED(Key, LSN); UpdateOp.IterateAttachments([&](CbFieldView Visitor) { m_AddedReferences.emplace_back(Visitor.AsAttachment()); }); return true; }); @@ -6447,16 +5921,37 @@ public: } else if (m_Project->LastOplogAccessTime(m_OplogId) > m_OplogAccessTime && ProjectStore::Oplog::ExistsAt(m_OplogBasePath)) { - ProjectStore::Oplog* Oplog = - new ProjectStore::Oplog(m_OplogId, m_Project.Get(), m_Project->m_CidStore, m_OplogBasePath, std::filesystem::path{}); - auto __ = MakeGuard([Oplog]() { - if (Oplog != nullptr) + Stopwatch OplogTimer; + { + Ref<ProjectStore::Oplog> Oplog(new ProjectStore::Oplog(m_Project->Log(), + m_Project->Identifier, + m_OplogId, + m_Project->m_CidStore, + m_OplogBasePath, + std::filesystem::path{}, + ProjectStore::Oplog::EMode::kBasicReadOnly)); + Oplog->Read(); + if (Ctx.Settings.Verbose) { - delete Oplog; + ZEN_INFO("GCV2: projectstore [LOCKSTATE] '{}': read oplog '{}/{}' in {}", + m_OplogBasePath, + m_Project->Identifier, + m_OplogId, + NiceTimeSpanMs(OplogTimer.GetElapsedTimeMs())); } - }); - Oplog->Read(); - Oplog->GetAttachmentsLocked(m_AddedReferences, Ctx.Settings.StoreProjectAttachmentMetaData); + + OplogTimer.Reset(); + + Oplog->GetAttachmentsLocked(m_AddedReferences, Ctx.Settings.StoreProjectAttachmentMetaData); + } + if (Ctx.Settings.Verbose) + { + ZEN_INFO("GCV2: projectstore [LOCKSTATE] '{}': read referenced attachments from oplog '{}/{}' in {}", + m_OplogBasePath, + m_Project->Identifier, + m_OplogId, + NiceTimeSpanMs(OplogTimer.GetElapsedTimeMs())); + } } FilterReferences(Ctx, fmt::format("projectstore [LOCKSTATE] '{}'", m_OplogBasePath), m_AddedReferences); } @@ -6495,7 +5990,7 @@ public: Ref<ProjectStore::Project> m_Project; std::string m_OplogId; std::filesystem::path m_OplogBasePath; - ProjectStore::Oplog* m_OplogWithUpdateCapture = nullptr; + bool m_OplogHasUpdateCapture = false; std::vector<IoHash> m_References; std::vector<IoHash> m_AddedReferences; GcClock::TimePoint m_OplogAccessTime; @@ -6608,7 +6103,7 @@ public: ProjectStore::Oplog::ValidationResult Result; Stopwatch Timer; - const auto _ = MakeGuard([&] { + const auto _ = MakeGuard([&] { if (!Ctx.Settings.Verbose) { return; @@ -6619,42 +6114,52 @@ public: m_OplogId, NiceTimeSpanMs(Timer.GetElapsedTimeMs()), Result.OpCount, - Result.LSNLow, - Result.LSNHigh, + Result.LSNLow.Number, + Result.LSNHigh.Number, Status); }); - ProjectStore::Oplog* TempOplog = nullptr; - auto __ = MakeGuard([this, &TempOplog]() { - if (TempOplog != nullptr) - { - delete TempOplog; - } - }); - ProjectStore::Oplog* Oplog = nullptr; - Ref<ProjectStore::Project> Project = m_ProjectStore.OpenProject(m_ProjectId); + Ref<ProjectStore::Oplog> Oplog; + Ref<ProjectStore::Project> Project = m_ProjectStore.OpenProject(m_ProjectId); if (Project) { - RwLock::SharedLockScope ___(Project->m_ProjectLock); - if (auto It = Project->m_Oplogs.find(m_OplogId); It != Project->m_Oplogs.end()) - { - Oplog = It->second.get(); - } - else { - std::filesystem::path OplogBasePath = Project->BasePathForOplog(m_OplogId); - TempOplog = new ProjectStore::Oplog(m_OplogId, Project.Get(), Project->m_CidStore, OplogBasePath, std::filesystem::path{}); - Oplog = TempOplog; - Oplog->Read(); - - if (Ctx.IsCancelledFlag) + RwLock::SharedLockScope __(Project->m_ProjectLock); + if (auto It = Project->m_Oplogs.find(m_OplogId); It != Project->m_Oplogs.end()) { - return; + Oplog = It->second; + } + else + { + Stopwatch OplogTimer; + + std::filesystem::path OplogBasePath = Project->BasePathForOplog(m_OplogId); + Oplog = Ref<ProjectStore::Oplog>(new ProjectStore::Oplog(Project->Log(), + Project->Identifier, + m_OplogId, + Project->m_CidStore, + OplogBasePath, + std::filesystem::path{}, + ProjectStore::Oplog::EMode::kBasicReadOnly)); + Oplog->Read(); + if (Ctx.Settings.Verbose) + { + ZEN_INFO("GCV2: projectstore [VALIDATE] '{}': read oplog '{}/{}' in {}", + OplogBasePath, + Project->Identifier, + m_OplogId, + NiceTimeSpanMs(OplogTimer.GetElapsedTimeMs())); + } + + if (Ctx.IsCancelledFlag) + { + return; + } } } - if (Oplog != nullptr) + if (Oplog) { - Result = Oplog->Validate(Ctx.IsCancelledFlag, nullptr); + Result = Oplog->Validate(Project->RootDir, Ctx.IsCancelledFlag, nullptr); if (Ctx.IsCancelledFlag) { return; @@ -6691,6 +6196,9 @@ ProjectStore::CreateReferenceValidators(GcCtx& Ctx) { return {}; } + + auto Log = [&Ctx]() { return Ctx.Logger; }; + DiscoverProjects(); std::vector<std::pair<std::string, std::string>> Oplogs; @@ -6721,14 +6229,8 @@ ProjectStore::CreateReferenceValidators(GcCtx& Ctx) Oid OpKeyStringAsOid(std::string_view OpKey) { - using namespace std::literals; - - CbObjectWriter Writer; - Writer << "key"sv << OpKey; - - const Oid OpId = ComputeOpKey(Writer.Save()); - - return OpId; + eastl::fixed_vector<uint8_t, 512> Buffer; + return OpKeyStringAsOid(OpKey, Buffer); } ////////////////////////////////////////////////////////////////////////// @@ -6738,14 +6240,14 @@ OpKeyStringAsOid(std::string_view OpKey) namespace testutils { using namespace std::literals; - std::string OidAsString(const Oid& Id) + static std::string OidAsString(const Oid& Id) { StringBuilder<25> OidStringBuilder; Id.ToString(OidStringBuilder); return OidStringBuilder.ToString(); } - CbPackage CreateBulkDataOplogPackage(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments) + static CbPackage CreateBulkDataOplogPackage(const Oid& Id, const std::span<const std::pair<Oid, CompressedBuffer>>& Attachments) { CbPackage Package; CbObjectWriter Object; @@ -6771,9 +6273,9 @@ namespace testutils { return Package; }; - CbPackage CreateFilesOplogPackage(const Oid& Id, - const std::filesystem::path ProjectRootDir, - const std::span<const std::pair<Oid, std::filesystem::path>>& Attachments) + static CbPackage CreateFilesOplogPackage(const Oid& Id, + const std::filesystem::path ProjectRootDir, + const std::span<const std::pair<Oid, std::filesystem::path>>& Attachments) { CbPackage Package; CbObjectWriter Object; @@ -6797,7 +6299,7 @@ namespace testutils { return Package; }; - std::vector<std::pair<Oid, CompressedBuffer>> CreateAttachments( + static std::vector<std::pair<Oid, CompressedBuffer>> CreateAttachments( const std::span<const size_t>& Sizes, OodleCompressionLevel CompressionLevel = OodleCompressionLevel::VeryFast, uint64_t BlockSize = 0) @@ -6813,7 +6315,7 @@ namespace testutils { return Result; } - uint64_t GetCompressedOffset(const CompressedBuffer& Buffer, uint64_t RawOffset) + static uint64_t GetCompressedOffset(const CompressedBuffer& Buffer, uint64_t RawOffset) { if (RawOffset > 0) { @@ -6945,8 +6447,6 @@ TEST_CASE("project.store.create") ScopedTemporaryDirectory TempDir; - auto JobQueue = MakeJobQueue(1, ""sv); - OpenProcessCache ProcessCache; GcManager Gc; CidStore CidStore(Gc); CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; @@ -6954,7 +6454,7 @@ TEST_CASE("project.store.create") std::string_view ProjectName("proj1"sv); std::filesystem::path BasePath = TempDir.Path() / "projectstore"; - ProjectStore ProjectStore(CidStore, BasePath, Gc, *JobQueue, ProcessCache, ProjectStore::Configuration{}); + ProjectStore ProjectStore(CidStore, BasePath, Gc, ProjectStore::Configuration{}); std::filesystem::path RootDir = TempDir.Path() / "root"; std::filesystem::path EngineRootDir = TempDir.Path() / "engine"; std::filesystem::path ProjectRootDir = TempDir.Path() / "game"; @@ -6976,15 +6476,13 @@ TEST_CASE("project.store.lifetimes") ScopedTemporaryDirectory TempDir; - auto JobQueue = MakeJobQueue(1, ""sv); - OpenProcessCache ProcessCache; GcManager Gc; CidStore CidStore(Gc); CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; CidStore.Initialize(CidConfig); std::filesystem::path BasePath = TempDir.Path() / "projectstore"; - ProjectStore ProjectStore(CidStore, BasePath, Gc, *JobQueue, ProcessCache, ProjectStore::Configuration{}); + ProjectStore ProjectStore(CidStore, BasePath, Gc, ProjectStore::Configuration{}); std::filesystem::path RootDir = TempDir.Path() / "root"; std::filesystem::path EngineRootDir = TempDir.Path() / "engine"; std::filesystem::path ProjectRootDir = TempDir.Path() / "game"; @@ -6996,145 +6494,19 @@ TEST_CASE("project.store.lifetimes") EngineRootDir.string(), ProjectRootDir.string(), ProjectFilePath.string())); - ProjectStore::Oplog* Oplog = Project->NewOplog("oplog1", {}); - CHECK(Oplog != nullptr); + Ref<ProjectStore::Oplog> Oplog = Project->NewOplog("oplog1", {}); + CHECK(Oplog); std::filesystem::path DeletePath; CHECK(Project->PrepareForDelete(DeletePath)); CHECK(!DeletePath.empty()); - CHECK(Project->OpenOplog("oplog1", /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true) == nullptr); + CHECK(!Project->OpenOplog("oplog1", /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true)); // Oplog is now invalid, but pointer can still be accessed since we store old oplog pointers CHECK(Oplog->OplogCount() == 0); // Project is still valid since we have a Ref to it CHECK(Project->Identifier == "proj1"sv); } -struct ExportForceDisableBlocksTrue_ForceTempBlocksFalse -{ - static const bool ForceDisableBlocks = true; - static const bool ForceEnableTempBlocks = false; -}; - -struct ExportForceDisableBlocksFalse_ForceTempBlocksFalse -{ - static const bool ForceDisableBlocks = false; - static const bool ForceEnableTempBlocks = false; -}; - -struct ExportForceDisableBlocksFalse_ForceTempBlocksTrue -{ - static const bool ForceDisableBlocks = false; - static const bool ForceEnableTempBlocks = true; -}; - -TEST_CASE_TEMPLATE("project.store.export", - Settings, - ExportForceDisableBlocksTrue_ForceTempBlocksFalse, - ExportForceDisableBlocksFalse_ForceTempBlocksFalse, - ExportForceDisableBlocksFalse_ForceTempBlocksTrue) -{ - using namespace std::literals; - using namespace testutils; - - ScopedTemporaryDirectory TempDir; - ScopedTemporaryDirectory ExportDir; - - auto JobQueue = MakeJobQueue(1, ""sv); - OpenProcessCache ProcessCache; - GcManager Gc; - CidStore CidStore(Gc); - CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; - CidStore.Initialize(CidConfig); - - std::filesystem::path BasePath = TempDir.Path() / "projectstore"; - ProjectStore ProjectStore(CidStore, BasePath, Gc, *JobQueue, ProcessCache, ProjectStore::Configuration{}); - std::filesystem::path RootDir = TempDir.Path() / "root"; - std::filesystem::path EngineRootDir = TempDir.Path() / "engine"; - std::filesystem::path ProjectRootDir = TempDir.Path() / "game"; - std::filesystem::path ProjectFilePath = TempDir.Path() / "game" / "game.uproject"; - - Ref<ProjectStore::Project> Project(ProjectStore.NewProject(BasePath / "proj1"sv, - "proj1"sv, - RootDir.string(), - EngineRootDir.string(), - ProjectRootDir.string(), - ProjectFilePath.string())); - ProjectStore::Oplog* Oplog = Project->NewOplog("oplog1", {}); - CHECK(Oplog != nullptr); - - Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), {})); - Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{77}))); - Oplog->AppendNewOplogEntry( - CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{7123, 583, 690, 99}))); - Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{55, 122}))); - Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage( - Oid::NewOid(), - CreateAttachments(std::initializer_list<size_t>{256u * 1024u, 92u * 1024u}, OodleCompressionLevel::None))); - - FileRemoteStoreOptions Options = { - RemoteStoreOptions{.MaxBlockSize = 64u * 1024, .MaxChunkEmbedSize = 32 * 1024u, .ChunkFileSizeLimit = 64u * 1024u}, - /*.FolderPath = */ ExportDir.Path(), - /*.Name = */ std::string("oplog1"), - /*OptionalBaseName = */ std::string(), - /*.ForceDisableBlocks = */ Settings::ForceDisableBlocks, - /*.ForceEnableTempBlocks = */ Settings::ForceEnableTempBlocks}; - std::shared_ptr<RemoteProjectStore> RemoteStore = CreateFileRemoteStore(Options); - RemoteProjectStore::RemoteStoreInfo StoreInfo = RemoteStore->GetInfo(); - - RemoteProjectStore::Result ExportResult = SaveOplog(CidStore, - *RemoteStore, - *Project.Get(), - *Oplog, - Options.MaxBlockSize, - Options.MaxChunkEmbedSize, - Options.ChunkFileSizeLimit, - true, - false, - false, - nullptr); - - CHECK(ExportResult.ErrorCode == 0); - - ProjectStore::Oplog* OplogImport = Project->NewOplog("oplog2", {}); - CHECK(OplogImport != nullptr); - - RemoteProjectStore::Result ImportResult = LoadOplog(CidStore, - *RemoteStore, - *OplogImport, - /*Force*/ false, - /*IgnoreMissingAttachments*/ false, - /*CleanOplog*/ false, - nullptr); - CHECK(ImportResult.ErrorCode == 0); - - RemoteProjectStore::Result ImportForceResult = LoadOplog(CidStore, - *RemoteStore, - *OplogImport, - /*Force*/ true, - /*IgnoreMissingAttachments*/ false, - /*CleanOplog*/ false, - nullptr); - CHECK(ImportForceResult.ErrorCode == 0); - - RemoteProjectStore::Result ImportCleanResult = LoadOplog(CidStore, - *RemoteStore, - *OplogImport, - /*Force*/ false, - /*IgnoreMissingAttachments*/ false, - /*CleanOplog*/ true, - nullptr); - CHECK(ImportCleanResult.ErrorCode == 0); - - RemoteProjectStore::Result ImportForceCleanResult = LoadOplog(CidStore, - *RemoteStore, - *OplogImport, - /*Force*/ true, - /*IgnoreMissingAttachments*/ false, - /*CleanOplog*/ true, - nullptr); - CHECK(ImportForceCleanResult.ErrorCode == 0); -} - TEST_CASE("project.store.gc") { using namespace std::literals; @@ -7142,15 +6514,13 @@ TEST_CASE("project.store.gc") ScopedTemporaryDirectory TempDir; - auto JobQueue = MakeJobQueue(1, ""sv); - OpenProcessCache ProcessCache; GcManager Gc; CidStore CidStore(Gc); CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; CidStore.Initialize(CidConfig); std::filesystem::path BasePath = TempDir.Path() / "projectstore"; - ProjectStore ProjectStore(CidStore, BasePath, Gc, *JobQueue, ProcessCache, ProjectStore::Configuration{}); + ProjectStore ProjectStore(CidStore, BasePath, Gc, ProjectStore::Configuration{}); std::filesystem::path RootDir = TempDir.Path() / "root"; std::filesystem::path EngineRootDir = TempDir.Path() / "engine"; @@ -7195,8 +6565,8 @@ TEST_CASE("project.store.gc") EngineRootDir.string(), Project1RootDir.string(), Project1FilePath.string())); - ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1", Project1OplogPath); - CHECK(Oplog != nullptr); + Ref<ProjectStore::Oplog> Oplog = Project1->NewOplog("oplog1", Project1OplogPath); + CHECK(Oplog); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), {})); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{77}))); @@ -7213,8 +6583,8 @@ TEST_CASE("project.store.gc") Project2RootDir.string(), Project2FilePath.string())); { - ProjectStore::Oplog* Oplog = Project2->NewOplog("oplog2", Project2Oplog1Path); - CHECK(Oplog != nullptr); + Ref<ProjectStore::Oplog> Oplog = Project2->NewOplog("oplog2", Project2Oplog1Path); + CHECK(Oplog); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), {})); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{177}))); @@ -7224,8 +6594,8 @@ TEST_CASE("project.store.gc") CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{535, 221}))); } { - ProjectStore::Oplog* Oplog = Project2->NewOplog("oplog3", Project2Oplog2Path); - CHECK(Oplog != nullptr); + Ref<ProjectStore::Oplog> Oplog = Project2->NewOplog("oplog3", Project2Oplog2Path); + CHECK(Oplog); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), {})); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), CreateAttachments(std::initializer_list<size_t>{137}))); @@ -7343,15 +6713,13 @@ TEST_CASE("project.store.gc.prep") ScopedTemporaryDirectory TempDir; - auto JobQueue = MakeJobQueue(1, ""sv); - OpenProcessCache ProcessCache; GcManager Gc; CidStore CidStore(Gc); CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas", .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; CidStore.Initialize(CidConfig); std::filesystem::path BasePath = TempDir.Path() / "projectstore"; - ProjectStore ProjectStore(CidStore, BasePath, Gc, *JobQueue, ProcessCache, ProjectStore::Configuration{}); + ProjectStore ProjectStore(CidStore, BasePath, Gc, ProjectStore::Configuration{}); std::filesystem::path RootDir = TempDir.Path() / "root"; std::filesystem::path EngineRootDir = TempDir.Path() / "engine"; @@ -7383,7 +6751,7 @@ TEST_CASE("project.store.gc.prep") EngineRootDir.string(), Project1RootDir.string(), Project1FilePath.string())); - ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); + Ref<ProjectStore::Oplog> Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), OpAttachments)); } { @@ -7414,13 +6782,13 @@ TEST_CASE("project.store.gc.prep") { // Make sure the chunks are stored but not the referencing op Ref<ProjectStore::Project> Project1 = ProjectStore.OpenProject("proj1"sv); - ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); + Ref<ProjectStore::Oplog> Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), OpAttachments)); Project1->DeleteOplog("oplog1"sv); } { Ref<ProjectStore::Project> Project1 = ProjectStore.OpenProject("proj1"sv); - ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); + Ref<ProjectStore::Oplog> Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); // Equivalent of a `prep` call with tracking of ops CHECK(Oplog->CheckPendingChunkReferences(OpChunkHashes, std::chrono::hours(1)).empty()); @@ -7461,7 +6829,7 @@ TEST_CASE("project.store.gc.prep") { Ref<ProjectStore::Project> Project1 = ProjectStore.OpenProject("proj1"sv); - ProjectStore::Oplog* Oplog = Project1->OpenOplog("oplog1"sv, true, true); + Ref<ProjectStore::Oplog> Oplog = Project1->OpenOplog("oplog1"sv, true, true); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), OpAttachments)); Oplog->RemovePendingChunkReferences(OpChunkHashes); CHECK(Oplog->GetPendingChunkReferencesLocked().size() == 0); @@ -7495,15 +6863,16 @@ TEST_CASE("project.store.gc.prep") { // Make sure the chunks are stored but not the referencing op Ref<ProjectStore::Project> Project1 = ProjectStore.OpenProject("proj1"sv); - ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); + Ref<ProjectStore::Oplog> Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(Oid::NewOid(), OpAttachments)); Project1->DeleteOplog("oplog1"sv); } - // Caution - putting breakpoints and stepping through this part of the test likely makes it fails due to expiry time of pending chunks + // Caution - putting breakpoints and stepping through this part of the test likely makes it fails due to expiry time of pending + // chunks { Ref<ProjectStore::Project> Project1 = ProjectStore.OpenProject("proj1"sv); - ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); + Ref<ProjectStore::Oplog> Oplog = Project1->NewOplog("oplog1"sv, Project1OplogPath); CHECK(Oplog->CheckPendingChunkReferences(OpChunkHashes, std::chrono::milliseconds(100)).empty()); } @@ -7522,8 +6891,8 @@ TEST_CASE("project.store.gc.prep") } Sleep(200); - // This pass they should also be retained since our age retention has kept them alive and they will now be picked up and the retention - // cleared + // This pass they should also be retained since our age retention has kept them alive and they will now be picked up and the + // retention cleared { GcSettings Settings = {.CacheExpireTime = GcClock::Now(), .ProjectStoreExpireTime = GcClock::Now(), @@ -7558,15 +6927,13 @@ TEST_CASE("project.store.rpc.getchunks") ScopedTemporaryDirectory TempDir; - auto JobQueue = MakeJobQueue(1, ""sv); - OpenProcessCache ProcessCache; GcManager Gc; CidStore CidStore(Gc); CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas"sv, .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; CidStore.Initialize(CidConfig); std::filesystem::path BasePath = TempDir.Path() / "projectstore"sv; - ProjectStore ProjectStore(CidStore, BasePath, Gc, *JobQueue, ProcessCache, ProjectStore::Configuration{}); + ProjectStore ProjectStore(CidStore, BasePath, Gc, ProjectStore::Configuration{}); std::filesystem::path RootDir = TempDir.Path() / "root"sv; std::filesystem::path EngineRootDir = TempDir.Path() / "engine"sv; @@ -7590,8 +6957,8 @@ TEST_CASE("project.store.rpc.getchunks") EngineRootDir.string(), Project1RootDir.string(), Project1FilePath.string())); - ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1"sv, {}); - CHECK(Oplog != nullptr); + Ref<ProjectStore::Oplog> Oplog = Project1->NewOplog("oplog1"sv, {}); + CHECK(Oplog); Attachments[OpIds[0]] = {}; Attachments[OpIds[1]] = CreateAttachments(std::initializer_list<size_t>{77}); Attachments[OpIds[2]] = @@ -7610,24 +6977,30 @@ TEST_CASE("project.store.rpc.getchunks") Oplog->AppendNewOplogEntry(CreateFilesOplogPackage(FilesOpId, RootDir, FilesOpIdAttachments)); } + auto GetChunks = [](zen::ProjectStore& ProjectStore, ProjectStore::Project& Project, ProjectStore::Oplog& Oplog, const CbObject& Cb) { + std::vector<ProjectStore::ChunkRequest> Requests = ProjectStore.ParseChunksRequests(Project, Oplog, Cb); + std::vector<ProjectStore::ChunkResult> Results = + Requests.empty() ? std::vector<ProjectStore::ChunkResult>{} : ProjectStore.GetChunks(Project, Oplog, Requests); + return ProjectStore.WriteChunksRequestResponse(Project, Oplog, std::move(Requests), std::move(Results)); + }; + + Ref<ProjectStore::Project> Project1 = ProjectStore.OpenProject("proj1"sv); + CHECK(Project1); + Ref<ProjectStore::Oplog> Oplog1 = Project1->OpenOplog("oplog1"sv, false, true); + CHECK(Oplog1); // Invalid request { CbObjectWriter Request; Request.BeginObject("WrongName"sv); Request.EndObject(); CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, "oplog1"sv, Request.Save(), Response); - CHECK_EQ(HttpResponseCode::BadRequest, Result.first); + CHECK_THROWS(GetChunks(ProjectStore, *Project1, *Oplog1, Request.Save())); } // Empty request { - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, std::vector<IoHash>{}, {}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ false, std::vector<IoHash>{}, {}, {})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(0, Chunks.Num()); @@ -7635,23 +7008,15 @@ TEST_CASE("project.store.rpc.getchunks") // Single non-existing chunk by RawHash IoHash NotFoundIoHash = IoHash::Max; { - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {NotFoundIoHash}, {}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ false, {NotFoundIoHash}, {}, {})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(0, Chunks.Num()); } { - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {NotFoundIoHash}, {}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ true, {NotFoundIoHash}, {}, {})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(0, Chunks.Num()); @@ -7659,21 +7024,15 @@ TEST_CASE("project.store.rpc.getchunks") // Single non-existing chunk by Id Oid NotFoundId = Oid::NewOid(); { - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {NotFoundId}, {}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ false, {NotFoundId}, {}, {})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(0, Chunks.Num()); } { - CbPackage Response; - auto Result = - ProjectStore.GetChunks("proj1"sv, "oplog1"sv, testutils::BuildChunksRequest(/*SkipData*/ true, {NotFoundId}, {}, {}), Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ true, {NotFoundId}, {}, {})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(0, Chunks.Num()); @@ -7684,13 +7043,11 @@ TEST_CASE("project.store.rpc.getchunks") IoHash FirstAttachmentHash = Attachments[OpIds[2]][1].second.DecodeRawHash(); uint64_t ResponseModTag = 0; { - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentHash}, {}, {}), - Response); + const CbPackage& Response = GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentHash}, {}, {})); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(1, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7710,13 +7067,11 @@ TEST_CASE("project.store.rpc.getchunks") } // Fetch with matching ModTag { - CbPackage Response; - auto Result = - ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentHash}, {}, {ResponseModTag}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentHash}, {}, {ResponseModTag})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7731,14 +7086,12 @@ TEST_CASE("project.store.rpc.getchunks") } // Fetch with mismatching ModTag { - CbPackage Response; - auto Result = ProjectStore.GetChunks( - "proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentHash}, {}, {uint64_t(ResponseModTag + 1)}), - Response); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentHash}, {}, {uint64_t(ResponseModTag + 1)})); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(1, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7758,12 +7111,10 @@ TEST_CASE("project.store.rpc.getchunks") } // Fresh modtime query { - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentHash}, {}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentHash}, {}, {})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7778,13 +7129,11 @@ TEST_CASE("project.store.rpc.getchunks") } // Modtime query with matching ModTag { - CbPackage Response; - auto Result = - ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentHash}, {}, {ResponseModTag}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentHash}, {}, {ResponseModTag})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7799,13 +7148,11 @@ TEST_CASE("project.store.rpc.getchunks") } // Modtime query with mismatching ModTag { - CbPackage Response; - auto Result = ProjectStore.GetChunks( - "proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentHash}, {}, {uint64_t(ResponseModTag + 1)}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentHash}, {}, {uint64_t(ResponseModTag + 1)})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7825,12 +7172,8 @@ TEST_CASE("project.store.rpc.getchunks") uint64_t ResponseModTag = 0; { // Full chunk request - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {})); CHECK_EQ(1, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7850,13 +7193,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Partial chunk request - CbPackage Response; - auto Result = - ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {{130 * 1024, 8100}}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {{130 * 1024, 8100}}, {})); CHECK_EQ(1, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7881,13 +7222,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Fetch with matching ModTag - CbPackage Response; - auto Result = - ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {ResponseModTag}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {ResponseModTag})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7902,13 +7241,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Fetch with mismatching ModTag - CbPackage Response; - auto Result = ProjectStore.GetChunks( - "proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {uint64_t(ResponseModTag + 1)}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {uint64_t(ResponseModTag + 1)})); CHECK_EQ(1, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7928,12 +7265,8 @@ TEST_CASE("project.store.rpc.getchunks") } // Fresh modtime query { - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7948,13 +7281,11 @@ TEST_CASE("project.store.rpc.getchunks") } // Modtime query with matching ModTag { - CbPackage Response; - auto Result = - ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {ResponseModTag}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {ResponseModTag})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7969,13 +7300,11 @@ TEST_CASE("project.store.rpc.getchunks") } // Modtime query with mismatching ModTag { - CbPackage Response; - auto Result = ProjectStore.GetChunks( - "proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {uint64_t(ResponseModTag + 1)}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {uint64_t(ResponseModTag + 1)})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -7996,12 +7325,8 @@ TEST_CASE("project.store.rpc.getchunks") uint64_t ResponseModTag = 0; { // Full chunk request - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {})); CHECK_EQ(1, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -8021,13 +7346,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Partial chunk request - CbPackage Response; - auto Result = - ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {{81823, 5434}}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {{81823, 5434}}, {})); CHECK_EQ(1, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -8049,13 +7372,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Fetch with matching ModTag - CbPackage Response; - auto Result = - ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {ResponseModTag}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {ResponseModTag})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -8070,13 +7391,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Fetch with mismatching ModTag - CbPackage Response; - auto Result = ProjectStore.GetChunks( - "proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {uint64_t(ResponseModTag + 1)}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, {FirstAttachmentId}, {}, {uint64_t(ResponseModTag + 1)})); CHECK_EQ(1, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -8096,12 +7415,8 @@ TEST_CASE("project.store.rpc.getchunks") } // Fresh modtime query { - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -8116,13 +7431,11 @@ TEST_CASE("project.store.rpc.getchunks") } // Modtime query with matching ModTag { - CbPackage Response; - auto Result = - ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {ResponseModTag}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {ResponseModTag})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -8137,13 +7450,11 @@ TEST_CASE("project.store.rpc.getchunks") } // Modtime query with mismatching ModTag { - CbPackage Response; - auto Result = ProjectStore.GetChunks( - "proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {uint64_t(ResponseModTag + 1)}), - Response); - CHECK_EQ(HttpResponseCode::OK, Result.first); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, {FirstAttachmentId}, {}, {uint64_t(ResponseModTag + 1)})); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(1, Chunks.Num()); @@ -8169,13 +7480,9 @@ TEST_CASE("project.store.rpc.getchunks") std::vector<uint64_t> ResponseModTags(3, 0); { // Fresh fetch - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, AttachmentHashes, {}, {}), - Response); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ false, AttachmentHashes, {}, {})); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(3, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8202,13 +7509,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Fetch with matching ModTag - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, AttachmentHashes, {}, ResponseModTags), - Response); + const CbPackage& Response = GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, AttachmentHashes, {}, ResponseModTags)); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8229,13 +7534,9 @@ TEST_CASE("project.store.rpc.getchunks") } { // Fresh modtime query - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, AttachmentHashes, {}, {}), - Response); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ true, AttachmentHashes, {}, {})); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8256,13 +7557,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Modtime query with matching ModTags - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, AttachmentHashes, {}, ResponseModTags), - Response); + const CbPackage& Response = GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, AttachmentHashes, {}, ResponseModTags)); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8288,13 +7587,12 @@ TEST_CASE("project.store.rpc.getchunks") { Tag++; } - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, AttachmentHashes, {}, MismatchingModTags), - Response); + const CbPackage& Response = + GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, AttachmentHashes, {}, MismatchingModTags)); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8326,13 +7624,9 @@ TEST_CASE("project.store.rpc.getchunks") std::vector<uint64_t> ResponseModTags(3, 0); { // Fresh fetch - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, AttachedIds, {}, {}), - Response); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ false, AttachedIds, {}, {})); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(3, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8359,13 +7653,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Fetch with matching ModTag - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ false, AttachedIds, {}, ResponseModTags), - Response); + const CbPackage& Response = GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ false, AttachedIds, {}, ResponseModTags)); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8386,13 +7678,9 @@ TEST_CASE("project.store.rpc.getchunks") } { // Fresh modtime query - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, AttachedIds, {}, {}), - Response); + const CbPackage& Response = + GetChunks(ProjectStore, *Project1, *Oplog1, testutils::BuildChunksRequest(/*SkipData*/ true, AttachedIds, {}, {})); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8413,13 +7701,11 @@ TEST_CASE("project.store.rpc.getchunks") } { // Modtime query with matching ModTags - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, AttachedIds, {}, ResponseModTags), - Response); + const CbPackage& Response = GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, AttachedIds, {}, ResponseModTags)); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8445,13 +7731,11 @@ TEST_CASE("project.store.rpc.getchunks") { Tag++; } - CbPackage Response; - auto Result = ProjectStore.GetChunks("proj1"sv, - "oplog1"sv, - testutils::BuildChunksRequest(/*SkipData*/ true, AttachedIds, {}, MismatchingModTags), - Response); + const CbPackage& Response = GetChunks(ProjectStore, + *Project1, + *Oplog1, + testutils::BuildChunksRequest(/*SkipData*/ true, AttachedIds, {}, MismatchingModTags)); - CHECK_EQ(HttpResponseCode::OK, Result.first); CHECK_EQ(0, Response.GetAttachments().size()); CbArrayView Chunks = Response.GetObject()["Chunks"].AsArrayView(); CHECK_EQ(3, Chunks.Num()); @@ -8480,15 +7764,13 @@ TEST_CASE("project.store.partial.read") ScopedTemporaryDirectory TempDir; - auto JobQueue = MakeJobQueue(1, ""sv); - OpenProcessCache ProcessCache; GcManager Gc; CidStore CidStore(Gc); CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas"sv, .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; CidStore.Initialize(CidConfig); std::filesystem::path BasePath = TempDir.Path() / "projectstore"sv; - ProjectStore ProjectStore(CidStore, BasePath, Gc, *JobQueue, ProcessCache, ProjectStore::Configuration{}); + ProjectStore ProjectStore(CidStore, BasePath, Gc, ProjectStore::Configuration{}); std::filesystem::path RootDir = TempDir.Path() / "root"sv; std::filesystem::path EngineRootDir = TempDir.Path() / "engine"sv; @@ -8510,8 +7792,8 @@ TEST_CASE("project.store.partial.read") EngineRootDir.string(), Project1RootDir.string(), Project1FilePath.string())); - ProjectStore::Oplog* Oplog = Project1->NewOplog("oplog1"sv, {}); - CHECK(Oplog != nullptr); + Ref<ProjectStore::Oplog> Oplog = Project1->NewOplog("oplog1"sv, {}); + CHECK(Oplog); Attachments[OpIds[0]] = {}; Attachments[OpIds[1]] = CreateAttachments(std::initializer_list<size_t>{77}); Attachments[OpIds[2]] = CreateAttachments(std::initializer_list<size_t>{7123, 9583, 690, 99}); @@ -8521,80 +7803,84 @@ TEST_CASE("project.store.partial.read") Oplog->AppendNewOplogEntry(CreateBulkDataOplogPackage(It.first, It.second)); } } + Ref<ProjectStore::Project> Project1 = ProjectStore.OpenProject("proj1"sv); + CHECK(Project1); + Ref<ProjectStore::Oplog> Oplog1 = Project1->OpenOplog("oplog1"sv, false, true); + CHECK(Oplog1); { uint64_t ModificationTag = 0; - IoBuffer Chunk; - CHECK(ProjectStore - .GetChunk("proj1"sv, "oplog1"sv, Attachments[OpIds[1]][0].second.DecodeRawHash().ToHexString(), Chunk, &ModificationTag) - .first == HttpResponseCode::OK); + + auto Result = ProjectStore.GetChunkRange(Log(), + *Project1, + *Oplog1, + Attachments[OpIds[1]][0].first, + 0, + ~0ull, + ZenContentType::kCompressedBinary, + &ModificationTag); + + CHECK_EQ(ProjectStore::GetChunkRangeResult::EError::Ok, Result.Error); + IoHash RawHash; uint64_t RawSize; - CompressedBuffer Attachment = CompressedBuffer::FromCompressed(SharedBuffer(Chunk), RawHash, RawSize); + CompressedBuffer Attachment = CompressedBuffer::FromCompressed(Result.Chunk, RawHash, RawSize); CHECK(RawSize == Attachments[OpIds[1]][0].second.DecodeRawSize()); - CHECK(ModificationTag != 0); - CHECK(ProjectStore - .GetChunk("proj1"sv, "oplog1"sv, Attachments[OpIds[1]][0].second.DecodeRawHash().ToHexString(), Chunk, &ModificationTag) - .first == HttpResponseCode::NotModified); + auto Result2 = ProjectStore.GetChunkRange(Log(), + *Project1, + *Oplog1, + Attachments[OpIds[1]][0].first, + 0, + ~0ull, + ZenContentType::kCompressedBinary, + &ModificationTag); + CHECK_EQ(ProjectStore::GetChunkRangeResult::EError::NotModified, Result2.Error); } { uint64_t FullChunkModificationTag = 0; { - CompositeBuffer ChunkResult; - HttpContentType ContentType; - CHECK(ProjectStore - .GetChunkRange("proj1"sv, - "oplog1"sv, - OidAsString(Attachments[OpIds[2]][1].first), - 0, - ~0ull, - HttpContentType::kCompressedBinary, - ChunkResult, - ContentType, - &FullChunkModificationTag) - .first == HttpResponseCode::OK); - CHECK(ChunkResult); - CHECK(CompressedBuffer::FromCompressedNoValidate(std::move(ChunkResult)).DecodeRawSize() == + auto Result = ProjectStore.GetChunkRange(Log(), + *Project1, + *Oplog1, + Attachments[OpIds[2]][1].first, + 0, + ~0ull, + ZenContentType::kCompressedBinary, + &FullChunkModificationTag); + CHECK_EQ(Result.Error, ProjectStore::GetChunkRangeResult::EError::Ok); + CHECK(Result.Chunk); + CHECK(CompressedBuffer::FromCompressedNoValidate(std::move(Result.Chunk)).DecodeRawSize() == Attachments[OpIds[2]][1].second.DecodeRawSize()); } { - CompositeBuffer ChunkResult; - HttpContentType ContentType; - CHECK(ProjectStore - .GetChunkRange("proj1"sv, - "oplog1"sv, - OidAsString(Attachments[OpIds[2]][1].first), - 0, - ~0ull, - HttpContentType::kCompressedBinary, - ChunkResult, - ContentType, - &FullChunkModificationTag) - .first == HttpResponseCode::NotModified); - } - } - { - CompositeBuffer PartialChunkResult; - uint64_t PartialChunkModificationTag = 0; - { - CompositeBuffer ChunkResult; - HttpContentType ContentType; - CHECK(ProjectStore - .GetChunkRange("proj1"sv, - "oplog1"sv, - OidAsString(Attachments[OpIds[2]][1].first), - 5, - 1773, - HttpContentType::kCompressedBinary, - PartialChunkResult, - ContentType, - &PartialChunkModificationTag) - .first == HttpResponseCode::OK); - CHECK(PartialChunkResult); + auto Result = ProjectStore.GetChunkRange(Log(), + *Project1, + *Oplog1, + Attachments[OpIds[2]][1].first, + 0, + ~0ull, + ZenContentType::kCompressedBinary, + &FullChunkModificationTag); + CHECK_EQ(Result.Error, ProjectStore::GetChunkRangeResult::EError::NotModified); + } + } + { + uint64_t PartialChunkModificationTag = 0; + { + auto Result = ProjectStore.GetChunkRange(Log(), + *Project1, + *Oplog1, + Attachments[OpIds[2]][1].first, + 5, + 1773, + ZenContentType::kCompressedBinary, + &PartialChunkModificationTag); + CHECK_EQ(Result.Error, ProjectStore::GetChunkRangeResult::EError::Ok); + IoHash PartialRawHash; uint64_t PartialRawSize; - CompressedBuffer PartialCompressedResult = CompressedBuffer::FromCompressed(PartialChunkResult, PartialRawHash, PartialRawSize); + CompressedBuffer PartialCompressedResult = CompressedBuffer::FromCompressed(Result.Chunk, PartialRawHash, PartialRawSize); CHECK(PartialRawSize >= 1773); uint64_t RawOffsetInPartialCompressed = GetCompressedOffset(PartialCompressedResult, 5); @@ -8606,51 +7892,19 @@ TEST_CASE("project.store.partial.read") } { - CompositeBuffer ChunkResult; - HttpContentType ContentType; - CHECK(ProjectStore - .GetChunkRange("proj1"sv, - "oplog1"sv, - OidAsString(Attachments[OpIds[2]][1].first), - 5, - 1773, - HttpContentType::kCompressedBinary, - PartialChunkResult, - ContentType, - &PartialChunkModificationTag) - .first == HttpResponseCode::NotModified); + auto Result = ProjectStore.GetChunkRange(Log(), + *Project1, + *Oplog1, + Attachments[OpIds[2]][1].first, + 0, + 1773, + ZenContentType::kCompressedBinary, + &PartialChunkModificationTag); + CHECK_EQ(Result.Error, ProjectStore::GetChunkRangeResult::EError::NotModified); } } } -TEST_CASE("project.store.block") -{ - using namespace std::literals; - using namespace testutils; - - std::vector<std::size_t> AttachmentSizes({7633, 6825, 5738, 8031, 7225, 566, 3656, 6006, 24, 3466, 1093, 4269, 2257, 3685, 3489, - 7194, 6151, 5482, 6217, 3511, 6738, 5061, 7537, 2759, 1916, 8210, 2235, 4024, 1582, 5251, - 491, 5464, 4607, 8135, 3767, 4045, 4415, 5007, 8876, 6761, 3359, 8526, 4097, 4855, 8225}); - - std::vector<std::pair<Oid, CompressedBuffer>> AttachmentsWithId = CreateAttachments(AttachmentSizes); - std::vector<std::pair<IoHash, FetchChunkFunc>> Chunks; - Chunks.reserve(AttachmentSizes.size()); - for (const auto& It : AttachmentsWithId) - { - Chunks.push_back( - std::make_pair(It.second.DecodeRawHash(), [Buffer = It.second](const IoHash&) -> std::pair<uint64_t, CompressedBuffer> { - return {Buffer.DecodeRawSize(), Buffer}; - })); - } - ChunkBlockDescription Block; - CompressedBuffer BlockBuffer = GenerateChunkBlock(std::move(Chunks), Block); - uint64_t HeaderSize; - CHECK(IterateChunkBlock( - BlockBuffer.Decompress(), - [](CompressedBuffer&&, const IoHash&) {}, - HeaderSize)); -} - TEST_CASE("project.store.iterateoplog") { using namespace std::literals; @@ -8658,15 +7912,13 @@ TEST_CASE("project.store.iterateoplog") ScopedTemporaryDirectory TempDir; - auto JobQueue = MakeJobQueue(1, ""sv); - OpenProcessCache ProcessCache; GcManager Gc; CidStore CidStore(Gc); CidStoreConfiguration CidConfig = {.RootDirectory = TempDir.Path() / "cas"sv, .TinyValueThreshold = 1024, .HugeValueThreshold = 4096}; CidStore.Initialize(CidConfig); std::filesystem::path BasePath = TempDir.Path() / "projectstore"sv; - ProjectStore ProjectStore(CidStore, BasePath, Gc, *JobQueue, ProcessCache, ProjectStore::Configuration{}); + ProjectStore ProjectStore(CidStore, BasePath, Gc, ProjectStore::Configuration{}); std::filesystem::path RootDir = TempDir.Path() / "root"sv; std::filesystem::path EngineRootDir = TempDir.Path() / "enginesv"; @@ -8690,8 +7942,8 @@ TEST_CASE("project.store.iterateoplog") EngineRootDir.string(), ProjectRootDir.string(), ProjectFilePath.string())); - ProjectStore::Oplog* Oplog = TestProject->NewOplog("oplog"sv, ProjectOplogPath); - CHECK(Oplog != nullptr); + Ref<ProjectStore::Oplog> Oplog = TestProject->NewOplog("oplog"sv, ProjectOplogPath); + CHECK(Oplog); struct TestOidData { @@ -8715,7 +7967,7 @@ TEST_CASE("project.store.iterateoplog") } }; auto IncrementCount = [&Count](CbObjectView /* Op */) { ++Count; }; - auto MarkFound = [&TestOids, &Count](uint32_t /* LSN */, const Oid& /* InId */, CbObjectView Op) { + auto MarkFound = [&TestOids, &Count](ProjectStore::LogSequenceNumber /* LSN */, const Oid& /* InId */, CbObjectView Op) { for (TestOidData& TestOid : TestOids) { if (Op["key"sv].AsString() == TestOid.Key) diff --git a/src/zenutil/referencemetadata.cpp b/src/zenstore/referencemetadata.cpp index bbcafcfba..e202b435b 100644 --- a/src/zenutil/referencemetadata.cpp +++ b/src/zenstore/referencemetadata.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/referencemetadata.h> +#include "referencemetadata.h" ////////////////////////////////////////////////////////////////////////// #include <zencore/xxhash.h> diff --git a/src/zenutil/include/zenutil/referencemetadata.h b/src/zenstore/referencemetadata.h index 5160bfb8b..5160bfb8b 100644 --- a/src/zenutil/include/zenutil/referencemetadata.h +++ b/src/zenstore/referencemetadata.h diff --git a/src/zenserver/vfs/vfsimpl.cpp b/src/zenstore/vfsimpl.cpp index 2bac6b756..0a918d452 100644 --- a/src/zenserver/vfs/vfsimpl.cpp +++ b/src/zenstore/vfsimpl.cpp @@ -1,13 +1,11 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include "vfsimpl.h" -#include "vfsservice.h" - -#include "projectstore/projectstore.h" +#include <zenstore/vfsimpl.h> #include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zenstore/cache/structuredcachestore.h> +#include <zenstore/projectstore.h> #include <zenvfs/projfs.h> #include <zenvfs/vfs.h> @@ -38,24 +36,13 @@ VfsOplogDataSource::ReadNamedData(std::string_view Path, void* Buffer, uint64_t void VfsOplogDataSource::ReadChunkData(const Oid& ChunkId, void* Buffer, uint64_t ByteOffset, uint64_t ByteCount) { - CompositeBuffer ChunkBuffer; - ZenContentType ContentType; - auto Result = m_ProjectStore->GetChunkRange(m_ProjectId, - m_OplogId, - ChunkId, - 0, - ~0ull, - ZenContentType::kCompressedBinary, - /* out */ ChunkBuffer, - /* out */ ContentType, - /* OptionalInOutModificationTag */ nullptr); - - if (Result.first == HttpResponseCode::OK) + IoBuffer ChunkBuffer = m_ProjectStore->GetChunk(m_ProjectId, m_OplogId, ChunkId); + if (ChunkBuffer) { ZEN_ASSERT(ChunkBuffer.GetSize() >= ByteOffset); ZEN_ASSERT(ChunkBuffer.GetSize() - ByteOffset >= ByteCount); MutableMemoryView Target(Buffer, ByteCount); - ChunkBuffer.CopyTo(Target, ByteOffset); + Target.CopyFrom(ChunkBuffer.GetView().Mid(ByteOffset, ByteCount)); } } @@ -117,17 +104,17 @@ VfsCacheDataSource::PopulateDirectory(std::string NodePath, VfsTreeNode& DirNode ////////////////////////////////////////////////////////////////////////// -VfsService::Impl::Impl() +VfsServiceImpl::VfsServiceImpl() { } -VfsService::Impl::~Impl() +VfsServiceImpl::~VfsServiceImpl() { Unmount(); } void -VfsService::Impl::Mount(std::string_view MountPoint) +VfsServiceImpl::Mount(std::string_view MountPoint) { ZEN_INFO("VFS mount requested at '{}'", MountPoint); @@ -149,7 +136,7 @@ VfsService::Impl::Mount(std::string_view MountPoint) } void -VfsService::Impl::Unmount() +VfsServiceImpl::Unmount() { if (m_MountpointPath.empty()) { @@ -164,7 +151,7 @@ VfsService::Impl::Unmount() } void -VfsService::Impl::AddService(Ref<ProjectStore>&& Ps) +VfsServiceImpl::AddService(Ref<ProjectStore>&& Ps) { m_ProjectStore = std::move(Ps); @@ -172,7 +159,7 @@ VfsService::Impl::AddService(Ref<ProjectStore>&& Ps) } void -VfsService::Impl::AddService(Ref<ZenCacheStore>&& Z$) +VfsServiceImpl::AddService(Ref<ZenCacheStore>&& Z$) { m_ZenCacheStore = std::move(Z$); @@ -180,7 +167,7 @@ VfsService::Impl::AddService(Ref<ZenCacheStore>&& Z$) } void -VfsService::Impl::RefreshVfs() +VfsServiceImpl::RefreshVfs() { if (m_VfsHost && m_MountpointPath.empty()) { @@ -195,7 +182,7 @@ VfsService::Impl::RefreshVfs() if (!m_VfsHost && !m_MountpointPath.empty()) { - m_VfsThread = std::thread(&VfsService::Impl::VfsThread, this); + m_VfsThread = std::thread(&VfsServiceImpl::VfsThread, this); m_VfsThreadRunning.Wait(); // At this stage, m_VfsHost should be initialized @@ -225,7 +212,7 @@ VfsService::Impl::RefreshVfs() } void -VfsService::Impl::VfsThread() +VfsServiceImpl::VfsThread() { SetCurrentThreadName("VFS"); @@ -363,12 +350,12 @@ VfsServiceDataSource::PopulateDirectory(std::string NodePath, VfsTreeNode& DirNo // Oplog contents enumeration - if (ProjectStore::Oplog* Oplog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true)) + if (Ref<ProjectStore::Oplog> Oplog = Project->OpenOplog(OplogId, /*AllowCompact*/ false, /*VerifyPathOnDisk*/ true)) { Ref<VfsOplogDataSource> DataSource = GetOplogDataSource(ProjectId, OplogId); // Get metadata for all chunks - std::vector<ProjectStore::Oplog::ChunkInfo> ChunkInfos = Oplog->GetAllChunksInfo(); + std::vector<ProjectStore::Oplog::ChunkInfo> ChunkInfos = Oplog->GetAllChunksInfo(Project->RootDir); std::unordered_map<zen::Oid, uint64_t> ChunkSizes; diff --git a/src/zenstore/workspaces.cpp b/src/zenstore/workspaces.cpp index 0ca2adab2..4e7bd79a3 100644 --- a/src/zenstore/workspaces.cpp +++ b/src/zenstore/workspaces.cpp @@ -622,17 +622,19 @@ Workspaces::GetWorkspaceShareChunks(const Oid& WorkspaceId, for (size_t Index = 0; Index < ChunkRequests.size(); Index++) { WorkLatch.AddCount(1); - WorkerPool.ScheduleWork([&, Index]() { - auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); }); - try - { - Chunks[Index] = GetOne(RootPath, *WorkspaceAndShare.second, ChunkRequests[Index]); - } - catch (const std::exception& Ex) - { - ZEN_WARN("Exception while fetching chunks, chunk {}: {}", ChunkRequests[Index].ChunkId, Ex.what()); - } - }); + WorkerPool.ScheduleWork( + [&, Index]() { + auto _ = MakeGuard([&WorkLatch]() { WorkLatch.CountDown(); }); + try + { + Chunks[Index] = GetOne(RootPath, *WorkspaceAndShare.second, ChunkRequests[Index]); + } + catch (const std::exception& Ex) + { + ZEN_WARN("Exception while fetching chunks, chunk {}: {}", ChunkRequests[Index].ChunkId, Ex.what()); + } + }, + WorkerThreadPool::EMode::DisableBacklog); } WorkLatch.CountDown(); WorkLatch.Wait(); diff --git a/src/zenstore/xmake.lua b/src/zenstore/xmake.lua index 031a66829..cf5f30d09 100644 --- a/src/zenstore/xmake.lua +++ b/src/zenstore/xmake.lua @@ -6,6 +6,6 @@ target('zenstore') add_headerfiles("**.h") add_files("**.cpp") add_includedirs("include", {public=true}) - add_deps("zencore", "zenutil") + add_deps("zencore", "zenutil", "zenvfs") add_packages("vcpkg::robin-map") add_packages("vcpkg::eastl", {public=true}); diff --git a/src/zenstore/zenstore.cpp b/src/zenstore/zenstore.cpp index 654fb3510..c563cc202 100644 --- a/src/zenstore/zenstore.cpp +++ b/src/zenstore/zenstore.cpp @@ -6,7 +6,9 @@ # 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> # include <zenstore/gc.h> # include <zenstore/hashkeyset.h> @@ -21,6 +23,7 @@ void zenstore_forcelinktests() { buildstore_forcelink(); + cachepolicy_forcelink(); CAS_forcelink(); filecas_forcelink(); blockstore_forcelink(); @@ -29,6 +32,7 @@ zenstore_forcelinktests() gc_forcelink(); hashkeyset_forcelink(); structured_cachestore_forcelink(); + prj_forcelink(); } } // namespace zen diff --git a/src/zenutil-test/xmake.lua b/src/zenutil-test/xmake.lua index 61a828f1c..061c79ddf 100644 --- a/src/zenutil-test/xmake.lua +++ b/src/zenutil-test/xmake.lua @@ -5,5 +5,5 @@ target("zenutil-test") set_group("tests") add_headerfiles("**.h") add_files("*.cpp") - add_deps("zenutil", "zencore") + add_deps("zenutil") add_packages("vcpkg::doctest") diff --git a/src/zenutil-test/zenutil-test.cpp b/src/zenutil-test/zenutil-test.cpp index eb4a17918..3e3a11a01 100644 --- a/src/zenutil-test/zenutil-test.cpp +++ b/src/zenutil-test/zenutil-test.cpp @@ -23,7 +23,15 @@ main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) zen::IgnoreChildSignals(); # endif - zen::TraceInit("zencore-test"); +# if ZEN_WITH_TRACE + zen::TraceInit("zenutil-test"); + zen::TraceOptions TraceCommandlineOptions; + if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) + { + TraceConfigure(TraceCommandlineOptions); + } +# endif // ZEN_WITH_TRACE + zen::logging::InitializeLogging(); zen::MaximizeOpenFileCount(); diff --git a/src/zenutil/commandlineoptions.cpp b/src/zenutil/commandlineoptions.cpp index afef7f6f2..040726c77 100644 --- a/src/zenutil/commandlineoptions.cpp +++ b/src/zenutil/commandlineoptions.cpp @@ -116,10 +116,15 @@ StripCommandlineQuotes(std::vector<std::string>& InOutArgs) { if (Arg.find('"', 1) == Arg.length() - 1) { - if (Arg.find(' ', 1) == std::string::npos) - { - Arg = Arg.substr(1, Arg.length() - 2); - } + Arg = Arg.substr(1, Arg.length() - 2); + } + } + else if (Arg.ends_with("\"")) + { + std::string::size_type EqualSign = Arg.find("=", 1); + if (EqualSign != std::string::npos && Arg[EqualSign + 1] == '\"') + { + Arg = Arg.substr(0, EqualSign + 1) + Arg.substr(EqualSign + 2, Arg.length() - (EqualSign + 2) - 1); } } RawArgs.push_back(const_cast<char*>(Arg.c_str())); @@ -209,12 +214,23 @@ TEST_CASE("CommandLine") CHECK_EQ(v2Stripped[5], std::string("--namespace=ue.oplog")); CHECK_EQ(v2Stripped[6], std::string("--bucket=citysample.packaged-build.fortnite-main.windows")); CHECK_EQ(v2Stripped[7], std::string("c:\\just\\a\\path")); - CHECK_EQ(v2Stripped[8], std::string("--access-token-path=\"C:\\Users\\dan.engelbrecht\\jupiter-token.json\"")); - CHECK_EQ(v2Stripped[9], std::string("\"D:\\Dev\\Spaced Folder\\Target\"")); - CHECK_EQ(v2Stripped[10], std::string("--alt-path=\"D:\\Dev\\Spaced Folder2\\Target\"")); + CHECK_EQ(v2Stripped[8], std::string("--access-token-path=C:\\Users\\dan.engelbrecht\\jupiter-token.json")); + CHECK_EQ(v2Stripped[9], std::string("D:\\Dev\\Spaced Folder\\Target")); + CHECK_EQ(v2Stripped[10], std::string("--alt-path=D:\\Dev\\Spaced Folder2\\Target")); CHECK_EQ(v2Stripped[11], std::string("07dn23ifiwesnvoasjncasab")); CHECK_EQ(v2Stripped[12], std::string("--build-part-name")); CHECK_EQ(v2Stripped[13], std::string("win64,linux,ps5")); + + std::vector<std::string> v3 = ParseCommandLine( + "--tracehost \"127.0.0.1\" builds download --url=\"https://jupiter.devtools.epicgames.com\" --build-part-name=\"win64\""); + std::vector<char*> v3Stripped = StripCommandlineQuotes(v3); + + CHECK_EQ(v3Stripped[0], std::string("--tracehost")); + CHECK_EQ(v3Stripped[1], std::string("127.0.0.1")); + CHECK_EQ(v3Stripped[2], std::string("builds")); + CHECK_EQ(v3Stripped[3], std::string("download")); + CHECK_EQ(v3Stripped[4], std::string("--url=https://jupiter.devtools.epicgames.com")); + CHECK_EQ(v3Stripped[5], std::string("--build-part-name=win64")); } #endif diff --git a/src/zenutil/include/zenutil/cache/cache.h b/src/zenutil/include/zenutil/cache/cache.h deleted file mode 100644 index 20299a667..000000000 --- a/src/zenutil/include/zenutil/cache/cache.h +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright Epic Games, Inc. All Rights Reserved. - -#pragma once - -#include <zenutil/cache/cachekey.h> -#include <zenutil/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}; -}; - -} // 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/zenutil/include/zenutil/cache/rpcrecording.h b/src/zenutil/include/zenutil/rpcrecording.h index f1ad35413..f1ad35413 100644 --- a/src/zenutil/include/zenutil/cache/rpcrecording.h +++ b/src/zenutil/include/zenutil/rpcrecording.h diff --git a/src/zenutil/include/zenutil/windows/service.h b/src/zenutil/include/zenutil/windows/windowsservice.h index ca0270a36..ca0270a36 100644 --- a/src/zenutil/include/zenutil/windows/service.h +++ b/src/zenutil/include/zenutil/windows/windowsservice.h diff --git a/src/zenutil/include/zenutil/zenserverprocess.h b/src/zenutil/include/zenutil/zenserverprocess.h index 563970363..b6a3d70ff 100644 --- a/src/zenutil/include/zenutil/zenserverprocess.h +++ b/src/zenutil/include/zenutil/zenserverprocess.h @@ -80,8 +80,9 @@ struct ZenServerInstance void EnableTermination() { m_Terminate = true; } void DisableShutdownOnDestroy() { m_ShutdownOnDestroy = false; } void Detach(); - inline int GetPid() { return m_Process.Pid(); } + inline int GetPid() const { return m_Process.Pid(); } inline void SetOwnerPid(int Pid) { m_OwnerPid = Pid; } + void* GetProcessHandle() const { return m_Process.Handle(); } bool IsRunning(); bool Terminate(); std::string GetLogOutput() const; diff --git a/src/zenutil/logging.cpp b/src/zenutil/logging.cpp index 8ff58ee73..806b96d52 100644 --- a/src/zenutil/logging.cpp +++ b/src/zenutil/logging.cpp @@ -42,6 +42,8 @@ InitializeLogging(const LoggingOptions& LogOptions) FinishInitializeLogging(LogOptions); } +static std::terminate_handler OldTerminateHandler = nullptr; + void BeginInitializeLogging(const LoggingOptions& LogOptions) { @@ -99,12 +101,43 @@ BeginInitializeLogging(const LoggingOptions& LogOptions) } } - std::set_terminate([]() { - void* Frames[8]; - uint32_t FrameCount = GetCallstack(2, 8, Frames); - CallstackFrames* Callstack = CreateCallstack(FrameCount, Frames); - ZEN_CRITICAL("Program exited abnormally via std::terminate()\n{}", CallstackToString(Callstack, " ")); - FreeCallstack(Callstack); + OldTerminateHandler = std::set_terminate([]() { + try + { + constexpr int SkipFrameCount = 4; + constexpr int FrameCount = 8; + uint8_t CallstackBuffer[CallstackRawMemorySize(SkipFrameCount, FrameCount)]; + CallstackFrames* Callstack = GetCallstackRaw(&CallstackBuffer[0], SkipFrameCount, FrameCount); + + fmt::basic_memory_buffer<char, 2048> Message; + auto Appender = fmt::appender(Message); + fmt::format_to(Appender, "Program exited abnormally via std::terminate()"); + + if (Callstack->FrameCount > 0) + { + fmt::format_to(Appender, "\n"); + + CallstackToStringRaw(Callstack, &Message, [](void* UserData, uint32_t FrameIndex, const char* FrameText) { + ZEN_UNUSED(FrameIndex); + fmt::basic_memory_buffer<char, 2048>* Message = (fmt::basic_memory_buffer<char, 2048>*)UserData; + auto Appender = fmt::appender(*Message); + fmt::format_to(Appender, " {}\n", FrameText); + }); + } + Message.push_back('\0'); + + // We use direct ZEN_LOG here instead of ZEN_ERROR as we don't care about *this* code location in the log + ZEN_LOG(Log(), zen::logging::level::Critical, "{}", Message.data()); + zen::logging::FlushLogging(); + } + catch (const std::exception&) + { + // Ignore any exceptions in terminate callback + } + if (OldTerminateHandler) + { + OldTerminateHandler(); + } }); // Default diff --git a/src/zenutil/cache/rpcrecording.cpp b/src/zenutil/rpcrecording.cpp index 46e80f6b7..54f27dee7 100644 --- a/src/zenutil/cache/rpcrecording.cpp +++ b/src/zenutil/rpcrecording.cpp @@ -1,6 +1,6 @@ // Copyright Epic Games, Inc. All Rights Reserved. -#include <zenutil/cache/rpcrecording.h> +#include <zenutil/rpcrecording.h> #include <zencore/basicfile.h> #include <zencore/compactbinarybuilder.h> diff --git a/src/zenutil/service.cpp b/src/zenutil/service.cpp index e4a9a951e..103fdaa2f 100644 --- a/src/zenutil/service.cpp +++ b/src/zenutil/service.cpp @@ -10,7 +10,7 @@ #if ZEN_PLATFORM_WINDOWS # include <zencore/windows.h> -# include <zenutil/windows/service.h> +# include <zenutil/windows/windowsservice.h> #endif #if ZEN_PLATFORM_MAC # include <zencore/filesystem.h> diff --git a/src/zenutil/windows/service.cpp b/src/zenutil/windows/windowsservice.cpp index b76ed7a66..ebb88b018 100644 --- a/src/zenutil/windows/service.cpp +++ b/src/zenutil/windows/windowsservice.cpp @@ -3,7 +3,7 @@ #include <zencore/zencore.h> #if ZEN_PLATFORM_WINDOWS -# include <zenutil/windows/service.h> +# include <zenutil/windows/windowsservice.h> # include <zencore/except.h> # include <zencore/thread.h> diff --git a/src/zenutil/workerpools.cpp b/src/zenutil/workerpools.cpp index 797034978..55c91e68e 100644 --- a/src/zenutil/workerpools.cpp +++ b/src/zenutil/workerpools.cpp @@ -27,19 +27,19 @@ namespace { const std::string_view Name; }; - WorkerPool BurstLargeWorkerPool = {.TreadCount = LargeWorkerThreadPoolTreadCount, .Name = "LargeThreadPool(burst)"}; - WorkerPool BackgroundLargeWorkerPool = {.TreadCount = LargeWorkerThreadPoolTreadCount, .Name = "LargeThreadPool(bkg)"}; + WorkerPool BurstLargeWorkerPool = {.TreadCount = LargeWorkerThreadPoolTreadCount, .Name = "large"}; + WorkerPool BackgroundLargeWorkerPool = {.TreadCount = LargeWorkerThreadPoolTreadCount, .Name = "large_bg"}; - WorkerPool BurstMediumWorkerPool = {.TreadCount = MediumWorkerThreadPoolTreadCount, .Name = "MediumThreadPool(burst)"}; - WorkerPool BackgroundMediumWorkerPool = {.TreadCount = MediumWorkerThreadPoolTreadCount, .Name = "MediumThreadPool(bkg)"}; + WorkerPool BurstMediumWorkerPool = {.TreadCount = MediumWorkerThreadPoolTreadCount, .Name = "medium"}; + WorkerPool BackgroundMediumWorkerPool = {.TreadCount = MediumWorkerThreadPoolTreadCount, .Name = "medium_bg"}; - WorkerPool BurstSmallWorkerPool = {.TreadCount = SmallWorkerThreadPoolTreadCount, .Name = "SmallThreadPool(burst)"}; - WorkerPool BackgroundSmallWorkerPool = {.TreadCount = SmallWorkerThreadPoolTreadCount, .Name = "SmallThreadPool(bkg)"}; + WorkerPool BurstSmallWorkerPool = {.TreadCount = SmallWorkerThreadPoolTreadCount, .Name = "small"}; + WorkerPool BackgroundSmallWorkerPool = {.TreadCount = SmallWorkerThreadPoolTreadCount, .Name = "small_bg"}; - WorkerPool BurstTinyWorkerPool = {.TreadCount = TinyWorkerThreadPoolTreadCount, .Name = "TinyThreadPool(burst)"}; - WorkerPool BackgroundTinyWorkerPool = {.TreadCount = TinyWorkerThreadPoolTreadCount, .Name = "TinyThreadPool(bkg)"}; + WorkerPool BurstTinyWorkerPool = {.TreadCount = TinyWorkerThreadPoolTreadCount, .Name = "tiny"}; + WorkerPool BackgroundTinyWorkerPool = {.TreadCount = TinyWorkerThreadPoolTreadCount, .Name = "tiny_bg"}; - WorkerPool SyncWorkerPool = {.TreadCount = 0, .Name = "SyncThreadPool"}; + WorkerPool SyncWorkerPool = {.TreadCount = 0, .Name = "synctp"}; WorkerThreadPool& EnsurePoolPtr(WorkerPool& Pool) { diff --git a/src/zenutil/zenserverprocess.cpp b/src/zenutil/zenserverprocess.cpp index 3f71c1357..6a93f0c63 100644 --- a/src/zenutil/zenserverprocess.cpp +++ b/src/zenutil/zenserverprocess.cpp @@ -656,7 +656,7 @@ ZenServerInstance::Shutdown() } else if (Ec) { - ZEN_WARN("Terminating zenserver proces as we failed to signal zenserver process {} ({}) to shut down. Reason: '{}'", + ZEN_WARN("Terminating zenserver process as we failed to signal zenserver process {} ({}) to shut down. Reason: '{}'", m_Name, m_Process.Pid(), Ec.message()); @@ -915,7 +915,7 @@ ZenServerInstance::Detach() uint16_t ZenServerInstance::WaitUntilReady() { - while (m_ReadyEvent.Wait(100) == false) + while (m_ReadyEvent.Wait(10) == false) { if (!m_Process.IsValid()) { @@ -938,7 +938,7 @@ bool ZenServerInstance::WaitUntilReady(int Timeout) { int TimeoutLeftMS = Timeout; - while (m_ReadyEvent.Wait(100) == false) + while (m_ReadyEvent.Wait(10) == false) { if (!m_Process.IsValid()) { @@ -950,7 +950,7 @@ ZenServerInstance::WaitUntilReady(int Timeout) ZEN_WARN("Wait abandoned by exited process"); return false; } - TimeoutLeftMS -= 100; + TimeoutLeftMS -= 10; if ((TimeoutLeftMS <= 0)) { ZEN_WARN("Wait abandoned due to timeout"); diff --git a/src/zenutil/zenutil.cpp b/src/zenutil/zenutil.cpp index 37b229c49..51c1ee72e 100644 --- a/src/zenutil/zenutil.cpp +++ b/src/zenutil/zenutil.cpp @@ -4,11 +4,8 @@ #if ZEN_WITH_TESTS -# include <zenutil/cache/cacherequests.h> -# include <zenutil/cache/rpcrecording.h> -# include <zenutil/chunkedfile.h> +# include <zenutil/rpcrecording.h> # include <zenutil/commandlineoptions.h> -# include <zenutil/parallelwork.h> # include <zenutil/wildcard.h> namespace zen { @@ -16,12 +13,8 @@ namespace zen { void zenutil_forcelinktests() { - cachepolicy_forcelink(); cache::rpcrecord_forcelink(); - cacherequests_forcelink(); - chunkedfile_forcelink(); commandlineoptions_forcelink(); - parallellwork_forcelink(); wildcard_forcelink(); } |