| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| #include "obj_storage_client.h" | |
| #include <cpp/sync_point.h> | |
| #include <glog/logging.h> | |
| #include <algorithm> | |
| #include <chrono> | |
| namespace doris { | |
| ObjStorageStatus obj_storage_status_from_http_code(int http_code, std::string message) { | |
| switch (http_code) { | |
| case 401: | |
| case 403: | |
| return {ObjStorageStatus::PERMISSION_DENIED, std::move(message)}; | |
| case 404: | |
| return {ObjStorageStatus::NOT_FOUND, std::move(message)}; | |
| case 429: | |
| return {ObjStorageStatus::LIMIT_REACH, std::move(message)}; | |
| default: | |
| return {http_code <= 0 ? ObjStorageStatus::NETWORK_ERROR : ObjStorageStatus::INTERNAL_ERROR, | |
| std::move(message)}; | |
| } | |
| } | |
| std::unique_ptr<ObjStorageListIterator> ObjStorageClient::list_objects(const ObjStoragePath& opts) { | |
| return std::make_unique<ObjStorageListIterator>(shared_from_this(), opts); | |
| } | |
| ObjStorageResponse ObjStorageClient::list_objects(const ObjStoragePath& opts, | |
| std::vector<ObjectMeta>* objects) { | |
| objects->clear(); | |
| auto iter = list_objects(opts); | |
| for (;;) { | |
| auto result = iter->next(); | |
| if (!result.object.has_value()) { | |
| if (!result.resp.ok()) { | |
| objects->clear(); | |
| } | |
| return result.resp; | |
| } | |
| objects->emplace_back(std::move(*result.object)); | |
| } | |
| } | |
| ObjStorageResponse ObjStorageListIterator::has_next() { | |
| if (!is_valid_) { | |
| return { | |
| .status = {ObjStorageStatus::INTERNAL_ERROR, "Iterator is invalid"}, | |
| .http_code = 0, | |
| }; | |
| } | |
| while (next_index_ == objects_.size()) { | |
| if (!has_more_) { | |
| return { | |
| .status = {ObjStorageStatus::END_OF_FILE, "No more results"}, | |
| .http_code = 200, | |
| }; | |
| } | |
| auto page = client_->list_objects_page(opts_, continuation_token_); | |
| if (!page.resp.ok()) { | |
| is_valid_ = false; | |
| return page.resp; | |
| } | |
| objects_ = std::move(page.objects); | |
| next_index_ = 0; | |
| continuation_token_ = std::move(page.continuation_token); | |
| has_more_ = page.has_more; | |
| } | |
| return ObjStorageResponse::OK(); | |
| } | |
| ObjStorageListResult ObjStorageListIterator::next() { | |
| auto response = has_next(); | |
| if (response.status.code == ObjStorageStatus::END_OF_FILE) { | |
| return {.resp = ObjStorageResponse::OK(), .object = {}}; | |
| } | |
| if (!response.ok()) { | |
| return {.resp = std::move(response), .object = {}}; | |
| } | |
| return { | |
| .resp = ObjStorageResponse::OK(), | |
| .object = std::move(objects_[next_index_++]), | |
| }; | |
| } | |
| ObjStorageResponse delete_objects_recursively( | |
| std::shared_ptr<ObjStorageClient> client, const ObjStoragePath& path, | |
| const ObjStorageRecursiveDeleteOptions& delete_options) { | |
| const auto start_time = std::chrono::steady_clock::now(); | |
| auto list_path = path; | |
| if (list_path.prefix.empty()) { | |
| list_path.prefix = list_path.key; | |
| } | |
| auto delete_batch_size = std::max<size_t>(1, client->capabilities().max_delete_batch); | |
| TEST_SYNC_POINT_CALLBACK("ObjStorageClient::delete_objects_recursively_", &delete_batch_size); | |
| delete_batch_size = std::max<size_t>(1, delete_batch_size); | |
| const auto max_tasks_per_batch = std::max<size_t>(1, delete_options.max_tasks_per_batch); | |
| std::vector<std::string> keys; | |
| keys.reserve(delete_batch_size); | |
| size_t pending_tasks = 0; | |
| size_t total_batches = 0; | |
| size_t num_deleted = 0; | |
| size_t error_count = 0; | |
| auto first_error = ObjStorageResponse::OK(); | |
| auto elapsed_milliseconds = [&]() { | |
| return std::chrono::duration_cast<std::chrono::milliseconds>( | |
| std::chrono::steady_clock::now() - start_time) | |
| .count(); | |
| }; | |
| auto finish = [&](ObjStorageResponse response) { | |
| LOG(INFO) << "delete objects under " << list_path.bucket << "/" << list_path.prefix | |
| << " finished, ret=" << response.status.code | |
| << ", total_batches=" << total_batches << ", num_deleted=" << num_deleted | |
| << ", error_count=" << error_count << ", cost=" << elapsed_milliseconds() | |
| << " ms"; | |
| return response; | |
| }; | |
| auto record_error = [&](ObjStorageResponse response) { | |
| if (response.ok()) { | |
| return; | |
| } | |
| ++error_count; | |
| if (first_error.ok()) { | |
| first_error = std::move(response); | |
| } | |
| }; | |
| auto wait_for_tasks = [&]() { | |
| if (pending_tasks == 0) { | |
| return ObjStorageResponse::OK(); | |
| } | |
| const auto tasks_in_batch = pending_tasks; | |
| pending_tasks = 0; | |
| auto response = delete_options.executor ? delete_options.executor->wait() | |
| : ObjStorageResponse::OK(); | |
| ++total_batches; | |
| LOG(INFO) << "delete objects under " << list_path.bucket << "/" << list_path.prefix | |
| << " batch " << total_batches << " completed" | |
| << ", tasks_in_batch=" << tasks_in_batch << ", total_deleted=" << num_deleted | |
| << ", elapsed=" << elapsed_milliseconds() << " ms"; | |
| return response; | |
| }; | |
| auto submit_delete_task = [&]() -> bool { | |
| ObjStorageDeleteTask task = [client, bucket = path.bucket, | |
| batch = std::move(keys)]() mutable { | |
| return client->delete_objects(ObjStoragePath {.bucket = std::move(bucket)}, | |
| std::move(batch)); | |
| }; | |
| keys.clear(); | |
| keys.reserve(delete_batch_size); | |
| ObjStorageResponse response; | |
| if (delete_options.executor) { | |
| response = delete_options.executor->submit(std::move(task)); | |
| } else { | |
| response = task(); | |
| } | |
| ++pending_tasks; | |
| if (!response.ok()) { | |
| record_error(std::move(response)); | |
| record_error(wait_for_tasks()); | |
| return false; | |
| } | |
| if (pending_tasks == max_tasks_per_batch) { | |
| record_error(wait_for_tasks()); | |
| } | |
| // Match the pre-refactor Recycler behavior: do not scan the next task batch after the | |
| // current batch reports a submit, delete, or wait failure. | |
| return first_error.ok(); | |
| }; | |
| auto iter = client->list_objects(list_path); | |
| for (;;) { | |
| auto result = iter->next(); | |
| if (!result.object.has_value()) { | |
| if (result.resp.ok()) { | |
| break; | |
| } | |
| if (!keys.empty()) { | |
| submit_delete_task(); | |
| } | |
| record_error(wait_for_tasks()); | |
| record_error(std::move(result.resp)); | |
| return finish(std::move(first_error)); | |
| } | |
| auto& object = *result.object; | |
| if (delete_options.expiration_time > 0 && object.mtime_s > delete_options.expiration_time) { | |
| continue; | |
| } | |
| ++num_deleted; | |
| keys.emplace_back(std::move(object.key)); | |
| if (keys.size() == delete_batch_size && !submit_delete_task()) { | |
| return finish(std::move(first_error)); | |
| } | |
| } | |
| if (!keys.empty() && !submit_delete_task()) { | |
| return finish(std::move(first_error)); | |
| } | |
| record_error(wait_for_tasks()); | |
| return finish(std::move(first_error)); | |
| } | |
| } // namespace doris |