github-actions[bot] commented on code in PR #66350:
URL: https://github.com/apache/doris/pull/66350#discussion_r3714530278
##########
be/test/io/s3_client_factory_test.cpp:
##########
@@ -80,23 +61,7 @@ S3ClientConf make_hash_collision_conf(std::string endpoint,
bool is_internal_buc
} // namespace
-TEST_F(S3ClientFactoryTest, WrapsAllClientsInNonCloudMode) {
- CloudModeConfigGuard guard(false);
- auto& factory = S3ClientFactory::instance();
-
- auto external_client =
-
factory.create(make_factory_conf("non-cloud-external-rate-limit.example.com",
false));
- auto internal_client =
-
factory.create(make_factory_conf("non-cloud-internal-rate-limit.example.com",
true));
-
- ASSERT_NE(external_client, nullptr);
- ASSERT_NE(internal_client, nullptr);
-
EXPECT_NE(std::dynamic_pointer_cast<io::RateLimitedObjStorageClient>(external_client),
nullptr);
-
EXPECT_NE(std::dynamic_pointer_cast<io::RateLimitedObjStorageClient>(internal_client),
nullptr);
-}
-
-TEST_F(S3ClientFactoryTest,
WrapsOnlyInternalClientsInCloudModeAndDistinguishesHashCollisions) {
- CloudModeConfigGuard guard(true);
+TEST_F(S3ClientFactoryTest, DistinguishesHashCollisions) {
Review Comment:
[P2] Keep behavioral coverage for cloud limiter selection
The deleted factory tests asserted that non-cloud clients are limited and
that cloud mode limits only internal storage-vault clients. This replacement
now checks only hash-collision/cache identity, and no remaining test switches
cloud mode and observes the `is_internal_bucket` policy branch in
`S3ClientFactory::create`. Please retain a behavior-level test (for example
with an observable counting policy/backend) for non-cloud, cloud-internal, and
cloud-external clients so this production admission boundary cannot silently
flip.
##########
common/cpp/client/azure_obj_storage_backend.cpp:
##########
@@ -0,0 +1,497 @@
+// 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_backend.h"
+
+#include <cctype>
+#include <string_view>
+
+#include "cpp/obj_retry_strategy.h"
+
+using namespace Azure::Storage::Blobs;
+
+namespace {
+std::string wrap_object_storage_path_msg(const
doris::ObjectStoragePathOptions& opts) {
+ return fmt::format("bucket {}, key {}, prefix {}, path {}", opts.bucket,
opts.key, opts.prefix,
+ opts.path.native());
+}
+
+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;
+}
+
+template <std::endian target, typename T>
+T to_endian(T value) {
+ if constexpr (std::endian::native == target) {
+ return value; // No swap needed
+ } else {
+ static_assert(std::endian::native == std::endian::big ||
+ std::endian::native == std::endian::little,
+ "Unsupported endianness");
+ return byte_swap(value);
+ }
+}
+
+inline void encode_fixed32_le(uint8_t* buf, uint32_t val) {
+ val = to_endian<std::endian::little>(val);
+ memcpy(buf, &val, sizeof(val));
+}
+
+auto base64_encode_part_num(int part_num) {
+ uint8_t buf[4];
+ encode_fixed32_le(buf, static_cast<uint32_t>(part_num));
+ return Aws::Utils::HashingUtils::Base64Encode({buf, sizeof(buf)});
+}
+
+constexpr char SAS_TOKEN_URL_TEMPLATE[] = "{}/{}/{}{}";
+constexpr char BlobNotFound[] = "BlobNotFound";
+} // namespace
+
+namespace doris {
+
+// 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);
+}
+
+template <typename Func>
+ObjectStorageResponse do_azure_client_call(Func f, const
ObjectStoragePathOptions& 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 = ObjectStorageStatus {TStatusCode::INTERNAL_ERROR,
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(),
tls_debug_context));
+ LOG(WARNING) << msg;
+ return {.status = ObjectStorageStatus {TStatusCode::INTERNAL_ERROR,
std::move(msg)},
+ .http_code = 0,
+ .request_id = ""};
+ }
+ return ObjectStorageResponse::OK();
+}
+
+struct AzureBatchDeleter {
+ AzureBatchDeleter(BlobContainerClient* client, const
ObjectStoragePathOptions& opts,
+ std::string_view tls_debug_context)
+ : _client(client),
+ _batch(client->CreateBatch()),
+ _opts(opts),
+ _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(_batch.DeleteBlob(blob_name));
+ }
+ ObjectStorageResponse execute() {
+ if (deferred_resps.empty()) {
+ return ObjectStorageResponse::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 != TStatusCode::OK) {
+ return resp;
+ }
+
+ for (auto&& defer_response : deferred_resps) {
+ try {
+ auto r = defer_response.GetResponse();
+ if (!r.Value.Deleted) {
+ auto msg = fmt::format("Azure batch delete failed, path
msg {}",
+
wrap_object_storage_path_msg(_opts));
+ LOG(WARNING) << msg;
+ return {.status = ObjectStorageStatus
{TStatusCode::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(_opts),
+ build_azure_tls_debug_suffix(fmt::format("{} {}",
e.what(), e.Message),
+ _tls_debug_context));
+ LOG(WARNING) << msg;
+ return {.status = ObjectStorageStatus
{TStatusCode::INTERNAL_ERROR, std::move(msg)},
+ .http_code = static_cast<int>(e.StatusCode),
+ .request_id = std::move(e.RequestId)};
+ }
+ }
+
+ return ObjectStorageResponse::OK();
+ }
+
+private:
+ BlobContainerClient* _client;
+ BlobContainerBatch _batch;
+ const ObjectStoragePathOptions& _opts;
+ std::string_view _tls_debug_context;
+ std::vector<Azure::Storage::DeferredResponse<Models::DeleteBlobResult>>
deferred_resps;
+};
+
+// Azure would do nothing
+ObjectStorageUploadResponse AzureObjStorageBackend::create_multipart_upload(
+ const ObjectStoragePathOptions& opts) {
+ return ObjectStorageUploadResponse {
+ .resp = ObjectStorageResponse::OK(),
+ };
+}
+
+ObjectStorageResponse AzureObjStorageBackend::put_object(const
ObjectStoragePathOptions& 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);
+}
+
+ObjectStorageUploadResponse AzureObjStorageBackend::upload_part(
+ const ObjectStoragePathOptions& opts, std::string_view stream, int
part_num) {
+ auto client = _client->GetBlockBlobClient(opts.key);
+ 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(base64_encode_part_num(part_num), 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 = ObjectStorageStatus {TStatusCode::INTERNAL_ERROR,
std::move(msg)},
+ .http_code = static_cast<int>(e.StatusCode),
+ .request_id = std::move(e.RequestId),
+ },
+ };
+ // clang-format on
+ }
+ return ObjectStorageUploadResponse {.resp = ObjectStorageResponse::OK()};
+}
+
+ObjectStorageResponse AzureObjStorageBackend::complete_multipart_upload(
+ const ObjectStoragePathOptions& opts,
+ const std::vector<ObjectCompleteMultiPart>& completed_parts) {
+ auto client = _client->GetBlockBlobClient(opts.key);
+ std::vector<std::string> string_block_ids;
+ std::ranges::transform(
+ completed_parts, std::back_inserter(string_block_ids),
+ [](const ObjectCompleteMultiPart& i) { return
base64_encode_part_num(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);
+}
+
+ObjectStorageHeadResponse AzureObjStorageBackend::head_object(
+ const ObjectStoragePathOptions& opts) {
+ try {
+ Models::BlobProperties properties = [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_head_latency);
+ return _client->GetBlockBlobClient(opts.key).GetProperties().Value;
+ }();
+ return {.resp = ObjectStorageResponse::OK(), .file_size =
properties.BlobSize};
+ } catch (Azure::Core::RequestFailedException& e) {
+ if (e.StatusCode == Azure::Core::Http::HttpStatusCode::NotFound) {
+ return ObjectStorageHeadResponse {
+ .resp = {.status = ObjectStorageStatus
{TStatusCode::NOT_FOUND, ""},
+ .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);
+ return ObjectStorageHeadResponse {
Review Comment:
[P2] Log Azure head failures before Recycler discards the response
Unlike the general Azure call helper and the parallel S3 head path, this
catch returns the detailed message, HTTP code, and request ID without logging
them. Recycler's `S3Accessor::exists` converts every non-404 response to `-1`,
and its production checker can then report only the object path and `ret=-1`;
the Azure failure and request ID are lost. Please emit the constructed warning
here (or preserve the full response at the adapter boundary) before returning.
##########
common/cpp/client/s3_obj_storage_backend.cpp:
##########
@@ -0,0 +1,582 @@
+// 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 "s3_obj_storage_backend.h"
+
+#include <cpp/client/obj_storage_client.h>
+#include <gen_cpp/Status_types.h>
+
+#include <algorithm>
+#include <chrono>
+
+#include "client_bvar.h"
+#include "cpp/obj_retry_strategy.h"
+
+namespace Aws::S3::Model {
+class DeleteObjectRequest;
+} // namespace Aws::S3::Model
+
+using Aws::S3::Model::CompletedPart;
+using Aws::S3::Model::CompletedMultipartUpload;
+using Aws::S3::Model::CompleteMultipartUploadRequest;
+using Aws::S3::Model::CreateMultipartUploadRequest;
+using Aws::S3::Model::UploadPartRequest;
+using Aws::S3::Model::UploadPartOutcome;
+
+namespace doris {
+using namespace Aws::S3::Model;
+namespace {
+
+constexpr int64_t S3_REQUEST_THRESHOLD_MS = 5000;
+
+int64_t elapsed_time_milliseconds(std::chrono::steady_clock::time_point start)
{
+ return
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now()
-
+ start)
+ .count();
+}
+
+void record_s3_request_failed(const Aws::S3::S3Error& error) {
+ record_object_request_failed(static_cast<int>(error.GetResponseCode()));
+}
+
+} // namespace
+
+ObjectStorageStatus s3fs_error(const Aws::S3::S3Error& err, std::string_view
msg) {
+ using namespace Aws::Http;
+ switch (err.GetResponseCode()) {
+ case HttpResponseCode::NOT_FOUND:
+ return {TStatusCode::NOT_FOUND,
+ fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ case HttpResponseCode::FORBIDDEN:
+ // TODO: no permission and other 4xx errors should be handled
separately
+ return {TStatusCode::NOT_AUTHORIZED,
+ fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ case HttpResponseCode::REQUEST_NOT_MADE:
+ return {-1, fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ default:
+ return {TStatusCode::INTERNAL_ERROR,
+ fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ }
+}
+
+ObjectStorageUploadResponse S3ObjStorageBackend::create_multipart_upload(
+ const ObjectStoragePathOptions& opts) {
+ CreateMultipartUploadRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ request.SetContentType("application/octet-stream");
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency scoped_latency(
+ client_bvar::s3_multi_part_upload_latency);
+ return _client->CreateMultipartUpload(request);
+ }(),
+ "s3_file_writer::create_multi_part_upload",
std::cref(request).get());
+ SYNC_POINT_CALLBACK("s3_file_writer::_open", &outcome);
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "CreateMultipartUpload cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key;
+
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(), fmt::format("failed to
CreateMultipartUpload: {} ",
+
opts.path.native()));
+ LOG(WARNING) << st.code << " request_id=" << request_id;
+ return ObjectStorageUploadResponse {
+ .resp = {.status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()},
+ };
+ }
+
+ return ObjectStorageUploadResponse {.resp = ObjectStorageResponse::OK(),
+ .upload_id
{outcome.GetResult().GetUploadId()}};
+}
+
+ObjectStorageResponse S3ObjStorageBackend::put_object(const
ObjectStoragePathOptions& opts,
+ std::string_view stream)
{
+ Aws::S3::Model::PutObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ auto string_view_stream =
std::make_shared<StringViewStream>(stream.data(), stream.size());
+ Aws::Utils::ByteBuffer
part_md5(Aws::Utils::HashingUtils::CalculateMD5(*string_view_stream));
+ request.SetContentMD5(Aws::Utils::HashingUtils::Base64Encode(part_md5));
+ request.SetBody(string_view_stream);
+ request.SetContentLength(stream.size());
+ request.SetContentType("application/octet-stream");
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_put_latency);
+ return _client->PutObject(request);
+ }(),
+ "s3_file_writer::put_object", std::cref(request).get(), &stream);
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(),
+ fmt::format("failed to put object: {}",
opts.path.native()));
+ LOG(WARNING) << st.code << ", request_id=" << request_id;
+ return ObjectStorageResponse {
+ .status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()};
+ }
+
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "PutObject cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key;
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageUploadResponse S3ObjStorageBackend::upload_part(const
ObjectStoragePathOptions& opts,
+ std::string_view
stream,
+ int part_num) {
+ UploadPartRequest request;
+ request.WithBucket(opts.bucket)
+ .WithKey(opts.key)
+ .WithPartNumber(part_num)
+ .WithUploadId(*opts.upload_id);
+ auto string_view_stream =
std::make_shared<StringViewStream>(stream.data(), stream.size());
+
+ request.SetBody(string_view_stream);
+
+ Aws::Utils::ByteBuffer
part_md5(Aws::Utils::HashingUtils::CalculateMD5(*string_view_stream));
+ request.SetContentMD5(Aws::Utils::HashingUtils::Base64Encode(part_md5));
+
+ request.SetContentLength(stream.size());
+ request.SetContentType("application/octet-stream");
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency scoped_latency(
+ client_bvar::s3_multi_part_upload_latency);
+
+ return _client->UploadPart(request);
+ }(),
+ "s3_file_writer::upload_part", std::cref(request).get(), &stream);
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ TEST_SYNC_POINT_CALLBACK("S3FileWriter::_upload_one_part", &outcome);
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(),
+ fmt::format("failed to UploadPart: {}, part_num
{}, upload_id={}",
+ opts.path.native(), part_num,
*opts.upload_id));
+
+ LOG(WARNING) << st.code << ", request_id=" << request_id;
+ return ObjectStorageUploadResponse {
+ .resp = {.status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()}};
+ }
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "UploadPart cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key
+ << ", part_num=" << part_num << ", upload_id=" << *opts.upload_id;
+ return ObjectStorageUploadResponse {.resp = ObjectStorageResponse::OK(),
+ .etag = outcome.GetResult().GetETag()};
+}
+
+ObjectStorageResponse S3ObjStorageBackend::complete_multipart_upload(
+ const ObjectStoragePathOptions& opts,
+ const std::vector<ObjectCompleteMultiPart>& completed_parts) {
+ CompleteMultipartUploadRequest request;
+
request.WithBucket(opts.bucket).WithKey(opts.key).WithUploadId(*opts.upload_id);
+
+ CompletedMultipartUpload completed_upload;
+ std::vector<CompletedPart> complete_parts;
+ std::ranges::transform(completed_parts, std::back_inserter(complete_parts),
+ [](const ObjectCompleteMultiPart& part_ptr) {
+ CompletedPart part;
+ part.SetPartNumber(part_ptr.part_num);
+ part.SetETag(part_ptr.etag);
+ return part;
+ });
+ completed_upload.SetParts(std::move(complete_parts));
+ request.WithMultipartUpload(completed_upload);
+
+ TEST_SYNC_POINT_RETURN_WITH_VALUE("S3FileWriter::_complete:3",
ObjectStorageResponse(), this);
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency scoped_latency(
+ client_bvar::s3_multi_part_upload_latency);
+ return _client->CompleteMultipartUpload(request);
+ }(),
+ "s3_file_writer::complete_multi_part", std::cref(request).get());
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(),
+ fmt::format("failed to CompleteMultipartUpload:
{}, upload_id={}",
+ opts.path.native(), *opts.upload_id));
+ LOG(WARNING) << st.code << ", request_id=" << request_id;
+ return {.status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()};
+ }
+
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "CompleteMultipartUpload cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key
+ << ", upload_id=" << *opts.upload_id;
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageHeadResponse S3ObjStorageBackend::head_object(const
ObjectStoragePathOptions& opts) {
+ Aws::S3::Model::HeadObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_head_latency);
+ return _client->HeadObject(request);
+ }(),
+ "s3_file_system::head_object", std::ref(request).get());
+
+ if (outcome.IsSuccess()) {
+ return {.resp = ObjectStorageResponse::OK(),
+ .file_size = outcome.GetResult().GetContentLength()};
+ } else if (outcome.GetError().GetResponseCode() ==
Aws::Http::HttpResponseCode::NOT_FOUND) {
+ return {.resp = {.status = TStatusCode::NOT_FOUND}, .file_size = 0};
+ } else {
+ record_s3_request_failed(outcome.GetError());
+ LOG(WARNING) << "failed to head object"
+ << "bucket " << opts.bucket << " key " << opts.key << "
responseCode "
+ << outcome.GetError() << " error " <<
outcome.GetError().GetMessage()
+ << " request_id " << outcome.GetError().GetRequestId();
+ return {.resp = {.status = s3fs_error(
+ outcome.GetError(),
+ fmt::format("failed to head object: {}",
opts.path.native())),
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()},
+ .file_size = -1};
+ }
+}
+
+ObjectStorageResponse S3ObjStorageBackend::get_object(const
ObjectStoragePathOptions& opts,
+ void* buffer, size_t
offset,
+ size_t bytes_read,
size_t* size_return) {
+ Aws::S3::Model::GetObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ request.SetRange(fmt::format("bytes={}-{}", offset, offset + bytes_read -
1));
+ request.SetResponseStreamFactory(AwsWriteableStreamFactory(buffer,
bytes_read));
+
+ auto outcome = [&]() {
+ client_bvar::ScopedLatency scoped_latency(client_bvar::s3_get_latency);
+ return _client->GetObject(request);
+ }();
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ return ObjectStorageResponse {
+ .status = s3fs_error(outcome.GetError(),
Review Comment:
[P2] Keep object and request context in GetObject errors
Both production callers populate only `bucket` and `key`, so `opts.path` is
empty here. They then rebuild a BE `Status` from only `status.code/msg`,
discarding this response's HTTP code and request ID; the deleted path used
`opts.key` and embedded provider code/type/request ID in the returned error. A
failed read therefore reaches operators without the object identity or request
context needed to diagnose it. Please use the key (or a path/key fallback) and
retain/log the provider classification and request ID; the incomplete-read
branch below should retain its successful request ID as well.
##########
common/cpp/client/s3_obj_storage_backend.cpp:
##########
@@ -0,0 +1,582 @@
+// 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 "s3_obj_storage_backend.h"
+
+#include <cpp/client/obj_storage_client.h>
+#include <gen_cpp/Status_types.h>
+
+#include <algorithm>
+#include <chrono>
+
+#include "client_bvar.h"
+#include "cpp/obj_retry_strategy.h"
+
+namespace Aws::S3::Model {
+class DeleteObjectRequest;
+} // namespace Aws::S3::Model
+
+using Aws::S3::Model::CompletedPart;
+using Aws::S3::Model::CompletedMultipartUpload;
+using Aws::S3::Model::CompleteMultipartUploadRequest;
+using Aws::S3::Model::CreateMultipartUploadRequest;
+using Aws::S3::Model::UploadPartRequest;
+using Aws::S3::Model::UploadPartOutcome;
+
+namespace doris {
+using namespace Aws::S3::Model;
+namespace {
+
+constexpr int64_t S3_REQUEST_THRESHOLD_MS = 5000;
+
+int64_t elapsed_time_milliseconds(std::chrono::steady_clock::time_point start)
{
+ return
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now()
-
+ start)
+ .count();
+}
+
+void record_s3_request_failed(const Aws::S3::S3Error& error) {
+ record_object_request_failed(static_cast<int>(error.GetResponseCode()));
+}
+
+} // namespace
+
+ObjectStorageStatus s3fs_error(const Aws::S3::S3Error& err, std::string_view
msg) {
+ using namespace Aws::Http;
+ switch (err.GetResponseCode()) {
+ case HttpResponseCode::NOT_FOUND:
+ return {TStatusCode::NOT_FOUND,
+ fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ case HttpResponseCode::FORBIDDEN:
+ // TODO: no permission and other 4xx errors should be handled
separately
+ return {TStatusCode::NOT_AUTHORIZED,
+ fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ case HttpResponseCode::REQUEST_NOT_MADE:
+ return {-1, fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ default:
+ return {TStatusCode::INTERNAL_ERROR,
+ fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ }
+}
+
+ObjectStorageUploadResponse S3ObjStorageBackend::create_multipart_upload(
+ const ObjectStoragePathOptions& opts) {
+ CreateMultipartUploadRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ request.SetContentType("application/octet-stream");
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency scoped_latency(
+ client_bvar::s3_multi_part_upload_latency);
+ return _client->CreateMultipartUpload(request);
+ }(),
+ "s3_file_writer::create_multi_part_upload",
std::cref(request).get());
+ SYNC_POINT_CALLBACK("s3_file_writer::_open", &outcome);
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "CreateMultipartUpload cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key;
+
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(), fmt::format("failed to
CreateMultipartUpload: {} ",
+
opts.path.native()));
+ LOG(WARNING) << st.code << " request_id=" << request_id;
+ return ObjectStorageUploadResponse {
+ .resp = {.status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()},
+ };
+ }
+
+ return ObjectStorageUploadResponse {.resp = ObjectStorageResponse::OK(),
+ .upload_id
{outcome.GetResult().GetUploadId()}};
+}
+
+ObjectStorageResponse S3ObjStorageBackend::put_object(const
ObjectStoragePathOptions& opts,
+ std::string_view stream)
{
+ Aws::S3::Model::PutObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ auto string_view_stream =
std::make_shared<StringViewStream>(stream.data(), stream.size());
+ Aws::Utils::ByteBuffer
part_md5(Aws::Utils::HashingUtils::CalculateMD5(*string_view_stream));
+ request.SetContentMD5(Aws::Utils::HashingUtils::Base64Encode(part_md5));
+ request.SetBody(string_view_stream);
+ request.SetContentLength(stream.size());
+ request.SetContentType("application/octet-stream");
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_put_latency);
+ return _client->PutObject(request);
+ }(),
+ "s3_file_writer::put_object", std::cref(request).get(), &stream);
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(),
+ fmt::format("failed to put object: {}",
opts.path.native()));
+ LOG(WARNING) << st.code << ", request_id=" << request_id;
+ return ObjectStorageResponse {
+ .status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()};
+ }
+
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "PutObject cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key;
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageUploadResponse S3ObjStorageBackend::upload_part(const
ObjectStoragePathOptions& opts,
+ std::string_view
stream,
+ int part_num) {
+ UploadPartRequest request;
+ request.WithBucket(opts.bucket)
+ .WithKey(opts.key)
+ .WithPartNumber(part_num)
+ .WithUploadId(*opts.upload_id);
+ auto string_view_stream =
std::make_shared<StringViewStream>(stream.data(), stream.size());
+
+ request.SetBody(string_view_stream);
+
+ Aws::Utils::ByteBuffer
part_md5(Aws::Utils::HashingUtils::CalculateMD5(*string_view_stream));
+ request.SetContentMD5(Aws::Utils::HashingUtils::Base64Encode(part_md5));
+
+ request.SetContentLength(stream.size());
+ request.SetContentType("application/octet-stream");
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency scoped_latency(
+ client_bvar::s3_multi_part_upload_latency);
+
+ return _client->UploadPart(request);
+ }(),
+ "s3_file_writer::upload_part", std::cref(request).get(), &stream);
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ TEST_SYNC_POINT_CALLBACK("S3FileWriter::_upload_one_part", &outcome);
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(),
+ fmt::format("failed to UploadPart: {}, part_num
{}, upload_id={}",
+ opts.path.native(), part_num,
*opts.upload_id));
+
+ LOG(WARNING) << st.code << ", request_id=" << request_id;
+ return ObjectStorageUploadResponse {
+ .resp = {.status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()}};
+ }
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "UploadPart cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key
+ << ", part_num=" << part_num << ", upload_id=" << *opts.upload_id;
+ return ObjectStorageUploadResponse {.resp = ObjectStorageResponse::OK(),
+ .etag = outcome.GetResult().GetETag()};
+}
+
+ObjectStorageResponse S3ObjStorageBackend::complete_multipart_upload(
+ const ObjectStoragePathOptions& opts,
+ const std::vector<ObjectCompleteMultiPart>& completed_parts) {
+ CompleteMultipartUploadRequest request;
+
request.WithBucket(opts.bucket).WithKey(opts.key).WithUploadId(*opts.upload_id);
+
+ CompletedMultipartUpload completed_upload;
+ std::vector<CompletedPart> complete_parts;
+ std::ranges::transform(completed_parts, std::back_inserter(complete_parts),
+ [](const ObjectCompleteMultiPart& part_ptr) {
+ CompletedPart part;
+ part.SetPartNumber(part_ptr.part_num);
+ part.SetETag(part_ptr.etag);
+ return part;
+ });
+ completed_upload.SetParts(std::move(complete_parts));
+ request.WithMultipartUpload(completed_upload);
+
+ TEST_SYNC_POINT_RETURN_WITH_VALUE("S3FileWriter::_complete:3",
ObjectStorageResponse(), this);
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency scoped_latency(
+ client_bvar::s3_multi_part_upload_latency);
+ return _client->CompleteMultipartUpload(request);
+ }(),
+ "s3_file_writer::complete_multi_part", std::cref(request).get());
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(),
+ fmt::format("failed to CompleteMultipartUpload:
{}, upload_id={}",
+ opts.path.native(), *opts.upload_id));
+ LOG(WARNING) << st.code << ", request_id=" << request_id;
+ return {.status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()};
+ }
+
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "CompleteMultipartUpload cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key
+ << ", upload_id=" << *opts.upload_id;
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageHeadResponse S3ObjStorageBackend::head_object(const
ObjectStoragePathOptions& opts) {
+ Aws::S3::Model::HeadObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_head_latency);
+ return _client->HeadObject(request);
+ }(),
+ "s3_file_system::head_object", std::ref(request).get());
+
+ if (outcome.IsSuccess()) {
+ return {.resp = ObjectStorageResponse::OK(),
+ .file_size = outcome.GetResult().GetContentLength()};
+ } else if (outcome.GetError().GetResponseCode() ==
Aws::Http::HttpResponseCode::NOT_FOUND) {
+ return {.resp = {.status = TStatusCode::NOT_FOUND}, .file_size = 0};
+ } else {
+ record_s3_request_failed(outcome.GetError());
+ LOG(WARNING) << "failed to head object"
+ << "bucket " << opts.bucket << " key " << opts.key << "
responseCode "
+ << outcome.GetError() << " error " <<
outcome.GetError().GetMessage()
+ << " request_id " << outcome.GetError().GetRequestId();
+ return {.resp = {.status = s3fs_error(
+ outcome.GetError(),
+ fmt::format("failed to head object: {}",
opts.path.native())),
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()},
+ .file_size = -1};
+ }
+}
+
+ObjectStorageResponse S3ObjStorageBackend::get_object(const
ObjectStoragePathOptions& opts,
+ void* buffer, size_t
offset,
+ size_t bytes_read,
size_t* size_return) {
+ Aws::S3::Model::GetObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ request.SetRange(fmt::format("bytes={}-{}", offset, offset + bytes_read -
1));
+ request.SetResponseStreamFactory(AwsWriteableStreamFactory(buffer,
bytes_read));
+
+ auto outcome = [&]() {
+ client_bvar::ScopedLatency scoped_latency(client_bvar::s3_get_latency);
+ return _client->GetObject(request);
+ }();
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ return ObjectStorageResponse {
+ .status = s3fs_error(outcome.GetError(),
+ fmt::format("failed to get object: {}",
opts.path.native())),
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId(),
+ };
+ }
+ *size_return = outcome.GetResult().GetContentLength();
+ SYNC_POINT_CALLBACK("s3_obj_storage_client::get_object", size_return);
+ if (*size_return != bytes_read) {
+ return ObjectStorageResponse {
+ .status = {TStatusCode::INTERNAL_ERROR,
+ fmt::format("incomplete read from {}, expect {},
got {}",
+ opts.path.native(), bytes_read,
*size_return)}};
+ }
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageListPage S3ObjStorageBackend::list_objects(const
ObjectStoragePathOptions& opts,
+ std::string_view
continuation_token) {
+ const auto& prefix = opts.prefix.empty() ? opts.key : opts.prefix;
+ Aws::S3::Model::ListObjectsV2Request request;
+
request.WithBucket(opts.bucket).WithPrefix(prefix).WithMaxKeys(OBJECT_LIST_PAGE_SIZE);
+ if (!continuation_token.empty()) {
+ request.SetContinuationToken(std::string(continuation_token));
+ }
+ TEST_SYNC_POINT_CALLBACK("S3ObjStorageBackend::list_objects", &request);
+
+ auto outcome = [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_list_latency);
+ return _client->ListObjectsV2(request);
+ }();
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+ if (!outcome.IsSuccess()) {
+ // Some S3-compatible providers (for example TOS) return NoSuchKey
instead of an empty page
+ // when a prefix does not exist.
+ if (outcome.GetError().GetErrorType() ==
Aws::S3::S3Errors::NO_SUCH_KEY) {
+ LOG(INFO) << fmt::format(
+ "NoSuchKey when listing objects, treat as empty response,
endpoint: {}, "
+ "bucket: {}, prefix: {}, request_id: {}",
+ _config.endpoint, request.GetBucket(),
request.GetPrefix(), request_id);
+ return {.resp = ObjectStorageResponse::OK()};
+ }
+
record_object_request_failed(static_cast<int>(outcome.GetError().GetResponseCode()));
+ const auto status = s3fs_error(outcome.GetError(),
+ fmt::format("failed to list objects:
{}, prefix: {}",
+ request.GetBucket(),
request.GetPrefix()));
+ LOG(WARNING) << fmt::format(
+ "failed to list objects, endpoint: {}, bucket: {}, prefix: {},
responseCode: {}, "
+ "error: {}, request_id: {}",
+ _config.endpoint, request.GetBucket(), request.GetPrefix(),
+ static_cast<int>(outcome.GetError().GetResponseCode()),
+ outcome.GetError().GetMessage(), request_id);
+ return {
+ .resp = {.status = status,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = request_id},
+ };
+ }
+
+ const auto& result = outcome.GetResult();
+ if (result.GetIsTruncated() && result.GetNextContinuationToken().empty()) {
+ LOG(WARNING) << fmt::format(
+ "failed to list objects, isTruncated but no continuation
token, endpoint: {}, "
+ "bucket: {}, prefix: {}, request_id: {}",
+ _config.endpoint, request.GetBucket(), request.GetPrefix(),
request_id);
+ return {
+ .resp = {.status = {TStatusCode::INTERNAL_ERROR,
+ fmt::format("failed to list objects: {},
prefix: {}",
+ request.GetBucket(),
request.GetPrefix())},
+ .http_code = 0,
+ .request_id = request_id},
+ };
+ }
+
+ ObjectStorageListPage page {
+ .resp = ObjectStorageResponse::OK(),
+ .continuation_token = result.GetNextContinuationToken(),
+ .has_more = result.GetIsTruncated(),
+ };
+ const auto& content = result.GetContents();
+ page.objects.reserve(content.size());
+ for (const auto& obj : content) {
+ DCHECK(obj.GetKey().starts_with(request.GetPrefix()))
+ << obj.GetKey() << ' ' << request.GetPrefix();
+ page.objects.emplace_back(ObjectMeta {.file_path = obj.GetKey(),
+ .size = obj.GetSize(),
+ .mtime_s =
obj.GetLastModified().Seconds()});
+ }
+ return page;
+}
+
+ObjectStorageResponse S3ObjStorageBackend::delete_objects(const
ObjectStoragePathOptions& opts,
+
std::vector<std::string> objs) {
+ size_t max_delete_batch = 1000;
+ TEST_SYNC_POINT_CALLBACK("S3ObjClient::delete_objects", &max_delete_batch);
+ TEST_SYNC_POINT_CALLBACK("S3ObjStorageClient::delete_objects",
&max_delete_batch);
+ max_delete_batch = std::max<size_t>(1, max_delete_batch);
+ for (size_t begin = 0; begin < objs.size(); begin += max_delete_batch) {
+ const size_t end = std::min(begin + max_delete_batch, objs.size());
+ if (end - begin == 1) {
+ auto single_opts = opts;
+ single_opts.key = std::move(objs[begin]);
+ auto resp = delete_object(single_opts);
+ if (!resp.ok()) {
+ return resp;
+ }
+ continue;
+ }
+
+ Aws::S3::Model::DeleteObjectsRequest delete_request;
+ delete_request.SetBucket(opts.bucket);
+ Aws::S3::Model::Delete del;
+ Aws::Vector<Aws::S3::Model::ObjectIdentifier> objects;
+ objects.reserve(end - begin);
+ for (size_t i = begin; i < end; ++i) {
+ Aws::S3::Model::ObjectIdentifier object;
+ object.SetKey(std::move(objs[i]));
+ objects.emplace_back(std::move(object));
+ }
+ del.WithObjects(std::move(objects)).SetQuiet(true);
+ delete_request.SetDelete(std::move(del));
+
+ auto delete_outcome = [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_delete_objects_latency);
+ return _client->DeleteObjects(delete_request);
+ }();
+ SYNC_POINT_CALLBACK("s3_obj_storage_client::delete_objects",
&delete_outcome);
+
SYNC_POINT_CALLBACK("s3_obj_storage_client::delete_objects_recursively",
&delete_outcome);
+ if (!delete_outcome.IsSuccess()) {
+ record_s3_request_failed(delete_outcome.GetError());
+ return ObjectStorageResponse {
Review Comment:
[P2] Preserve S3 delete failure diagnostics for Recycler
This return carries the provider message, HTTP code, and request ID, but the
Recycler adapters for prefix, batch, and single deletion immediately collapse
`ObjectStorageResponse` to `status.code`; recursive execution can additionally
replace it with a generic cancellation error. The operation URI is pre-logged,
but because this backend no longer logs failed responses, production recycling
loses the provider error, HTTP code, and request ID needed to correlate that
URI with the failed request. Please log those response fields before returning
(or preserve/log the full response at the adapter boundary); this is separate
from the already-fixed noisy success logging.
##########
common/cpp/client/s3_obj_storage_backend.cpp:
##########
@@ -0,0 +1,582 @@
+// 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 "s3_obj_storage_backend.h"
+
+#include <cpp/client/obj_storage_client.h>
+#include <gen_cpp/Status_types.h>
+
+#include <algorithm>
+#include <chrono>
+
+#include "client_bvar.h"
+#include "cpp/obj_retry_strategy.h"
+
+namespace Aws::S3::Model {
+class DeleteObjectRequest;
+} // namespace Aws::S3::Model
+
+using Aws::S3::Model::CompletedPart;
+using Aws::S3::Model::CompletedMultipartUpload;
+using Aws::S3::Model::CompleteMultipartUploadRequest;
+using Aws::S3::Model::CreateMultipartUploadRequest;
+using Aws::S3::Model::UploadPartRequest;
+using Aws::S3::Model::UploadPartOutcome;
+
+namespace doris {
+using namespace Aws::S3::Model;
+namespace {
+
+constexpr int64_t S3_REQUEST_THRESHOLD_MS = 5000;
+
+int64_t elapsed_time_milliseconds(std::chrono::steady_clock::time_point start)
{
+ return
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now()
-
+ start)
+ .count();
+}
+
+void record_s3_request_failed(const Aws::S3::S3Error& error) {
+ record_object_request_failed(static_cast<int>(error.GetResponseCode()));
+}
+
+} // namespace
+
+ObjectStorageStatus s3fs_error(const Aws::S3::S3Error& err, std::string_view
msg) {
+ using namespace Aws::Http;
+ switch (err.GetResponseCode()) {
+ case HttpResponseCode::NOT_FOUND:
+ return {TStatusCode::NOT_FOUND,
+ fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ case HttpResponseCode::FORBIDDEN:
+ // TODO: no permission and other 4xx errors should be handled
separately
+ return {TStatusCode::NOT_AUTHORIZED,
+ fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ case HttpResponseCode::REQUEST_NOT_MADE:
+ return {-1, fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ default:
+ return {TStatusCode::INTERNAL_ERROR,
+ fmt::format("{}: {} {}", msg, err.GetExceptionName(),
err.GetMessage())};
+ }
+}
+
+ObjectStorageUploadResponse S3ObjStorageBackend::create_multipart_upload(
+ const ObjectStoragePathOptions& opts) {
+ CreateMultipartUploadRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ request.SetContentType("application/octet-stream");
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency scoped_latency(
+ client_bvar::s3_multi_part_upload_latency);
+ return _client->CreateMultipartUpload(request);
+ }(),
+ "s3_file_writer::create_multi_part_upload",
std::cref(request).get());
+ SYNC_POINT_CALLBACK("s3_file_writer::_open", &outcome);
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "CreateMultipartUpload cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key;
+
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(), fmt::format("failed to
CreateMultipartUpload: {} ",
+
opts.path.native()));
+ LOG(WARNING) << st.code << " request_id=" << request_id;
+ return ObjectStorageUploadResponse {
+ .resp = {.status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()},
+ };
+ }
+
+ return ObjectStorageUploadResponse {.resp = ObjectStorageResponse::OK(),
+ .upload_id
{outcome.GetResult().GetUploadId()}};
+}
+
+ObjectStorageResponse S3ObjStorageBackend::put_object(const
ObjectStoragePathOptions& opts,
+ std::string_view stream)
{
+ Aws::S3::Model::PutObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ auto string_view_stream =
std::make_shared<StringViewStream>(stream.data(), stream.size());
+ Aws::Utils::ByteBuffer
part_md5(Aws::Utils::HashingUtils::CalculateMD5(*string_view_stream));
+ request.SetContentMD5(Aws::Utils::HashingUtils::Base64Encode(part_md5));
+ request.SetBody(string_view_stream);
+ request.SetContentLength(stream.size());
+ request.SetContentType("application/octet-stream");
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_put_latency);
+ return _client->PutObject(request);
+ }(),
+ "s3_file_writer::put_object", std::cref(request).get(), &stream);
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(),
+ fmt::format("failed to put object: {}",
opts.path.native()));
+ LOG(WARNING) << st.code << ", request_id=" << request_id;
+ return ObjectStorageResponse {
+ .status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()};
+ }
+
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "PutObject cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key;
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageUploadResponse S3ObjStorageBackend::upload_part(const
ObjectStoragePathOptions& opts,
+ std::string_view
stream,
+ int part_num) {
+ UploadPartRequest request;
+ request.WithBucket(opts.bucket)
+ .WithKey(opts.key)
+ .WithPartNumber(part_num)
+ .WithUploadId(*opts.upload_id);
+ auto string_view_stream =
std::make_shared<StringViewStream>(stream.data(), stream.size());
+
+ request.SetBody(string_view_stream);
+
+ Aws::Utils::ByteBuffer
part_md5(Aws::Utils::HashingUtils::CalculateMD5(*string_view_stream));
+ request.SetContentMD5(Aws::Utils::HashingUtils::Base64Encode(part_md5));
+
+ request.SetContentLength(stream.size());
+ request.SetContentType("application/octet-stream");
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency scoped_latency(
+ client_bvar::s3_multi_part_upload_latency);
+
+ return _client->UploadPart(request);
+ }(),
+ "s3_file_writer::upload_part", std::cref(request).get(), &stream);
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ TEST_SYNC_POINT_CALLBACK("S3FileWriter::_upload_one_part", &outcome);
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(),
+ fmt::format("failed to UploadPart: {}, part_num
{}, upload_id={}",
+ opts.path.native(), part_num,
*opts.upload_id));
+
+ LOG(WARNING) << st.code << ", request_id=" << request_id;
+ return ObjectStorageUploadResponse {
+ .resp = {.status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()}};
+ }
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "UploadPart cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key
+ << ", part_num=" << part_num << ", upload_id=" << *opts.upload_id;
+ return ObjectStorageUploadResponse {.resp = ObjectStorageResponse::OK(),
+ .etag = outcome.GetResult().GetETag()};
+}
+
+ObjectStorageResponse S3ObjStorageBackend::complete_multipart_upload(
+ const ObjectStoragePathOptions& opts,
+ const std::vector<ObjectCompleteMultiPart>& completed_parts) {
+ CompleteMultipartUploadRequest request;
+
request.WithBucket(opts.bucket).WithKey(opts.key).WithUploadId(*opts.upload_id);
+
+ CompletedMultipartUpload completed_upload;
+ std::vector<CompletedPart> complete_parts;
+ std::ranges::transform(completed_parts, std::back_inserter(complete_parts),
+ [](const ObjectCompleteMultiPart& part_ptr) {
+ CompletedPart part;
+ part.SetPartNumber(part_ptr.part_num);
+ part.SetETag(part_ptr.etag);
+ return part;
+ });
+ completed_upload.SetParts(std::move(complete_parts));
+ request.WithMultipartUpload(completed_upload);
+
+ TEST_SYNC_POINT_RETURN_WITH_VALUE("S3FileWriter::_complete:3",
ObjectStorageResponse(), this);
+
+ const auto start = std::chrono::steady_clock::now();
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency scoped_latency(
+ client_bvar::s3_multi_part_upload_latency);
+ return _client->CompleteMultipartUpload(request);
+ }(),
+ "s3_file_writer::complete_multi_part", std::cref(request).get());
+ const auto elapsed_ms = elapsed_time_milliseconds(start);
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ auto st = s3fs_error(outcome.GetError(),
+ fmt::format("failed to CompleteMultipartUpload:
{}, upload_id={}",
+ opts.path.native(), *opts.upload_id));
+ LOG(WARNING) << st.code << ", request_id=" << request_id;
+ return {.status = st,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()};
+ }
+
+ LOG_IF(INFO, elapsed_ms > S3_REQUEST_THRESHOLD_MS)
+ << "CompleteMultipartUpload cost=" << elapsed_ms << "ms"
+ << ", request_id=" << request_id << ", bucket=" << opts.bucket <<
", key=" << opts.key
+ << ", upload_id=" << *opts.upload_id;
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageHeadResponse S3ObjStorageBackend::head_object(const
ObjectStoragePathOptions& opts) {
+ Aws::S3::Model::HeadObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+
+ auto outcome = SYNC_POINT_HOOK_RETURN_VALUE(
+ [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_head_latency);
+ return _client->HeadObject(request);
+ }(),
+ "s3_file_system::head_object", std::ref(request).get());
+
+ if (outcome.IsSuccess()) {
+ return {.resp = ObjectStorageResponse::OK(),
+ .file_size = outcome.GetResult().GetContentLength()};
+ } else if (outcome.GetError().GetResponseCode() ==
Aws::Http::HttpResponseCode::NOT_FOUND) {
+ return {.resp = {.status = TStatusCode::NOT_FOUND}, .file_size = 0};
+ } else {
+ record_s3_request_failed(outcome.GetError());
+ LOG(WARNING) << "failed to head object"
+ << "bucket " << opts.bucket << " key " << opts.key << "
responseCode "
+ << outcome.GetError() << " error " <<
outcome.GetError().GetMessage()
+ << " request_id " << outcome.GetError().GetRequestId();
+ return {.resp = {.status = s3fs_error(
+ outcome.GetError(),
+ fmt::format("failed to head object: {}",
opts.path.native())),
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId()},
+ .file_size = -1};
+ }
+}
+
+ObjectStorageResponse S3ObjStorageBackend::get_object(const
ObjectStoragePathOptions& opts,
+ void* buffer, size_t
offset,
+ size_t bytes_read,
size_t* size_return) {
+ Aws::S3::Model::GetObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ request.SetRange(fmt::format("bytes={}-{}", offset, offset + bytes_read -
1));
+ request.SetResponseStreamFactory(AwsWriteableStreamFactory(buffer,
bytes_read));
+
+ auto outcome = [&]() {
+ client_bvar::ScopedLatency scoped_latency(client_bvar::s3_get_latency);
+ return _client->GetObject(request);
+ }();
+ if (!outcome.IsSuccess()) {
+ record_s3_request_failed(outcome.GetError());
+ return ObjectStorageResponse {
+ .status = s3fs_error(outcome.GetError(),
+ fmt::format("failed to get object: {}",
opts.path.native())),
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = outcome.GetError().GetRequestId(),
+ };
+ }
+ *size_return = outcome.GetResult().GetContentLength();
+ SYNC_POINT_CALLBACK("s3_obj_storage_client::get_object", size_return);
+ if (*size_return != bytes_read) {
+ return ObjectStorageResponse {
+ .status = {TStatusCode::INTERNAL_ERROR,
+ fmt::format("incomplete read from {}, expect {},
got {}",
+ opts.path.native(), bytes_read,
*size_return)}};
+ }
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageListPage S3ObjStorageBackend::list_objects(const
ObjectStoragePathOptions& opts,
+ std::string_view
continuation_token) {
+ const auto& prefix = opts.prefix.empty() ? opts.key : opts.prefix;
+ Aws::S3::Model::ListObjectsV2Request request;
+
request.WithBucket(opts.bucket).WithPrefix(prefix).WithMaxKeys(OBJECT_LIST_PAGE_SIZE);
+ if (!continuation_token.empty()) {
+ request.SetContinuationToken(std::string(continuation_token));
+ }
+ TEST_SYNC_POINT_CALLBACK("S3ObjStorageBackend::list_objects", &request);
+
+ auto outcome = [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_list_latency);
+ return _client->ListObjectsV2(request);
+ }();
+
+ const auto& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+ if (!outcome.IsSuccess()) {
+ // Some S3-compatible providers (for example TOS) return NoSuchKey
instead of an empty page
+ // when a prefix does not exist.
+ if (outcome.GetError().GetErrorType() ==
Aws::S3::S3Errors::NO_SUCH_KEY) {
+ LOG(INFO) << fmt::format(
+ "NoSuchKey when listing objects, treat as empty response,
endpoint: {}, "
+ "bucket: {}, prefix: {}, request_id: {}",
+ _config.endpoint, request.GetBucket(),
request.GetPrefix(), request_id);
+ return {.resp = ObjectStorageResponse::OK()};
+ }
+
record_object_request_failed(static_cast<int>(outcome.GetError().GetResponseCode()));
+ const auto status = s3fs_error(outcome.GetError(),
+ fmt::format("failed to list objects:
{}, prefix: {}",
+ request.GetBucket(),
request.GetPrefix()));
+ LOG(WARNING) << fmt::format(
+ "failed to list objects, endpoint: {}, bucket: {}, prefix: {},
responseCode: {}, "
+ "error: {}, request_id: {}",
+ _config.endpoint, request.GetBucket(), request.GetPrefix(),
+ static_cast<int>(outcome.GetError().GetResponseCode()),
+ outcome.GetError().GetMessage(), request_id);
+ return {
+ .resp = {.status = status,
+ .http_code =
static_cast<int>(outcome.GetError().GetResponseCode()),
+ .request_id = request_id},
+ };
+ }
+
+ const auto& result = outcome.GetResult();
+ if (result.GetIsTruncated() && result.GetNextContinuationToken().empty()) {
+ LOG(WARNING) << fmt::format(
+ "failed to list objects, isTruncated but no continuation
token, endpoint: {}, "
+ "bucket: {}, prefix: {}, request_id: {}",
+ _config.endpoint, request.GetBucket(), request.GetPrefix(),
request_id);
+ return {
+ .resp = {.status = {TStatusCode::INTERNAL_ERROR,
+ fmt::format("failed to list objects: {},
prefix: {}",
+ request.GetBucket(),
request.GetPrefix())},
+ .http_code = 0,
+ .request_id = request_id},
+ };
+ }
+
+ ObjectStorageListPage page {
+ .resp = ObjectStorageResponse::OK(),
+ .continuation_token = result.GetNextContinuationToken(),
+ .has_more = result.GetIsTruncated(),
+ };
+ const auto& content = result.GetContents();
+ page.objects.reserve(content.size());
+ for (const auto& obj : content) {
+ DCHECK(obj.GetKey().starts_with(request.GetPrefix()))
+ << obj.GetKey() << ' ' << request.GetPrefix();
+ page.objects.emplace_back(ObjectMeta {.file_path = obj.GetKey(),
+ .size = obj.GetSize(),
+ .mtime_s =
obj.GetLastModified().Seconds()});
+ }
+ return page;
+}
+
+ObjectStorageResponse S3ObjStorageBackend::delete_objects(const
ObjectStoragePathOptions& opts,
+
std::vector<std::string> objs) {
+ size_t max_delete_batch = 1000;
+ TEST_SYNC_POINT_CALLBACK("S3ObjClient::delete_objects", &max_delete_batch);
+ TEST_SYNC_POINT_CALLBACK("S3ObjStorageClient::delete_objects",
&max_delete_batch);
+ max_delete_batch = std::max<size_t>(1, max_delete_batch);
+ for (size_t begin = 0; begin < objs.size(); begin += max_delete_batch) {
+ const size_t end = std::min(begin + max_delete_batch, objs.size());
+ if (end - begin == 1) {
+ auto single_opts = opts;
+ single_opts.key = std::move(objs[begin]);
+ auto resp = delete_object(single_opts);
+ if (!resp.ok()) {
+ return resp;
+ }
+ continue;
+ }
+
+ Aws::S3::Model::DeleteObjectsRequest delete_request;
+ delete_request.SetBucket(opts.bucket);
+ Aws::S3::Model::Delete del;
+ Aws::Vector<Aws::S3::Model::ObjectIdentifier> objects;
+ objects.reserve(end - begin);
+ for (size_t i = begin; i < end; ++i) {
+ Aws::S3::Model::ObjectIdentifier object;
+ object.SetKey(std::move(objs[i]));
+ objects.emplace_back(std::move(object));
+ }
+ del.WithObjects(std::move(objects)).SetQuiet(true);
+ delete_request.SetDelete(std::move(del));
+
+ auto delete_outcome = [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_delete_objects_latency);
+ return _client->DeleteObjects(delete_request);
+ }();
+ SYNC_POINT_CALLBACK("s3_obj_storage_client::delete_objects",
&delete_outcome);
+
SYNC_POINT_CALLBACK("s3_obj_storage_client::delete_objects_recursively",
&delete_outcome);
+ if (!delete_outcome.IsSuccess()) {
+ record_s3_request_failed(delete_outcome.GetError());
+ return ObjectStorageResponse {
+ .status = s3fs_error(delete_outcome.GetError(),
+ fmt::format("failed to delete dir
{}", opts.key)),
+ .http_code =
static_cast<int>(delete_outcome.GetError().GetResponseCode()),
+ .request_id = delete_outcome.GetError().GetRequestId()};
+ }
+ if (!delete_outcome.GetResult().GetErrors().empty()) {
+ const auto& error = delete_outcome.GetResult().GetErrors().front();
+ return ObjectStorageResponse {
+ .status = {TStatusCode::INTERNAL_ERROR,
+ fmt::format("failed to delete object {}: {},
request_id={}",
+ error.GetKey(), error.GetMessage(),
+
delete_outcome.GetResult().GetRequestId())}};
+ }
+ }
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageResponse S3ObjStorageBackend::delete_object(const
ObjectStoragePathOptions& opts) {
+ Aws::S3::Model::DeleteObjectRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+
+ auto outcome = [&]() {
+ client_bvar::ScopedLatency
scoped_latency(client_bvar::s3_delete_object_latency);
+
+ return _client->DeleteObject(request);
+ }();
+ TEST_SYNC_POINT_CALLBACK("S3ObjClient::delete_object", &outcome);
+ TEST_SYNC_POINT_CALLBACK("S3ObjStorageClient::delete_object", &outcome);
+ if (outcome.IsSuccess() ||
+ outcome.GetError().GetResponseCode() ==
Aws::Http::HttpResponseCode::NOT_FOUND) {
+ return ObjectStorageResponse::OK();
+ }
+ record_s3_request_failed(outcome.GetError());
+ return ObjectStorageResponse {
+ .status = {TStatusCode::INTERNAL_ERROR,
Review Comment:
[P2] Preserve the permission status for single-object deletes
For a 403 `DeleteObject` response this branch returns `INTERNAL_ERROR`,
while the new multi-object failure path uses `s3fs_error` and the deleted BE
implementation also used its permission-aware mapper.
`S3FileSystem::delete_file_impl` forwards this code directly, and a one-key
`delete_objects` batch delegates here, so credentials without delete permission
now surface as a generic internal failure and classification changes with batch
cardinality. Please return `s3fs_error(outcome.GetError(), ...)` here while
retaining the HTTP code and request ID.
##########
common/cpp/client/auth/aws_credential_factory.cpp:
##########
@@ -0,0 +1,109 @@
+// 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 "aws_credential_factory.h"
+
+#include <aws/core/auth/AWSCredentials.h>
+#include <aws/core/auth/AWSCredentialsProvider.h>
+#include <aws/core/auth/AWSCredentialsProviderChain.h>
+#include <aws/core/auth/STSCredentialsProvider.h>
+#include <aws/core/platform/Environment.h>
+#include <aws/identity-management/auth/STSAssumeRoleCredentialsProvider.h>
+#include <aws/sts/STSClient.h>
+
+#include "cpp/custom_aws_credentials_provider_chain.h"
+
+namespace doris {
+namespace {
+
+using Provider = Aws::Auth::AWSCredentialsProvider;
+
+std::shared_ptr<Provider> create_v2_base_provider(CredProviderType type) {
+ switch (type) {
+ case CredProviderType::Env:
+ return
std::make_shared<Aws::Auth::EnvironmentAWSCredentialsProvider>();
+ case CredProviderType::SystemProperties:
+ return
std::make_shared<Aws::Auth::ProfileConfigFileAWSCredentialsProvider>();
+ case CredProviderType::WebIdentity:
+ return
std::make_shared<Aws::Auth::STSAssumeRoleWebIdentityCredentialsProvider>();
+ case CredProviderType::Container:
+ return std::make_shared<Aws::Auth::TaskRoleCredentialsProvider>(
+
Aws::Environment::GetEnv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI").c_str());
+ case CredProviderType::Anonymous:
+ return std::make_shared<Aws::Auth::AnonymousAWSCredentialsProvider>();
+ case CredProviderType::Default:
+ case CredProviderType::Simple:
+ return std::make_shared<CustomAwsCredentialsProviderChain>();
+ case CredProviderType::InstanceProfile:
+ return
std::make_shared<Aws::Auth::InstanceProfileCredentialsProvider>();
+ }
+ return nullptr;
+}
+
+AwsCredentialResult assume_role(const AwsCredentialOptions& options,
+ std::shared_ptr<Provider> base_provider) {
+ auto sts_client =
+ std::make_shared<Aws::STS::STSClient>(base_provider,
options.sts_client_config);
+ return {
+ .provider =
std::make_shared<Aws::Auth::STSAssumeRoleCredentialsProvider>(
+ options.role_arn, Aws::String(), options.external_id,
+ Aws::Auth::DEFAULT_CREDS_LOAD_FREQ_SECONDS,
std::move(sts_client)),
+ };
+}
+
+} // namespace
+
+AwsCredentialResult AwsCredentialFactory::create(const AwsCredentialOptions&
options) {
+ const bool has_access_key = !options.access_key.empty();
+ const bool has_secret_key = !options.secret_key.empty();
+
+ if (has_access_key && has_secret_key) {
+ Aws::Auth::AWSCredentials credentials(options.access_key,
options.secret_key);
+ if (!options.session_token.empty()) {
+ credentials.SetSessionToken(options.session_token);
+ }
+ return {
+ .provider =
std::make_shared<Aws::Auth::SimpleAWSCredentialsProvider>(
+ std::move(credentials)),
+ };
+ }
+
+ if (options.version == AwsCredentialProviderVersion::V1) {
+ if (options.provider_type == CredProviderType::InstanceProfile) {
+ auto base =
std::make_shared<Aws::Auth::InstanceProfileCredentialsProvider>();
+ return options.role_arn.empty() ? AwsCredentialResult {.provider =
std::move(base)}
+ : assume_role(options,
std::move(base));
+ }
+ if (options.empty_credentials == EmptyCredentialsBehavior::ANONYMOUS) {
Review Comment:
[P2] Preserve the V1 fallback for accepted partial credentials
BE passes `ANONYMOUS` here, but this condition does not verify that both
static fields are empty. Current validation and tests accept AK-only or SK-only
configurations when a role ARN is present; on the
non-`InstanceProfile`/default-provider path the deleted V1 logic fell back to
`DefaultAWSCredentialsProviderChain`, while this branch now sends anonymous
requests. Because `v1` remains supported, those accepted configurations can
lose working environment/default credentials. Please select anonymous only when
both fields are empty (or reject partial pairs consistently) and add
provider-selection tests for both variants.
##########
common/cpp/client/obj_storage_client.cpp:
##########
@@ -0,0 +1,298 @@
+// 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 <algorithm>
+#include <iterator>
+
+namespace doris {
+namespace {
+
+ObjStorageRateLimitToken acquire_rate_limit(
+ const std::shared_ptr<const ObjStorageRateLimitPolicy>& policy,
ObjStorageRequestType type,
+ size_t estimated_bytes = 0) {
+ if (!policy) {
+ return {};
+ }
+ return policy->acquire(type, estimated_bytes);
+}
+
+} // namespace
+
+ObjStorageRateLimitToken ObjStorageClient::acquire(ObjStorageRequestType type,
+ size_t estimated_bytes)
const {
+ return acquire_rate_limit(rate_limit_policy_, type, estimated_bytes);
+}
+
+ObjectStorageUploadResponse ObjStorageClient::create_multipart_upload(
+ const ObjectStoragePathOptions& opts) {
+ auto rate_limit = acquire(ObjStorageRequestType::PUT);
+ if (!rate_limit.resp.ok()) {
+ return {.resp = std::move(rate_limit.resp)};
+ }
+ return backend_->create_multipart_upload(opts);
+}
+
+ObjectStorageResponse ObjStorageClient::put_object(const
ObjectStoragePathOptions& opts,
+ std::string_view stream) {
+ auto rate_limit = acquire(ObjStorageRequestType::PUT, stream.size());
+ if (!rate_limit.resp.ok()) {
+ return rate_limit.resp;
+ }
+ return backend_->put_object(opts, stream);
+}
+
+ObjectStorageUploadResponse ObjStorageClient::upload_part(const
ObjectStoragePathOptions& opts,
+ std::string_view
stream, int part_num) {
+ auto rate_limit = acquire(ObjStorageRequestType::PUT, stream.size());
+ if (!rate_limit.resp.ok()) {
+ return {.resp = std::move(rate_limit.resp)};
+ }
+ return backend_->upload_part(opts, stream, part_num);
+}
+
+ObjectStorageResponse ObjStorageClient::complete_multipart_upload(
+ const ObjectStoragePathOptions& opts,
+ const std::vector<ObjectCompleteMultiPart>& completed_parts) {
+ auto rate_limit = acquire(ObjStorageRequestType::PUT);
+ if (!rate_limit.resp.ok()) {
+ return rate_limit.resp;
+ }
+ return backend_->complete_multipart_upload(opts, completed_parts);
+}
+
+ObjectStorageHeadResponse ObjStorageClient::head_object(const
ObjectStoragePathOptions& opts) {
+ auto rate_limit = acquire(ObjStorageRequestType::GET);
+ if (!rate_limit.resp.ok()) {
+ return {.resp = std::move(rate_limit.resp)};
+ }
+ return backend_->head_object(opts);
+}
+
+ObjectStorageResponse ObjStorageClient::get_object(const
ObjectStoragePathOptions& opts,
+ void* buffer, size_t
offset, size_t bytes_read,
+ size_t* size_return) {
+ auto rate_limit = acquire(ObjStorageRequestType::GET, bytes_read);
+ if (!rate_limit.resp.ok()) {
+ return rate_limit.resp;
+ }
+ auto response = backend_->get_object(opts, buffer, offset, bytes_read,
size_return);
+ if (response.ok()) {
+ rate_limit.settle_bytes(*size_return);
Review Comment:
[P2] Port the direct admission-policy tests to the shared facade
This facade is now the sole mapping from each object API to GET/PUT
admission, estimated bytes, and short-read settlement, but the PR deletes the
direct suite that asserted those contracts without adding an equivalent
`ObjStorageClient` suite. The remaining changed tests count list/recursive
requests or exercise integration paths; they do not pin rejection propagation,
per-API request type, payload charging, or this `settle_bytes` call. Please
port those direct cases to the shared facade so a future mapping or bypass
regression is caught.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]