mirror of
https://github.com/apache/doris.git
synced 2026-11-04 17:52:47 +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.
222 lines
8.0 KiB
C++
222 lines
8.0 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 "obj_storage_client.h"
|
|
|
|
#include <cpp/sync_point.h>
|
|
#include <glog/logging.h>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
|
|
namespace doris {
|
|
ObjStorageStatus obj_storage_status_from_http_code(int http_code, std::string message) {
|
|
switch (http_code) {
|
|
case 401:
|
|
case 403:
|
|
return {ObjStorageStatus::PERMISSION_DENIED, std::move(message)};
|
|
case 404:
|
|
return {ObjStorageStatus::NOT_FOUND, std::move(message)};
|
|
case 429:
|
|
return {ObjStorageStatus::LIMIT_REACH, std::move(message)};
|
|
default:
|
|
return {http_code <= 0 ? ObjStorageStatus::NETWORK_ERROR : ObjStorageStatus::INTERNAL_ERROR,
|
|
std::move(message)};
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<ObjStorageListIterator> ObjStorageClient::list_objects(const ObjStoragePath& opts) {
|
|
return std::make_unique<ObjStorageListIterator>(shared_from_this(), opts);
|
|
}
|
|
|
|
ObjStorageResponse ObjStorageClient::list_objects(const ObjStoragePath& opts,
|
|
std::vector<ObjectMeta>* objects) {
|
|
objects->clear();
|
|
auto iter = list_objects(opts);
|
|
for (;;) {
|
|
auto result = iter->next();
|
|
if (!result.object.has_value()) {
|
|
if (!result.resp.ok()) {
|
|
objects->clear();
|
|
}
|
|
return result.resp;
|
|
}
|
|
objects->emplace_back(std::move(*result.object));
|
|
}
|
|
}
|
|
|
|
ObjStorageResponse ObjStorageListIterator::has_next() {
|
|
if (!is_valid_) {
|
|
return {
|
|
.status = {ObjStorageStatus::INTERNAL_ERROR, "Iterator is invalid"},
|
|
.http_code = 0,
|
|
};
|
|
}
|
|
while (next_index_ == objects_.size()) {
|
|
if (!has_more_) {
|
|
return {
|
|
.status = {ObjStorageStatus::END_OF_FILE, "No more results"},
|
|
.http_code = 200,
|
|
};
|
|
}
|
|
auto page = client_->list_objects_page(opts_, continuation_token_);
|
|
if (!page.resp.ok()) {
|
|
is_valid_ = false;
|
|
return page.resp;
|
|
}
|
|
objects_ = std::move(page.objects);
|
|
next_index_ = 0;
|
|
continuation_token_ = std::move(page.continuation_token);
|
|
has_more_ = page.has_more;
|
|
}
|
|
return ObjStorageResponse::OK();
|
|
}
|
|
|
|
ObjStorageListResult ObjStorageListIterator::next() {
|
|
auto response = has_next();
|
|
if (response.status.code == ObjStorageStatus::END_OF_FILE) {
|
|
return {.resp = ObjStorageResponse::OK(), .object = {}};
|
|
}
|
|
if (!response.ok()) {
|
|
return {.resp = std::move(response), .object = {}};
|
|
}
|
|
return {
|
|
.resp = ObjStorageResponse::OK(),
|
|
.object = std::move(objects_[next_index_++]),
|
|
};
|
|
}
|
|
|
|
ObjStorageResponse delete_objects_recursively(
|
|
std::shared_ptr<ObjStorageClient> client, const ObjStoragePath& path,
|
|
const ObjStorageRecursiveDeleteOptions& delete_options) {
|
|
const auto start_time = std::chrono::steady_clock::now();
|
|
auto list_path = path;
|
|
if (list_path.prefix.empty()) {
|
|
list_path.prefix = list_path.key;
|
|
}
|
|
auto delete_batch_size = std::max<size_t>(1, client->capabilities().max_delete_batch);
|
|
TEST_SYNC_POINT_CALLBACK("ObjStorageClient::delete_objects_recursively_", &delete_batch_size);
|
|
delete_batch_size = std::max<size_t>(1, delete_batch_size);
|
|
const auto max_tasks_per_batch = std::max<size_t>(1, delete_options.max_tasks_per_batch);
|
|
std::vector<std::string> keys;
|
|
keys.reserve(delete_batch_size);
|
|
size_t pending_tasks = 0;
|
|
size_t total_batches = 0;
|
|
size_t num_deleted = 0;
|
|
size_t error_count = 0;
|
|
auto first_error = ObjStorageResponse::OK();
|
|
|
|
auto elapsed_milliseconds = [&]() {
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now() - start_time)
|
|
.count();
|
|
};
|
|
auto finish = [&](ObjStorageResponse response) {
|
|
LOG(INFO) << "delete objects under " << list_path.bucket << "/" << list_path.prefix
|
|
<< " finished, ret=" << response.status.code
|
|
<< ", total_batches=" << total_batches << ", num_deleted=" << num_deleted
|
|
<< ", error_count=" << error_count << ", cost=" << elapsed_milliseconds()
|
|
<< " ms";
|
|
return response;
|
|
};
|
|
auto record_error = [&](ObjStorageResponse response) {
|
|
if (response.ok()) {
|
|
return;
|
|
}
|
|
++error_count;
|
|
if (first_error.ok()) {
|
|
first_error = std::move(response);
|
|
}
|
|
};
|
|
|
|
auto wait_for_tasks = [&]() {
|
|
if (pending_tasks == 0) {
|
|
return ObjStorageResponse::OK();
|
|
}
|
|
const auto tasks_in_batch = pending_tasks;
|
|
pending_tasks = 0;
|
|
auto response = delete_options.executor ? delete_options.executor->wait()
|
|
: ObjStorageResponse::OK();
|
|
++total_batches;
|
|
LOG(INFO) << "delete objects under " << list_path.bucket << "/" << list_path.prefix
|
|
<< " batch " << total_batches << " completed"
|
|
<< ", tasks_in_batch=" << tasks_in_batch << ", total_deleted=" << num_deleted
|
|
<< ", elapsed=" << elapsed_milliseconds() << " ms";
|
|
return response;
|
|
};
|
|
auto submit_delete_task = [&]() -> bool {
|
|
ObjStorageDeleteTask task = [client, bucket = path.bucket,
|
|
batch = std::move(keys)]() mutable {
|
|
return client->delete_objects(ObjStoragePath {.bucket = std::move(bucket)},
|
|
std::move(batch));
|
|
};
|
|
keys.clear();
|
|
keys.reserve(delete_batch_size);
|
|
|
|
ObjStorageResponse response;
|
|
if (delete_options.executor) {
|
|
response = delete_options.executor->submit(std::move(task));
|
|
} else {
|
|
response = task();
|
|
}
|
|
++pending_tasks;
|
|
if (!response.ok()) {
|
|
record_error(std::move(response));
|
|
record_error(wait_for_tasks());
|
|
return false;
|
|
}
|
|
if (pending_tasks == max_tasks_per_batch) {
|
|
record_error(wait_for_tasks());
|
|
}
|
|
// Match the pre-refactor Recycler behavior: do not scan the next task batch after the
|
|
// current batch reports a submit, delete, or wait failure.
|
|
return first_error.ok();
|
|
};
|
|
|
|
auto iter = client->list_objects(list_path);
|
|
for (;;) {
|
|
auto result = iter->next();
|
|
if (!result.object.has_value()) {
|
|
if (result.resp.ok()) {
|
|
break;
|
|
}
|
|
if (!keys.empty()) {
|
|
submit_delete_task();
|
|
}
|
|
record_error(wait_for_tasks());
|
|
record_error(std::move(result.resp));
|
|
return finish(std::move(first_error));
|
|
}
|
|
auto& object = *result.object;
|
|
if (delete_options.expiration_time > 0 && object.mtime_s > delete_options.expiration_time) {
|
|
continue;
|
|
}
|
|
++num_deleted;
|
|
keys.emplace_back(std::move(object.key));
|
|
if (keys.size() == delete_batch_size && !submit_delete_task()) {
|
|
return finish(std::move(first_error));
|
|
}
|
|
}
|
|
if (!keys.empty() && !submit_delete_task()) {
|
|
return finish(std::move(first_error));
|
|
}
|
|
record_error(wait_for_tasks());
|
|
return finish(std::move(first_error));
|
|
}
|
|
|
|
} // namespace doris
|