aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil/filebuildstorage.cpp
diff options
context:
space:
mode:
authorDan Engelbrecht <[email protected]>2025-02-26 15:10:14 +0100
committerGitHub Enterprise <[email protected]>2025-02-26 15:10:14 +0100
commit7d8fe45af3b49d800f84f0ddce051c0b3b2e837d (patch)
treec8dd564dcf247d7b2537bb5c2ebfbca57bafd205 /src/zenutil/filebuildstorage.cpp
parentimprovements and infrastructure for upcoming builds api command line (#284) (diff)
downloadzen-7d8fe45af3b49d800f84f0ddce051c0b3b2e837d.tar.xz
zen-7d8fe45af3b49d800f84f0ddce051c0b3b2e837d.zip
builds upload command (#278)
- Feature: **EXPERIMENTAL** New `zen builds` command to list, upload and download folders to Cloud Build API - `builds list` list available builds (**INCOMPLETE - FILTERING MISSING**) - `builds upload` upload a folder to Cloud Build API - `--local-path` source folder to upload - `--create-build` creates a new parent build object (using the object id), if omitted a parent build must exist and `--build-id` must be given - `--build-id` an Oid in hex form for the Build identifier to use - omit to have the id auto generated - `--build-part-id` and Oid in hex form for the Build Part identifier for the folder - omit to have the id auto generated - `--build-part-name` name of the build part - if omitted the name of the leaf folder name give in `--local-path` - `--metadata-path` path to a json formatted file with meta data information about the build. Meta-data must be provided if `--create-build` is set - `--metadata` key-value pairs separated by ';' with build meta data for the build. (key1=value1;key2=value2). Meta-data must be provided if `--create-build` is set - `--clean` ignore any existing blocks of chunk data and upload a fresh set of blocks - `--allow-multipart` enable usage of multi-part http upload requests - `--manifest-path` path to text file listing files to include in upload. Exclude to upload everything in `--local-path` - `builds download` download a folder from Cloud Build API (**INCOMPLETE - WILL WIPE UNTRACKED DATA FROM TARGET FOLDER**) - `--local-path` target folder to download to - `--build-id` an Oid in hex form for the Build identifier to use - `--build-part-id` a comma separated list of Oid in hex for the build part identifier(s) to download - mutually exclusive to `--build-part-name` - `--build-part-name` a comma separated list of names for the build part(s) to download - if omitted the name of the leaf folder name give in `--local-path` - `--clean` deletes all data in target folder before downloading (NON-CLEAN IS NOT IMPLEMENTED YET) - `--allow-multipart` enable usage of multi-part http download reqeusts - `builds diff` download a folder from Cloud Build API - `--local-path` target folder to download to - `--compare-path` folder to compare target with - `--only-chunked` compare only files that would be chunked - `builds fetch-blob` fetch and validate a blob from remote store - `--build-id` an Oid in hex form for the Build identifier to use - `--blob-hash` an IoHash in hex form identifying the blob to download - `builds validate part` fetch a build part and validate all referenced attachments - `--build-id` an Oid in hex form for the Build identifier to use - `--build-part-id` an Oid in hex for the build part identifier to validate - mutually exclusive to `--build-part-name` - `--build-part-name` a name for the build part to validate - mutually exclusive to `--build-part-id` - `builds test` a series of operation that uploads, downloads and test various aspects of incremental operations - `--local-path` source folder to upload - Options for Cloud Build API remote store (`list`, `upload`, `download`, `fetch-blob`, `validate-part`) - `--url` Cloud Builds URL - `--assume-http2` assume that the builds endpoint is a HTTP/2 endpoint skipping HTTP/1.1 upgrade handshake - `--namespace` Builds Storage namespace - `--bucket` Builds Storage bucket - Authentication options for Cloud Build API - Auth token - `--access-token` http auth Cloud Storage access token - `--access-token-env` name of environment variable that holds the Http auth Cloud Storage access token - `--access-token-path` path to json file that holds the Http auth Cloud Storage access token - OpenId authentication - `--openid-provider-name` Open ID provider name - `--openid-provider-url` Open ID provider url - `--openid-client-id`Open ID client id - `--openid-refresh-token` Open ID refresh token - `--encryption-aes-key` 256 bit AES encryption key for storing OpenID credentials - `--encryption-aes-iv` 128 bit AES encryption initialization vector for storing OpenID credentials - OAuth authentication - `--oauth-url` OAuth provier url - `--oauth-clientid` OAuth client id - `--oauth-clientsecret` OAuth client secret - Options for file based remote store used for for testing purposes (`list`, `upload`, `download`, `fetch-blob`, `validate-part`, `test`) - `--storage-path` path to folder to store builds data - `--json-metadata` enable json output in store for all compact binary objects (off by default) - Output options for all builds commands - `--plain-progress` use plain line-by-line progress output - `--verbose`
Diffstat (limited to 'src/zenutil/filebuildstorage.cpp')
-rw-r--r--src/zenutil/filebuildstorage.cpp616
1 files changed, 616 insertions, 0 deletions
diff --git a/src/zenutil/filebuildstorage.cpp b/src/zenutil/filebuildstorage.cpp
new file mode 100644
index 000000000..78ebcdd55
--- /dev/null
+++ b/src/zenutil/filebuildstorage.cpp
@@ -0,0 +1,616 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenutil/filebuildstorage.h>
+
+#include <zencore/basicfile.h>
+#include <zencore/compactbinarybuilder.h>
+#include <zencore/compactbinaryvalidation.h>
+#include <zencore/fmtutils.h>
+#include <zencore/scopeguard.h>
+#include <zencore/timer.h>
+
+namespace zen {
+
+using namespace std::literals;
+
+class FileBuildStorage : public BuildStorage
+{
+public:
+ explicit FileBuildStorage(const std::filesystem::path& StoragePath,
+ BuildStorage::Statistics& Stats,
+ bool EnableJsonOutput,
+ double LatencySec,
+ double DelayPerKBSec)
+ : m_StoragePath(StoragePath)
+ , m_Stats(Stats)
+ , m_EnableJsonOutput(EnableJsonOutput)
+ , m_LatencySec(LatencySec)
+ , m_DelayPerKBSec(DelayPerKBSec)
+ {
+ CreateDirectories(GetBuildsFolder());
+ CreateDirectories(GetBlobsFolder());
+ CreateDirectories(GetBlobsMetadataFolder());
+ }
+
+ virtual ~FileBuildStorage() {}
+
+ virtual CbObject ListBuilds(CbObject Query) override
+ {
+ ZEN_UNUSED(Query);
+
+ SimulateLatency(Query.GetSize(), 0);
+
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ const std::filesystem::path BuildFolder = GetBuildsFolder();
+ DirectoryContent Content;
+ GetDirectoryContent(BuildFolder, DirectoryContentFlags::IncludeDirs, Content);
+ CbObjectWriter Writer;
+ Writer.BeginArray("results");
+ {
+ for (const std::filesystem::path& BuildPath : Content.Directories)
+ {
+ Oid BuildId = Oid::TryFromHexString(BuildPath.stem().string());
+ if (BuildId != Oid::Zero)
+ {
+ Writer.BeginObject();
+ {
+ Writer.AddObjectId("buildId", BuildId);
+ Writer.AddObject("metadata", ReadBuild(BuildId)["metadata"sv].AsObjectView());
+ }
+ Writer.EndObject();
+ }
+ }
+ }
+ Writer.EndArray(); // builds
+ Writer.Save();
+ SimulateLatency(Writer.GetSaveSize(), 0);
+ return Writer.Save();
+ }
+
+ virtual CbObject PutBuild(const Oid& BuildId, const CbObject& MetaData) override
+ {
+ SimulateLatency(MetaData.GetSize(), 0);
+
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ CbObjectWriter BuildObject;
+ BuildObject.AddObject("metadata", MetaData);
+ BuildObject.AddInteger("chunkSize"sv, 32u * 1024u * 1024u);
+ WriteBuild(BuildId, BuildObject.Save());
+
+ CbObjectWriter BuildResponse;
+ BuildResponse.AddInteger("chunkSize"sv, 32u * 1024u * 1024u);
+ BuildResponse.Save();
+
+ SimulateLatency(0, BuildResponse.GetSaveSize());
+ return BuildResponse.Save();
+ }
+
+ virtual CbObject GetBuild(const Oid& BuildId) override
+ {
+ SimulateLatency(0, 0);
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ CbObject Build = ReadBuild(BuildId);
+ SimulateLatency(0, Build.GetSize());
+ return Build;
+ }
+
+ virtual void FinalizeBuild(const Oid& BuildId) override
+ {
+ SimulateLatency(0, 0);
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ ZEN_UNUSED(BuildId);
+ SimulateLatency(0, 0);
+ }
+
+ virtual std::pair<IoHash, std::vector<IoHash>> PutBuildPart(const Oid& BuildId,
+ const Oid& BuildPartId,
+ std::string_view PartName,
+ const CbObject& MetaData) override
+ {
+ SimulateLatency(MetaData.GetSize(), 0);
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ const std::filesystem::path BuildPartDataPath = GetBuildPartPath(BuildId, BuildPartId);
+ CreateDirectories(BuildPartDataPath.parent_path());
+
+ TemporaryFile::SafeWriteFile(BuildPartDataPath, MetaData.GetView());
+ m_WrittenBytes += MetaData.GetSize();
+ WriteAsJson(BuildPartDataPath, MetaData);
+
+ IoHash RawHash = IoHash::HashBuffer(MetaData.GetView());
+
+ CbObjectWriter Writer;
+ {
+ CbObject BuildObject = ReadBuild(BuildId);
+ CbObjectView PartsObject = BuildObject["parts"sv].AsObjectView();
+ CbObjectView MetaDataView = BuildObject["metadata"sv].AsObjectView();
+
+ Writer.AddObject("metadata"sv, MetaDataView);
+ Writer.BeginObject("parts"sv);
+ {
+ for (CbFieldView PartView : PartsObject)
+ {
+ if (PartView.GetName() != PartName)
+ {
+ Writer.AddObjectId(PartView.GetName(), PartView.AsObjectId());
+ }
+ }
+ Writer.AddObjectId(PartName, BuildPartId);
+ }
+ Writer.EndObject(); // parts
+ }
+ WriteBuild(BuildId, Writer.Save());
+
+ std::vector<IoHash> NeededAttachments = GetNeededAttachments(MetaData);
+
+ SimulateLatency(0, sizeof(IoHash) * NeededAttachments.size());
+
+ return std::make_pair(RawHash, std::move(NeededAttachments));
+ }
+
+ virtual CbObject GetBuildPart(const Oid& BuildId, const Oid& BuildPartId) override
+ {
+ SimulateLatency(0, 0);
+
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ const std::filesystem::path BuildPartDataPath = GetBuildPartPath(BuildId, BuildPartId);
+
+ IoBuffer Payload = ReadFile(BuildPartDataPath).Flatten();
+ m_Stats.TotalBytesRead += Payload.GetSize();
+
+ ZEN_ASSERT(ValidateCompactBinary(Payload.GetView(), CbValidateMode::Default) == CbValidateError::None);
+
+ CbObject BuildPartObject = CbObject(SharedBuffer(Payload));
+
+ SimulateLatency(0, BuildPartObject.GetSize());
+
+ return BuildPartObject;
+ }
+
+ virtual std::vector<IoHash> FinalizeBuildPart(const Oid& BuildId, const Oid& BuildPartId, const IoHash& PartHash) override
+ {
+ SimulateLatency(0, 0);
+
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ const std::filesystem::path BuildPartDataPath = GetBuildPartPath(BuildId, BuildPartId);
+ IoBuffer Payload = ReadFile(BuildPartDataPath).Flatten();
+ m_Stats.TotalBytesRead += Payload.GetSize();
+ IoHash RawHash = IoHash::HashBuffer(Payload.GetView());
+ if (RawHash != PartHash)
+ {
+ throw std::runtime_error(
+ fmt::format("Failed finalizing build part {}: Expected hash {}, got {}", BuildPartId, PartHash, RawHash));
+ }
+
+ CbObject BuildPartObject = CbObject(SharedBuffer(Payload));
+ std::vector<IoHash> NeededAttachments(GetNeededAttachments(BuildPartObject));
+
+ SimulateLatency(0, NeededAttachments.size() * sizeof(IoHash));
+
+ return NeededAttachments;
+ }
+
+ virtual void PutBuildBlob(const Oid& BuildId,
+ const IoHash& RawHash,
+ ZenContentType ContentType,
+ const CompositeBuffer& Payload) override
+ {
+ ZEN_UNUSED(BuildId);
+ ZEN_ASSERT(ContentType == ZenContentType::kCompressedBinary);
+ SimulateLatency(Payload.GetSize(), 0);
+
+ ZEN_ASSERT_SLOW(ValidateCompressedBuffer(RawHash, Payload));
+
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ const std::filesystem::path BlockPath = GetBlobPayloadPath(RawHash);
+ if (!std::filesystem::is_regular_file(BlockPath))
+ {
+ CreateDirectories(BlockPath.parent_path());
+ TemporaryFile::SafeWriteFile(BlockPath, Payload.Flatten().GetView());
+ }
+ m_Stats.TotalBytesWritten += Payload.GetSize();
+ SimulateLatency(0, 0);
+ }
+
+ virtual std::vector<std::function<void()>> PutLargeBuildBlob(const Oid& BuildId,
+ const IoHash& RawHash,
+ ZenContentType ContentType,
+ uint64_t PayloadSize,
+ std::function<IoBuffer(uint64_t Offset, uint64_t Size)>&& Transmitter,
+ std::function<void(uint64_t, bool)>&& OnSentBytes) override
+ {
+ ZEN_UNUSED(BuildId);
+ ZEN_UNUSED(ContentType);
+ SimulateLatency(0, 0);
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ const std::filesystem::path BlockPath = GetBlobPayloadPath(RawHash);
+ if (!std::filesystem::is_regular_file(BlockPath))
+ {
+ CreateDirectories(BlockPath.parent_path());
+
+ struct WorkloadData
+ {
+ std::function<IoBuffer(uint64_t Offset, uint64_t Size)> Transmitter;
+ std::function<void(uint64_t, bool)> OnSentBytes;
+ TemporaryFile TempFile;
+ std::atomic<size_t> PartsLeft;
+ };
+
+ std::shared_ptr<WorkloadData> Workload(std::make_shared<WorkloadData>());
+ Workload->Transmitter = std::move(Transmitter);
+ Workload->OnSentBytes = std::move(OnSentBytes);
+ std::error_code Ec;
+ Workload->TempFile.CreateTemporary(BlockPath.parent_path(), Ec);
+
+ if (Ec)
+ {
+ throw std::runtime_error(
+ fmt::format("Failed opening temporary file '{}': {} ({})", Workload->TempFile.GetPath(), Ec.message(), Ec.value()));
+ }
+
+ std::vector<std::function<void()>> WorkItems;
+ uint64_t Offset = 0;
+ while (Offset < PayloadSize)
+ {
+ uint64_t Size = Min(32u * 1024u * 1024u, PayloadSize - Offset);
+
+ WorkItems.push_back([this, RawHash, BlockPath, Workload, Offset, Size]() {
+ IoBuffer PartPayload = Workload->Transmitter(Offset, Size);
+ SimulateLatency(PartPayload.GetSize(), 0);
+
+ std::error_code Ec;
+ Workload->TempFile.Write(PartPayload, Offset, Ec);
+ if (Ec)
+ {
+ throw std::runtime_error(fmt::format("Failed writing to temporary file '{}': {} ({})",
+ Workload->TempFile.GetPath(),
+ Ec.message(),
+ Ec.value()));
+ }
+ uint64_t BytesWritten = PartPayload.GetSize();
+ m_Stats.TotalBytesWritten += BytesWritten;
+ const bool IsLastPart = Workload->PartsLeft.fetch_sub(1) == 1;
+ if (IsLastPart)
+ {
+ Workload->TempFile.Flush();
+ ZEN_ASSERT_SLOW(ValidateCompressedBuffer(RawHash, CompositeBuffer(Workload->TempFile.ReadAll())));
+ Workload->TempFile.MoveTemporaryIntoPlace(BlockPath, Ec);
+ if (Ec)
+ {
+ throw std::runtime_error(fmt::format("Failed moving temporary file '{}' to '{}': {} ({})",
+ Workload->TempFile.GetPath(),
+ BlockPath,
+ Ec.message(),
+ Ec.value()));
+ }
+ }
+ Workload->OnSentBytes(BytesWritten, IsLastPart);
+ SimulateLatency(0, 0);
+ });
+
+ Offset += Size;
+ }
+ Workload->PartsLeft.store(WorkItems.size());
+
+ SimulateLatency(0, 0);
+ return WorkItems;
+ }
+ SimulateLatency(0, 0);
+ return {};
+ }
+
+ virtual IoBuffer GetBuildBlob(const Oid& BuildId, const IoHash& RawHash) override
+ {
+ ZEN_UNUSED(BuildId);
+ SimulateLatency(0, 0);
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ const std::filesystem::path BlockPath = GetBlobPayloadPath(RawHash);
+ if (std::filesystem::is_regular_file(BlockPath))
+ {
+ IoBuffer Payload = ReadFile(BlockPath).Flatten();
+ ZEN_ASSERT_SLOW(ValidateCompressedBuffer(RawHash, CompositeBuffer(SharedBuffer(Payload))));
+ m_Stats.TotalBytesRead += Payload.GetSize();
+ Payload.SetContentType(ZenContentType::kCompressedBinary);
+ SimulateLatency(0, Payload.GetSize());
+ return Payload;
+ }
+ SimulateLatency(0, 0);
+ return IoBuffer{};
+ }
+
+ virtual std::vector<std::function<void()>> GetLargeBuildBlob(
+ const Oid& BuildId,
+ const IoHash& RawHash,
+ uint64_t ChunkSize,
+ std::function<void(uint64_t Offset, const IoBuffer& Chunk, uint64_t BytesRemaining)>&& Receiver) override
+ {
+ ZEN_UNUSED(BuildId);
+ SimulateLatency(0, 0);
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ const std::filesystem::path BlockPath = GetBlobPayloadPath(RawHash);
+ if (std::filesystem::is_regular_file(BlockPath))
+ {
+ struct WorkloadData
+ {
+ std::atomic<uint64_t> BytesRemaining;
+ IoBuffer BlobFile;
+ std::function<void(uint64_t Offset, const IoBuffer& Chunk, uint64_t BytesRemaining)> Receiver;
+ };
+
+ std::shared_ptr<WorkloadData> Workload(std::make_shared<WorkloadData>());
+ Workload->BlobFile = IoBufferBuilder::MakeFromFile(BlockPath);
+ const uint64_t BlobSize = Workload->BlobFile.GetSize();
+
+ Workload->Receiver = std::move(Receiver);
+ Workload->BytesRemaining = BlobSize;
+
+ std::vector<std::function<void()>> WorkItems;
+ uint64_t Offset = 0;
+ while (Offset < BlobSize)
+ {
+ uint64_t Size = Min(ChunkSize, BlobSize - Offset);
+ WorkItems.push_back([this, BlockPath, Workload, Offset, Size]() {
+ SimulateLatency(0, 0);
+ IoBuffer PartPayload(Workload->BlobFile, Offset, Size);
+ m_Stats.TotalBytesRead += PartPayload.GetSize();
+ uint64_t ByteRemaning = Workload->BytesRemaining.fetch_sub(Size);
+ Workload->Receiver(Offset, PartPayload, ByteRemaning);
+ SimulateLatency(Size, PartPayload.GetSize());
+ });
+
+ Offset += Size;
+ }
+ SimulateLatency(0, 0);
+ return WorkItems;
+ }
+ return {};
+ }
+
+ virtual void PutBlockMetadata(const Oid& BuildId, const IoHash& BlockRawHash, const CbObject& MetaData) override
+ {
+ ZEN_UNUSED(BuildId);
+
+ SimulateLatency(MetaData.GetSize(), 0);
+
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ const std::filesystem::path BlockMetaDataPath = GetBlobMetadataPath(BlockRawHash);
+ CreateDirectories(BlockMetaDataPath.parent_path());
+ TemporaryFile::SafeWriteFile(BlockMetaDataPath, MetaData.GetView());
+ m_Stats.TotalBytesWritten += MetaData.GetSize();
+ WriteAsJson(BlockMetaDataPath, MetaData);
+ SimulateLatency(0, 0);
+ }
+
+ virtual std::vector<ChunkBlockDescription> FindBlocks(const Oid& BuildId) override
+ {
+ ZEN_UNUSED(BuildId);
+ SimulateLatency(0, 0);
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ DirectoryContent Content;
+ GetDirectoryContent(GetBlobsMetadataFolder(), DirectoryContentFlags::IncludeFiles, Content);
+ std::vector<ChunkBlockDescription> Result;
+ for (const std::filesystem::path& MetaDataFile : Content.Files)
+ {
+ IoHash ChunkHash;
+ if (IoHash::TryParse(MetaDataFile.stem().string(), ChunkHash))
+ {
+ std::filesystem::path BlockPath = GetBlobPayloadPath(ChunkHash);
+ if (std::filesystem::is_regular_file(BlockPath))
+ {
+ IoBuffer BlockMetaDataPayload = ReadFile(MetaDataFile).Flatten();
+
+ m_Stats.TotalBytesRead += BlockMetaDataPayload.GetSize();
+
+ CbObject BlockObject = CbObject(SharedBuffer(BlockMetaDataPayload));
+ Result.emplace_back(ParseChunkBlockDescription(BlockObject));
+ }
+ }
+ }
+ SimulateLatency(0, sizeof(IoHash) * Result.size());
+ return Result;
+ }
+
+ virtual std::vector<ChunkBlockDescription> GetBlockMetadata(const Oid& BuildId, std::span<const IoHash> BlockHashes) override
+ {
+ ZEN_UNUSED(BuildId);
+ SimulateLatency(0, 0);
+ Stopwatch ExecutionTimer;
+ auto _ = MakeGuard([&]() { m_Stats.TotalExecutionTimeUs += ExecutionTimer.GetElapsedTimeUs(); });
+ m_Stats.TotalRequestCount++;
+
+ std::vector<ChunkBlockDescription> Result;
+ for (const IoHash& BlockHash : BlockHashes)
+ {
+ std::filesystem::path MetaDataFile = GetBlobMetadataPath(BlockHash);
+ if (std::filesystem::is_regular_file(MetaDataFile))
+ {
+ IoBuffer BlockMetaDataPayload = ReadFile(MetaDataFile).Flatten();
+
+ m_Stats.TotalBytesRead += BlockMetaDataPayload.GetSize();
+
+ CbObject BlockObject = CbObject(SharedBuffer(BlockMetaDataPayload));
+ Result.emplace_back(ParseChunkBlockDescription(BlockObject));
+ }
+ }
+ SimulateLatency(sizeof(BlockHashes) * BlockHashes.size(), sizeof(ChunkBlockDescription) * Result.size());
+ return Result;
+ }
+
+protected:
+ std::filesystem::path GetBuildsFolder() const { return m_StoragePath / "builds"; }
+ std::filesystem::path GetBlobsFolder() const { return m_StoragePath / "blobs"; }
+ std::filesystem::path GetBlobsMetadataFolder() const { return m_StoragePath / "blocks"; }
+ std::filesystem::path GetBuildFolder(const Oid& BuildId) const { return GetBuildsFolder() / BuildId.ToString(); }
+
+ std::filesystem::path GetBuildPath(const Oid& BuildId) const { return GetBuildFolder(BuildId) / "metadata.cb"; }
+
+ std::filesystem::path GetBuildPartFolder(const Oid& BuildId, const Oid& BuildPartId) const
+ {
+ return GetBuildFolder(BuildId) / "parts" / BuildPartId.ToString();
+ }
+
+ std::filesystem::path GetBuildPartPath(const Oid& BuildId, const Oid& BuildPartId) const
+ {
+ return GetBuildPartFolder(BuildId, BuildPartId) / "metadata.cb";
+ }
+
+ std::filesystem::path GetBlobPayloadPath(const IoHash& RawHash) const { return GetBlobsFolder() / fmt::format("{}.cbz", RawHash); }
+
+ std::filesystem::path GetBlobMetadataPath(const IoHash& RawHash) const
+ {
+ return GetBlobsMetadataFolder() / fmt::format("{}.cb", RawHash);
+ }
+
+ void SimulateLatency(uint64_t ReceiveSize, uint64_t SendSize)
+ {
+ double SleepSec = m_LatencySec;
+ if (m_DelayPerKBSec > 0.0)
+ {
+ SleepSec += m_DelayPerKBSec * (double(SendSize + ReceiveSize) / 1024u);
+ }
+ if (SleepSec > 0)
+ {
+ Sleep(int(SleepSec * 1000));
+ }
+ }
+
+ void WriteAsJson(const std::filesystem::path& OriginalPath, CbObjectView Data) const
+ {
+ if (m_EnableJsonOutput)
+ {
+ ExtendableStringBuilder<128> SB;
+ CompactBinaryToJson(Data, SB);
+ std::filesystem::path JsonPath = OriginalPath;
+ JsonPath.replace_extension(".json");
+ std::string_view JsonMetaData = SB.ToView();
+ TemporaryFile::SafeWriteFile(JsonPath, MemoryView(JsonMetaData.data(), JsonMetaData.length()));
+ }
+ }
+
+ void WriteBuild(const Oid& BuildId, CbObjectView Data)
+ {
+ const std::filesystem::path BuildDataPath = GetBuildPath(BuildId);
+ CreateDirectories(BuildDataPath.parent_path());
+ TemporaryFile::SafeWriteFile(BuildDataPath, Data.GetView());
+ m_Stats.TotalBytesWritten += Data.GetSize();
+ WriteAsJson(BuildDataPath, Data);
+ }
+
+ CbObject ReadBuild(const Oid& BuildId)
+ {
+ const std::filesystem::path BuildDataPath = GetBuildPath(BuildId);
+ FileContents Content = ReadFile(BuildDataPath);
+ if (Content.ErrorCode)
+ {
+ throw std::runtime_error(fmt::format("Failed reading build '{}' from '{}': {} ({})",
+ BuildId,
+ BuildDataPath,
+ Content.ErrorCode.message(),
+ Content.ErrorCode.value()));
+ }
+ IoBuffer Payload = Content.Flatten();
+ m_Stats.TotalBytesRead += Payload.GetSize();
+ ZEN_ASSERT(ValidateCompactBinary(Payload.GetView(), CbValidateMode::Default) == CbValidateError::None);
+ CbObject BuildObject = CbObject(SharedBuffer(Payload));
+ return BuildObject;
+ }
+
+ std::vector<IoHash> GetNeededAttachments(CbObjectView BuildPartObject)
+ {
+ std::vector<IoHash> NeededAttachments;
+ BuildPartObject.IterateAttachments([&](CbFieldView FieldView) {
+ const IoHash AttachmentHash = FieldView.AsBinaryAttachment();
+ const std::filesystem::path BlockPath = GetBlobPayloadPath(AttachmentHash);
+ if (!std::filesystem::is_regular_file(BlockPath))
+ {
+ NeededAttachments.push_back(AttachmentHash);
+ }
+ });
+ return NeededAttachments;
+ }
+
+ bool ValidateCompressedBuffer(const IoHash& RawHash, const CompositeBuffer& Payload)
+ {
+ IoHash VerifyHash;
+ uint64_t VerifySize;
+ CompressedBuffer ValidateBuffer = CompressedBuffer::FromCompressed(Payload, VerifyHash, VerifySize);
+ if (!ValidateBuffer)
+ {
+ return false;
+ }
+ if (VerifyHash != RawHash)
+ {
+ return false;
+ }
+ CompositeBuffer Decompressed = ValidateBuffer.DecompressToComposite();
+ if (!Decompressed)
+ {
+ return false;
+ }
+ IoHash Hash = IoHash::HashBuffer(Decompressed);
+ if (Hash != RawHash)
+ {
+ return false;
+ }
+ return true;
+ }
+
+private:
+ const std::filesystem::path m_StoragePath;
+ BuildStorage::Statistics& m_Stats;
+ const bool m_EnableJsonOutput = false;
+ std::atomic<uint64_t> m_WrittenBytes;
+
+ const double m_LatencySec = 0.0;
+ const double m_DelayPerKBSec = 0.0;
+};
+
+std::unique_ptr<BuildStorage>
+CreateFileBuildStorage(const std::filesystem::path& StoragePath,
+ BuildStorage::Statistics& Stats,
+ bool EnableJsonOutput,
+ double LatencySec,
+ double DelayPerKBSec)
+{
+ return std::make_unique<FileBuildStorage>(StoragePath, Stats, EnableJsonOutput, LatencySec, DelayPerKBSec);
+}
+
+} // namespace zen