Coverage Report

Created: 2026-11-04 22:21

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 <chrono> // IWYU pragma: keep
27
#include <filesystem>
28
#include <fstream> // IWYU pragma: keep
29
#include <future>
30
#include <memory>
31
#include <variant>
32
33
#include "common/check.h"
34
#include "common/config.h"
35
#include "common/logging.h"
36
#include "common/status.h"
37
#include "cpp/obj-client/obj_storage_client.h"
38
#include "cpp/obj-client/s3_common.h"
39
#include "cpp/sync_point.h"
40
#include "io/fs/err_utils.h"
41
#include "io/fs/file_system.h"
42
#include "io/fs/file_writer.h"
43
#include "io/fs/local_file_system.h"
44
#include "io/fs/remote_file_system.h"
45
#include "io/fs/s3_file_reader.h"
46
#include "io/fs/s3_file_writer.h"
47
#include "io/obj_storage/obj_client_factory.h"
48
#include "io/obj_storage/obj_storage_uri.h"
49
#include "runtime/exec_env.h"
50
#include "runtime/thread_context.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
2.34k
    if (!client) {                                              \
61
1
        return Status::InvalidArgument("init s3 client error"); \
62
1
    }
63
#endif
64
65
// Removes the first and last '/'.
66
30
std::string normalize_prefix(std::string prefix) {
67
30
    if (prefix.empty()) {
68
5
        return prefix;
69
5
    }
70
25
    size_t start = prefix.find_first_not_of('/');
71
25
    if (start == std::string::npos) {
72
0
        return "";
73
0
    }
74
25
    size_t end = prefix.find_last_not_of('/');
75
25
    return prefix.substr(start, end - start + 1);
76
25
}
77
78
} // namespace
79
80
115
std::string S3FileSystem::full_s3_path(std::string_view key) const {
81
115
    return _client->full_s3_path(_bucket, key);
82
115
}
83
84
Result<std::shared_ptr<S3FileSystem>> S3FileSystem::create(ObjStorageConfig config,
85
26
                                                           std::string id) {
86
26
    std::shared_ptr<S3FileSystem> fs(new S3FileSystem(std::move(config), std::move(id)));
87
26
    RETURN_IF_ERROR_RESULT(fs->init());
88
26
    return fs;
89
26
}
90
91
Result<std::shared_ptr<S3FileSystem>> S3FileSystem::refresh_resource(
92
6
        std::shared_ptr<S3FileSystem> fs, ObjStorageConfig config) {
93
6
    const auto& holder = fs->client_holder();
94
6
    auto current = holder->state();
95
6
    if (current->config.backend.index() == config.client.backend.index()) {
96
3
        RETURN_IF_ERROR_RESULT(holder->reset(config.client));
97
3
        return fs;
98
3
    }
99
3
    bool same_target =
100
3
            config.bucket == fs->bucket() && normalize_prefix(config.prefix) == fs->prefix();
101
3
    LOG(WARNING) << "object storage backend of resource " << fs->id() << " changed from "
102
3
                 << current->config.to_string() << " to " << config.client.to_string() << ", "
103
3
                 << (same_target ? "rebinding its client" : "building a new file system");
104
3
    if (same_target) {
105
2
        RETURN_IF_ERROR_RESULT(holder->rebind(config.client));
106
2
        return fs;
107
2
    }
108
1
    return create(std::move(config), fs->id());
109
3
}
110
111
3
Status S3FileSystem::refresh_vault(const ObjClientConfig& config) {
112
3
    auto current = _client->state();
113
3
    if (current->config.backend.index() != config.backend.index()) {
114
1
        auto reason = Status::InvalidArgument<false>(
115
1
                "storage vault {} changed backend from {} to {}, its file system is disabled "
116
1
                "until BE restarts",
117
1
                id(), current->config.to_string(), config.to_string());
118
1
        _client->invalidate(reason);
119
1
        return reason;
120
1
    }
121
2
    return _client->reset(config);
122
3
}
123
124
S3FileSystem::S3FileSystem(ObjStorageConfig config, std::string id)
125
28
        : RemoteFileSystem(config.prefix, std::move(id), FileSystemType::S3),
