common/cpp/obj-client/azure_obj_storage_client.cpp
Line | Count | Source |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | |
18 | | #include "azure_obj_storage_client.h" |
19 | | |
20 | | #include <butil/guid.h> |
21 | | |
22 | | #include <cctype> |
23 | | #include <cstdint> |
24 | | #include <string_view> |
25 | | #include <vector> |
26 | | |
27 | | #include "cpp/obj_retry_strategy.h" |
28 | | |
29 | | using namespace Azure::Storage::Blobs; |
30 | | |
31 | | namespace { |
32 | 1 | std::string wrap_object_storage_path_msg(const doris::ObjStoragePath& opts) { |
33 | 1 | return fmt::format("bucket {}, key {}, prefix {}, path {}", opts.bucket, opts.key, opts.prefix, |
34 | 1 | opts.path.native()); |
35 | 1 | } |
36 | | |
37 | 1 | doris::ObjStoragePath with_object_key(const doris::ObjStoragePath& opts, std::string_view key) { |
38 | 1 | auto path = opts; |
39 | 1 | path.key = key; |
40 | 1 | return path; |
41 | 1 | } |
42 | | |
43 | 6 | std::string to_lower_ascii(std::string_view input) { |
44 | 6 | std::string lowered(input); |
45 | 6 | std::transform(lowered.begin(), lowered.end(), lowered.begin(), |
46 | 231 | [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); }); |
47 | 6 | return lowered; |
48 | 6 | } |
49 | | |
50 | 3 | std::string encode_azure_block_id(std::string_view upload_id, int part_num) { |
51 | | // Azure has no multipart upload namespace. Include the writer UUID in every block ID so |
52 | | // concurrent writers for the same key cannot stage interchangeable blocks. |
53 | 3 | std::vector<unsigned char> raw_id(upload_id.begin(), upload_id.end()); |
54 | 3 | auto part = static_cast<uint32_t>(part_num); |
55 | 15 | for (size_t i = 0; i < sizeof(part); ++i) { |
56 | 12 | raw_id.push_back(static_cast<unsigned char>(part >> (i * 8))); |
57 | 12 | } |
58 | 3 | Aws::Utils::ByteBuffer bytes(raw_id.data(), raw_id.size()); |
59 | 3 | return Aws::Utils::HashingUtils::Base64Encode(bytes); |
60 | 3 | } |
61 | | |
62 | | constexpr char SAS_TOKEN_URL_TEMPLATE[] = "{}/{}/{}{}"; |
63 | | constexpr char BlobNotFound[] = "BlobNotFound"; |
64 | | } // namespace |
65 | | |
66 | | namespace doris { |
67 | | |
68 | 3 | std::string azure_multipart_block_id(std::string_view upload_id, int part_num) { |
69 | 3 | return encode_azure_block_id(upload_id, part_num); |
70 | 3 | } |
71 | | |
72 | | std::string build_azure_batch_delete_failure_message(const ObjStoragePath& opts, |
73 | 1 | std::string_view key) { |
74 | 1 | return fmt::format("Azure batch delete failed, path msg {}", |
75 | 1 | wrap_object_storage_path_msg(with_object_key(opts, key))); |
76 | 1 | } |
77 | | |
78 | | // As Azure's doc said, the batch size is 256 |
79 | | // You can find out the num in https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch?tabs=microsoft-entra-id |
80 | | // > Each batch request supports a maximum of 256 subrequests. |
81 | | constexpr size_t BlobBatchMaxOperations = 256; |
82 | | |
83 | 6 | bool is_azure_tls_ca_error_message(std::string_view message) { |
84 | 6 | std::string lower = to_lower_ascii(message); |
85 | 6 | return lower.find("ssl ca cert") != std::string::npos || |
86 | 6 | lower.find("peer failed verification") != std::string::npos || |
87 | 6 | lower.find("unable to get local issuer certificate") != std::string::npos || |
88 | 6 | lower.find("problem with the ssl ca cert") != std::string::npos; |
89 | 6 | } |
90 | | |
91 | | std::string build_azure_tls_debug_suffix(std::string_view error_message, |
92 | 3 | std::string_view tls_debug_context) { |
93 | 3 | if (tls_debug_context.empty() || !is_azure_tls_ca_error_message(error_message)) { |
94 | 2 | return ""; |
95 | 2 | } |
96 | 1 | return fmt::format(", {}", tls_debug_context); |
97 | 3 | } |
98 | | |
99 | | static ObjStorageResponse make_azure_std_exception_response(const std::exception& e, |
100 | | const ObjStoragePath& opts, |
101 | 0 | std::string_view tls_debug_context) { |
102 | 0 | auto msg = fmt::format("Azure request failed because {}, path msg {}{}", e.what(), |
103 | 0 | wrap_object_storage_path_msg(opts), |
104 | 0 | build_azure_tls_debug_suffix(e.what(), tls_debug_context)); |
105 | 0 | LOG(WARNING) << msg; |
106 | 0 | return {.status = ObjStorageStatus {ObjStorageStatus::INTERNAL_ERROR, std::move(msg)}, |
107 | 0 | .http_code = 0, |
108 | 0 | .request_id = ""}; |
109 | 0 | } |
110 | | |
111 | | template <typename Func> |
112 | | ObjStorageResponse do_azure_client_call(Func f, const ObjStoragePath& opts, |
113 | 0 | std::string_view tls_debug_context) { |
114 | 0 | try { |
115 | 0 | f(); |
116 | 0 | } catch (Azure::Core::RequestFailedException& e) { |
117 | 0 | doris::record_object_request_failed(static_cast<int>(e.StatusCode)); |
118 | 0 | auto msg = fmt::format( |
119 | 0 | "Azure request failed because {}, error msg {}, http code {}, path msg {}{}", |
120 | 0 | e.what(), e.Message, static_cast<int>(e.StatusCode), |
121 | 0 | wrap_object_storage_path_msg(opts), |
122 | 0 | build_azure_tls_debug_suffix(fmt::format("{} {}", e.what(), e.Message), |
123 | 0 | tls_debug_context)); |
124 | 0 | LOG(WARNING) << msg; |
125 | 0 | return {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
126 | 0 | std::move(msg)), |
127 | 0 | .http_code = static_cast<int>(e.StatusCode), |
128 | 0 | .request_id = std::move(e.RequestId)}; |
129 | 0 | } catch (const std::exception& e) { |
130 | 0 | return make_azure_std_exception_response(e, opts, tls_debug_context); |
131 | 0 | } |
132 | 0 | return ObjStorageResponse::OK(); |
133 | 0 | } Unexecuted instantiation: _ZN5doris20do_azure_client_callIZNS_17AzureBatchDeleter7executeEvEUlvE_EENS_18ObjStorageResponseET_RKNS_14ObjStoragePathESt17basic_string_viewIcSt11char_traitsIcEE Unexecuted instantiation: azure_obj_storage_client.cpp:_ZN5doris20do_azure_client_callIZNS_21AzureObjStorageClient10put_objectERKNS_14ObjStoragePathESt17basic_string_viewIcSt11char_traitsIcEEE3$_0EENS_18ObjStorageResponseET_S4_S8_ Unexecuted instantiation: azure_obj_storage_client.cpp:_ZN5doris20do_azure_client_callIZNS_21AzureObjStorageClient25complete_multipart_uploadERKNS_14ObjStoragePathERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKSt6vectorINS_23ObjStorageCompletedPartESaISE_EEE3$_1EENS_18ObjStorageResponseET_S4_St17basic_string_viewIcS8_E Unexecuted instantiation: azure_obj_storage_client.cpp:_ZN5doris20do_azure_client_callIZNS_21AzureObjStorageClient10get_objectERKNS_14ObjStoragePathEPvmmPmE3$_0EENS_18ObjStorageResponseET_S4_St17basic_string_viewIcSt11char_traitsIcEE |
134 | | |
135 | | struct AzureBatchDeleter { |
136 | | AzureBatchDeleter(BlobContainerClient* client, const ObjStoragePath& opts, |
137 | | std::string_view representative_key, std::string_view tls_debug_context) |
138 | 0 | : _client(client), |
139 | 0 | _batch(client->CreateBatch()), |
140 | 0 | _opts(with_object_key(opts, representative_key)), |
141 | 0 | _tls_debug_context(tls_debug_context) {} |
142 | | // Submit one blob to be deleted in `AzureBatchDeleter::execute` |
143 | 0 | void delete_blob(const std::string& blob_name) { |
144 | 0 | deferred_resps.emplace_back(DeferredDeleteResponse { |
145 | 0 | .key = blob_name, .response = _batch.DeleteBlob(blob_name)}); |
146 | 0 | } |
147 | 0 | ObjStorageResponse execute() { |
148 | 0 | if (deferred_resps.empty()) { |
149 | 0 | return ObjStorageResponse::OK(); |
150 | 0 | } |
151 | 0 | auto resp = do_azure_client_call( |
152 | 0 | [&]() { |
153 | 0 | client_bvar::ScopedLatency scoped_latency( |
154 | 0 | client_bvar::s3_delete_objects_latency); |
155 | 0 | _client->SubmitBatch(_batch); |
156 | 0 | }, |
157 | 0 | _opts, _tls_debug_context); |
158 | 0 | if (resp.status.code != ObjStorageStatus::OK) { |
159 | 0 | return resp; |
160 | 0 | } |
161 | | |
162 | 0 | for (auto&& deferred : deferred_resps) { |
163 | 0 | try { |
164 | 0 | auto r = deferred.response.GetResponse(); |
165 | 0 | if (!r.Value.Deleted) { |
166 | 0 | auto msg = build_azure_batch_delete_failure_message(_opts, deferred.key); |
167 | 0 | LOG(WARNING) << msg; |
168 | 0 | return {.status = ObjStorageStatus {ObjStorageStatus::INTERNAL_ERROR, |
169 | 0 | std::move(msg)}, |
170 | 0 | .http_code = 0, |
171 | 0 | .request_id = ""}; |
172 | 0 | } |
173 | 0 | } catch (Azure::Core::RequestFailedException& e) { |
174 | 0 | if (Azure::Core::Http::HttpStatusCode::NotFound == e.StatusCode && |
175 | 0 | 0 == strcmp(e.ErrorCode.c_str(), BlobNotFound)) { |
176 | 0 | continue; |
177 | 0 | } |
178 | 0 | doris::record_object_request_failed(static_cast<int>(e.StatusCode)); |
179 | 0 | auto msg = fmt::format( |
180 | 0 | "Azure request failed because {}, error msg {}, http code {}, path msg " |
181 | 0 | "{}{}", |
182 | 0 | e.what(), e.Message, static_cast<int>(e.StatusCode), |
183 | 0 | wrap_object_storage_path_msg(with_object_key(_opts, deferred.key)), |
184 | 0 | build_azure_tls_debug_suffix(fmt::format("{} {}", e.what(), e.Message), |
185 | 0 | _tls_debug_context)); |
186 | 0 | LOG(WARNING) << msg; |
187 | 0 | return {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
188 | 0 | std::move(msg)), |
189 | 0 | .http_code = static_cast<int>(e.StatusCode), |
190 | 0 | .request_id = std::move(e.RequestId)}; |
191 | 0 | } |
192 | 0 | } |
193 | | |
194 | 0 | return ObjStorageResponse::OK(); |
195 | 0 | } |
196 | | |
197 | | private: |
198 | | struct DeferredDeleteResponse { |
199 | | std::string key; |
200 | | Azure::Storage::DeferredResponse<Models::DeleteBlobResult> response; |
201 | | }; |
202 | | |
203 | | BlobContainerClient* _client; |
204 | | BlobContainerBatch _batch; |
205 | | ObjStoragePath _opts; |
206 | | std::string_view _tls_debug_context; |
207 | | std::vector<DeferredDeleteResponse> deferred_resps; |
208 | | }; |
209 | | |
210 | 2 | ObjStorageUploadResult AzureObjStorageClient::create_multipart_upload(const ObjStoragePath&) { |
211 | | // Azure has no provider-side multipart session. This local UUID namespaces the writer's |
212 | | // staged block IDs and is carried through the same interface as an S3 upload ID. |
213 | 2 | auto upload_id = butil::GenerateGUID(); |
214 | 2 | if (upload_id.empty()) { |
215 | 0 | return {.resp = {.status = {ObjStorageStatus::INTERNAL_ERROR, |
216 | 0 | "failed to generate Azure multipart upload ID"}}}; |
217 | 0 | } |
218 | 2 | return ObjStorageUploadResult { |
219 | 2 | .resp = ObjStorageResponse::OK(), |
220 | 2 | .upload_id = std::move(upload_id), |
221 | 2 | }; |
222 | 2 | } |
223 | | |
224 | | ObjStorageResponse AzureObjStorageClient::put_object(const ObjStoragePath& opts, |
225 | 0 | std::string_view stream) { |
226 | 0 | auto client = _client->GetBlockBlobClient(opts.key); |
227 | 0 | return do_azure_client_call( |
228 | 0 | [&]() { |
229 | 0 | client_bvar::ScopedLatency scoped_latency(client_bvar::s3_put_latency); |
230 | 0 | client.UploadFrom(reinterpret_cast<const uint8_t*>(stream.data()), stream.size()); |
231 | 0 | }, |
232 | 0 | opts, _config.tls_debug_context); |
233 | 0 | } |
234 | | |
235 | | ObjStorageUploadResult AzureObjStorageClient::upload_part(const ObjStoragePath& opts, |
236 | | const std::string& upload_id, |
237 | 0 | std::string_view stream, int part_num) { |
238 | 0 | DCHECK(!upload_id.empty()); |
239 | 0 | auto client = _client->GetBlockBlobClient(opts.key); |
240 | 0 | std::string block_id = azure_multipart_block_id(upload_id, part_num); |
241 | 0 | try { |
242 | 0 | Azure::Core::IO::MemoryBodyStream memory_body( |
243 | 0 | reinterpret_cast<const uint8_t*>(stream.data()), stream.size()); |
244 | | // The blockId must be base64 encoded |
245 | 0 | client_bvar::ScopedLatency scoped_latency(client_bvar::s3_multi_part_upload_latency); |
246 | 0 | client.StageBlock(block_id, memory_body); |
247 | 0 | } catch (Azure::Core::RequestFailedException& e) { |
248 | 0 | record_object_request_failed(static_cast<int>(e.StatusCode)); |
249 | 0 | auto tls_debug_suffix = build_azure_tls_debug_suffix( |
250 | 0 | fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context); |
251 | 0 | auto msg = fmt::format( |
252 | 0 | "Azure request failed because {}, error msg {}, http code {}, path msg {}{}", |
253 | 0 | e.what(), e.Message, static_cast<int>(e.StatusCode), |
254 | 0 | wrap_object_storage_path_msg(opts), tls_debug_suffix); |
255 | 0 | LOG(WARNING) << msg; |
256 | | // clang-format off |
257 | 0 | return { |
258 | 0 | .resp = { |
259 | 0 | .status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
260 | 0 | std::move(msg)), |
261 | 0 | .http_code = static_cast<int>(e.StatusCode), |
262 | 0 | .request_id = std::move(e.RequestId), |
263 | 0 | }, |
264 | 0 | }; |
265 | | // clang-format on |
266 | 0 | } catch (const std::exception& e) { |
267 | 0 | return {.resp = make_azure_std_exception_response(e, opts, _config.tls_debug_context)}; |
268 | 0 | } |
269 | 0 | return ObjStorageUploadResult {.resp = ObjStorageResponse::OK(), .etag = std::move(block_id)}; |
270 | 0 | } |
271 | | |
272 | | ObjStorageResponse AzureObjStorageClient::complete_multipart_upload( |
273 | | const ObjStoragePath& opts, const std::string& upload_id, |
274 | 0 | const std::vector<ObjStorageCompletedPart>& completed_parts) { |
275 | 0 | DCHECK(!upload_id.empty()); |
276 | 0 | auto client = _client->GetBlockBlobClient(opts.key); |
277 | 0 | std::vector<std::string> string_block_ids; |
278 | 0 | std::ranges::transform(completed_parts, std::back_inserter(string_block_ids), |
279 | 0 | [&upload_id](const ObjStorageCompletedPart& i) { |
280 | 0 | return azure_multipart_block_id(upload_id, i.part_num); |
281 | 0 | }); |
282 | 0 | return do_azure_client_call( |
283 | 0 | [&]() { |
284 | 0 | client_bvar::ScopedLatency scoped_latency( |
285 | 0 | client_bvar::s3_multi_part_upload_latency); |
286 | 0 | client.CommitBlockList(string_block_ids); |
287 | 0 | }, |
288 | 0 | opts, _config.tls_debug_context); |
289 | 0 | } |
290 | | |
291 | 0 | ObjStorageHeadResult AzureObjStorageClient::head_object(const ObjStoragePath& opts) { |
292 | 0 | try { |
293 | 0 | Models::BlobProperties properties = [&]() { |
294 | 0 | client_bvar::ScopedLatency scoped_latency(client_bvar::s3_head_latency); |
295 | 0 | return _client->GetBlockBlobClient(opts.key).GetProperties().Value; |
296 | 0 | }(); |
297 | 0 | return {.resp = ObjStorageResponse::OK(), .file_size = properties.BlobSize}; |
298 | 0 | } catch (Azure::Core::RequestFailedException& e) { |
299 | 0 | if (e.StatusCode == Azure::Core::Http::HttpStatusCode::NotFound) { |
300 | 0 | return ObjStorageHeadResult { |
301 | 0 | .resp = {.status = obj_storage_status_from_http_code( |
302 | 0 | static_cast<int>(e.StatusCode), ""), |
303 | 0 | .http_code = static_cast<int>(e.StatusCode), |
304 | 0 | .request_id = std::move(e.RequestId)}, |
305 | 0 | }; |
306 | 0 | } |
307 | 0 | record_object_request_failed(static_cast<int>(e.StatusCode)); |
308 | 0 | auto tls_debug_suffix = build_azure_tls_debug_suffix( |
309 | 0 | fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context); |
310 | 0 | auto msg = fmt::format( |
311 | 0 | "Azure request failed because {}, error msg {}, http code {}, path msg {}{}", |
312 | 0 | e.what(), e.Message, static_cast<int>(e.StatusCode), |
313 | 0 | wrap_object_storage_path_msg(opts), tls_debug_suffix); |
314 | 0 | LOG(WARNING) << msg << ", request_id=" << e.RequestId; |
315 | 0 | return ObjStorageHeadResult { |
316 | 0 | .resp = {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
317 | 0 | std::move(msg)), |
318 | 0 | .http_code = static_cast<int>(e.StatusCode), |
319 | 0 | .request_id = std::move(e.RequestId)}, |
320 | 0 | }; |
321 | 0 | } catch (const std::exception& e) { |
322 | 0 | return {.resp = make_azure_std_exception_response(e, opts, _config.tls_debug_context)}; |
323 | 0 | } |
324 | 0 | } |
325 | | |
326 | | ObjStorageResponse AzureObjStorageClient::get_object(const ObjStoragePath& opts, void* buffer, |
327 | | size_t offset, size_t bytes_read, |
328 | 0 | size_t* size_return) { |
329 | 0 | auto client = _client->GetBlockBlobClient(opts.key); |
330 | 0 | auto response = do_azure_client_call( |
331 | 0 | [&]() { |
332 | 0 | DownloadBlobToOptions download_opts; |
333 | 0 | Azure::Core::Http::HttpRange range {.Offset = static_cast<int64_t>(offset), |
334 | 0 | .Length = bytes_read}; |
335 | 0 | download_opts.Range = range; |
336 | 0 | client_bvar::ScopedLatency scoped_latency(client_bvar::s3_get_latency); |
337 | 0 | auto resp = client.DownloadTo(reinterpret_cast<uint8_t*>(buffer), bytes_read, |
338 | 0 | download_opts); |
339 | 0 | *size_return = resp.Value.ContentRange.Length.Value(); |
340 | 0 | }, |
341 | 0 | opts, _config.tls_debug_context); |
342 | 0 | if (!response.ok() || *size_return == bytes_read) { |
343 | 0 | return response; |
344 | 0 | } |
345 | 0 | return { |
346 | 0 | .status = {ObjStorageStatus::INTERNAL_ERROR, |
347 | 0 | fmt::format("incomplete read from {}, expect {}, got {}", |
348 | 0 | wrap_object_storage_path_msg(opts), bytes_read, *size_return)}, |
349 | 0 | .http_code = response.http_code, |
350 | 0 | .request_id = std::move(response.request_id), |
351 | 0 | }; |
352 | 0 | } |
353 | | |
354 | | ObjStorageListPageResult AzureObjStorageClient::list_objects_page( |
355 | 0 | const ObjStoragePath& opts, std::string_view continuation_token) { |
356 | 0 | const auto& prefix = opts.prefix.empty() ? opts.key : opts.prefix; |
357 | 0 | ListBlobsOptions request; |
358 | 0 | request.Prefix = prefix; |
359 | 0 | request.PageSizeHint = static_cast<int32_t>(capabilities().max_list_page); |
360 | 0 | if (!continuation_token.empty()) { |
361 | 0 | request.ContinuationToken = std::string(continuation_token); |
362 | 0 | } |
363 | 0 | TEST_SYNC_POINT_CALLBACK("AzureObjStorageClient::list_objects", &request); |
364 | |
|
365 | 0 | try { |
366 | 0 | auto response = [&]() { |
367 | 0 | client_bvar::ScopedLatency scoped_latency(client_bvar::s3_list_latency); |
368 | 0 | return _client->ListBlobs(request); |
369 | 0 | }(); |
370 | 0 | const bool has_more = response.NextPageToken.HasValue(); |
371 | 0 | auto next_token = has_more ? response.NextPageToken.Value() : std::string {}; |
372 | 0 | if (has_more && next_token.empty()) { |
373 | 0 | return { |
374 | 0 | .resp = {.status = {ObjStorageStatus::INTERNAL_ERROR, |
375 | 0 | "Azure list response has an empty continuation token"}, |
376 | 0 | .http_code = 0}, |
377 | 0 | }; |
378 | 0 | } |
379 | 0 | ObjStorageListPageResult page {.resp = ObjStorageResponse::OK(), |
380 | 0 | .continuation_token = std::move(next_token), |
381 | 0 | .has_more = has_more}; |
382 | 0 | page.objects.reserve(response.Blobs.size()); |
383 | 0 | for (auto&& item : response.Blobs) { |
384 | 0 | DCHECK(item.Name.starts_with(*request.Prefix)) << item.Name << ' ' << *request.Prefix; |
385 | 0 | page.objects.emplace_back(ObjectMeta { |
386 | 0 | .key = std::move(item.Name), |
387 | 0 | .size = item.BlobSize, |
388 | | // `Azure::DateTime` adds the offset of `SystemClockEpoch` to the given Unix timestamp, |
389 | | // so here we need to subtract this offset to obtain the Unix timestamp of the mtime. |
390 | | // https://github.com/Azure/azure-sdk-for-cpp/blob/azure-core_1.12.0/sdk/core/azure-core/inc/azure/core/datetime.hpp#L129 |
391 | 0 | .mtime_s = duration_cast<std::chrono::seconds>(item.Details.LastModified - |
392 | 0 | SystemClockEpoch) |
393 | 0 | .count()}); |
394 | 0 | } |
395 | 0 | return page; |
396 | 0 | } catch (Azure::Core::RequestFailedException& e) { |
397 | 0 | record_object_request_failed(static_cast<int>(e.StatusCode)); |
398 | 0 | auto tls_debug_suffix = build_azure_tls_debug_suffix( |
399 | 0 | fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context); |
400 | 0 | LOG(WARNING) << fmt::format("Azure request failed because {}, url: {}, prefix: {}{}", |
401 | 0 | e.what(), _client->GetUrl(), request.Prefix.Value(), |
402 | 0 | tls_debug_suffix); |
403 | 0 | return { |
404 | 0 | .resp = {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
405 | 0 | e.Message + tls_debug_suffix), |
406 | 0 | .http_code = static_cast<int>(e.StatusCode), |
407 | 0 | .request_id = std::move(e.RequestId)}, |
408 | 0 | }; |
409 | 0 | } catch (std::exception& e) { |
410 | 0 | LOG(WARNING) << fmt::format("Azure request failed because {}, url: {}, prefix: {}", |
411 | 0 | e.what(), _client->GetUrl(), request.Prefix.Value()); |
412 | 0 | return { |
413 | 0 | .resp = {.status = {ObjStorageStatus::INTERNAL_ERROR, e.what()}, |
414 | 0 | .http_code = 0, |
415 | 0 | .request_id = ""}, |
416 | 0 | }; |
417 | 0 | } |
418 | 0 | } |
419 | | |
420 | | // As Azure's doc said, the batch size is 256 |
421 | | // You can find out the num in https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch?tabs=microsoft-entra-id |
422 | | // > Each batch request supports a maximum of 256 subrequests. |
423 | | ObjStorageResponse AzureObjStorageClient::delete_objects(const ObjStoragePath& opts, |
424 | 0 | std::vector<std::string> objs) { |
425 | | // TODO(ByteYue) : use range to adate this code when compiler is ready |
426 | | // auto chunkedView = objs | std::views::chunk(BlobBatchMaxOperations); |
427 | 0 | auto begin = std::begin(objs); |
428 | 0 | auto end = std::end(objs); |
429 | |
|
430 | 0 | while (begin != end) { |
431 | 0 | auto chunk_end = begin; |
432 | 0 | size_t batch_size = BlobBatchMaxOperations; |
433 | 0 | TEST_SYNC_POINT_CALLBACK("AzureObjClient::delete_objects", &batch_size); |
434 | 0 | TEST_SYNC_POINT_CALLBACK("AzureObjStorageClient::delete_objects", &batch_size); |
435 | 0 | batch_size = std::max<size_t>(1, batch_size); |
436 | 0 | std::advance(chunk_end, |
437 | 0 | std::min(batch_size, static_cast<size_t>(std::distance(begin, end)))); |
438 | |
|
439 | 0 | auto deleter = AzureBatchDeleter(_client.get(), opts, *begin, _config.tls_debug_context); |
440 | 0 | std::ranges::for_each(std::ranges::subrange(begin, chunk_end), |
441 | 0 | [&](const std::string& obj) { deleter.delete_blob(obj); }); |
442 | 0 | begin = chunk_end; |
443 | 0 | if (auto resp = deleter.execute(); resp.status.code != ObjStorageStatus::OK) { |
444 | 0 | return resp; |
445 | 0 | } |
446 | 0 | } |
447 | 0 | return ObjStorageResponse::OK(); |
448 | 0 | } |
449 | | |
450 | 0 | ObjStorageResponse AzureObjStorageClient::delete_object(const ObjStoragePath& opts) { |
451 | 0 | try { |
452 | 0 | auto resp = [&]() { |
453 | 0 | client_bvar::ScopedLatency scoped_latency(client_bvar::s3_delete_object_latency); |
454 | 0 | return _client->DeleteBlob(opts.key); |
455 | 0 | }(); |
456 | 0 | if (!resp.Value.Deleted) { |
457 | 0 | return { |
458 | 0 | .status = ObjStorageStatus {ObjStorageStatus::IO_ERROR, |
459 | 0 | "Delete azure blob failed"}, |
460 | 0 | .http_code = 0, |
461 | 0 | .request_id = "", |
462 | 0 | }; |
463 | 0 | } |
464 | 0 | return ObjStorageResponse::OK(); |
465 | 0 | } catch (Azure::Core::RequestFailedException& e) { |
466 | 0 | if (e.StatusCode == Azure::Core::Http::HttpStatusCode::NotFound && |
467 | 0 | e.ErrorCode == BlobNotFound) { |
468 | 0 | return ObjStorageResponse::OK(); |
469 | 0 | } |
470 | 0 | record_object_request_failed(static_cast<int>(e.StatusCode)); |
471 | 0 | auto tls_debug_suffix = build_azure_tls_debug_suffix( |
472 | 0 | fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context); |
473 | 0 | auto msg = fmt::format( |
474 | 0 | "Azure request failed because {}, error msg {}, http code {}, path msg {}{}", |
475 | 0 | e.what(), e.Message, static_cast<int>(e.StatusCode), |
476 | 0 | wrap_object_storage_path_msg(opts), tls_debug_suffix); |
477 | 0 | LOG(WARNING) << msg; |
478 | 0 | return { |
479 | 0 | .status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
480 | 0 | std::move(msg)), |
481 | 0 | .http_code = static_cast<int>(e.StatusCode), |
482 | 0 | .request_id = std::move(e.RequestId), |
483 | 0 | }; |
484 | 0 | } catch (std::exception& e) { |
485 | 0 | auto msg = fmt::format("Azure request failed because {}, path msg {}{}", e.what(), |
486 | 0 | wrap_object_storage_path_msg(opts), |
487 | 0 | build_azure_tls_debug_suffix(e.what(), _config.tls_debug_context)); |
488 | 0 | LOG(WARNING) << msg; |
489 | 0 | return { |
490 | 0 | .status = ObjStorageStatus {ObjStorageStatus::INTERNAL_ERROR, std::move(msg)}, |
491 | 0 | .http_code = 0, |
492 | 0 | .request_id = "", |
493 | 0 | }; |
494 | 0 | } |
495 | 0 | } |
496 | | |
497 | | std::string AzureObjStorageClient::generate_presigned_url(const ObjStoragePath& opts, |
498 | 0 | int64_t expiration_secs) { |
499 | 0 | Azure::Storage::Sas::BlobSasBuilder sas_builder; |
500 | 0 | sas_builder.ExpiresOn = |
501 | 0 | std::chrono::system_clock::now() + std::chrono::seconds(expiration_secs); |
502 | 0 | sas_builder.BlobContainerName = opts.bucket; |
503 | 0 | sas_builder.BlobName = opts.key; |
504 | 0 | sas_builder.Resource = Azure::Storage::Sas::BlobSasResource::Blob; |
505 | 0 | sas_builder.Protocol = Azure::Storage::Sas::SasProtocol::HttpsOnly; |
506 | 0 | sas_builder.SetPermissions(Azure::Storage::Sas::BlobSasPermissions::Read); |
507 | |
|
508 | 0 | auto credential = _credential; |
509 | 0 | if (credential == nullptr) { |
510 | 0 | credential = std::make_shared<Azure::Storage::StorageSharedKeyCredential>(_config.ak, |
511 | 0 | _config.sk); |
512 | 0 | } |
513 | 0 | std::string sasToken = sas_builder.GenerateSasToken(*credential); |
514 | |
|
515 | 0 | auto sasURL = |
516 | 0 | fmt::format(SAS_TOKEN_URL_TEMPLATE, _config.endpoint, opts.bucket, opts.key, sasToken); |
517 | 0 | if (sasURL.find("://") == std::string::npos) { |
518 | 0 | sasURL = "https://" + sasURL; |
519 | 0 | } |
520 | 0 | return sasURL; |
521 | 0 | } |
522 | | |
523 | | ObjStorageResponse AzureObjStorageClient::get_lifecycle(const std::string& /*bucket*/, |
524 | 0 | int64_t* expiration_days) { |
525 | | // TODO(plat1ko) |
526 | 0 | *expiration_days = INT64_MAX; |
527 | 0 | return ObjStorageResponse::OK(); |
528 | 0 | } |
529 | | |
530 | 0 | ObjStorageResponse AzureObjStorageClient::check_versioning(const std::string& /*bucket*/) { |
531 | | // TODO(plat1ko) |
532 | 0 | return ObjStorageResponse::OK(); |
533 | 0 | } |
534 | | |
535 | | ObjStorageResponse AzureObjStorageClient::abort_multipart_upload(const ObjStoragePath& opts, |
536 | 0 | const std::string& upload_id) { |
537 | | // delete uncommitted blobs |
538 | | // https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob?tabs=microsoft-entra-id#remarks |
539 | 0 | return delete_object(opts); |
540 | 0 | } |
541 | | } // namespace doris |