diff options
author | Sascha Roloff <sascha.roloff@huawei.com> | 2023-11-17 10:22:25 +0100 |
---|---|---|
committer | Sascha Roloff <sascha.roloff@huawei.com> | 2023-11-22 16:18:17 +0100 |
commit | 3b1095f7e3584f37c984a79ad7a2b94ebaa0700f (patch) | |
tree | 517f1113d4a19215b6403d38cc07a26a47c3090c /src/buildtool/execution_api/remote | |
parent | 44a7c680289ba6812583746013f350d63942c894 (diff) | |
download | justbuild-3b1095f7e3584f37c984a79ad7a2b94ebaa0700f.tar.gz |
Implement blob splitting protocol on just client side
Diffstat (limited to 'src/buildtool/execution_api/remote')
9 files changed, 288 insertions, 86 deletions
diff --git a/src/buildtool/execution_api/remote/TARGETS b/src/buildtool/execution_api/remote/TARGETS index 1ea38e40..381a8620 100644 --- a/src/buildtool/execution_api/remote/TARGETS +++ b/src/buildtool/execution_api/remote/TARGETS @@ -43,6 +43,8 @@ , ["src/buildtool/compatibility", "compatibility"] , ["src/buildtool/execution_api/bazel_msg", "bazel_msg_factory"] , ["src/buildtool/execution_api/utils", "outputscheck"] + , ["src/buildtool/compatibility", "compatibility"] + , ["src/buildtool/crypto", "hash_function"] , ["@", "grpc", "", "grpc++"] ] } @@ -66,6 +68,8 @@ , ["@", "fmt", "", "fmt"] , ["src/buildtool/compatibility", "compatibility"] , ["src/buildtool/multithreading", "task_system"] + , ["src/buildtool/file_system", "file_system_manager"] + , ["src/buildtool/file_system", "object_type"] ] } , "config": diff --git a/src/buildtool/execution_api/remote/bazel/bazel_ac_client.hpp b/src/buildtool/execution_api/remote/bazel/bazel_ac_client.hpp index b3a62af6..864d1254 100644 --- a/src/buildtool/execution_api/remote/bazel/bazel_ac_client.hpp +++ b/src/buildtool/execution_api/remote/bazel/bazel_ac_client.hpp @@ -28,7 +28,7 @@ #include "src/buildtool/logging/logger.hpp" /// Implements client side for service defined here: -/// https://github.com/bazelbuild/bazel/blob/4b6ad34dbba15dacebfb6cbf76fa741649cdb007/third_party/remoteapis/build/bazel/remote/execution/v2/remote_execution.proto#L137 +/// https://github.com/bazelbuild/remote-apis/blob/e1fe21be4c9ae76269a5a63215bb3c72ed9ab3f0/build/bazel/remote/execution/v2/remote_execution.proto#L144 class BazelAcClient { public: BazelAcClient(std::string const& server, Port port) noexcept; diff --git a/src/buildtool/execution_api/remote/bazel/bazel_api.cpp b/src/buildtool/execution_api/remote/bazel/bazel_api.cpp index 74e8286f..19ff52f6 100644 --- a/src/buildtool/execution_api/remote/bazel/bazel_api.cpp +++ b/src/buildtool/execution_api/remote/bazel/bazel_api.cpp @@ -16,12 +16,9 @@ #include <algorithm> #include <atomic> -#include <map> -#include <memory> -#include <optional> -#include <string> +#include <cstdint> #include <unordered_map> -#include <vector> +#include <unordered_set> #include "fmt/core.h" #include "src/buildtool/common/bazel_types.hpp" @@ -37,9 +34,150 @@ #include "src/buildtool/execution_api/remote/bazel/bazel_network.hpp" #include "src/buildtool/execution_api/remote/bazel/bazel_response.hpp" #include "src/buildtool/file_system/file_system_manager.hpp" +#include "src/buildtool/file_system/object_type.hpp" #include "src/buildtool/logging/logger.hpp" #include "src/buildtool/multithreading/task_system.hpp" +namespace { + +[[nodiscard]] auto IsAvailable( + std::vector<bazel_re::Digest> const& digests, + gsl::not_null<IExecutionApi*> const& api) noexcept + -> std::vector<bazel_re::Digest> { + std::vector<ArtifactDigest> artifact_digests; + artifact_digests.reserve(digests.size()); + for (auto const& digest : digests) { + artifact_digests.emplace_back(digest); + } + auto const& missing_artifact_digests = api->IsAvailable(artifact_digests); + std::vector<bazel_re::Digest> missing_digests; + missing_digests.reserve(missing_artifact_digests.size()); + for (auto const& digest : missing_artifact_digests) { + missing_digests.emplace_back(static_cast<bazel_re::Digest>(digest)); + } + return missing_digests; +} + +[[nodiscard]] auto RetrieveToCas( + std::vector<bazel_re::Digest> const& digests, + gsl::not_null<IExecutionApi*> const& api, + std::shared_ptr<BazelNetwork> const& network, + std::unordered_map<ArtifactDigest, Artifact::ObjectInfo> const& + info_map) noexcept -> bool { + + // Fetch blobs from this CAS. + auto size = digests.size(); + auto reader = network->ReadBlobs(digests); + auto blobs = reader.Next(); + std::size_t count{}; + BlobContainer container{}; + while (not blobs.empty()) { + if (count + blobs.size() > size) { + Logger::Log(LogLevel::Error, "received more blobs than requested."); + return false; + } + for (auto const& blob : blobs) { + try { + auto digest = ArtifactDigest{blob.digest}; + auto exec = info_map.contains(digest) + ? IsExecutableObject(info_map.at(digest).type) + : false; + container.Emplace(BazelBlob{blob.digest, blob.data, exec}); + } catch (std::exception const& ex) { + Logger::Log( + LogLevel::Error, "failed to emplace blob: ", ex.what()); + return false; + } + } + count += blobs.size(); + blobs = reader.Next(); + } + + if (count != size) { + Logger::Log(LogLevel::Error, "could not retrieve all requested blobs."); + return false; + } + + // Upload blobs to other CAS. + return api->Upload(container, /*skip_find_missing=*/true); +} + +[[nodiscard]] auto RetrieveToCasSplitted( + Artifact::ObjectInfo const& artifact_info, + gsl::not_null<IExecutionApi*> const& api, + std::shared_ptr<BazelNetwork> const& network, + std::unordered_map<ArtifactDigest, Artifact::ObjectInfo> const& + info_map) noexcept -> bool { + + // Split blob into chunks at the remote side and retrieve chunk digests. + auto chunk_digests = network->SplitBlob(artifact_info.digest); + if (not chunk_digests) { + // If blob splitting failed, fall back to regular fetching. + return ::RetrieveToCas({artifact_info.digest}, api, network, info_map); + } + + // Fetch unknown chunks. + auto digest_set = std::unordered_set<bazel_re::Digest>{ + (*chunk_digests).begin(), (*chunk_digests).end()}; + auto unique_digests = + std::vector<bazel_re::Digest>{digest_set.begin(), digest_set.end()}; + auto missing_digests = ::IsAvailable(unique_digests, api); + if (not ::RetrieveToCas(missing_digests, api, network, info_map)) { + return false; + } + + // Assemble blob from chunks. + std::string blob_data{}; + for (auto const& chunk_digest : *chunk_digests) { + auto info = Artifact::ObjectInfo{.digest = ArtifactDigest{chunk_digest}, + .type = ObjectType::File}; + auto chunk_data = api->RetrieveToMemory(info); + if (not chunk_data) { + Logger::Log(LogLevel::Error, + "could not load blob chunk in memory: ", + chunk_digest.hash()); + return false; + } + blob_data += *chunk_data; + } + + Logger::Log( + LogLevel::Debug, + [&artifact_info, &unique_digests, &missing_digests, &blob_data]() { + auto missing_digest_set = std::unordered_set<bazel_re::Digest>{ + missing_digests.begin(), missing_digests.end()}; + std::uint64_t transmitted_bytes{0}; + for (auto const& chunk_digest : unique_digests) { + if (missing_digest_set.contains(chunk_digest)) { + transmitted_bytes += chunk_digest.size_bytes(); + } + } + double transmission_factor = + not blob_data.empty() + ? 100.0 * transmitted_bytes / blob_data.size() + : 100.0; + return fmt::format( + "Blob splitting saved {} bytes ({:.2f}%) of network traffic " + "when fetching {}.\n", + blob_data.size() - transmitted_bytes, + 100.0 - transmission_factor, + artifact_info.ToString()); + }); + + // Upload blob to other CAS. + BlobContainer container{}; + try { + auto exec = IsExecutableObject(artifact_info.type); + container.Emplace(BazelBlob{artifact_info.digest, blob_data, exec}); + } catch (std::exception const& ex) { + Logger::Log(LogLevel::Error, "failed to emplace blob: ", ex.what()); + return false; + } + return api->Upload(container, /*skip_find_missing=*/true); +} + +} // namespace + BazelApi::BazelApi(std::string const& instance_name, std::string const& host, Port port, @@ -218,46 +356,15 @@ auto BazelApi::CreateAction( blob_digests.push_back(info.digest); } - // Fetch blobs from this CAS. - auto size = blob_digests.size(); - auto reader = network_->ReadBlobs(std::move(blob_digests)); - auto blobs = reader.Next(); - std::size_t count{}; - BlobContainer container{}; - while (not blobs.empty()) { - if (count + blobs.size() > size) { - Logger::Log(LogLevel::Error, "received more blobs than requested."); - return false; - } - for (auto& blob : blobs) { - try { - auto exec = IsExecutableObject( - info_map[ArtifactDigest{blob.digest}].type); - container.Emplace(BazelBlob{blob.digest, blob.data, exec}); - } catch (std::exception const& ex) { - Logger::Log( - LogLevel::Error, "failed to emplace blob: ", ex.what()); - return false; - } - } - count += blobs.size(); - blobs = reader.Next(); - } - - if (count != size) { - Logger::Log(LogLevel::Error, "could not retrieve all requested blobs."); - return false; - } - - // Upload blobs to other CAS. - return api->Upload(container, /*skip_find_missing=*/true); + return ::RetrieveToCas(blob_digests, api, network_, info_map); } /// NOLINTNEXTLINE(misc-no-recursion) [[nodiscard]] auto BazelApi::ParallelRetrieveToCas( std::vector<Artifact::ObjectInfo> const& artifacts_info, gsl::not_null<IExecutionApi*> const& api, - std::size_t jobs) noexcept -> bool { + std::size_t jobs, + bool use_blob_splitting) noexcept -> bool { // Return immediately if target CAS is this CAS if (this == api) { @@ -288,7 +395,8 @@ auto BazelApi::CreateAction( auto const infos = network_->ReadDirectTreeEntries( info.digest, std::filesystem::path{}); if (not infos or - not ParallelRetrieveToCas(infos->second, api, jobs)) { + not ParallelRetrieveToCas( + infos->second, api, jobs, use_blob_splitting)) { return false; } } @@ -297,47 +405,15 @@ auto BazelApi::CreateAction( // as size, but this is handled by the remote execution engine, so // no need to regenerate the digest. ts.QueueTask( - [this, digest = info.digest, &api, &failure, &info_map]() { - auto reader = network_->ReadBlobs({digest}); - auto blobs = reader.Next(); - std::size_t count{}; - BlobContainer container{}; - while (not blobs.empty()) { - if (count + blobs.size() > 1) { - Logger::Log(LogLevel::Error, - "received more blobs than requested."); - failure = true; - return; - } - for (auto& blob : blobs) { - try { - auto exec = IsExecutableObject( - info_map[ArtifactDigest{blob.digest}].type); - container.Emplace( - BazelBlob{blob.digest, blob.data, exec}); - } catch (std::exception const& ex) { - Logger::Log(LogLevel::Error, - "failed to emplace blob: {}", - ex.what()); - failure = true; - return; - } - } - count += blobs.size(); - blobs = reader.Next(); - } - if (count != 1) { - Logger::Log(LogLevel::Error, - "could not retrieve all requested blobs."); - failure = true; - return; - } - auto result = - api->Upload(container, /*skip_find_missing=*/true); - if (not result) { - failure = true; + [this, &info, &api, &failure, &info_map, use_blob_splitting]() { + if (use_blob_splitting + ? ::RetrieveToCasSplitted( + info, api, network_, info_map) + : ::RetrieveToCas( + {info.digest}, api, network_, info_map)) { return; } + failure = true; }); } } catch (std::exception const& ex) { @@ -350,7 +426,8 @@ auto BazelApi::CreateAction( } [[nodiscard]] auto BazelApi::RetrieveToMemory( - Artifact::ObjectInfo const& artifact_info) -> std::optional<std::string> { + Artifact::ObjectInfo const& artifact_info) noexcept + -> std::optional<std::string> { auto blobs = network_->ReadBlobs({artifact_info.digest}).Next(); if (blobs.size() == 1) { return blobs.at(0).data; diff --git a/src/buildtool/execution_api/remote/bazel/bazel_api.hpp b/src/buildtool/execution_api/remote/bazel/bazel_api.hpp index 3b1b5193..d0b38d32 100644 --- a/src/buildtool/execution_api/remote/bazel/bazel_api.hpp +++ b/src/buildtool/execution_api/remote/bazel/bazel_api.hpp @@ -15,12 +15,15 @@ #ifndef INCLUDED_SRC_BUILDTOOL_EXECUTION_API_REMOTE_BAZEL_BAZEL_API_HPP #define INCLUDED_SRC_BUILDTOOL_EXECUTION_API_REMOTE_BAZEL_BAZEL_API_HPP +#include <filesystem> +#include <map> #include <memory> +#include <optional> #include <string> -#include <utility> #include <vector> #include "gsl/gsl" +#include "src/buildtool/common/artifact.hpp" #include "src/buildtool/common/artifact_digest.hpp" #include "src/buildtool/common/remote/port.hpp" #include "src/buildtool/execution_api/bazel_msg/bazel_common.hpp" @@ -68,7 +71,8 @@ class BazelApi final : public IExecutionApi { [[nodiscard]] auto ParallelRetrieveToCas( std::vector<Artifact::ObjectInfo> const& artifacts_info, gsl::not_null<IExecutionApi*> const& api, - std::size_t jobs) noexcept -> bool final; + std::size_t jobs, + bool use_blob_splitting) noexcept -> bool final; [[nodiscard]] auto RetrieveToCas( std::vector<Artifact::ObjectInfo> const& artifacts_info, @@ -88,7 +92,7 @@ class BazelApi final : public IExecutionApi { const noexcept -> std::vector<ArtifactDigest> final; [[nodiscard]] auto RetrieveToMemory( - Artifact::ObjectInfo const& artifact_info) + Artifact::ObjectInfo const& artifact_info) noexcept -> std::optional<std::string> final; private: diff --git a/src/buildtool/execution_api/remote/bazel/bazel_cas_client.cpp b/src/buildtool/execution_api/remote/bazel/bazel_cas_client.cpp index 66381fde..1ba7d7f1 100644 --- a/src/buildtool/execution_api/remote/bazel/bazel_cas_client.cpp +++ b/src/buildtool/execution_api/remote/bazel/bazel_cas_client.cpp @@ -14,10 +14,16 @@ #include "src/buildtool/execution_api/remote/bazel/bazel_cas_client.hpp" +#include <mutex> +#include <shared_mutex> +#include <unordered_map> + #include "grpcpp/grpcpp.h" #include "gsl/gsl" #include "src/buildtool/common/bazel_types.hpp" #include "src/buildtool/common/remote/client_common.hpp" +#include "src/buildtool/compatibility/native_support.hpp" +#include "src/buildtool/crypto/hash_function.hpp" #include "src/buildtool/execution_api/common/execution_common.hpp" namespace { @@ -29,6 +35,67 @@ namespace { "{}/blobs/{}/{}", instance_name, digest.hash(), digest.size_bytes()); } +// In order to determine whether blob splitting is supported at the remote, a +// trial request to the remote CAS service is issued. This is just a workaround +// until the blob split API extension is accepted as part of the official remote +// execution protocol. Then, the ordinary way to determine server capabilities +// can be employed by using the capabilities service. +[[nodiscard]] auto BlobSplitSupport( + std::string const& instance_name, + std::unique_ptr<bazel_re::ContentAddressableStorage::Stub> const& + stub) noexcept -> bool { + // Create empty blob. + std::string empty_str{}; + std::string hash = HashFunction::ComputeBlobHash(empty_str).HexString(); + bazel_re::Digest digest{}; + digest.set_hash(NativeSupport::Prefix(hash, false)); + digest.set_size_bytes(empty_str.size()); + + // Upload empty blob. + grpc::ClientContext update_context{}; + bazel_re::BatchUpdateBlobsRequest update_request{}; + bazel_re::BatchUpdateBlobsResponse update_response{}; + update_request.set_instance_name(instance_name); + auto* request = update_request.add_requests(); + request->mutable_digest()->CopyFrom(digest); + request->set_data(empty_str); + grpc::Status update_status = stub->BatchUpdateBlobs( + &update_context, update_request, &update_response); + if (not update_status.ok()) { + return false; + } + + // Request splitting empty blob. + grpc::ClientContext split_context{}; + bazel_re::SplitBlobRequest split_request{}; + bazel_re::SplitBlobResponse split_response{}; + split_request.set_instance_name(instance_name); + split_request.mutable_blob_digest()->CopyFrom(digest); + grpc::Status split_status = + stub->SplitBlob(&split_context, split_request, &split_response); + return split_status.ok(); +} + +// Cached version of blob-split support request. +[[nodiscard]] auto BlobSplitSupportCached( + std::string const& instance_name, + std::unique_ptr<bazel_re::ContentAddressableStorage::Stub> const& + stub) noexcept -> bool { + static auto mutex = std::shared_mutex{}; + static auto blob_split_support_map = + std::unordered_map<std::string, bool>{}; + { + auto lock = std::shared_lock(mutex); + if (blob_split_support_map.contains(instance_name)) { + return blob_split_support_map[instance_name]; + } + } + auto supported = BlobSplitSupport(instance_name, stub); + auto lock = std::unique_lock(mutex); + blob_split_support_map[instance_name] = supported; + return supported; +} + } // namespace BazelCasClient::BazelCasClient(std::string const& server, Port port) noexcept @@ -195,6 +262,26 @@ auto BazelCasClient::ReadSingleBlob(std::string const& instance_name, return std::nullopt; } +auto BazelCasClient::SplitBlob(std::string const& instance_name, + bazel_re::Digest const& digest) noexcept + -> std::optional<std::vector<bazel_re::Digest>> { + if (not BlobSplitSupportCached(instance_name, stub_)) { + return std::nullopt; + } + grpc::ClientContext context{}; + bazel_re::SplitBlobRequest request{}; + request.set_instance_name(instance_name); + request.mutable_blob_digest()->CopyFrom(digest); + bazel_re::SplitBlobResponse response{}; + grpc::Status status = stub_->SplitBlob(&context, request, &response); + std::vector<bazel_re::Digest> result{}; + if (not status.ok()) { + LogStatus(&logger_, LogLevel::Debug, status); + return std::nullopt; + } + return ProcessResponseContents<bazel_re::Digest>(response); +} + template <class T_OutputIter> auto BazelCasClient::FindMissingBlobs(std::string const& instance_name, T_OutputIter const& start, @@ -331,6 +418,14 @@ auto GetResponseContents<bazel_re::Directory, bazel_re::GetTreeResponse>( return response.directories(); } +// Specialization of GetResponseContents for 'SplitBlobResponse' +template <> +auto GetResponseContents<bazel_re::Digest, bazel_re::SplitBlobResponse>( + bazel_re::SplitBlobResponse const& response) noexcept + -> pb::RepeatedPtrField<bazel_re::Digest> const& { + return response.chunk_digests(); +} + } // namespace detail template <class T_Request, class T_Content, class T_OutputIter> diff --git a/src/buildtool/execution_api/remote/bazel/bazel_cas_client.hpp b/src/buildtool/execution_api/remote/bazel/bazel_cas_client.hpp index 6750f054..99da5c3e 100644 --- a/src/buildtool/execution_api/remote/bazel/bazel_cas_client.hpp +++ b/src/buildtool/execution_api/remote/bazel/bazel_cas_client.hpp @@ -17,6 +17,7 @@ #include <functional> #include <memory> +#include <optional> #include <string> #include <utility> #include <vector> @@ -31,7 +32,7 @@ #include "src/buildtool/logging/logger.hpp" /// Implements client side for serivce defined here: -/// https://github.com/bazelbuild/bazel/blob/4b6ad34dbba15dacebfb6cbf76fa741649cdb007/third_party/remoteapis/build/bazel/remote/execution/v2/remote_execution.proto#L243 +/// https://github.com/bazelbuild/remote-apis/blob/e1fe21be4c9ae76269a5a63215bb3c72ed9ab3f0/build/bazel/remote/execution/v2/remote_execution.proto#L317 class BazelCasClient { public: BazelCasClient(std::string const& server, Port port) noexcept; @@ -128,6 +129,14 @@ class BazelCasClient { bazel_re::Digest const& digest) noexcept -> std::optional<BazelBlob>; + /// @brief Split single blob into chunks + /// @param[in] instance_name Name of the CAS instance + /// @param[in] digest Blob digest to be splitted + /// @return The chunk digests of the splitted blob + [[nodiscard]] auto SplitBlob(std::string const& instance_name, + bazel_re::Digest const& digest) noexcept + -> std::optional<std::vector<bazel_re::Digest>>; + private: std::unique_ptr<ByteStreamClient> stream_{}; std::unique_ptr<bazel_re::ContentAddressableStorage::Stub> stub_; diff --git a/src/buildtool/execution_api/remote/bazel/bazel_execution_client.hpp b/src/buildtool/execution_api/remote/bazel/bazel_execution_client.hpp index 227757a9..31ee1144 100644 --- a/src/buildtool/execution_api/remote/bazel/bazel_execution_client.hpp +++ b/src/buildtool/execution_api/remote/bazel/bazel_execution_client.hpp @@ -28,7 +28,7 @@ #include "src/buildtool/logging/logger.hpp" /// Implements client side for service defined here: -/// https://github.com/bazelbuild/bazel/blob/4b6ad34dbba15dacebfb6cbf76fa741649cdb007/third_party/remoteapis/build/bazel/remote/execution/v2/remote_execution.proto#L42 +/// https://github.com/bazelbuild/remote-apis/blob/e1fe21be4c9ae76269a5a63215bb3c72ed9ab3f0/build/bazel/remote/execution/v2/remote_execution.proto#L44 class BazelExecutionClient { public: struct ExecutionOutput { diff --git a/src/buildtool/execution_api/remote/bazel/bazel_network.cpp b/src/buildtool/execution_api/remote/bazel/bazel_network.cpp index 4f8020ef..ef3837d3 100644 --- a/src/buildtool/execution_api/remote/bazel/bazel_network.cpp +++ b/src/buildtool/execution_api/remote/bazel/bazel_network.cpp @@ -153,6 +153,11 @@ auto BazelNetwork::IsAvailable(std::vector<bazel_re::Digest> const& digests) return cas_->FindMissingBlobs(instance_name_, digests); } +auto BazelNetwork::SplitBlob(bazel_re::Digest const& digest) const noexcept + -> std::optional<std::vector<bazel_re::Digest>> { + return cas_->SplitBlob(instance_name_, digest); +} + template <class T_Iter> auto BazelNetwork::DoUploadBlobs(T_Iter const& first, T_Iter const& last) noexcept -> bool { diff --git a/src/buildtool/execution_api/remote/bazel/bazel_network.hpp b/src/buildtool/execution_api/remote/bazel/bazel_network.hpp index 9bd411da..ff944264 100644 --- a/src/buildtool/execution_api/remote/bazel/bazel_network.hpp +++ b/src/buildtool/execution_api/remote/bazel/bazel_network.hpp @@ -15,10 +15,15 @@ #ifndef INCLUDED_SRC_BUILDTOOL_EXECUTION_API_REMOTE_BAZEL_BAZEL_NETWORK_HPP #define INCLUDED_SRC_BUILDTOOL_EXECUTION_API_REMOTE_BAZEL_BAZEL_NETWORK_HPP +#include <filesystem> #include <memory> #include <optional> +#include <string> #include <unordered_map> +#include <utility> +#include <vector> +#include "gsl/gsl" #include "src/buildtool/common/bazel_types.hpp" #include "src/buildtool/common/remote/port.hpp" #include "src/buildtool/execution_api/bazel_msg/bazel_blob.hpp" @@ -70,6 +75,9 @@ class BazelNetwork { [[nodiscard]] auto IsAvailable(std::vector<bazel_re::Digest> const& digests) const noexcept -> std::vector<bazel_re::Digest>; + [[nodiscard]] auto SplitBlob(bazel_re::Digest const& digest) const noexcept + -> std::optional<std::vector<bazel_re::Digest>>; + /// \brief Uploads blobs to CAS /// \param blobs The blobs to upload /// \param skip_find_missing Skip finding missing blobs, just upload all |