Coverage Report

Created: 2026-08-06 19:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
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