// Copyright 2023 Huawei Cloud Computing Technology Co., Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #ifndef INCLUDED_SRC_BUILDTOOL_STORAGE_LOCAL_CAS_HPP #define INCLUDED_SRC_BUILDTOOL_STORAGE_LOCAL_CAS_HPP #include #include #include #include #include #include #include "gsl/gsl" #include "src/buildtool/common/artifact_digest.hpp" #include "src/buildtool/common/protocol_traits.hpp" #include "src/buildtool/crypto/hash_function.hpp" #include "src/buildtool/file_system/object_cas.hpp" #include "src/buildtool/file_system/object_type.hpp" #include "src/buildtool/storage/config.hpp" #include "src/buildtool/storage/large_object_cas.hpp" // IWYU pragma: keep #include "src/buildtool/storage/uplinker.hpp" #include "src/utils/cpp/expected.hpp" #include "src/utils/cpp/tmp_dir.hpp" /// \brief The local (logical) CAS for storing blobs and trees. /// Blobs can be stored/queried as executable or non-executable. Trees might be /// treated differently depending on the compatibility mode. Supports global /// uplinking across all generations. The uplink is automatically performed for /// every entry that is read and every entry that is stored and already exists /// in an older generation. /// \tparam kDoGlobalUplink Enable global uplinking. template class LocalCAS { public: /// Local CAS generation used by GC without global uplink. using LocalGenerationCAS = LocalCAS; /// \brief Create local CAS with base path. /// Note that the base path is concatenated by a single character /// 'f'/'x'/'t' for each internally used physical CAS. /// \param base The base path for the CAS. explicit LocalCAS( GenerationConfig const& config, gsl::not_null const*> const& uplinker) : cas_file_{config.storage_config->hash_function, config.cas_f, MakeUplinker(config, uplinker)}, cas_exec_{config.storage_config->hash_function, config.cas_x, MakeUplinker(config, uplinker)}, cas_tree_{config.storage_config->hash_function, config.cas_t, MakeUplinker(config, uplinker)}, cas_file_large_{this, config, uplinker}, cas_tree_large_{this, config, uplinker}, hash_function_{config.storage_config->hash_function} {} [[nodiscard]] auto GetHashFunction() const noexcept -> HashFunction { return hash_function_; } /// \brief Obtain path to the storage root. /// \param type Type of the storage to be obtained. /// \param large True if a large storage is needed. [[nodiscard]] auto StorageRoot(ObjectType type, bool large = false) const noexcept -> std::filesystem::path const& { if (large) { return IsTreeObject(type) ? cas_tree_large_.StorageRoot() : cas_file_large_.StorageRoot(); } switch (type) { case ObjectType::Tree: return cas_tree_.StorageRoot(); case ObjectType::Executable: return cas_exec_.StorageRoot(); default: return cas_file_.StorageRoot(); } } /// \brief Store blob from file path with x-bit. /// \tparam kOwner Indicates ownership for optimization (hardlink). /// \param file_path The path of the file to store as blob. /// \param is_executable Store blob with executable permissions. /// \returns Digest of the stored blob or nullopt otherwise. template [[nodiscard]] auto StoreBlob(std::filesystem::path const& file_path, bool is_executable) const noexcept -> std::optional { return is_executable ? cas_exec_.StoreBlobFromFile(file_path, kOwner) : cas_file_.StoreBlobFromFile(file_path, kOwner); } /// \brief Store blob from bytes with x-bit (default: non-executable). /// \param bytes The bytes to create the blob from. /// \param is_executable Store blob with executable permissions. /// \returns Digest of the stored blob or nullopt otherwise. [[nodiscard]] auto StoreBlob(std::string const& bytes, bool is_executable = false) const noexcept -> std::optional { return is_executable ? cas_exec_.StoreBlobFromBytes(bytes) : cas_file_.StoreBlobFromBytes(bytes); } /// \brief Store tree from file path. /// \tparam kOwner Indicates ownership for optimization (hardlink). /// \param file_path The path of the file to store as tree. /// \returns Digest of the stored tree or nullopt otherwise. template [[nodiscard]] auto StoreTree(std::filesystem::path const& file_path) const noexcept -> std::optional { return cas_tree_.StoreBlobFromFile(file_path, kOwner); } /// \brief Store tree from bytes. /// \param bytes The bytes to create the tree from. /// \returns Digest of the stored tree or nullopt otherwise. [[nodiscard]] auto StoreTree(std::string const& bytes) const noexcept -> std::optional { return cas_tree_.StoreBlobFromBytes(bytes); } /// \brief Obtain blob path from digest with x-bit. /// Performs a synchronization if blob is only available with inverse x-bit. /// \param digest Digest of the blob to lookup. /// \param is_executable Lookup blob with executable permissions. /// \returns Path to the blob if found or nullopt otherwise. [[nodiscard]] auto BlobPath(ArtifactDigest const& digest, bool is_executable) const noexcept -> std::optional { auto const path = BlobPathNoSync(digest, is_executable); return path ? path : TrySyncBlob(digest, is_executable); } /// \brief Obtain blob path from digest with x-bit. /// Synchronization between file/executable CAS is skipped. /// \param digest Digest of the blob to lookup. /// \param is_executable Lookup blob with executable permissions. /// \returns Path to the blob if found or nullopt otherwise. [[nodiscard]] auto BlobPathNoSync(ArtifactDigest const& digest, bool is_executable) const noexcept -> std::optional { return is_executable ? cas_exec_.BlobPath(digest) : cas_file_.BlobPath(digest); } /// \brief Split a blob into chunks. /// \param digest The digest of a blob to be split. /// \returns Digests of the parts of the large object or an /// error code on failure. [[nodiscard]] auto SplitBlob(ArtifactDigest const& digest) const noexcept -> expected, LargeObjectError> { return cas_file_large_.Split(digest); } /// \brief Splice a blob from parts. /// \param digest The expected digest of the result. /// \param parts The parts of the large object. /// \param is_executable Splice the blob with executable permissions. /// \return The digest of the result or an error code on /// failure. [[nodiscard]] auto SpliceBlob(ArtifactDigest const& digest, std::vector const& parts, bool is_executable) const noexcept -> expected { return is_executable ? Splice(digest, parts) : Splice(digest, parts); } /// \brief Obtain tree path from digest. /// \param digest Digest of the tree to lookup. /// \returns Path to the tree if found or nullopt otherwise. [[nodiscard]] auto TreePath(ArtifactDigest const& digest) const noexcept -> std::optional { return cas_tree_.BlobPath(digest); } /// \brief Split a tree into chunks. /// \param digest The digest of a tree to be split. /// \returns Digests of the parts of the large object or an /// error code on failure. [[nodiscard]] auto SplitTree(ArtifactDigest const& digest) const noexcept -> expected, LargeObjectError> { return cas_tree_large_.Split(digest); } /// \brief Splice a tree from parts. /// \param digest The expected digest of the result. /// \param parts The parts of the large object. /// \return The digest of the result or an error code on /// failure. [[nodiscard]] auto SpliceTree(ArtifactDigest const& digest, std::vector const& parts) const noexcept -> expected { return Splice(digest, parts); } /// \brief Check whether all parts of the tree are in the storage. /// \param tree_digest Digest of the tree to be checked. /// \param tree_data Content of the tree. /// \return An error on fail. [[nodiscard]] auto CheckTreeInvariant(ArtifactDigest const& tree_digest, std::string const& tree_data) const noexcept -> std::optional; /// \brief Check whether all parts of the tree are in the storage. /// \param tree_digest Digest of the tree to be checked. /// \param file Content of the tree. /// \return An error on fail. [[nodiscard]] auto CheckTreeInvariant(ArtifactDigest const& tree_digest, std::filesystem::path const& file) const noexcept -> std::optional; /// \brief Uplink blob from this generation to latest LocalCAS generation. /// Performs a synchronization if requested and if blob is only available /// with inverse x-bit. This function is only available for instances that /// are used as local GC generations (i.e., disabled global uplink). /// \tparam kIsLocalGeneration True if this instance is a local generation. /// \param latest The latest LocalCAS generation. /// \param digest The digest of the blob to uplink. /// \param is_executable Uplink blob with executable permissions. /// \param skip_sync Do not sync between file/executable CAS. /// \param splice_result Create the result of splicing in the latest /// generation. /// \returns True if blob was successfully uplinked. template requires(kIsLocalGeneration) [[nodiscard]] auto LocalUplinkBlob( LocalGenerationCAS const& latest, ArtifactDigest const& digest, bool is_executable, bool skip_sync = false, bool splice_result = false) const noexcept -> bool; /// \brief Uplink tree from this generation to latest LocalCAS generation. /// This function is only available for instances that are used as local GC /// generations (i.e., disabled global uplink). Trees are uplinked deep, /// including all referenced entries. Note that in compatible mode we do not /// have the notion of "tree" and instead trees are stored as blobs. /// Therefore, in compatible mode this function is only used by instances /// that are aware of trees, such as output directories in action results or /// tree artifacts from target cache. /// \tparam kIsLocalGeneration True if this instance is a local generation. /// \param latest The latest LocalCAS generation. /// \param digest The digest of the tree to uplink. /// \param splice_result Create the result of splicing in the latest /// generation. /// \returns True if tree was successfully uplinked. template requires(kIsLocalGeneration) [[nodiscard]] auto LocalUplinkTree( LocalGenerationCAS const& latest, ArtifactDigest const& digest, bool splice_result = false) const noexcept -> bool; /// \brief Uplink large entry from this generation to latest LocalCAS /// generation. This function is only available for instances that are used /// as local GC generations (i.e., disabled global uplink). /// \tparam kType Type of the large entry to be uplinked. /// \tparam kIsLocalGeneration True if this instance is a local generation. /// \param latest The latest LocalCAS generation. /// \param latest_large The latest LargeObjectCAS /// \param digest The digest of the large entry to uplink. /// \returns True if the large entry was successfully uplinked. template requires(kIsLocalGeneration) [[nodiscard]] auto LocalUplinkLargeObject( LocalGenerationCAS const& latest, ArtifactDigest const& digest) const noexcept -> bool; private: ObjectCAS cas_file_; ObjectCAS cas_exec_; ObjectCAS cas_tree_; LargeObjectCAS cas_file_large_; LargeObjectCAS cas_tree_large_; HashFunction hash_function_; /// \brief Provides uplink via "exists callback" for physical object CAS. template [[nodiscard]] static auto MakeUplinker( GenerationConfig const& config, gsl::not_null const*> const& uplinker) { if constexpr (kDoGlobalUplink) { bool const native = ProtocolTraits::IsNative( config.storage_config->hash_function.GetType()); return [native, uplinker](auto const& digest, auto const& /*path*/) { if constexpr (IsTreeObject(kType)) { // in non-compatible mode, do explicit deep tree uplink // in compatible mode, treat all trees as blobs if (native) { return uplinker->UplinkTree(digest); } } return uplinker->UplinkBlob(digest, IsExecutableObject(kType)); }; } else { return std::nullopt; } } /// \brief Try to sync blob between file CAS and executable CAS. /// \param digest Blob digest. /// \param to_executable Sync direction. /// \returns Path to blob in target CAS. [[nodiscard]] auto TrySyncBlob(ArtifactDigest const& digest, bool to_executable) const noexcept -> std::optional { auto const src_blob = BlobPathNoSync(digest, not to_executable); if (src_blob and StoreBlob(*src_blob, to_executable)) { return BlobPathNoSync(digest, to_executable); } return std::nullopt; } template requires(kIsLocalGeneration) [[nodiscard]] auto LocalUplinkGitTree( LocalGenerationCAS const& latest, ArtifactDigest const& digest, bool splice_result = false) const noexcept -> bool; template requires(kIsLocalGeneration) [[nodiscard]] auto LocalUplinkBazelDirectory( LocalGenerationCAS const& latest, ArtifactDigest const& digest, gsl::not_null*> const& seen, bool splice_result = false) const noexcept -> bool; template requires(kIsLocalGeneration) [[nodiscard]] auto TrySplice(ArtifactDigest const& digest) const noexcept -> TmpFile::Ptr; template [[nodiscard]] auto Splice(ArtifactDigest const& digest, std::vector const& parts) const noexcept -> expected; }; #ifndef BOOTSTRAP_BUILD_TOOL // NOLINTNEXTLINE(misc-header-include-cycle) #include "src/buildtool/storage/local_cas.tpp" // IWYU pragma: export #else template auto LocalCAS::CheckTreeInvariant( ArtifactDigest const& tree_digest, std::string const& tree_data) const noexcept -> std::optional { return std::nullopt; } template auto LocalCAS::CheckTreeInvariant( ArtifactDigest const& tree_digest, std::filesystem::path const& file) const noexcept -> std::optional { return std::nullopt; } template template auto LocalCAS::Splice(ArtifactDigest const& digest, std::vector const& parts) const noexcept -> expected { return unexpected{ LargeObjectError{LargeObjectErrorCode::Internal, "not allowed"}}; } #endif #endif // INCLUDED_SRC_BUILDTOOL_STORAGE_LOCAL_CAS_HPP