126
28
          _bucket(std::move(config.bucket)),
127
          // FIXME(plat1ko): Normalize prefix
128
28
          _prefix(normalize_prefix(std::move(config.prefix))),
129
28
          _client(std::make_shared<ObjClientHolder>(std::move(config.client))) {}
130
131
26
Status S3FileSystem::init() {
132
26
    return _client->init();
133
26
}
134
135
2.37k
Result<std::string> S3FileSystem::_get_key(const Path& full_path) const {
136
    // FIXME(plat1ko): Check bucket in full path and support relative path
137
2.37k
    auto uri = ObjStorageUri::parse_s3(full_path.native());
138
2.37k
    if (!uri.has_value()) {
139
0
        return ResultError(std::move(uri).error());
140
0
    }
141
2.37k
    return std::move(uri.value().key);
142
2.37k
}
143
144
28
S3FileSystem::~S3FileSystem() = default;
145
146
Status S3FileSystem::create_file_impl(const Path& file, FileWriterPtr* writer,
147
2.16k
                                      const FileWriterOptions* opts) {
148
2.16k
    auto client = _client->get();
149
2.16k
    CHECK_S3_CLIENT(client);
150
2.16k
    auto key = DORIS_TRY(_get_key(file));
151
2.16k
    *writer = std::make_unique<S3FileWriter>(_client, _bucket, std::move(key), opts);
152
2.16k
    return Status::OK();
153
2.16k
}
154
155
Status S3FileSystem::open_file_internal(const Path& file, FileReaderSPtr* reader,
156
29
                                        const FileReaderOptions& opts) {
157
29
    TEST_SYNC_POINT_CALLBACK("S3FileSystem::open_file_internal", &file, &opts);
158
29
    auto key = DORIS_TRY(_get_key(file));
159
29
    *reader = DORIS_TRY(S3FileReader::create(_client, _bucket, key, opts.file_size, nullptr));
160
26
    return Status::OK();
161
29
}
162
163
64
Status S3FileSystem::create_directory_impl(const Path& dir, bool failed_if_exists) {
164
64
    return Status::OK();
165
64
}
166
167
0
Status S3FileSystem::delete_file_impl(const Path& file) {
168
0
    auto client = _client->get();
169
0
    CHECK_S3_CLIENT(client);
170
171
0
    auto key = DORIS_TRY(_get_key(file));
172
173
0
    auto resp = client->delete_object({.bucket = _bucket, .key = key});
174
175
0
    if (resp.status.code == ErrorCode::OK || resp.status.code == ErrorCode::NOT_FOUND) {
176
0
        return Status::OK();
177
0
    }
178
0
    return std::move(Status(resp.status.code, std::move(resp.status.msg))
179
0
                             .append(fmt::format("failed to delete file {}", full_s3_path(key))));
180
0
}
181
182
115
Status S3FileSystem::delete_directory_impl(const Path& dir) {
183
115
    auto client = _client->get();
184
115
    CHECK_S3_CLIENT(client);
185
186
115
    auto prefix = DORIS_TRY(_get_key(dir));
187
115
    if (!prefix.empty() && prefix.back() != '/') {
188
115
        prefix.push_back('/');
189
115
    }
190
191
115
    ObjStoragePath delete_path {
192
115
            .path = full_s3_path(prefix),
193
115
            .bucket = _bucket,
194
115
            .prefix = prefix,
195
115
    };
196
115
    auto resp = delete_objects_recursively(std::move(client), delete_path);
197
115
    return {resp.status.code, std::move(resp.status.msg)};
198
115
}
199
200
0
Status S3FileSystem::batch_delete_impl(const std::vector<Path>& remote_files) {
201
0
    auto client = _client->get();
202
0
    CHECK_S3_CLIENT(client);
203
204
    // `DeleteObjectsRequest` can only contain 1000 keys at most.
205
0
    constexpr size_t max_delete_batch = 1000;
206
0
    auto path_iter = remote_files.begin();
207
208
0
    do {
209
0
        std::vector<std::string> objects;
210
0
        auto path_begin = path_iter;
211
0
        for (; path_iter != remote_files.end() && (path_iter - path_begin < max_delete_batch);
212
0
             ++path_iter) {
213
0
            auto key = DORIS_TRY(_get_key(*path_iter));
214
0
            objects.emplace_back(std::move(key));
215
0
        }
216
0
        if (objects.empty()) {
217
0
            return Status::OK();
218
0
        }
219
        // clang-format off
220
0
        if (auto resp = client->delete_objects( {.bucket = _bucket,}, std::move(objects)); resp.status.code != ErrorCode::OK) {
221
0
            return {resp.status.code, std::move(resp.status.msg)};
222
0
        }
223
        // clang-format on
224
0
    } while (path_iter != remote_files.end());
225
226
0
    return Status::OK();
227
0
}
228
229
4
Status S3FileSystem::exists_impl(const Path& path, bool* res) const {
230
4
    auto client = _client->get();
231
4
    CHECK_S3_CLIENT(client);
232
4
    auto key = DORIS_TRY(_get_key(path));
233
234
4
    VLOG_DEBUG << "key:" << key << " path:" << path;
235
236
4
    auto resp = client->head_object({.bucket = _bucket, .key = key});
237
238
4
    if (resp.resp.status.code == ErrorCode::OK) {
239
0
        *res = true;
240
4
    } else if (resp.resp.status.code == ErrorCode::NOT_FOUND) {
241
4
        *res = false;
242
4
    } else {
243
0
        return std::move(
244
0
                Status(resp.resp.status.code, std::move(resp.resp.status.msg))
245
0
                        .append(fmt::format(" failed to check exists {}", full_s3_path(key))));
246
0
    }
247
4
    return Status::OK();
248
4
}
249
250
2
Status S3FileSystem::file_size_impl(const Path& file, int64_t* file_size) const {
251
2
    auto key = DORIS_TRY(_get_key(file));
252
2
    *file_size = DORIS_TRY(_client->object_file_size(_bucket, key));
253
2
    return Status::OK();
254
2
}
255
256
Status S3FileSystem::list_impl(const Path& dir, bool only_file, std::vector<FileInfo>* files,
257
64
                               bool* exists) {
258
    // For object storage, this path is always not exist.
259
    // So we ignore this property and set exists to true.
260
64
    *exists = true;
261
64
    auto client = _client->get();
262
64
    CHECK_S3_CLIENT(client);
263
64
    auto prefix = DORIS_TRY(_get_key(dir));
264
64
    if (!prefix.empty() && prefix.back() != '/') {
265
64
        prefix.push_back('/');
266
64
    }
267
268
64
    std::vector<ObjectMeta> objects;
269
64
    auto resp = client->list_objects({.bucket = _bucket, .prefix = prefix}, &objects);
270
64
    if (!resp.ok()) {
271
0
        files->clear();
272
0
        return {resp.status.code, std::move(resp.status.msg)};
273
0
    }
274
64
    files->reserve(files->size() + objects.size());
275
74
    for (auto& obj : objects) {
276
74
        obj.key.erase(0, prefix.size());
277
74
        bool is_dir = obj.key.empty() || obj.key.back() == '/';
278
74
        files->emplace_back(FileInfo {
279
74
                .file_name = std::move(obj.key), .file_size = obj.size, .is_file = !is_dir});
280
74
    }
281
282
64
    return Status::OK();
283
64
}
284
285
0
Status S3FileSystem::rename_impl(const Path& orig_name, const Path& new_name) {
286
0
    return Status::NotSupported("S3FileSystem::rename_impl");
287
0
}
288
289
0
Status S3FileSystem::upload_impl(const Path& local_file, const Path& remote_file) {
290
0
    auto client = _client->get();
291
0
    CHECK_S3_CLIENT(client);
292
293
0
    auto key = DORIS_TRY(_get_key(remote_file));
294
0
    auto start = std::chrono::steady_clock::now();
295
0
    FileWriterPtr obj_writer;
296
0
    RETURN_IF_ERROR(create_file_impl(key, &obj_writer, nullptr));
297
0
    FileReaderSPtr local_reader;
298
0
    RETURN_IF_ERROR(io::global_local_filesystem()->open_file(local_file, &local_reader));
299
0
    size_t local_buffer_size = config::s3_file_system_local_upload_buffer_size;
300
0
    std::unique_ptr<char[]> write_buffer =
301
0
            std::make_unique_for_overwrite<char[]>(local_buffer_size);
302
0
    size_t cur_read = 0;
303
0
    while (cur_read < local_reader->size()) {
304
0
        size_t bytes_read = 0;
305
0
        RETURN_IF_ERROR(local_reader->read_at(
306
0
                cur_read, Slice {write_buffer.get(), local_buffer_size}, &bytes_read));
307
0
        RETURN_IF_ERROR(obj_writer->append({write_buffer.get(), bytes_read}));
308
0
        cur_read += bytes_read;
309
0
    }
310
0
    RETURN_IF_ERROR(obj_writer->close());
311
0
    auto duration = std::chrono::duration<float>(std::chrono::steady_clock::now() - start);
312
313
0
    auto size = local_reader->size();
314
0
    LOG(INFO) << "Upload " << local_file.native() << " to " << full_s3_path(key)
315
0
              << ", duration=" << duration.count() << ", bytes=" << size;
316
317
0
    return Status::OK();
318
0
}
319
320
Status S3FileSystem::batch_upload_impl(const std::vector<Path>& local_files,
321
0
                                       const std::vector<Path>& remote_files) {
322
0
    auto client = _client->get();
323
0
    CHECK_S3_CLIENT(client);
324
325
0
    if (local_files.size() != remote_files.size()) {
326
0
        return Status::InvalidArgument("local_files.size({}) != remote_files.size({})",
327
0
                                       local_files.size(), remote_files.size());
328
0
    }
329
330
0
    std::vector<FileWriterPtr> obj_writers(local_files.size());
331
332
0
    auto upload_task = [&, this](size_t idx) {
333
0
        SCOPED_ATTACH_TASK(ExecEnv::GetInstance()->s3_file_buffer_tracker());
334
0
        const auto& local_file = local_files[idx];
335
0
        const auto& remote_file = remote_files[idx];
336
0
        auto& obj_writer = obj_writers[idx];
337
0
        auto key = DORIS_TRY(_get_key(remote_file));
338
0
        LOG(INFO) << "Start to upload " << local_file.native() << " to " << full_s3_path(key);
339
0
        RETURN_IF_ERROR(create_file_impl(key, &obj_writer, nullptr));
340
0
        FileReaderSPtr local_reader;
341
0
        RETURN_IF_ERROR(io::global_local_filesystem()->open_file(local_file, &local_reader));
342
0
        size_t local_buffer_size = config::s3_file_system_local_upload_buffer_size;
343
0
        std::unique_ptr<char[]> write_buffer =
344
0
                std::make_unique_for_overwrite<char[]>(local_buffer_size);
345
0
        size_t cur_read = 0;
346
0
        while (cur_read < local_reader->size()) {
347
0
            size_t bytes_read = 0;
348
0
            RETURN_IF_ERROR(local_reader->read_at(
349
0
                    cur_read, Slice {write_buffer.get(), local_buffer_size}, &bytes_read));
350
0
            RETURN_IF_ERROR((*obj_writer).append({write_buffer.get(), bytes_read}));
351
0
            cur_read += bytes_read;
352
0
        }
353
0
        RETURN_IF_ERROR((*obj_writer).close());
354
0
        return Status::OK();
355
0
    };
356
357
0
    Status s = Status::OK();
358
0
    std::vector<std::future<Status>> futures;
359
0
    for (int i = 0; i < local_files.size(); ++i) {
360
0
        auto task = std::make_shared<std::packaged_task<Status(size_t idx)>>(upload_task);
361
0
        futures.emplace_back(task->get_future());
362
0
        auto st = ExecEnv::GetInstance()->s3_file_system_thread_pool()->submit_func(
363
0
                [t = std::move(task), idx = i]() mutable { (*t)(idx); });
364
        // We shouldn't return immediately since the previous submitted tasks might still be running in the thread pool
365
0
        if (!st.ok()) {
366
0
            s = st;
367
0
            break;
368
0
        }
369
0
    }
370
0
    for (auto&& f : futures) {
371
0
        auto cur_s = f.get();
372
0
        if (!cur_s.ok()) {
373
0
            s = std::move(cur_s);
374
0
        }
375
0
    }
376
0
    return s;
377
0
}
378
379
0
Status S3FileSystem::download_impl(const Path& remote_file, const Path& local_file) {
380
0
    auto client = _client->get();
381
0
    CHECK_S3_CLIENT(client);
382
0
    auto key = DORIS_TRY(_get_key(remote_file));
383
0
    int64_t size;
384
0
    RETURN_IF_ERROR(file_size(remote_file, &size));
385
0
    std::unique_ptr<char[]> buf = std::make_unique_for_overwrite<char[]>(size);
386
0
    size_t bytes_read = 0;
387
    // clang-format off
388
0
    auto resp = client->get_object( {.bucket = _bucket, .key = key,},
389
0
            buf.get(), 0, size, &bytes_read);
390
    // clang-format on
391
0
    if (resp.status.code != ErrorCode::OK) {
392
0
        return {resp.status.code, std::move(resp.status.msg)};
393
0
    }
394
0
    std::ofstream local_file_s;
395
0
    local_file_s.open(local_file, std::ios::out | std::ios::binary);
396
0
    if (local_file_s.good()) {
397
0
        local_file_s << StringViewStream(buf.get(), size).rdbuf();
398
0
    } else {
399
0
        return localfs_error(errno, fmt::format("failed to write file {}", local_file.native()));
400
0
    }
401
402
0
    return Status::OK();
403
0
}
404
405
// oss has public endpoint and private endpoint, is_public_endpoint determines
406
// whether to return a public endpoint.
407
std::string S3FileSystem::generate_presigned_url(const Path& path, int64_t expiration_secs,
408
6
                                                 bool is_public_endpoint) const {
409
    // An empty prefix must not add a separator absent from the uploaded object key.
410
6
    std::string key =
411
6
            _prefix.empty() ? path.native() : fmt::format("{}/{}", _prefix, path.native());
412
6
    std::shared_ptr<ObjStorageClient> client;
413
    // Read the conf and the client from one snapshot so a concurrent reset cannot pair the
414
    // endpoint of one generation with the client of another.
415
6
    auto state = _client->state();
416
    // Checked first: the branch below builds a client of its own, which would go around
417
    // ObjClientHolder::invalidate.
418
6
    if (state->client == nullptr) {
419
4
        LOG(WARNING) << "no object storage client for presigned URL, resource_id=" << id();
420
4
        return {};
421
4
    }
422
2
    if (is_public_endpoint && state->config.endpoint().ends_with(OSS_PRIVATE_ENDPOINT_SUFFIX)) {
423
        // Only S3-protocol backends have an OSS private endpoint; the public one differs from it
424
        // only by the "-internal" part of the host.
425
0
        auto public_config = state->config;
426
0
        auto* s3 = std::get_if<S3ClientConfig>(&public_config.backend);
427
0
        DORIS_CHECK(s3 != nullptr);
428
0
        s3->endpoint.erase(s3->endpoint.size() - OSS_PRIVATE_ENDPOINT_SUFFIX.size(),
429
0
                           LEN_OF_OSS_PRIVATE_SUFFIX);
430
0
        auto client_result = ObjClientFactory::instance().get_or_create(public_config);
431
0
        if (!client_result) {
432
0
            LOG(WARNING) << "failed to create S3 client for presigned URL: "
433
0
                         << client_result.error();
434
0
            return {};
435
0
        }
436
0
        client = std::move(client_result).value();
437
2
    } else {
438
2
        client = state->client;
439
2
    }
440
2
    return client->generate_presigned_url({.bucket = _bucket, .key = key, .prefix = ""},
441
2
                                          expiration_secs);
442
2
}
443
444
} // namespace doris::io