Coverage Report

Created: 2026-10-28 10:08

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
be/src/io/fs/s3_file_system.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_system.h"
19
20
#include <fmt/format.h>
21
22
#include <cstddef>
23
24
#include "common/compiler_util.h" // IWYU pragma: keep
25
// IWYU pragma: no_include <bits/chrono.h>
26
#include <aws/core/utils/threading/Executor.h>
27
#include <aws/s3/S3Client.h>
28
29
#include <chrono> // IWYU pragma: keep
30
#include <filesystem>
31
#include <fstream> // IWYU pragma: keep
32
#include <future>
33
#include <memory>
34
35
#include "common/config.h"
36
#include "common/logging.h"
37
#include "common/status.h"
38
#include "cpp/obj-client/obj_storage_client.h"
39
#include "cpp/obj-client/s3_common.h"
40
#include "cpp/sync_point.h"
41
#include "io/fs/err_utils.h"
42
#include "io/fs/file_system.h"
43
#include "io/fs/file_writer.h"
44
#include "io/fs/local_file_system.h"
45
#include "io/fs/remote_file_system.h"
46
#include "io/fs/s3_file_reader.h"
47
#include "io/fs/s3_file_writer.h"
48
#include "runtime/exec_env.h"
49
#include "runtime/thread_context.h"
50
#include "util/s3_uri.h"
51
#include "util/s3_util.h"
52
53
namespace doris::io {
54
namespace {
55
constexpr std::string_view OSS_PRIVATE_ENDPOINT_SUFFIX = "-internal.aliyuncs.com";
56
constexpr int LEN_OF_OSS_PRIVATE_SUFFIX = 9; // length of "-internal"
57
58
#ifndef CHECK_S3_CLIENT
59
#define CHECK_S3_CLIENT(client)                                 \
60
76.9k
    if (!client) {                                              \
61
0
        return Status::InvalidArgument("init s3 client error"); \
62
0
    }
63
#endif
64
65
715k
Result<std::string> get_key(const Path& full_path) {
66
    // FIXME(plat1ko): Check bucket in full path and support relative path
67
715k
    S3URI uri(full_path.native());
68
715k
    RETURN_IF_ERROR_RESULT(uri.parse());
69
715k
    return uri.get_key();
70
715k
}
71
72
} // namespace
73
74
1.76k
ObjClientHolder::ObjClientHolder(S3ClientConf conf) : _conf(std::move(conf)) {}
75
76
1.74k
ObjClientHolder::~ObjClientHolder() = default;
77
78
1.69k
Status ObjClientHolder::init() {
79
1.69k
    _client = DORIS_TRY(S3ClientFactory::instance().create(_conf));
80
1.68k
    return Status::OK();
81
1.69k
}
82
83
612
Status ObjClientHolder::reset(const S3ClientConf& conf) {
84
    // Two credential updates must not publish in SDK-construction completion
85
    // order: a slow older update could otherwise replace the newer client.
86
612
    std::unique_lock reset_lock(_reset_mtx);
87
612
    S3ClientConf reset_conf;
88
612
    {
89
612
        std::shared_lock lock(_mtx);
90
612
        reset_conf = _conf;
91
612
        reset_conf.provider = conf.provider;
92
612
        reset_conf.ak = conf.ak;
93
612
        reset_conf.sk = conf.sk;
94
612
        reset_conf.token = conf.token;
95
612
        reset_conf.bucket = conf.bucket;
96
612
        reset_conf.connect_timeout_ms = conf.connect_timeout_ms;
97
612
        reset_conf.max_connections = conf.max_connections;
98
612
        reset_conf.request_timeout_ms = conf.request_timeout_ms;
99
612
        reset_conf.use_virtual_addressing = conf.use_virtual_addressing;
100
612
        reset_conf.is_internal_bucket = conf.is_internal_bucket;
101
102
612
        reset_conf.role_arn = conf.role_arn;
103
612
        reset_conf.external_id = conf.external_id;
104
612
        reset_conf.cred_provider_type = conf.cred_provider_type;
105
612
        reset_conf.credential = conf.credential;
106
107
        // Compare full-field equality of the merged conf, not get_hash(): the hash is
108
        // an XOR of crc32s and distinct configurations can collide, which would skip a
109
        // required client rebuild (e.g. a credential update).
110
612
        if (reset_conf == _conf) {
111
603
            return Status::OK(); // Same conf
112
603
        }
113
612
    }
114
115
9
    auto client = DORIS_TRY(S3ClientFactory::instance().create(reset_conf));
116
117
8
    LOG(WARNING) << "reset s3 client with new conf: " << conf.to_string();
118
119
8
    {
120
8
        std::lock_guard lock(_mtx);
121
        // client takes the old owner; its possibly expensive final release is
122
        // after both holder locks are released, never inside the publication lock.
123
8
        _client.swap(client);
124
8
        _conf = std::move(reset_conf);
125
8
    }
126
8
    reset_lock.unlock();
127
128
8
    return Status::OK();
129
9
}
130
131
Result<int64_t> ObjClientHolder::object_file_size(const std::string& bucket,
132
29.2k
                                                  const std::string& key) const {
133
29.2k
    auto client = get();
134
29.2k
    if (!client) {
135
0
        return ResultError(Status::InvalidArgument("init s3 client error"));
136
0
    }
137
138
29.2k
    auto resp = client->head_object({
139
29.2k
            .bucket = bucket,
140
29.2k
            .key = key,
141
29.2k
    });
142
143
29.2k
    if (resp.resp.status.code != ErrorCode::OK) {
144
3
        return ResultError(std::move(Status(resp.resp.status.code, std::move(resp.resp.status.msg))
145
3
                                             .append(fmt::format("failed to head s3 file {}",
146
3
                                                                 full_s3_path(bucket, key)))));
147
3
    }
148
149
29.2k
    return resp.file_size;
150
29.2k
}
151
152
699
std::string ObjClientHolder::full_s3_path(std::string_view bucket, std::string_view key) const {
153
699
    std::shared_lock lock(_mtx);
154
699
    return fmt::format("{}/{}/{}", _conf.endpoint, bucket, key);
155
699
}
156
157
696
std::string S3FileSystem::full_s3_path(std::string_view key) const {
158
696
    return _client->full_s3_path(_bucket, key);
159
696
}
160
161
52
Result<std::shared_ptr<S3FileSystem>> S3FileSystem::create(S3Conf s3_conf, std::string id) {
162
52
    std::shared_ptr<S3FileSystem> fs(new S3FileSystem(std::move(s3_conf), std::move(id)));
163
52
    RETURN_IF_ERROR_RESULT(fs->init());
164
44
    return fs;
165
52
}
166
167
S3FileSystem::S3FileSystem(S3Conf s3_conf, std::string id)
168
55
        : RemoteFileSystem(s3_conf.prefix, std::move(id), FileSystemType::S3),
169
55
          _bucket(std::move(s3_conf.bucket)),
170
55
          _prefix(std::move(s3_conf.prefix)),
171
55
          _client(std::make_shared<ObjClientHolder>(std::move(s3_conf.client_conf))) {
172
    // FIXME(plat1ko): Normalize prefix
173
    // remove the first and last '/'
174
55
    if (!_prefix.empty()) {
175
51
        size_t start = _prefix.find_first_not_of('/');
176
51
        if (start == std::string::npos) {
177
0
            _prefix = "";
178
51
        } else {
179
51
            size_t end = _prefix.find_last_not_of('/');
180
51
            if (start > 0 || end < _prefix.size() - 1) {
181
0
                _prefix = _prefix.substr(start, end - start + 1);
182
0
            }
183
51
        }
184
51
    }
185
55
}
186
187
52
Status S3FileSystem::init() {
188
52
    return _client->init();
189
52
}
190
191
42
S3FileSystem::~S3FileSystem() = default;
192
193
Status S3FileSystem::create_file_impl(const Path& file, FileWriterPtr* writer,
194
76.0k
                                      const FileWriterOptions* opts) {
195
76.0k
    auto client = _client->get();
196
76.0k
    CHECK_S3_CLIENT(client);
197
76.0k
    auto key = DORIS_TRY(get_key(file));
198
76.0k
    *writer = std::make_unique<S3FileWriter>(_client, _bucket, std::move(key), opts);
199
76.0k
    return Status::OK();
200
76.0k
}
201
202
Status S3FileSystem::open_file_internal(const Path& file, FileReaderSPtr* reader,
203
639k
                                        const FileReaderOptions& opts) {
204
639k
    TEST_SYNC_POINT_CALLBACK("S3FileSystem::open_file_internal", &file, &opts);
205
639k
    auto key = DORIS_TRY(get_key(file));
206
639k
    *reader = DORIS_TRY(S3FileReader::create(_client, _bucket, key, opts.file_size, nullptr));
207
639k
    return Status::OK();
208
639k
}
209
210
62
Status S3FileSystem::create_directory_impl(const Path& dir, bool failed_if_exists) {
211
62
    return Status::OK();
212
62
}
213
214
0
Status S3FileSystem::delete_file_impl(const Path& file) {
215
0
    auto client = _client->get();
216
0
    CHECK_S3_CLIENT(client);
217
218
0
    auto key = DORIS_TRY(get_key(file));
219
220
0
    auto resp = client->delete_object({.bucket = _bucket, .key = key});
221
222
0
    if (resp.status.code == ErrorCode::OK || resp.status.code == ErrorCode::NOT_FOUND) {
223
0
        return Status::OK();
224
0
    }
225
0
    return std::move(Status(resp.status.code, std::move(resp.status.msg))
226
0
                             .append(fmt::format("failed to delete file {}", full_s3_path(key))));
227
0
}
228
229
113
Status S3FileSystem::delete_directory_impl(const Path& dir) {
230
113
    auto client = _client->get();
231
113
    CHECK_S3_CLIENT(client);
232
233
113
    auto prefix = DORIS_TRY(get_key(dir));
234
113
    if (!prefix.empty() && prefix.back() != '/') {
235
113
        prefix.push_back('/');
236
113
    }
237
238
113
    ObjStoragePath delete_path {
239
113
            .path = full_s3_path(prefix),
240
113
            .bucket = _bucket,
241
113
            .prefix = prefix,
242
113
    };
243
113
    auto resp = delete_objects_recursively(std::move(client), delete_path);
244
113
    return {resp.status.code, std::move(resp.status.msg)};
245
113
}
246
247
0
Status S3FileSystem::batch_delete_impl(const std::vector<Path>& remote_files) {
248
0
    auto client = _client->get();
249
0
    CHECK_S3_CLIENT(client);
250
251
    // `DeleteObjectsRequest` can only contain 1000 keys at most.
252
0
    constexpr size_t max_delete_batch = 1000;
253
0
    auto path_iter = remote_files.begin();
254
255
0
    do {
256
0
        std::vector<std::string> objects;
257
0
        auto path_begin = path_iter;
258
0
        for (; path_iter != remote_files.end() && (path_iter - path_begin < max_delete_batch);
259
0
             ++path_iter) {
260
0
            auto key = DORIS_TRY(get_key(*path_iter));
261
0
            objects.emplace_back(std::move(key));
262
0
        }
263
0
        if (objects.empty()) {
264
0
            return Status::OK();
265
0
        }
266
        // clang-format off
267
0
        if (auto resp = client->delete_objects( {.bucket = _bucket,}, std::move(objects)); resp.status.code != ErrorCode::OK) {
268
0
            return {resp.status.code, std::move(resp.status.msg)};
269
0
        }
270
        // clang-format on
271
0
    } while (path_iter != remote_files.end());
272
273
0
    return Status::OK();
274
0
}
275
276
58
Status S3FileSystem::exists_impl(const Path& path, bool* res) const {
277
58
    auto client = _client->get();
278
58
    CHECK_S3_CLIENT(client);
279
58
    auto key = DORIS_TRY(get_key(path));
280
281
58
    VLOG_DEBUG << "key:" << key << " path:" << path;
282
283
58
    auto resp = client->head_object({.bucket = _bucket, .key = key});
284
285
58
    if (resp.resp.status.code == ErrorCode::OK) {
286
54
        *res = true;
287
54
    } else if (resp.resp.status.code == ErrorCode::NOT_FOUND) {
288
4
        *res = false;
289
4
    } else {
290
0
        return std::move(
291
0
                Status(resp.resp.status.code, std::move(resp.resp.status.msg))
292
0
                        .append(fmt::format(" failed to check exists {}", full_s3_path(key))));
293
0
    }
294
58
    return Status::OK();
295
58
}
296
297
70
Status S3FileSystem::file_size_impl(const Path& file, int64_t* file_size) const {
298
70
    auto key = DORIS_TRY(get_key(file));
299
70
    *file_size = DORIS_TRY(_client->object_file_size(_bucket, key));
300
70
    return Status::OK();
301
70
}
302
303
Status S3FileSystem::list_impl(const Path& dir, bool only_file, std::vector<FileInfo>* files,
304
71
                               bool* exists) {
305
    // For object storage, this path is always not exist.
306
    // So we ignore this property and set exists to true.
307
71
    *exists = true;
308
71
    auto client = _client->get();
309
71
    CHECK_S3_CLIENT(client);
310
71
    auto prefix = DORIS_TRY(get_key(dir));
311
71
    if (!prefix.empty() && prefix.back() != '/') {
312
71
        prefix.push_back('/');
313
71
    }
314
315
71
    std::vector<ObjectMeta> objects;
316
71
    auto resp = client->list_objects({.bucket = _bucket, .prefix = prefix}, &objects);
317
71
    if (!resp.ok()) {
318
0
        files->clear();
319
0
        return {resp.status.code, std::move(resp.status.msg)};
320
0
    }
321
71
    files->reserve(files->size() + objects.size());
322
88
    for (auto& obj : objects) {
323
88
        obj.key.erase(0, prefix.size());
324
88
        bool is_dir = obj.key.empty() || obj.key.back() == '/';
325
88
        files->emplace_back(FileInfo {
326
88
                .file_name = std::move(obj.key), .file_size = obj.size, .is_file = !is_dir});
327
88
    }
328
329
71
    return Status::OK();
330
71
}
331
332
0
Status S3FileSystem::rename_impl(const Path& orig_name, const Path& new_name) {
333
0
    return Status::NotSupported("S3FileSystem::rename_impl");
334
0
}
335
336
583
Status S3FileSystem::upload_impl(const Path& local_file, const Path& remote_file) {
337
583
    auto client = _client->get();
338
583
    CHECK_S3_CLIENT(client);
339
340
583
    auto key = DORIS_TRY(get_key(remote_file));
341
583
    auto start = std::chrono::steady_clock::now();
342
583
    FileWriterPtr obj_writer;
343
583
    RETURN_IF_ERROR(create_file_impl(key, &obj_writer, nullptr));
344
583
    FileReaderSPtr local_reader;
345
583
    RETURN_IF_ERROR(io::global_local_filesystem()->open_file(local_file, &local_reader));
346
583
    size_t local_buffer_size = config::s3_file_system_local_upload_buffer_size;
347
583
    std::unique_ptr<char[]> write_buffer =
348
583
            std::make_unique_for_overwrite<char[]>(local_buffer_size);
349
583
    size_t cur_read = 0;
350
1.16k
    while (cur_read < local_reader->size()) {
351
583
        size_t bytes_read = 0;
352
583
        RETURN_IF_ERROR(local_reader->read_at(
353
583
                cur_read, Slice {write_buffer.get(), local_buffer_size}, &bytes_read));
354
583
        RETURN_IF_ERROR(obj_writer->append({write_buffer.get(), bytes_read}));
355
583
        cur_read += bytes_read;
356
583
    }
357
583
    RETURN_IF_ERROR(obj_writer->close());
358
583
    auto duration = std::chrono::duration<float>(std::chrono::steady_clock::now() - start);
359
360
583
    auto size = local_reader->size();
361
583
    LOG(INFO) << "Upload " << local_file.native() << " to " << full_s3_path(key)
362
583
              << ", duration=" << duration.count() << ", bytes=" << size;
363
364
583
    return Status::OK();
365
583
}
366
367
Status S3FileSystem::batch_upload_impl(const std::vector<Path>& local_files,
368
0
                                       const std::vector<Path>& remote_files) {
369
0
    auto client = _client->get();
370
0
    CHECK_S3_CLIENT(client);
371
372
0
    if (local_files.size() != remote_files.size()) {
373
0
        return Status::InvalidArgument("local_files.size({}) != remote_files.size({})",
374
0
                                       local_files.size(), remote_files.size());
375
0
    }
376
377
0
    std::vector<FileWriterPtr> obj_writers(local_files.size());
378
379
0
    auto upload_task = [&, this](size_t idx) {
380
0
        SCOPED_ATTACH_TASK(ExecEnv::GetInstance()->s3_file_buffer_tracker());
381
0
        const auto& local_file = local_files[idx];
382
0
        const auto& remote_file = remote_files[idx];
383
0
        auto& obj_writer = obj_writers[idx];
384
0
        auto key = DORIS_TRY(get_key(remote_file));
385
0
        LOG(INFO) << "Start to upload " << local_file.native() << " to " << full_s3_path(key);
386
0
        RETURN_IF_ERROR(create_file_impl(key, &obj_writer, nullptr));
387
0
        FileReaderSPtr local_reader;
388
0
        RETURN_IF_ERROR(io::global_local_filesystem()->open_file(local_file, &local_reader));
389
0
        size_t local_buffer_size = config::s3_file_system_local_upload_buffer_size;
390
0
        std::unique_ptr<char[]> write_buffer =
391
0
                std::make_unique_for_overwrite<char[]>(local_buffer_size);
392
0
        size_t cur_read = 0;
393
0
        while (cur_read < local_reader->size()) {
394
0
            size_t bytes_read = 0;
395
0
            RETURN_IF_ERROR(local_reader->read_at(
396
0
                    cur_read, Slice {write_buffer.get(), local_buffer_size}, &bytes_read));
397
0
            RETURN_IF_ERROR((*obj_writer).append({write_buffer.get(), bytes_read}));
398
0
            cur_read += bytes_read;
399
0
        }
400
0
        RETURN_IF_ERROR((*obj_writer).close());
401
0
        return Status::OK();
402
0
    };
403
404
0
    Status s = Status::OK();
405
0
    std::vector<std::future<Status>> futures;
406
0
    for (int i = 0; i < local_files.size(); ++i) {
407
0
        auto task = std::make_shared<std::packaged_task<Status(size_t idx)>>(upload_task);
408
0
        futures.emplace_back(task->get_future());
409
0
        auto st = ExecEnv::GetInstance()->s3_file_system_thread_pool()->submit_func(
410
0
                [t = std::move(task), idx = i]() mutable { (*t)(idx); });
411
        // We shouldn't return immediately since the previous submitted tasks might still be running in the thread pool
412
0
        if (!st.ok()) {
413
0
            s = st;
414
0
            break;
415
0
        }
416
0
    }
417
0
    for (auto&& f : futures) {
418
0
        auto cur_s = f.get();
419
0
        if (!cur_s.ok()) {
420
0
            s = std::move(cur_s);
421
0
        }
422
0
    }
423
0
    return s;
424
0
}
425
426
0
Status S3FileSystem::download_impl(const Path& remote_file, const Path& local_file) {
427
0
    auto client = _client->get();
428
0
    CHECK_S3_CLIENT(client);
429
0
    auto key = DORIS_TRY(get_key(remote_file));
430
0
    int64_t size;
431
0
    RETURN_IF_ERROR(file_size(remote_file, &size));
432
0
    std::unique_ptr<char[]> buf = std::make_unique_for_overwrite<char[]>(size);
433
0
    size_t bytes_read = 0;
434
    // clang-format off
435
0
    auto resp = client->get_object( {.bucket = _bucket, .key = key,},
436
0
            buf.get(), 0, size, &bytes_read);
437
    // clang-format on
438
0
    if (resp.status.code != ErrorCode::OK) {
439
0
        return {resp.status.code, std::move(resp.status.msg)};
440
0
    }
441
0
    Aws::OFStream local_file_s;
442
0
    local_file_s.open(local_file, std::ios::out | std::ios::binary);
443
0
    if (local_file_s.good()) {
444
0
        local_file_s << StringViewStream(buf.get(), size).rdbuf();
445
0
    } else {
446
0
        return localfs_error(errno, fmt::format("failed to write file {}", local_file.native()));
447
0
    }
448
449
0
    return Status::OK();
450
0
}
451
452
// oss has public endpoint and private endpoint, is_public_endpoint determines
453
// whether to return a public endpoint.
454
std::string S3FileSystem::generate_presigned_url(const Path& path, int64_t expiration_secs,
455
585
                                                 bool is_public_endpoint) const {
456
    // An empty prefix must not add a separator absent from the uploaded object key.
457
585
    std::string key =
458
585
            _prefix.empty() ? path.native() : fmt::format("{}/{}", _prefix, path.native());
459
585
    std::shared_ptr<ObjStorageClient> client;
460
585
    auto new_s3_conf = _client->s3_client_conf();
461
585
    if (is_public_endpoint && new_s3_conf.endpoint.ends_with(OSS_PRIVATE_ENDPOINT_SUFFIX)) {
462
583
        new_s3_conf.endpoint.erase(new_s3_conf.endpoint.size() - OSS_PRIVATE_ENDPOINT_SUFFIX.size(),
463
583
                                   LEN_OF_OSS_PRIVATE_SUFFIX);
464
583
        auto client_result = S3ClientFactory::instance().create(new_s3_conf);
465
583
        if (!client_result) {
466
0
            LOG(WARNING) << "failed to create S3 client for presigned URL: "
467
0
                         << client_result.error();
468
0
            return {};
469
0
        }
470
583
        client = std::move(client_result).value();
471
583
    } else {
472
2
        client = _client->get();
473
2
    }
474
585
    return client->generate_presigned_url({.bucket = _bucket, .key = key, .prefix = ""},
475
585
                                          expiration_secs);
476
585
}
477
478
} // namespace doris::io