be/src/io/fs/s3_file_writer.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 "io/fs/s3_file_writer.h" |
19 | | |
20 | | #include <aws/s3/model/CompletedPart.h> |
21 | | #include <bvar/recorder.h> |
22 | | #include <bvar/reducer.h> |
23 | | #include <bvar/window.h> |
24 | | #include <fmt/core.h> |
25 | | #include <glog/logging.h> |
26 | | |
27 | | #include <sstream> |
28 | | #include <tuple> |
29 | | #include <utility> |
30 | | |
31 | | #include "common/config.h" |
32 | | #include "common/status.h" |
33 | | #include "cpp/sync_point.h" |
34 | | #include "io/cache/block_file_cache.h" |
35 | | #include "io/cache/block_file_cache_factory.h" |
36 | | #include "io/cache/file_block.h" |
37 | | #include "io/cache/file_cache_common.h" |
38 | | #include "io/fs/file_writer.h" |
39 | | #include "io/fs/path.h" |
40 | | #include "io/fs/s3_file_bufferpool.h" |
41 | | #include "io/fs/s3_file_system.h" |
42 | | #include "io/fs/s3_obj_storage_client.h" |
43 | | #include "runtime/exec_env.h" |
44 | | #include "util/debug_points.h" |
45 | | #include "util/s3_util.h" |
46 | | #include "util/stopwatch.hpp" |
47 | | |
48 | | namespace doris::io { |
49 | | |
50 | | bvar::Adder<uint64_t> s3_file_writer_total("s3_file_writer_total_num"); |
51 | | bvar::Adder<uint64_t> s3_bytes_written_total("s3_file_writer_bytes_written"); |
52 | | bvar::Adder<uint64_t> s3_file_created_total("s3_file_writer_file_created"); |
53 | | bvar::Adder<uint64_t> s3_file_being_written("s3_file_writer_file_being_written"); |
54 | | bvar::Adder<uint64_t> s3_file_writer_async_close_queuing("s3_file_writer_async_close_queuing"); |
55 | | bvar::Adder<uint64_t> s3_file_writer_async_close_processing( |
56 | | "s3_file_writer_async_close_processing"); |
57 | | bvar::IntRecorder s3_file_writer_first_append_to_close_ms_recorder; |
58 | | bvar::Window<bvar::IntRecorder> s3_file_writer_first_append_to_close_ms_window( |
59 | | "s3_file_writer_first_append_to_close_ms", |
60 | | &s3_file_writer_first_append_to_close_ms_recorder, /*window_size=*/10); |
61 | | |
62 | | S3FileWriter::S3FileWriter(std::shared_ptr<ObjClientHolder> client, std::string bucket, |
63 | | std::string key, const FileWriterOptions* opts) |
64 | 83.9k | : _obj_storage_path_opts({.path = fmt::format("s3://{}/{}", bucket, key), |
65 | 83.9k | .bucket = std::move(bucket), |
66 | 83.9k | .key = std::move(key)}), |
67 | 83.9k | _used_by_s3_committer(opts ? opts->used_by_s3_committer : false), |
68 | 83.9k | _obj_client(std::move(client)) { |
69 | 83.9k | s3_file_writer_total << 1; |
70 | 83.9k | s3_file_being_written << 1; |
71 | 83.9k | Aws::Http::SetCompliantRfc3986Encoding(true); |
72 | | |
73 | 83.9k | init_cache_builder(opts, _obj_storage_path_opts.path); |
74 | 83.9k | } |
75 | | |
76 | 77.9k | S3FileWriter::~S3FileWriter() { |
77 | 77.9k | if (_async_close_pack != nullptr) { |
78 | | // For thread safety |
79 | 0 | std::ignore = _async_close_pack->future.get(); |
80 | 0 | _async_close_pack = nullptr; |
81 | 77.9k | } else if (state() == State::OPENED) { |
82 | 63.4k | WARN_IF_ERROR(abort(), "failed to abort unfinished S3 writer"); |
83 | 63.4k | } else { |
84 | | // Consider one situation where the file writer is destructed after it submit at least one async task |
85 | | // without calling close(), then there exists one occasion where the async task is executed right after |
86 | | // the correspoding S3 file writer is already destructed |
87 | 14.5k | _wait_until_finish(fmt::format("wait s3 file {} upload to be finished", |
88 | 14.5k | _obj_storage_path_opts.path.native())); |
89 | 14.5k | } |
90 | 77.9k | if (state() == State::OPENED && !_failed) { |
91 | 0 | s3_bytes_written_total << _bytes_appended; |
92 | 0 | } |
93 | 77.9k | s3_file_being_written << -1; |
94 | 77.9k | } |
95 | | |
96 | 63.4k | Status S3FileWriter::abort() { |
97 | 63.4k | if (state() == State::CLOSED) { |
98 | 0 | return Status::OK(); |
99 | 0 | } |
100 | 63.4k | if (state() == State::ASYNC_CLOSING) { |
101 | 0 | return Status::InternalError("cannot abort an asynchronously closing S3 writer"); |
102 | 0 | } |
103 | 63.4k | RETURN_IF_ERROR(_abort_impl()); |
104 | 63.4k | _state = State::CLOSED; |
105 | 63.4k | return Status::OK(); |
106 | 63.4k | } |
107 | | |
108 | 3 | std::function<void()> S3FileWriter::failed_report_cleanup() const { |
109 | 3 | auto client_holder = _obj_client; |
110 | 3 | auto path_opts = _obj_storage_path_opts; |
111 | 3 | return [client_holder = std::move(client_holder), path_opts = std::move(path_opts)] { |
112 | | // The writer is already CLOSED, but ownership was not transferred; bypass abort()'s |
113 | | // state guard while retaining the provider client and the exact upload identity. |
114 | 3 | const auto& client = client_holder->get(); |
115 | 3 | if (client == nullptr) { |
116 | 0 | LOG(WARNING) << "failed to abort a rejected external-file report: invalid object " |
117 | 0 | "storage client"; |
118 | 0 | return; |
119 | 0 | } |
120 | 3 | auto response = client->abort_multipart_upload(path_opts); |
121 | 3 | if (response.status.code != ErrorCode::OK) { |
122 | 0 | LOG(WARNING) << "failed to abort a rejected external-file report for " |
123 | 0 | << path_opts.path.native() << ": " << response.status.msg; |
124 | 0 | } |
125 | 3 | }; |
126 | 3 | } |
127 | | |
128 | 63.3k | Status S3FileWriter::_abort_impl() { |
129 | 63.3k | _wait_until_finish( |
130 | 63.3k | fmt::format("wait s3 file {} before abort", _obj_storage_path_opts.path.native())); |
131 | 63.3k | _pending_buf.reset(); |
132 | 63.3k | if (_multipart_upload_started) { |
133 | 8 | const auto& client = _obj_client->get(); |
134 | 8 | if (client == nullptr) { |
135 | 0 | return Status::InternalError("invalid obj storage client"); |
136 | 0 | } |
137 | 8 | auto response = client->abort_multipart_upload(_obj_storage_path_opts); |
138 | 8 | if (response.status.code != ErrorCode::OK) { |
139 | 0 | return {response.status.code, std::move(response.status.msg)}; |
140 | 0 | } |
141 | 8 | } |
142 | | // Once abort returns, no destructor or retry may complete the abandoned upload. |
143 | 63.3k | return Status::OK(); |
144 | 63.3k | } |
145 | | |
146 | 109 | Status S3FileWriter::_create_multi_upload_request() { |
147 | 109 | LOG(INFO) << "create_multi_upload_request " << _obj_storage_path_opts.path.native(); |
148 | 109 | const auto& client = _obj_client->get(); |
149 | 109 | if (nullptr == client) { |
150 | 0 | return Status::InternalError<false>("invalid obj storage client"); |
151 | 0 | } |
152 | 109 | auto resp = client->create_multipart_upload(_obj_storage_path_opts); |
153 | 109 | if (resp.resp.status.code == ErrorCode::OK) { |
154 | | // Some providers identify staged uploads by block IDs instead of a server-issued upload ID. |
155 | 108 | _multipart_upload_started = true; |
156 | 108 | _obj_storage_path_opts.upload_id = resp.upload_id; |
157 | 108 | } |
158 | 109 | return {resp.resp.status.code, std::move(resp.resp.status.msg)}; |
159 | 109 | } |
160 | | |
161 | 100k | void S3FileWriter::_wait_until_finish(std::string_view task_name) { |
162 | 100k | auto timeout_duration = config::s3_file_writer_log_interval_second; |
163 | 100k | auto msg = fmt::format( |
164 | 100k | "{} multipart upload already takes {} seconds, bucket={}, key={}, upload_id={}", |
165 | 100k | task_name, timeout_duration, _obj_storage_path_opts.bucket, |
166 | 100k | _obj_storage_path_opts.path.native(), |
167 | 100k | _obj_storage_path_opts.upload_id.has_value() ? *_obj_storage_path_opts.upload_id : ""); |
168 | 100k | timespec current_time; |
169 | | // We don't need high accuracy here, so we use time(nullptr) |
170 | | // since it's the fastest way to get current time(second) |
171 | 100k | auto current_time_second = time(nullptr); |
172 | 100k | current_time.tv_sec = current_time_second + timeout_duration; |
173 | 100k | current_time.tv_nsec = 0; |
174 | | // bthread::countdown_event::timed_wait() should use absolute time |
175 | 100k | while (0 != _countdown_event.timed_wait(current_time)) { |
176 | 12 | current_time.tv_sec += timeout_duration; |
177 | 12 | LOG(WARNING) << msg; |
178 | 12 | } |
179 | 100k | } |
180 | | |
181 | 27.0k | Status S3FileWriter::close(bool non_block) { |
182 | 27.0k | if (state() == State::CLOSED) { |
183 | 0 | return Status::InternalError("S3FileWriter already closed, file path {}, file key {}", |
184 | 0 | _obj_storage_path_opts.path.native(), |
185 | 0 | _obj_storage_path_opts.key); |
186 | 0 | } |
187 | 27.0k | if (state() == State::ASYNC_CLOSING) { |
188 | 5.05k | if (non_block) { |
189 | 0 | return Status::InternalError("Don't submit async close multi times"); |
190 | 0 | } |
191 | 5.05k | CHECK(_async_close_pack != nullptr); |
192 | 5.05k | _st = _async_close_pack->future.get(); |
193 | 5.05k | _async_close_pack = nullptr; |
194 | | // We should wait for all the pre async task to be finished |
195 | 5.05k | _state = State::CLOSED; |
196 | | // The next time we call close() with no matter non_block true or false, it would always return the |
197 | | // '_st' value because this writer is already closed. |
198 | 5.05k | if (!non_block && _st.ok()) { |
199 | 5.04k | _record_close_latency(); |
200 | 5.04k | } |
201 | 5.05k | return _st; |
202 | 5.05k | } |
203 | 21.9k | if (non_block) { |
204 | 16.5k | _state = State::ASYNC_CLOSING; |
205 | 16.5k | _async_close_pack = std::make_unique<AsyncCloseStatusPack>(); |
206 | 16.5k | _async_close_pack->future = _async_close_pack->promise.get_future(); |
207 | 16.5k | s3_file_writer_async_close_queuing << 1; |
208 | 16.5k | Status submit_status = Status::OK(); |
209 | 16.5k | DBUG_EXECUTE_IF("S3FileWriter.close.submit_async_close.inject_error", { |
210 | 16.5k | submit_status = Status::IOError("S3FileWriter.close.submit_async_close.inject_error"); |
211 | 16.5k | }); |
212 | 16.6k | if (submit_status.ok()) { |
213 | 16.6k | submit_status = |
214 | 16.6k | ExecEnv::GetInstance()->non_block_close_thread_pool()->submit_func([&]() { |
215 | 16.6k | s3_file_writer_async_close_queuing << -1; |
216 | 16.6k | s3_file_writer_async_close_processing << 1; |
217 | 16.6k | _st = _close_impl(); |
218 | 16.6k | if (!_st.ok()) { |
219 | | // A failed completion must not leave server-side multipart state behind. |
220 | 5 | WARN_IF_ERROR(_abort_impl(), "failed to abort incomplete S3 upload"); |
221 | 5 | } |
222 | 16.6k | _async_close_pack->promise.set_value(_st); |
223 | 16.6k | s3_file_writer_async_close_processing << -1; |
224 | 16.6k | }); |
225 | 16.6k | } |
226 | 16.5k | if (!submit_status.ok()) { |
227 | 0 | s3_file_writer_async_close_queuing << -1; |
228 | 0 | LOG(WARNING) << "failed to submit async close for " |
229 | 0 | << _obj_storage_path_opts.path.native() |
230 | 0 | << ", fallback to sync close, status=" << submit_status; |
231 | 0 | _st = _close_impl(); |
232 | 0 | if (!_st.ok()) { |
233 | 0 | WARN_IF_ERROR(_abort_impl(), "failed to abort incomplete S3 upload"); |
234 | 0 | } |
235 | 0 | _async_close_pack->promise.set_value(_st); |
236 | 0 | return _st; |
237 | 0 | } |
238 | 16.5k | return Status::OK(); |
239 | 16.5k | } |
240 | 5.38k | _st = _close_impl(); |
241 | 5.38k | if (!_st.ok()) { |
242 | 19 | WARN_IF_ERROR(_abort_impl(), "failed to abort incomplete S3 upload"); |
243 | 19 | } |
244 | 5.38k | _state = State::CLOSED; |
245 | 5.38k | if (!non_block && _st.ok()) { |
246 | 5.36k | _record_close_latency(); |
247 | 5.36k | } |
248 | 5.38k | return _st; |
249 | 21.9k | } |
250 | | |
251 | 21.9k | void S3FileWriter::_record_close_latency() { |
252 | 21.9k | if (_close_latency_recorded || !_first_append_timestamp.has_value()) { |
253 | 5 | return; |
254 | 5 | } |
255 | 21.9k | auto now = std::chrono::steady_clock::now(); |
256 | 21.9k | auto latency_ms = |
257 | 21.9k | std::chrono::duration_cast<std::chrono::milliseconds>(now - *_first_append_timestamp) |
258 | 21.9k | .count(); |
259 | 21.9k | s3_file_writer_first_append_to_close_ms_recorder << latency_ms; |
260 | 21.9k | if (auto* sampler = s3_file_writer_first_append_to_close_ms_recorder.get_sampler()) { |
261 | 21.9k | sampler->take_sample(); |
262 | 21.9k | } |
263 | 21.9k | _close_latency_recorded = true; |
264 | 21.9k | } |
265 | | |
266 | 39.0k | Status S3FileWriter::try_finish_close() { |
267 | 39.0k | if (state() == State::CLOSED) { |
268 | 0 | return _st; |
269 | 0 | } |
270 | 39.0k | if (state() != State::ASYNC_CLOSING) { |
271 | 0 | return Status::NotSupported("S3FileWriter is not async closing"); |
272 | 0 | } |
273 | 39.0k | CHECK(_async_close_pack != nullptr); |
274 | 39.0k | if (_async_close_pack->future.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { |
275 | 27.5k | return Status::NeedSendAgain("async close is not finished"); |
276 | 27.5k | } |
277 | 11.5k | _st = _async_close_pack->future.get(); |
278 | 11.5k | _async_close_pack = nullptr; |
279 | 11.5k | _state = State::CLOSED; |
280 | 11.5k | if (_st.ok()) { |
281 | 11.5k | _record_close_latency(); |
282 | 11.5k | } |
283 | 11.5k | return _st; |
284 | 39.0k | } |
285 | | |
286 | 22.3k | bool S3FileWriter::_complete_part_task_callback(Status s) { |
287 | 22.3k | bool ret = false; |
288 | 22.3k | if (!s.ok()) [[unlikely]] { |
289 | 20 | VLOG_NOTICE << "failed at key: " << _obj_storage_path_opts.key |
290 | 0 | << ", status: " << s.to_string(); |
291 | 20 | std::unique_lock<std::mutex> _lck {_completed_lock}; |
292 | 20 | _failed = true; |
293 | 20 | ret = true; |
294 | 20 | _st = std::move(s); |
295 | 20 | } |
296 | | // After the signal, there is a scenario where the previous invocation of _wait_until_finish |
297 | | // returns to the caller, and subsequently, the S3 file writer is destructed. |
298 | | // This means that accessing _failed afterwards would result in a heap use after free vulnerability. |
299 | 22.3k | _countdown_event.signal(); |
300 | 22.3k | return ret; |
301 | 22.3k | } |
302 | | |
303 | 22.3k | Status S3FileWriter::_build_upload_buffer() { |
304 | 22.3k | auto builder = FileBufferBuilder(); |
305 | 22.3k | builder.set_type(BufferType::UPLOAD) |
306 | 22.3k | .set_upload_callback([part_num = _cur_part_num, this](UploadFileBuffer& buf) { |
307 | 462 | _upload_one_part(part_num, buf); |
308 | 462 | }) |
309 | 22.3k | .set_file_offset(_bytes_appended) |
310 | 22.3k | .set_sync_after_complete_task([this](auto&& PH1) { |
311 | 22.3k | return _complete_part_task_callback(std::forward<decltype(PH1)>(PH1)); |
312 | 22.3k | }) |
313 | 22.7k | .set_is_cancelled([this]() { return _failed.load(); }); |
314 | 22.3k | if (_cache_builder != nullptr) { |
315 | | // We would load the data into file cache asynchronously which indicates |
316 | | // that this instance of S3FileWriter might have been destructed when we |
317 | | // try to do writing into file cache, so we make the lambda capture the variable |
318 | | // we need by value to extend their lifetime |
319 | 5.63k | int64_t id = get_tablet_id(_obj_storage_path_opts.path.native()).value_or(0); |
320 | 5.63k | builder.set_allocate_file_blocks_holder([builder = *_cache_builder, |
321 | 5.63k | offset = _bytes_appended, |
322 | 5.63k | tablet_id = id]() -> FileBlocksHolderPtr { |
323 | 5.63k | return builder.allocate_cache_holder(offset, config::s3_write_buffer_size, tablet_id); |
324 | 5.63k | }); |
325 | 5.63k | } |
326 | 22.3k | RETURN_IF_ERROR(builder.build(&_pending_buf)); |
327 | 22.3k | auto* buf = dynamic_cast<UploadFileBuffer*>(_pending_buf.get()); |
328 | 22.3k | DCHECK(buf != nullptr); |
329 | 22.3k | return Status::OK(); |
330 | 22.3k | } |
331 | | |
332 | 22.3k | Status S3FileWriter::_submit_upload_buffer(const std::shared_ptr<FileBuffer>& buf) { |
333 | 22.3k | _countdown_event.add_count(); |
334 | 22.3k | DBUG_EXECUTE_IF("S3FileWriter.submit_upload_buffer.inject_error", { |
335 | 22.3k | auto st = Status::IOError("S3FileWriter.submit_upload_buffer.inject_error"); |
336 | 22.3k | _complete_part_task_callback(st); |
337 | 22.3k | return st; |
338 | 22.3k | }); |
339 | 22.3k | auto st = FileBuffer::submit(buf); |
340 | 22.3k | if (!st.ok()) [[unlikely]] { |
341 | 0 | _complete_part_task_callback(st); |
342 | 0 | } |
343 | 22.3k | return st; |
344 | 22.3k | } |
345 | | |
346 | 21.9k | Status S3FileWriter::_close_impl() { |
347 | 18.4E | VLOG_DEBUG << "S3FileWriter::close, path: " << _obj_storage_path_opts.path.native(); |
348 | | |
349 | 21.9k | DBUG_EXECUTE_IF("S3FileWriter._close_impl.inject_error", { |
350 | 21.9k | if (_obj_storage_path_opts.key.ends_with(".dat")) { |
351 | 21.9k | return Status::IOError("S3FileWriter._close_impl.inject_error"); |
352 | 21.9k | } |
353 | 21.9k | }); |
354 | | |
355 | 21.9k | if (_cur_part_num == 1 && _pending_buf) { // data size is less than config::s3_write_buffer_size |
356 | 21.8k | RETURN_IF_ERROR(_set_upload_to_remote_less_than_buffer_size()); |
357 | 21.8k | } |
358 | | |
359 | 21.9k | if (_bytes_appended == 0) { |
360 | 7 | DCHECK_EQ(_cur_part_num, 1); |
361 | | // No data written, but need to create an empty file |
362 | 7 | RETURN_IF_ERROR(_build_upload_buffer()); |
363 | 7 | if (!_used_by_s3_committer) { |
364 | 7 | auto* pending_buf = dynamic_cast<UploadFileBuffer*>(_pending_buf.get()); |
365 | 7 | pending_buf->set_upload_to_remote([this](UploadFileBuffer& buf) { _put_object(buf); }); |
366 | 7 | } else { |
367 | 0 | RETURN_IF_ERROR(_create_multi_upload_request()); |
368 | 0 | } |
369 | 7 | } |
370 | | |
371 | 21.9k | if (_pending_buf != nullptr) { // there is remaining data in buffer need to be uploaded |
372 | 21.9k | auto st = _submit_upload_buffer(_pending_buf); |
373 | 21.9k | _pending_buf = nullptr; |
374 | 21.9k | if (!st.ok()) { |
375 | 0 | _wait_until_finish("pending buffer submit failed"); |
376 | 0 | return st; |
377 | 0 | } |
378 | 21.9k | } |
379 | | |
380 | 21.9k | RETURN_IF_ERROR(_complete()); |
381 | 21.9k | SYNC_POINT_RETURN_WITH_VALUE("s3_file_writer::close", Status()); |
382 | | |
383 | 21.9k | return Status::OK(); |
384 | 21.9k | } |
385 | | |
386 | 411k | Status S3FileWriter::appendv(const Slice* data, size_t data_cnt) { |
387 | 411k | if (state() != State::OPENED) [[unlikely]] { |
388 | 0 | return Status::InternalError("append to closed file: {}", |
389 | 0 | _obj_storage_path_opts.path.native()); |
390 | 0 | } |
391 | | |
392 | 411k | if (!_first_append_timestamp.has_value()) { |
393 | 20.9k | _first_append_timestamp = std::chrono::steady_clock::now(); |
394 | 20.9k | } |
395 | | |
396 | 411k | size_t buffer_size = config::s3_write_buffer_size; |
397 | 411k | TEST_SYNC_POINT_RETURN_WITH_VALUE("s3_file_writer::appenv", Status()); |
398 | 1.31M | for (size_t i = 0; i < data_cnt; i++) { |
399 | 906k | size_t data_size = data[i].get_size(); |
400 | 1.76M | for (size_t pos = 0, data_size_to_append = 0; pos < data_size; pos += data_size_to_append) { |
401 | 861k | if (_failed) { |
402 | 0 | return _st; |
403 | 0 | } |
404 | 861k | if (!_pending_buf) { |
405 | 20.9k | RETURN_IF_ERROR(_build_upload_buffer()); |
406 | 20.9k | } |
407 | | // we need to make sure all parts except the last one to be 5MB or more |
408 | | // and shouldn't be larger than buf |
409 | 861k | data_size_to_append = std::min(data_size - pos, _pending_buf->get_file_offset() + |
410 | 861k | buffer_size - _bytes_appended); |
411 | | |
412 | | // if the buffer has memory buf inside, the data would be written into memory first then S3 then file cache |
413 | | // it would be written to cache then S3 if the buffer doesn't have memory preserved |
414 | 861k | RETURN_IF_ERROR(_pending_buf->append_data( |
415 | 861k | Slice {data[i].get_data() + pos, data_size_to_append})); |
416 | 861k | TEST_SYNC_POINT_CALLBACK("s3_file_writer::appenv_1", &_pending_buf, _cur_part_num); |
417 | | |
418 | | // If this is the last part and the data size is less than s3_write_buffer_size, |
419 | | // the pending_buf will be handled by _close_impl() and _complete() |
420 | | // If this is the last part and the data size is equal to s3_write_buffer_size, |
421 | | // the pending_buf is handled here and submitted. it will be waited by _complete() |
422 | 861k | if (_pending_buf->get_size() == buffer_size) { |
423 | | // only create multiple upload request when the data size is |
424 | | // larger or equal to s3_write_buffer_size than one memory buffer |
425 | 81 | if (_cur_part_num == 1) { |
426 | 59 | RETURN_IF_ERROR(_create_multi_upload_request()); |
427 | 59 | } |
428 | 81 | _cur_part_num++; |
429 | 81 | auto st = _submit_upload_buffer(_pending_buf); |
430 | 81 | _pending_buf = nullptr; |
431 | 81 | RETURN_IF_ERROR(st); |
432 | 81 | } |
433 | 861k | _bytes_appended += data_size_to_append; |
434 | 861k | } |
435 | 906k | } |
436 | 411k | return Status::OK(); |
437 | 411k | } |
438 | | |
439 | 462 | void S3FileWriter::_upload_one_part(int part_num, UploadFileBuffer& buf) { |
440 | 462 | VLOG_DEBUG << "upload_one_part " << _obj_storage_path_opts.path.native() |
441 | 0 | << " part=" << part_num; |
442 | 462 | if (buf.is_cancelled()) { |
443 | 0 | LOG_INFO("file {} skip part {} because previous failure {}", |
444 | 0 | _obj_storage_path_opts.path.native(), part_num, _st); |
445 | 0 | return; |
446 | 0 | } |
447 | 462 | const auto& client = _obj_client->get(); |
448 | 462 | if (nullptr == client) { |
449 | 0 | LOG_WARNING("failed to upload part, key={}, part_num={} bacause of null obj client", |
450 | 0 | _obj_storage_path_opts.key, part_num); |
451 | 0 | buf.set_status(Status::InternalError<false>("invalid obj storage client")); |
452 | 0 | return; |
453 | 0 | } |
454 | 462 | auto resp = client->upload_part(_obj_storage_path_opts, buf.get_string_view_data(), part_num); |
455 | 462 | if (resp.resp.status.code != ErrorCode::OK) { |
456 | 1 | LOG_WARNING("failed to upload part, key={}, part_num={}, status={}", |
457 | 1 | _obj_storage_path_opts.key, part_num, resp.resp.status.msg); |
458 | 1 | buf.set_status(Status(resp.resp.status.code, std::move(resp.resp.status.msg))); |
459 | 1 | return; |
460 | 1 | } |
461 | 461 | s3_bytes_written_total << buf.get_size(); |
462 | | |
463 | 461 | ObjectCompleteMultiPart completed_part { |
464 | 461 | part_num, resp.etag.has_value() ? std::move(resp.etag.value()) : ""}; |
465 | | |
466 | 461 | std::unique_lock<std::mutex> lck {_completed_lock}; |
467 | 461 | _completed_parts.emplace_back(std::move(completed_part)); |
468 | 461 | } |
469 | | |
470 | | // if enabled check |
471 | | // 1. issue a head object request for existence check |
472 | | // 2. check the file size |
473 | | Status check_after_upload(ObjStorageClient* client, const ObjectStorageResponse& upload_res, |
474 | | const ObjectStoragePathOptions& path_opt, int64_t bytes_appended, |
475 | 21.9k | const std::string& put_or_comp) { |
476 | 21.9k | if (!config::enable_s3_object_check_after_upload) return Status::OK(); |
477 | | |
478 | 21.9k | auto head_res = client->head_object(path_opt); |
479 | | |
480 | | // clang-format off |
481 | 21.9k | auto err_msg = [&]() { |
482 | 0 | std::stringstream ss; |
483 | 0 | ss << "failed to check object after upload=" << put_or_comp |
484 | 0 | << " file_path=" << path_opt.path.native() |
485 | 0 | << fmt::format(" {}_err=", put_or_comp) << upload_res.status.msg |
486 | 0 | << fmt::format(" {}_code=", put_or_comp) << upload_res.status.code |
487 | 0 | << fmt::format(" {}_http_code=", put_or_comp) << upload_res.http_code |
488 | 0 | << fmt::format(" {}_request_id=", put_or_comp) << upload_res.request_id |
489 | 0 | << " head_err=" << head_res.resp.status.msg |
490 | 0 | << " head_code=" << head_res.resp.status.code |
491 | 0 | << " head_http_code=" << head_res.resp.http_code |
492 | 0 | << " head_request_id=" << head_res.resp.request_id; |
493 | 0 | return ss.str(); |
494 | 0 | }; |
495 | | // clang-format on |
496 | | |
497 | | // TODO(gavin): make it fail by injection |
498 | 21.9k | TEST_SYNC_POINT_CALLBACK("S3FileWriter::check_after_load", &head_res); |
499 | 21.9k | if (head_res.resp.status.code != ErrorCode::OK && head_res.resp.http_code != 200) { |
500 | 0 | LOG(WARNING) << "failed to issue head object after upload, " << err_msg(); |
501 | 0 | DCHECK(false) << "failed to issue head object after upload, " << err_msg(); |
502 | | // FIXME(gavin): we should retry if this HEAD fails? |
503 | 0 | return Status::IOError( |
504 | 0 | "failed to issue head object after upload, status_code={}, http_code={}, err={}", |
505 | 0 | head_res.resp.status.code, head_res.resp.http_code, head_res.resp.status.msg); |
506 | 0 | } |
507 | 21.9k | if (head_res.file_size != bytes_appended) { |
508 | 0 | LOG(WARNING) << "failed to check size after upload, expected_size=" << bytes_appended |
509 | 0 | << " actual_size=" << head_res.file_size << err_msg(); |
510 | 0 | DCHECK_EQ(bytes_appended, head_res.file_size) |
511 | 0 | << "failed to check size after upload," << err_msg(); |
512 | 0 | return Status::IOError( |
513 | 0 | "failed to check object size after upload, expected_size={} actual_size={}", |
514 | 0 | bytes_appended, head_res.file_size); |
515 | 0 | } |
516 | 21.9k | return Status::OK(); |
517 | 21.9k | } |
518 | | |
519 | 21.9k | Status S3FileWriter::_complete() { |
520 | 21.9k | const auto& client = _obj_client->get(); |
521 | 21.9k | if (nullptr == client) { |
522 | 0 | return Status::InternalError<false>("invalid obj storage client"); |
523 | 0 | } |
524 | 21.9k | if (_failed) { |
525 | 0 | _wait_until_finish("early quit"); |
526 | 0 | return _st; |
527 | 0 | } |
528 | | // When the part num is only one, it means the data is less than 5MB so we can just put it. |
529 | 21.9k | if (_cur_part_num == 1) { |
530 | 21.8k | _wait_until_finish("PutObject"); |
531 | 21.8k | return _st; |
532 | 21.8k | } |
533 | | // Wait multipart load and finish. |
534 | 105 | _wait_until_finish("Complete"); |
535 | 105 | TEST_SYNC_POINT_CALLBACK("S3FileWriter::_complete:1", |
536 | 105 | std::make_pair(&_failed, &_completed_parts)); |
537 | 105 | if (_used_by_s3_committer) { // S3 committer will complete multipart upload file on FE side. |
538 | 1 | s3_file_created_total << 1; // Assume that it will be created successfully |
539 | 1 | return Status::OK(); |
540 | 1 | } |
541 | | |
542 | | // check number of parts |
543 | 104 | int64_t expected_num_parts1 = (_bytes_appended / config::s3_write_buffer_size) + |
544 | 104 | !!(_bytes_appended % config::s3_write_buffer_size); |
545 | 104 | int64_t expected_num_parts2 = |
546 | 104 | (_bytes_appended % config::s3_write_buffer_size) ? _cur_part_num : _cur_part_num - 1; |
547 | 104 | DCHECK_EQ(expected_num_parts1, expected_num_parts2) |
548 | 0 | << " bytes_appended=" << _bytes_appended << " cur_part_num=" << _cur_part_num |
549 | 0 | << " s3_write_buffer_size=" << config::s3_write_buffer_size; |
550 | 104 | if (_failed || _completed_parts.size() != static_cast<size_t>(expected_num_parts1) || |
551 | 104 | expected_num_parts1 != expected_num_parts2) { |
552 | 3 | _st = Status::InternalError( |
553 | 3 | "failed to complete multipart upload, error status={} failed={} #complete_parts={} " |
554 | 3 | "#expected_parts={} " |
555 | 3 | "completed_parts_list={} file_path={} file_size={} has left buffer not uploaded={}", |
556 | 3 | _st, _failed, _completed_parts.size(), expected_num_parts1, _dump_completed_part(), |
557 | 3 | _obj_storage_path_opts.path.native(), _bytes_appended, _pending_buf != nullptr); |
558 | 3 | LOG(WARNING) << _st; |
559 | 3 | return _st; |
560 | 3 | } |
561 | | // make sure _completed_parts are ascending order |
562 | 101 | std::sort(_completed_parts.begin(), _completed_parts.end(), |
563 | 1.08k | [](auto& p1, auto& p2) { return p1.part_num < p2.part_num; }); |
564 | 101 | TEST_SYNC_POINT_CALLBACK("S3FileWriter::_complete:2", &_completed_parts); |
565 | 101 | LOG(INFO) << "complete_multipart_upload " << _obj_storage_path_opts.path.native() |
566 | 101 | << " size=" << _bytes_appended << " number_parts=" << _completed_parts.size() |
567 | 101 | << " s3_write_buffer_size=" << config::s3_write_buffer_size; |
568 | 101 | auto resp = client->complete_multipart_upload(_obj_storage_path_opts, _completed_parts); |
569 | 101 | if (resp.status.code != ErrorCode::OK) { |
570 | 2 | LOG_WARNING("failed to complete multipart upload, err={}, file_path={}", resp.status.msg, |
571 | 2 | _obj_storage_path_opts.path.native()); |
572 | 2 | return {resp.status.code, std::move(resp.status.msg)}; |
573 | 2 | } |
574 | | |
575 | 99 | RETURN_IF_ERROR(check_after_upload(client.get(), resp, _obj_storage_path_opts, _bytes_appended, |
576 | 99 | "complete_multipart")); |
577 | | |
578 | 99 | s3_file_created_total << 1; |
579 | 99 | return Status::OK(); |
580 | 99 | } |
581 | | |
582 | 21.8k | Status S3FileWriter::_set_upload_to_remote_less_than_buffer_size() { |
583 | 21.8k | auto* buf = dynamic_cast<UploadFileBuffer*>(_pending_buf.get()); |
584 | 21.8k | DCHECK(buf != nullptr); |
585 | 21.8k | if (_used_by_s3_committer) { |
586 | | // If used_by_s3_committer, we always use multi-parts uploading. |
587 | 0 | buf->set_upload_to_remote([part_num = _cur_part_num, this](UploadFileBuffer& buf) { |
588 | 0 | _upload_one_part(part_num, buf); |
589 | 0 | }); |
590 | 0 | DCHECK(_cur_part_num == 1); |
591 | 0 | RETURN_IF_ERROR(_create_multi_upload_request()); |
592 | 21.8k | } else { |
593 | | // if we only need to upload one file less than 5MB, we can just |
594 | | // call PutObject to reduce the network IO |
595 | 21.8k | buf->set_upload_to_remote([this](UploadFileBuffer& b) { _put_object(b); }); |
596 | 21.8k | } |
597 | 21.8k | return Status::OK(); |
598 | 21.8k | } |
599 | | |
600 | 20.8k | void S3FileWriter::_put_object(UploadFileBuffer& buf) { |
601 | 20.8k | MonotonicStopWatch timer; |
602 | 20.8k | timer.start(); |
603 | | |
604 | 20.8k | if (state() == State::CLOSED) { |
605 | 0 | DCHECK(state() != State::CLOSED) |
606 | 0 | << "state=" << (int)state() << " path=" << _obj_storage_path_opts.path.native(); |
607 | 0 | LOG_WARNING("failed to put object because file closed, file path {}", |
608 | 0 | _obj_storage_path_opts.path.native()); |
609 | 0 | buf.set_status(Status::InternalError<false>("try to put closed file")); |
610 | 0 | return; |
611 | 0 | } |
612 | 20.8k | const auto& client = _obj_client->get(); |
613 | 20.8k | if (nullptr == client) { |
614 | 0 | buf.set_status(Status::InternalError<false>("invalid obj storage client")); |
615 | 0 | return; |
616 | 0 | } |
617 | 20.8k | TEST_SYNC_POINT_RETURN_WITH_VOID("S3FileWriter::_put_object", this, &buf); |
618 | 20.8k | auto resp = client->put_object(_obj_storage_path_opts, buf.get_string_view_data()); |
619 | 20.8k | timer.stop(); |
620 | | |
621 | 20.8k | if (resp.status.code != ErrorCode::OK) { |
622 | 17 | LOG_WARNING("failed to put object, put object failed because {}, file path {}, time={}ms", |
623 | 17 | resp.status.msg, _obj_storage_path_opts.path.native(), |
624 | 17 | timer.elapsed_time_milliseconds()); |
625 | 17 | buf.set_status({resp.status.code, std::move(resp.status.msg)}); |
626 | 17 | return; |
627 | 17 | } |
628 | | |
629 | 20.8k | auto st = check_after_upload(client.get(), resp, _obj_storage_path_opts, _bytes_appended, |
630 | 20.8k | "put_object"); |
631 | 20.8k | if (!st.ok()) { |
632 | 0 | buf.set_status(st); |
633 | 0 | return; |
634 | 0 | } |
635 | | |
636 | 20.8k | LOG(INFO) << "put_object " << _obj_storage_path_opts.path.native() |
637 | 20.8k | << " size=" << _bytes_appended << " time=" << timer.elapsed_time_milliseconds() |
638 | 20.8k | << "ms"; |
639 | 20.8k | s3_file_created_total << 1; |
640 | 20.8k | s3_bytes_written_total << buf.get_size(); |
641 | 20.8k | } |
642 | | |
643 | 3 | std::string S3FileWriter::_dump_completed_part() const { |
644 | 3 | std::stringstream ss; |
645 | 3 | ss << "part_numbers:"; |
646 | 3 | for (const auto& part : _completed_parts) { |
647 | 2 | ss << " " << part.part_num; |
648 | 2 | } |
649 | 3 | return ss.str(); |
650 | 3 | } |
651 | | |
652 | | } // namespace doris::io |