github-actions[bot] commented on code in PR #66350:
URL: https://github.com/apache/doris/pull/66350#discussion_r3706695258
##########
cloud/src/recycler/s3_accessor.cpp:
##########
@@ -503,29 +553,30 @@ int S3Accessor::delete_files(const
std::vector<std::string>& paths) {
keys.emplace_back(get_key(path));
}
- return obj_client_->delete_objects(conf_.bucket, std::move(keys),
{.executor = worker_pool})
- .ret;
+ return obj_client_->delete_objects({.bucket = conf_.bucket},
std::move(keys)).status.code;
}
int S3Accessor::delete_file(const std::string& path) {
LOG_INFO("delete file").tag("uri", to_uri(path));
- int ret = obj_client_->delete_object({.bucket = conf_.bucket, .key =
get_key(path)}).ret;
- static_assert(ObjectStorageResponse::OK == 0);
- if (ret == ObjectStorageResponse::OK || ret ==
ObjectStorageResponse::NOT_FOUND) {
+ int ret =
+ obj_client_->delete_object({.bucket = conf_.bucket, .key =
get_key(path)}).status.code;
+ static_assert(ObjectStorageStatus::OK == 0);
+ if (ret == ObjectStorageStatus::OK || ret ==
ObjectStorageStatus::NOT_FOUND) {
return 0;
}
return ret;
}
int S3Accessor::put_file(const std::string& path, const std::string& content) {
- return obj_client_->put_object({.bucket = conf_.bucket, .key =
get_key(path)}, content).ret;
+ return obj_client_->put_object({.bucket = conf_.bucket, .key =
get_key(path)}, content)
+ .status.code;
}
int S3Accessor::list_prefix(const std::string& path_prefix,
std::unique_ptr<ListIterator>* res) {
- size_t prefix_length = conf_.prefix.empty() ? 0 : conf_.prefix.length() +
1;
*res = std::make_unique<S3ListIterator>(
- obj_client_->list_objects({.bucket = conf_.bucket, .key =
get_key(path_prefix)}),
- prefix_length);
+ obj_client_,
Review Comment:
[P1] Preserve Recycler's exists return contract
The common response uses positive Thrift codes (`NOT_FOUND` is 31, while
internal/auth errors are also positive), but `StorageVaultAccessor::exists`
promises 0 for present, 1 for absent, and a negative value for errors.
`checker.cpp` consequently treats an S3 permission or server failure as a
missing file, and the existing accessor tests still require 1 for a 404. Map OK
to 0, NOT_FOUND to 1, and every other status to a negative error at this
adapter boundary.
##########
gensrc/proto/cloud.proto:
##########
@@ -309,6 +309,7 @@ message ObjectStoreInfoPB {
optional CredProviderTypePB cred_provider_type = 17;
optional string role_arn = 18; // aws assumed role's arn
optional string external_id = 19; // aws assumed role's external_id if
configure
+ optional string token = 20; // optional session token paired with ak/sk
Review Comment:
[P1] Give the token the same secret lifecycle as SK
This field is currently left plaintext by `create_object_info_with_encrypt`
and external-stage creation while SK is encrypted. It can then be emitted by
full-instance INFO logs that do not call `hide_token`, returned by the list-all
external-stage display path, and serialized by `SHOW STORAGE VAULT` for any
`USAGE` grantee because that converter masks only SK. Please encrypt/decrypt
the token like other credential secrets and explicitly strip or mask it from
every log and display response.
##########
common/cpp/client/s3_obj_storage_provider.cpp:
##########
@@ -0,0 +1,558 @@
+// 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_provider.h"
+
+#include <cpp/client/obj_storage_client.h>
+#include <gen_cpp/Status_types.h>
+
+#include <algorithm>
+
+#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 {
+
+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 S3ObjStorageProvider::create_multipart_upload(
+ const ObjectStoragePathOptions& opts) {
+ CreateMultipartUploadRequest request;
+ request.WithBucket(opts.bucket).WithKey(opts.key);
+ request.SetContentType("application/octet-stream");
+
+ 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& request_id = outcome.IsSuccess() ?
outcome.GetResult().GetRequestId()
+ :
outcome.GetError().GetRequestId();
+
+ LOG(INFO) << "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 S3ObjStorageProvider::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");
+
+ 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& 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(INFO) << "request_id = " << request_id << ", bucket = " << opts.bucket
Review Comment:
[P2] Avoid INFO logging every successful object write
This unconditional success log turns normal PutObject traffic into one INFO
record per object; create and complete multipart have the same pattern. The
deleted BE implementation logged these only when they exceeded the 5-second
threshold, and Recycler logged PutObject failures only, so ingest/file-cache
traffic can now generate log I/O proportional to storage QPS. Restore
slow-request-only logging or lower these success messages to VLOG while keeping
latency bvars for aggregate observability.
##########
cloud/src/recycler/s3_accessor.cpp:
##########
@@ -567,11 +618,11 @@ int S3Accessor::abort_multipart_upload(const std::string&
path, const std::strin
}
int S3Accessor::get_life_cycle(int64_t* expiration_days) {
- return obj_client_->get_life_cycle(conf_.bucket, expiration_days).ret;
Review Comment:
[P1] Finish converting the GCS iterator to value semantics
This declaration changes `iter` from a pointer to a stack
`ObjectListIterator`, and the new loop correctly calls `iter.next()`, but the
post-loop check at line 678 still calls `iter->is_valid()`. The common iterator
defines no `operator->`, so the Cloud target cannot compile regardless of which
provider is selected at runtime. Change that remaining check to
`iter.is_valid()`.
##########
common/cpp/client/obj_storage_client.cpp:
##########
@@ -0,0 +1,299 @@
+// 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 {
+
+ObjStorageRateLimitToken ObjStorageClient::acquire(ObjStorageRequestType type,
+ size_t estimated_bytes)
const {
+ if (!rate_limit_policy_) {
+ return {};
+ }
+ return rate_limit_policy_->acquire(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 provider_->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 provider_->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 provider_->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 provider_->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 provider_->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 = provider_->get_object(opts, buffer, offset, bytes_read,
size_return);
+ if (response.ok()) {
+ rate_limit.settle_bytes(*size_return);
+ }
+ return response;
+}
+
+ObjectStorageListPage ObjStorageClient::list_objects(const
ObjectStoragePathOptions& opts,
+ std::string_view
continuation_token) {
+ auto rate_limit = acquire(ObjStorageRequestType::GET);
+ if (!rate_limit.resp.ok()) {
+ return {.resp = std::move(rate_limit.resp)};
+ }
+ return provider_->list_objects(opts, continuation_token);
+}
+
+ObjectStorageResponse ObjectListIterator::has_next() {
+ if (!is_valid_) {
+ return {
+ .status = {TStatusCode::INTERNAL_ERROR, "Iterator is invalid"},
+ .http_code = 0,
+ };
+ }
+ while (next_index_ == objects_.size()) {
+ if (!has_more_) {
+ return {
+ .status = {TStatusCode::END_OF_FILE, "No more results"},
+ .http_code = 200,
+ };
+ }
+ auto page = client_->list_objects(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 ObjectStorageResponse::OK();
+}
+
+ObjectStorageListResponse ObjectListIterator::next() {
+ auto response = has_next();
+ if (response.status.code == ObjectStorageStatus::END_OF_FILE) {
+ return {.resp = ObjectStorageResponse::OK(), .results_ = {}};
+ }
+ if (!response.ok()) {
+ return {.resp = std::move(response), .results_ = {}};
+ }
+ return {
+ .resp = ObjectStorageResponse::OK(),
+ .results_ = std::move(objects_[next_index_++]),
+ };
+}
+
+ObjectStorageResponse ObjStorageClient::delete_objects(const
ObjectStoragePathOptions& opts,
+
std::vector<std::string> objs) {
+ // TODO: One public delete_objects call may issue multiple SDK requests.
Move batching to an
+ // upper-layer helper and make the client API handle exactly one
provider-sized batch, so one
+ // client call maps to one rate-limit admission and one SDK request.
+ const auto max_batch_size = std::max<size_t>(1,
provider_->capabilities().max_delete_batch);
+ for (size_t begin = 0; begin < objs.size(); begin += max_batch_size) {
+ const auto end = std::min(begin + max_batch_size, objs.size());
+ auto rate_limit = acquire(ObjStorageRequestType::PUT);
+ if (!rate_limit.resp.ok()) {
+ return rate_limit.resp;
+ }
+ std::vector<std::string> batch(std::make_move_iterator(objs.begin() +
begin),
+ std::make_move_iterator(objs.begin() +
end));
+ auto response = provider_->delete_objects(opts, std::move(batch));
+ if (!response.ok()) {
+ return response;
+ }
+ }
+ return ObjectStorageResponse::OK();
+}
+
+ObjectStorageResponse ObjStorageClient::delete_object(const
ObjectStoragePathOptions& opts) {
+ auto rate_limit = acquire(ObjStorageRequestType::PUT);
+ if (!rate_limit.resp.ok()) {
+ return rate_limit.resp;
+ }
+ return provider_->delete_object(opts);
+}
+
+ObjectStorageResponse ObjStorageClient::delete_objects_recursively(
+ const ObjectStoragePathOptions& opts, const RecursiveDeleteOptions&
options) {
+ // TODO: A recursive delete may issue multiple list and delete SDK
requests. Keep the previous
+ // BE behavior of charging it as one logical PUT call in this refactor.
Move recursive
+ // orchestration above ObjStorageClient in a follow-up, using the one-page
list API and a
+ // one-batch delete API so every client call maps to one admission and one
SDK request.
+ auto rate_limit = acquire(ObjStorageRequestType::PUT);
Review Comment:
[P1] Keep Recycler recursive deletes under request-level admission
After this single PUT admission, every list page and delete batch calls the
provider directly, so `RecyclerObjStorageRateLimitPolicy` is never invoked for
the actual SDK requests. The deleted Recycler implementations admitted every
GET page and PUT batch; a large prefix can now issue unbounded pages and
concurrent deletes after one token and one fault-injection decision. Route each
page/batch through the policy-bearing facade, or make BE's
one-logical-operation behavior an explicit BE-only mode.
##########
cloud/src/recycler/s3_accessor.cpp:
##########
@@ -207,18 +220,28 @@ std::optional<S3Conf> S3Conf::from_obj_store_info(const
ObjectStoreInfoPB& obj_i
s3_conf.provider = S3Conf::AZURE;
break;
default:
- LOG_WARNING("unknown provider type {}").tag("obj_info",
proto_to_json(obj_info));
+ LOG_WARNING("unknown object storage provider")
+ .tag("provider", obj_info.provider())
+ .tag("bucket", obj_info.bucket());
return std::nullopt;
}
if (!skip_aksk) {
+ if (obj_info.ak().empty() != obj_info.sk().empty()) {
+ LOG_WARNING("access key and secret key must be provided together")
+ .tag("provider", obj_info.provider())
+ .tag("bucket", obj_info.bucket());
+ return std::nullopt;
+ }
if (!obj_info.ak().empty() && !obj_info.sk().empty()) {
if (obj_info.has_encryption_info()) {
AkSkPair plain_ak_sk_pair;
int ret = decrypt_ak_sk_helper(obj_info.ak(), obj_info.sk(),
obj_info.encryption_info(),
&plain_ak_sk_pair);
if (ret != 0) {
- LOG_WARNING("fail to decrypt ak sk").tag("obj_info",
proto_to_json(obj_info));
+ LOG_WARNING("fail to decrypt ak sk")
+ .tag("provider", obj_info.provider())
+ .tag("bucket", obj_info.bucket());
return std::nullopt;
} else {
Review Comment:
[P1] Preserve token across the complete DDL and metadata round trip
FE creation accepts and validates `s3.session_token`, but
`CloudObjectStoreAdapter` never puts it in `ObjectStoreInfoPB`, and the S3
vault ALTER allow-list rejects token rotation. Even for direct protobuf paths,
`object_info_pb_factory` and internal-stage reconstruction omit the token,
while meta-service alteration neither updates nor clears it with AK/SK.
Temporary credentials can therefore validate and still reach the new
BE/Recycler/FE consumers with an empty or stale token. Thread token through the
FE builder/allow-list and every meta create, alter, and reconstruction path as
part of the AK/SK tuple, with end-to-end tests.
--
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]