Files
doris/common/cpp/obj-client/azure_obj_storage_client.cpp
T
hui lai c71d1aec0f [refactor](storage) Unify BE and Recycler object clients (#66350)
### 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.
2026-08-17 11:38:54 +08:00

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