mirror of
https://github.com/apache/doris.git
synced 2026-11-04 18:02:19 +00:00
### 1. What does this PR do?
BE and Cloud Recycler previously maintained separate object-storage
abstractions and separate S3/Azure implementations. Although both sides
called the same cloud-provider SDKs, credential construction, error
conversion, metrics, pagination, batch deletion, and compatibility
behavior were duplicated and could evolve differently.
This PR consolidates those implementations under `common/cpp/client`
while keeping the original decorator-oriented client abstraction:
- `ObjStorageClient` is the common interface used by upper layers and
implemented by all concrete clients and decorators.
- `S3ObjStorageClient` and `AzureObjStorageClient` implement provider
operations through the AWS and Azure SDKs.
- `RateLimitedObjStorageClient` implements the same interface and
decorates another `ObjStorageClient`.
- BE's `S3ClientFactory` and Recycler's `S3Accessor` construct the
provider client and wrap it with their own rate-limit policy.
- Shared request/response types, page-based listing, lazy iteration,
recursive deletion orchestration, credentials, metrics, and error
conversion live in the common layer.
Before the refactor, BE and Recycler reached the SDKs through separate
stacks:
```text
BE call sites Recycler call sites
| |
v v
BE object client Recycler S3Accessor
+ BE limiter + Recycler limiter
| |
v v
BE S3 / Azure clients Recycler S3 / Azure clients
| |
v v
AWS / Azure SDK AWS / Azure SDK
```
After the refactor, BE and Recycler keep their own upper-layer
composition points and policies, while both use the same common
interface, decorator, and provider implementations:
```text
AFTER
+-------------------------- BE --------------------------+ +---------------------- Recycler ----------------------+
| BE call sites S3RateLimiterManager | | Recycler call sites AccessorRateLimiter |
| | | | | | | |
| v v | | v v |
| S3ClientFactory BE policy | | S3Accessor Recycler policy |
+-----------------------------+--------------------------+ +-----------------------------+-------------------------+
| |
+------------------------------+-------------------------------+
|
v
ObjStorageClient
^
|
RateLimitedObjStorageClient
(policy_: ObjStorageRateLimitPolicy)
|
+----------+----------+
| |
v v
S3ObjStorageClient AzureObjStorageClient
| |
v v
AWS SDK Azure SDK
```
This keeps cross-cutting behavior composable. Future decorators, such as
tracing, retries, or additional admission controls, can be added without
changing provider implementations or introducing a facade coupled to
every policy.
### 2. How are the different behaviors unified?
| Behavior | Unified behavior |
| --- | --- |
| Client API | BE and Recycler use the same `ObjStorageClient` interface
and request/response types. `doris::io` aliases keep BE call sites
source-compatible, while Recycler adapters preserve its integer-facing
API. |
| Provider clients | `S3ObjStorageClient` and `AzureObjStorageClient`
are shared by both callers and directly implement `ObjStorageClient`. |
| Rate limiting | `RateLimitedObjStorageClient` wraps any
`ObjStorageClient`. BE and Recycler inject their own
`ObjStorageRateLimitPolicy` when constructing the client. |
| Error model | `ObjectStorageResponse` carries a Doris status and
preserves the HTTP status and request ID when available. Recycler
adapters retain caller-facing contracts such as the 0/1/negative
`exists` result. |
| Listing | `list_objects` returns one page. `ObjectListIterator` owns
the continuation token and requests the next page only after consuming
the current page. |
| Missing S3 prefix | The shared S3 implementation preserves the
S3-compatible `NoSuchKey`-as-empty behavior for both callers. |
| Batch deletion | Provider implementations enforce their SDK limits: up
to 1000 objects for S3 and 256 subrequests for Azure. |
| Recursive deletion | `ObjStorageClient` provides the shared
provider-independent listing, expiration filtering, grouping, execution,
and error-propagation flow. Recycler can supply its `SyncExecutor`
adapter; BE uses the synchronous fallback. |
| AWS credentials | `AwsCredentialFactory` implements static
credentials, the default provider chain, role ARN, and external ID once.
|
| Azure credentials | `AzureAuthFactory` creates the shared-key
credential and container client for both callers. Shared error
conversion retains the BE TLS diagnostic context. |
| Metrics | Provider clients share request-failure metrics and latency
timers. The previous BE slow-S3-upload logging behavior is retained. |
### 3. Rate-limit boundary and follow-up
The decorator currently preserves the pre-refactor BE semantics:
- A direct `list_objects` or `delete_objects` call is admitted once
before delegation.
- `delete_objects_recursively` is admitted once as one logical PUT
operation and then delegates to the wrapped client.
- Therefore, list pages and provider delete batches performed inside
recursive deletion do not yet pass back through the decorator
individually.
Making recursive deletion invoke the decorated page-list and
delete-batch operations requires a separate change to avoid either
bypassing the decorator or double-counting requests. That request-level
admission behavior will be handled in a follow-up PR rather than mixed
into this structural refactor. This temporarily changes Recycler
recursive-delete admission to match the existing BE behavior.
542 lines
26 KiB
C++
542 lines
26 KiB
C++
// Licensed to the Apache Software Foundation (ASF) under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The ASF licenses this file
|
|
// to you 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.
|
|
|
|
#include "azure_obj_storage_client.h"
|
|
|
|
#include <butil/guid.h>
|
|
|
|
#include <cctype>
|
|
#include <cstdint>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include "cpp/obj_retry_strategy.h"
|
|
|
|
using namespace Azure::Storage::Blobs;
|
|
|
|
namespace {
|
|
std::string wrap_object_storage_path_msg(const doris::ObjStoragePath& opts) {
|
|
return fmt::format("bucket {}, key {}, prefix {}, path {}", opts.bucket, opts.key, opts.prefix,
|
|
opts.path.native());
|
|
}
|
|
|
|
doris::ObjStoragePath with_object_key(const doris::ObjStoragePath& opts, std::string_view key) {
|
|
auto path = opts;
|
|
path.key = key;
|
|
return path;
|
|
}
|
|
|
|
std::string to_lower_ascii(std::string_view input) {
|
|
std::string lowered(input);
|
|
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
|
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
|
return lowered;
|
|
}
|
|
|
|
std::string encode_azure_block_id(std::string_view upload_id, int part_num) {
|
|
// Azure has no multipart upload namespace. Include the writer UUID in every block ID so
|
|
// concurrent writers for the same key cannot stage interchangeable blocks.
|
|
std::vector<unsigned char> raw_id(upload_id.begin(), upload_id.end());
|
|
auto part = static_cast<uint32_t>(part_num);
|
|
for (size_t i = 0; i < sizeof(part); ++i) {
|
|
raw_id.push_back(static_cast<unsigned char>(part >> (i * 8)));
|
|
}
|
|
Aws::Utils::ByteBuffer bytes(raw_id.data(), raw_id.size());
|
|
return Aws::Utils::HashingUtils::Base64Encode(bytes);
|
|
}
|
|
|
|
constexpr char SAS_TOKEN_URL_TEMPLATE[] = "{}/{}/{}{}";
|
|
constexpr char BlobNotFound[] = "BlobNotFound";
|
|
} // namespace
|
|
|
|
namespace doris {
|
|
|
|
std::string azure_multipart_block_id(std::string_view upload_id, int part_num) {
|
|
return encode_azure_block_id(upload_id, part_num);
|
|
}
|
|
|
|
std::string build_azure_batch_delete_failure_message(const ObjStoragePath& opts,
|
|
std::string_view key) {
|
|
return fmt::format("Azure batch delete failed, path msg {}",
|
|
wrap_object_storage_path_msg(with_object_key(opts, key)));
|
|
}
|
|
|
|
// As Azure's doc said, the batch size is 256
|
|
// You can find out the num in https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch?tabs=microsoft-entra-id
|
|
// > Each batch request supports a maximum of 256 subrequests.
|
|
constexpr size_t BlobBatchMaxOperations = 256;
|
|
|
|
bool is_azure_tls_ca_error_message(std::string_view message) {
|
|
std::string lower = to_lower_ascii(message);
|
|
return lower.find("ssl ca cert") != std::string::npos ||
|
|
lower.find("peer failed verification") != std::string::npos ||
|
|
lower.find("unable to get local issuer certificate") != std::string::npos ||
|
|
lower.find("problem with the ssl ca cert") != std::string::npos;
|
|
}
|
|
|
|
std::string build_azure_tls_debug_suffix(std::string_view error_message,
|
|
std::string_view tls_debug_context) {
|
|
if (tls_debug_context.empty() || !is_azure_tls_ca_error_message(error_message)) {
|
|
return "";
|
|
}
|
|
return fmt::format(", {}", tls_debug_context);
|
|
}
|
|
|
|
static ObjStorageResponse make_azure_std_exception_response(const std::exception& e,
|
|
const ObjStoragePath& opts,
|
|
std::string_view tls_debug_context) {
|
|
auto msg = fmt::format("Azure request failed because {}, path msg {}{}", e.what(),
|
|
wrap_object_storage_path_msg(opts),
|
|
build_azure_tls_debug_suffix(e.what(), tls_debug_context));
|
|
LOG(WARNING) << msg;
|
|
return {.status = ObjStorageStatus {ObjStorageStatus::INTERNAL_ERROR, std::move(msg)},
|
|
.http_code = 0,
|
|
.request_id = ""};
|
|
}
|
|
|
|
template <typename Func>
|
|
ObjStorageResponse do_azure_client_call(Func f, const ObjStoragePath& opts,
|
|
std::string_view tls_debug_context) {
|
|
try {
|
|
f();
|
|
} catch (Azure::Core::RequestFailedException& e) {
|
|
doris::record_object_request_failed(static_cast<int>(e.StatusCode));
|
|
auto msg = fmt::format(
|
|
"Azure request failed because {}, error msg {}, http code {}, path msg {}{}",
|
|
e.what(), e.Message, static_cast<int>(e.StatusCode),
|
|
wrap_object_storage_path_msg(opts),
|
|
build_azure_tls_debug_suffix(fmt::format("{} {}", e.what(), e.Message),
|
|
tls_debug_context));
|
|
LOG(WARNING) << msg;
|
|
return {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode),
|
|
std::move(msg)),
|
|
.http_code = static_cast<int>(e.StatusCode),
|
|
.request_id = std::move(e.RequestId)};
|
|
} catch (const std::exception& e) {
|
|
return make_azure_std_exception_response(e, opts, tls_debug_context);
|
|
}
|
|
return ObjStorageResponse::OK();
|
|
}
|
|
|
|
struct AzureBatchDeleter {
|
|
AzureBatchDeleter(BlobContainerClient* client, const ObjStoragePath& opts,
|
|
std::string_view representative_key, std::string_view tls_debug_context)
|
|
: _client(client),
|
|
_batch(client->CreateBatch()),
|
|
_opts(with_object_key(opts, representative_key)),
|
|
_tls_debug_context(tls_debug_context) {}
|
|
// Submit one blob to be deleted in `AzureBatchDeleter::execute`
|
|
void delete_blob(const std::string& blob_name) {
|
|
deferred_resps.emplace_back(DeferredDeleteResponse {
|
|
.key = blob_name, .response = _batch.DeleteBlob(blob_name)});
|
|
}
|
|
ObjStorageResponse execute() {
|
|
if (deferred_resps.empty()) {
|
|
return ObjStorageResponse::OK();
|
|
}
|
|
auto resp = do_azure_client_call(
|
|
[&]() {
|
|
client_bvar::ScopedLatency scoped_latency(
|
|
client_bvar::s3_delete_objects_latency);
|
|
_client->SubmitBatch(_batch);
|
|
},
|
|
_opts, _tls_debug_context);
|
|
if (resp.status.code != ObjStorageStatus::OK) {
|
|
return resp;
|
|
}
|
|
|
|
for (auto&& deferred : deferred_resps) {
|
|
try {
|
|
auto r = deferred.response.GetResponse();
|
|
if (!r.Value.Deleted) {
|
|
auto msg = build_azure_batch_delete_failure_message(_opts, deferred.key);
|
|
LOG(WARNING) << msg;
|
|
return {.status = ObjStorageStatus {ObjStorageStatus::INTERNAL_ERROR,
|
|
std::move(msg)},
|
|
.http_code = 0,
|
|
.request_id = ""};
|
|
}
|
|
} catch (Azure::Core::RequestFailedException& e) {
|
|
if (Azure::Core::Http::HttpStatusCode::NotFound == e.StatusCode &&
|
|
0 == strcmp(e.ErrorCode.c_str(), BlobNotFound)) {
|
|
continue;
|
|
}
|
|
doris::record_object_request_failed(static_cast<int>(e.StatusCode));
|
|
auto msg = fmt::format(
|
|
"Azure request failed because {}, error msg {}, http code {}, path msg "
|
|
"{}{}",
|
|
e.what(), e.Message, static_cast<int>(e.StatusCode),
|
|
wrap_object_storage_path_msg(with_object_key(_opts, deferred.key)),
|
|
build_azure_tls_debug_suffix(fmt::format("{} {}", e.what(), e.Message),
|
|
_tls_debug_context));
|
|
LOG(WARNING) << msg;
|
|
return {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode),
|
|
std::move(msg)),
|
|
.http_code = static_cast<int>(e.StatusCode),
|
|
.request_id = std::move(e.RequestId)};
|
|
}
|
|
}
|
|
|
|
return ObjStorageResponse::OK();
|
|
}
|
|
|
|
private:
|
|
struct DeferredDeleteResponse {
|
|
std::string key;
|
|
Azure::Storage::DeferredResponse<Models::DeleteBlobResult> response;
|
|
};
|
|
|
|
BlobContainerClient* _client;
|
|
BlobContainerBatch _batch;
|
|
ObjStoragePath _opts;
|
|
std::string_view _tls_debug_context;
|
|
std::vector<DeferredDeleteResponse> deferred_resps;
|
|
};
|
|
|
|
ObjStorageUploadResult AzureObjStorageClient::create_multipart_upload(const ObjStoragePath&) {
|
|
// Azure has no provider-side multipart session. This local UUID namespaces the writer's
|
|
// staged block IDs and is carried through the same interface as an S3 upload ID.
|
|
auto upload_id = butil::GenerateGUID();
|
|
if (upload_id.empty()) {
|
|
return {.resp = {.status = {ObjStorageStatus::INTERNAL_ERROR,
|
|
"failed to generate Azure multipart upload ID"}}};
|
|
}
|
|
return ObjStorageUploadResult {
|
|
.resp = ObjStorageResponse::OK(),
|
|
.upload_id = std::move(upload_id),
|
|
};
|
|
}
|
|
|
|
ObjStorageResponse AzureObjStorageClient::put_object(const ObjStoragePath& opts,
|
|
std::string_view stream) {
|
|
auto client = _client->GetBlockBlobClient(opts.key);
|
|
return do_azure_client_call(
|
|
[&]() {
|
|
client_bvar::ScopedLatency scoped_latency(client_bvar::s3_put_latency);
|
|
client.UploadFrom(reinterpret_cast<const uint8_t*>(stream.data()), stream.size());
|
|
},
|
|
opts, _config.tls_debug_context);
|
|
}
|
|
|
|
ObjStorageUploadResult AzureObjStorageClient::upload_part(const ObjStoragePath& opts,
|
|
const std::string& upload_id,
|
|
std::string_view stream, int part_num) {
|
|
DCHECK(!upload_id.empty());
|
|
auto client = _client->GetBlockBlobClient(opts.key);
|
|
std::string block_id = azure_multipart_block_id(upload_id, part_num);
|
|
try {
|
|
Azure::Core::IO::MemoryBodyStream memory_body(
|
|
reinterpret_cast<const uint8_t*>(stream.data()), stream.size());
|
|
// The blockId must be base64 encoded
|
|
client_bvar::ScopedLatency scoped_latency(client_bvar::s3_multi_part_upload_latency);
|
|
client.StageBlock(block_id, memory_body);
|
|
} catch (Azure::Core::RequestFailedException& e) {
|
|
record_object_request_failed(static_cast<int>(e.StatusCode));
|
|
auto tls_debug_suffix = build_azure_tls_debug_suffix(
|
|
fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context);
|
|
auto msg = fmt::format(
|
|
"Azure request failed because {}, error msg {}, http code {}, path msg {}{}",
|
|
e.what(), e.Message, static_cast<int>(e.StatusCode),
|
|
wrap_object_storage_path_msg(opts), tls_debug_suffix);
|
|
LOG(WARNING) << msg;
|
|
// clang-format off
|
|
return {
|
|
.resp = {
|
|
.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode),
|
|
std::move(msg)),
|
|
.http_code = static_cast<int>(e.StatusCode),
|
|
.request_id = std::move(e.RequestId),
|
|
},
|
|
};
|
|
// clang-format on
|
|
} catch (const std::exception& e) {
|
|
return {.resp = make_azure_std_exception_response(e, opts, _config.tls_debug_context)};
|
|
}
|
|
return ObjStorageUploadResult {.resp = ObjStorageResponse::OK(), .etag = std::move(block_id)};
|
|
}
|
|
|
|
ObjStorageResponse AzureObjStorageClient::complete_multipart_upload(
|
|
const ObjStoragePath& opts, const std::string& upload_id,
|
|
const std::vector<ObjStorageCompletedPart>& completed_parts) {
|
|
DCHECK(!upload_id.empty());
|
|
auto client = _client->GetBlockBlobClient(opts.key);
|
|
std::vector<std::string> string_block_ids;
|
|
std::ranges::transform(completed_parts, std::back_inserter(string_block_ids),
|
|
[&upload_id](const ObjStorageCompletedPart& i) {
|
|
return azure_multipart_block_id(upload_id, i.part_num);
|
|
});
|
|
return do_azure_client_call(
|
|
[&]() {
|
|
client_bvar::ScopedLatency scoped_latency(
|
|
client_bvar::s3_multi_part_upload_latency);
|
|
client.CommitBlockList(string_block_ids);
|
|
},
|
|
opts, _config.tls_debug_context);
|
|
}
|
|
|
|
ObjStorageHeadResult AzureObjStorageClient::head_object(const ObjStoragePath& opts) {
|
|
try {
|
|
Models::BlobProperties properties = [&]() {
|
|
client_bvar::ScopedLatency scoped_latency(client_bvar::s3_head_latency);
|
|
return _client->GetBlockBlobClient(opts.key).GetProperties().Value;
|
|
}();
|
|
return {.resp = ObjStorageResponse::OK(), .file_size = properties.BlobSize};
|
|
} catch (Azure::Core::RequestFailedException& e) {
|
|
if (e.StatusCode == Azure::Core::Http::HttpStatusCode::NotFound) {
|
|
return ObjStorageHeadResult {
|
|
.resp = {.status = obj_storage_status_from_http_code(
|
|
static_cast<int>(e.StatusCode), ""),
|
|
.http_code = static_cast<int>(e.StatusCode),
|
|
.request_id = std::move(e.RequestId)},
|
|
};
|
|
}
|
|
record_object_request_failed(static_cast<int>(e.StatusCode));
|
|
auto tls_debug_suffix = build_azure_tls_debug_suffix(
|
|
fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context);
|
|
auto msg = fmt::format(
|
|
"Azure request failed because {}, error msg {}, http code {}, path msg {}{}",
|
|
e.what(), e.Message, static_cast<int>(e.StatusCode),
|
|
wrap_object_storage_path_msg(opts), tls_debug_suffix);
|
|
LOG(WARNING) << msg << ", request_id=" << e.RequestId;
|
|
return ObjStorageHeadResult {
|
|
.resp = {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode),
|
|
std::move(msg)),
|
|
.http_code = static_cast<int>(e.StatusCode),
|
|
.request_id = std::move(e.RequestId)},
|
|
};
|
|
} catch (const std::exception& e) {
|
|
return {.resp = make_azure_std_exception_response(e, opts, _config.tls_debug_context)};
|
|
}
|
|
}
|
|
|
|
ObjStorageResponse AzureObjStorageClient::get_object(const ObjStoragePath& opts, void* buffer,
|
|
size_t offset, size_t bytes_read,
|
|
size_t* size_return) {
|
|
auto client = _client->GetBlockBlobClient(opts.key);
|
|
auto response = do_azure_client_call(
|
|
[&]() {
|
|
DownloadBlobToOptions download_opts;
|
|
Azure::Core::Http::HttpRange range {.Offset = static_cast<int64_t>(offset),
|
|
.Length = bytes_read};
|
|
download_opts.Range = range;
|
|
client_bvar::ScopedLatency scoped_latency(client_bvar::s3_get_latency);
|
|
auto resp = client.DownloadTo(reinterpret_cast<uint8_t*>(buffer), bytes_read,
|
|
download_opts);
|
|
*size_return = resp.Value.ContentRange.Length.Value();
|
|
},
|
|
opts, _config.tls_debug_context);
|
|
if (!response.ok() || *size_return == bytes_read) {
|
|
return response;
|
|
}
|
|
return {
|
|
.status = {ObjStorageStatus::INTERNAL_ERROR,
|
|
fmt::format("incomplete read from {}, expect {}, got {}",
|
|
wrap_object_storage_path_msg(opts), bytes_read, *size_return)},
|
|
.http_code = response.http_code,
|
|
.request_id = std::move(response.request_id),
|
|
};
|
|
}
|
|
|
|
ObjStorageListPageResult AzureObjStorageClient::list_objects_page(
|
|
const ObjStoragePath& opts, std::string_view continuation_token) {
|
|
const auto& prefix = opts.prefix.empty() ? opts.key : opts.prefix;
|
|
ListBlobsOptions request;
|
|
request.Prefix = prefix;
|
|
request.PageSizeHint = static_cast<int32_t>(capabilities().max_list_page);
|
|
if (!continuation_token.empty()) {
|
|
request.ContinuationToken = std::string(continuation_token);
|
|
}
|
|
TEST_SYNC_POINT_CALLBACK("AzureObjStorageClient::list_objects", &request);
|
|
|
|
try {
|
|
auto response = [&]() {
|
|
client_bvar::ScopedLatency scoped_latency(client_bvar::s3_list_latency);
|
|
return _client->ListBlobs(request);
|
|
}();
|
|
const bool has_more = response.NextPageToken.HasValue();
|
|
auto next_token = has_more ? response.NextPageToken.Value() : std::string {};
|
|
if (has_more && next_token.empty()) {
|
|
return {
|
|
.resp = {.status = {ObjStorageStatus::INTERNAL_ERROR,
|
|
"Azure list response has an empty continuation token"},
|
|
.http_code = 0},
|
|
};
|
|
}
|
|
ObjStorageListPageResult page {.resp = ObjStorageResponse::OK(),
|
|
.continuation_token = std::move(next_token),
|
|
.has_more = has_more};
|
|
page.objects.reserve(response.Blobs.size());
|
|
for (auto&& item : response.Blobs) {
|
|
DCHECK(item.Name.starts_with(*request.Prefix)) << item.Name << ' ' << *request.Prefix;
|
|
page.objects.emplace_back(ObjectMeta {
|
|
.key = std::move(item.Name),
|
|
.size = item.BlobSize,
|
|
// `Azure::DateTime` adds the offset of `SystemClockEpoch` to the given Unix timestamp,
|
|
// so here we need to subtract this offset to obtain the Unix timestamp of the mtime.
|
|
// https://github.com/Azure/azure-sdk-for-cpp/blob/azure-core_1.12.0/sdk/core/azure-core/inc/azure/core/datetime.hpp#L129
|
|
.mtime_s = duration_cast<std::chrono::seconds>(item.Details.LastModified -
|
|
SystemClockEpoch)
|
|
.count()});
|
|
}
|
|
return page;
|
|
} catch (Azure::Core::RequestFailedException& e) {
|
|
record_object_request_failed(static_cast<int>(e.StatusCode));
|
|
auto tls_debug_suffix = build_azure_tls_debug_suffix(
|
|
fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context);
|
|
LOG(WARNING) << fmt::format("Azure request failed because {}, url: {}, prefix: {}{}",
|
|
e.what(), _client->GetUrl(), request.Prefix.Value(),
|
|
tls_debug_suffix);
|
|
return {
|
|
.resp = {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode),
|
|
e.Message + tls_debug_suffix),
|
|
.http_code = static_cast<int>(e.StatusCode),
|
|
.request_id = std::move(e.RequestId)},
|
|
};
|
|
} catch (std::exception& e) {
|
|
LOG(WARNING) << fmt::format("Azure request failed because {}, url: {}, prefix: {}",
|
|
e.what(), _client->GetUrl(), request.Prefix.Value());
|
|
return {
|
|
.resp = {.status = {ObjStorageStatus::INTERNAL_ERROR, e.what()},
|
|
.http_code = 0,
|
|
.request_id = ""},
|
|
};
|
|
}
|
|
}
|
|
|
|
// As Azure's doc said, the batch size is 256
|
|
// You can find out the num in https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch?tabs=microsoft-entra-id
|
|
// > Each batch request supports a maximum of 256 subrequests.
|
|
ObjStorageResponse AzureObjStorageClient::delete_objects(const ObjStoragePath& opts,
|
|
std::vector<std::string> objs) {
|
|
// TODO(ByteYue) : use range to adate this code when compiler is ready
|
|
// auto chunkedView = objs | std::views::chunk(BlobBatchMaxOperations);
|
|
auto begin = std::begin(objs);
|
|
auto end = std::end(objs);
|
|
|
|
while (begin != end) {
|
|
auto chunk_end = begin;
|
|
size_t batch_size = BlobBatchMaxOperations;
|
|
TEST_SYNC_POINT_CALLBACK("AzureObjClient::delete_objects", &batch_size);
|
|
TEST_SYNC_POINT_CALLBACK("AzureObjStorageClient::delete_objects", &batch_size);
|
|
batch_size = std::max<size_t>(1, batch_size);
|
|
std::advance(chunk_end,
|
|
std::min(batch_size, static_cast<size_t>(std::distance(begin, end))));
|
|
|
|
auto deleter = AzureBatchDeleter(_client.get(), opts, *begin, _config.tls_debug_context);
|
|
std::ranges::for_each(std::ranges::subrange(begin, chunk_end),
|
|
[&](const std::string& obj) { deleter.delete_blob(obj); });
|
|
begin = chunk_end;
|
|
if (auto resp = deleter.execute(); resp.status.code != ObjStorageStatus::OK) {
|
|
return resp;
|
|
}
|
|
}
|
|
return ObjStorageResponse::OK();
|
|
}
|
|
|
|
ObjStorageResponse AzureObjStorageClient::delete_object(const ObjStoragePath& opts) {
|
|
try {
|
|
auto resp = [&]() {
|
|
client_bvar::ScopedLatency scoped_latency(client_bvar::s3_delete_object_latency);
|
|
return _client->DeleteBlob(opts.key);
|
|
}();
|
|
if (!resp.Value.Deleted) {
|
|
return {
|
|
.status = ObjStorageStatus {ObjStorageStatus::IO_ERROR,
|
|
"Delete azure blob failed"},
|
|
.http_code = 0,
|
|
.request_id = "",
|
|
};
|
|
}
|
|
return ObjStorageResponse::OK();
|
|
} catch (Azure::Core::RequestFailedException& e) {
|
|
if (e.StatusCode == Azure::Core::Http::HttpStatusCode::NotFound &&
|
|
e.ErrorCode == BlobNotFound) {
|
|
return ObjStorageResponse::OK();
|
|
}
|
|
record_object_request_failed(static_cast<int>(e.StatusCode));
|
|
auto tls_debug_suffix = build_azure_tls_debug_suffix(
|
|
fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context);
|
|
auto msg = fmt::format(
|
|
"Azure request failed because {}, error msg {}, http code {}, path msg {}{}",
|
|
e.what(), e.Message, static_cast<int>(e.StatusCode),
|
|
wrap_object_storage_path_msg(opts), tls_debug_suffix);
|
|
LOG(WARNING) << msg;
|
|
return {
|
|
.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode),
|
|
std::move(msg)),
|
|
.http_code = static_cast<int>(e.StatusCode),
|
|
.request_id = std::move(e.RequestId),
|
|
};
|
|
} catch (std::exception& e) {
|
|
auto msg = fmt::format("Azure request failed because {}, path msg {}{}", e.what(),
|
|
wrap_object_storage_path_msg(opts),
|
|
build_azure_tls_debug_suffix(e.what(), _config.tls_debug_context));
|
|
LOG(WARNING) << msg;
|
|
return {
|
|
.status = ObjStorageStatus {ObjStorageStatus::INTERNAL_ERROR, std::move(msg)},
|
|
.http_code = 0,
|
|
.request_id = "",
|
|
};
|
|
}
|
|
}
|
|
|
|
std::string AzureObjStorageClient::generate_presigned_url(const ObjStoragePath& opts,
|
|
int64_t expiration_secs) {
|
|
Azure::Storage::Sas::BlobSasBuilder sas_builder;
|
|
sas_builder.ExpiresOn =
|
|
std::chrono::system_clock::now() + std::chrono::seconds(expiration_secs);
|
|
sas_builder.BlobContainerName = opts.bucket;
|
|
sas_builder.BlobName = opts.key;
|
|
sas_builder.Resource = Azure::Storage::Sas::BlobSasResource::Blob;
|
|
sas_builder.Protocol = Azure::Storage::Sas::SasProtocol::HttpsOnly;
|
|
sas_builder.SetPermissions(Azure::Storage::Sas::BlobSasPermissions::Read);
|
|
|
|
auto credential = _credential;
|
|
if (credential == nullptr) {
|
|
credential = std::make_shared<Azure::Storage::StorageSharedKeyCredential>(_config.ak,
|
|
_config.sk);
|
|
}
|
|
std::string sasToken = sas_builder.GenerateSasToken(*credential);
|
|
|
|
auto sasURL =
|
|
fmt::format(SAS_TOKEN_URL_TEMPLATE, _config.endpoint, opts.bucket, opts.key, sasToken);
|
|
if (sasURL.find("://") == std::string::npos) {
|
|
sasURL = "https://" + sasURL;
|
|
}
|
|
return sasURL;
|
|
}
|
|
|
|
ObjStorageResponse AzureObjStorageClient::get_lifecycle(const std::string& /*bucket*/,
|
|
int64_t* expiration_days) {
|
|
// TODO(plat1ko)
|
|
*expiration_days = INT64_MAX;
|
|
return ObjStorageResponse::OK();
|
|
}
|
|
|
|
ObjStorageResponse AzureObjStorageClient::check_versioning(const std::string& /*bucket*/) {
|
|
// TODO(plat1ko)
|
|
return ObjStorageResponse::OK();
|
|
}
|
|
|
|
ObjStorageResponse AzureObjStorageClient::abort_multipart_upload(const ObjStoragePath& opts,
|
|
const std::string& upload_id) {
|
|
// delete uncommitted blobs
|
|
// https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob?tabs=microsoft-entra-id#remarks
|
|
return delete_object(opts);
|
|
}
|
|
} // namespace doris
|