Coverage Report

Created: 2026-08-06 13:04

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
common/cpp/client/obj_storage_client.h
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
#pragma once
19
20
#include <gen_cpp/Status_types.h>
21
22
#include <cstdint>
23
#include <filesystem>
24
#include <functional>
25
#include <memory>
26
#include <optional>
27
#include <string>
28
#include <string_view>
29
#include <utility>
30
#include <vector>
31
32
namespace doris {
33
// Names are in lexico order.
34
enum class ObjStorageType : uint8_t {
35
    UNKNOWN = 0,
36
    AWS = 1,
37
    AZURE = 2,
38
    BOS = 3,
39
    COS = 4,
40
    OSS = 5,
41
    OBS = 6,
42
    GCP = 7,
43
    TOS = 8,
44
};
45
46
/// eg:
47
///     s3://bucket1/path/to/file.txt
48
/// path:   s3://bucket1/path/to/file.txt
49
/// bucket: bucket1
50
/// key:    path/to/file.txt
51
struct ObjectStoragePathOptions {
52
    std::filesystem::path path = "";
53
    std::string bucket {};                               // blob container in azure
54
    std::string key {};                                  // blob name in azure
55
    std::string prefix {};                               // for list and recursive delete
56
    std::optional<std::string> upload_id = std::nullopt; // only used for S3 upload
57
};
58
59
struct ObjectClientConfig {
60
    std::string endpoint {};
61
    std::string ak {};
62
    std::string sk {};
63
    std::string tls_debug_context {};
64
};
65
66
struct ObjectMeta {
67
    std::string file_path {};
68
    int64_t size {0};
69
    int64_t mtime_s {0};
70
};
71
72
struct ObjectCompleteMultiPart {
73
    int part_num = 0;
74
    std::string etag {};
75
};
76
77
struct ObjectStorageStatus {
78
    enum Code : int {
79
        UNDEFINED = -1,
80
        OK = TStatusCode::OK,
81
        NOT_FOUND = TStatusCode::NOT_FOUND,
82
        END_OF_FILE = TStatusCode::END_OF_FILE,
83
        RATE_LIMIT = TStatusCode::LIMIT_REACH,
84
    };
85
86
0
    ObjectStorageStatus(int r = OK, std::string msg = "") : code(r), msg(std::move(msg)) {}
87
    // clang-format off
88
    int code {OK}; // To unify the error handle logic with BE, we'd better use the same error code as BE
89
    // clang-format on
90
    std::string msg;
91
};
92
93
// We only store error code along with err_msg instead of Status to unify BE and recycler's error handle logic
94
struct ObjectStorageResponse {
95
    ObjectStorageStatus status {0, ""};
96
    int http_code {200};
97
    std::string request_id {};
98
0
    static ObjectStorageResponse OK() {
99
        // clang-format off
100
0
        return {
101
0
                .status = ObjectStorageStatus{0, ""},
102
0
                .http_code = 200,
103
0
                .request_id = ""
104
0
        };
105
        // clang-format on
106
0
    }
107
108
0
    static ObjectStorageResponse rate_limit(std::string message) {
109
0
        return {
110
0
                .status = ObjectStorageStatus {ObjectStorageStatus::RATE_LIMIT, std::move(message)},
111
0
                .http_code = 429,
112
0
        };
113
0
    }
114
115
0
    bool ok() const { return status.code == ObjectStorageStatus::OK; }
116
};
117
118
enum class ObjStorageRequestType {
119
    GET,
120
    PUT,
121
};
122
123
inline constexpr int32_t OBJECT_LIST_PAGE_SIZE = 1000;
124
125
// One admission result for one object-storage backend request. `settle` is used by
126
// byte-aware limiters to refund a short read after the call completes.
127
struct ObjStorageRateLimitToken {
128
    ObjectStorageResponse resp = ObjectStorageResponse::OK();
129
    std::function<void(size_t)> settle {};
130
131
1
    void settle_bytes(size_t actual_bytes) const {
132
1
        if (settle) {
133
1
            settle(actual_bytes);
134
1
        }
135
1
    }
136
};
137
138
class ObjStorageRateLimitPolicy {
139
public:
140
    virtual ~ObjStorageRateLimitPolicy() = default;
141
    virtual ObjStorageRateLimitToken acquire(ObjStorageRequestType type,
142
                                             size_t estimated_bytes) const = 0;
143
};
144
145
struct ObjectStorageUploadResponse {
146
    ObjectStorageResponse resp = ObjectStorageResponse::OK();
147
    std::optional<std::string> upload_id = std::nullopt;
148
    std::optional<std::string> etag = std::nullopt;
149
};
150
151
struct ObjectStorageHeadResponse {
152
    ObjectStorageResponse resp = ObjectStorageResponse::OK();
153
    long long file_size {0};
154
};
155
156
struct ObjectStorageListResponse {
157
    ObjectStorageResponse resp = ObjectStorageResponse::OK();
158
    std::optional<ObjectMeta> results_ = std::nullopt;
159
};
160
161
struct ObjectStorageListPage {
162
    ObjectStorageResponse resp = ObjectStorageResponse::OK();
163
    std::vector<ObjectMeta> objects {};
164
    std::string continuation_token {};
165
    bool has_more = false;
166
};
167
168
struct ObjStorageCapabilities {
169
    size_t max_delete_batch = 1;
170
};
171
172
using ObjStorageDeleteTask = std::function<ObjectStorageResponse()>;
173
174
// A streaming executor for recursive deletion. submit() must enqueue the task immediately so the
175
// producer is subject to the executor's queue backpressure while it continues listing. wait()
176
// completes the current synchronization batch and prepares the executor for the next one.
177
struct ObjStorageDeleteExecutor {
178
    std::function<ObjectStorageResponse(ObjStorageDeleteTask)> submit {};
179
    std::function<ObjectStorageResponse()> wait {};
180
181
0
    explicit operator bool() const { return submit && wait; }
182
};
183
184
struct RecursiveDeleteOptions {
185
    int64_t expiration_time = 0;
186
    size_t max_tasks_per_batch = 1;
187
    ObjStorageDeleteExecutor executor {};
188
};
189
190
class ObjStorageBackend {
191
public:
192
0
    virtual ~ObjStorageBackend() = default;
193
    // Create a multi-part upload request. On AWS-compatible systems, it will return an upload ID, but not on Azure.
194
    // The input parameters should include the bucket and key for the object storage.
195
    virtual ObjectStorageUploadResponse create_multipart_upload(
196
            const ObjectStoragePathOptions& opts) = 0;
197
    // To directly upload a piece of data to object storage and generate a user-visible file.
198
    // You need to clearly specify the bucket and key
199
    virtual ObjectStorageResponse put_object(const ObjectStoragePathOptions& opts,
200
                                             std::string_view stream) = 0;
201
    // To upload a part of a large file to object storage as a temporary file, which is not visible to the user
202
    // The temporary file's ID is the value of the part_num passed in
203
    // You need to specify the bucket and key along with the upload_id if it's AWS-compatible system
204
    // For the same bucket and key, as well as the same part_num, it will directly replace the original temporary file.
205
    virtual ObjectStorageUploadResponse upload_part(const ObjectStoragePathOptions& opts,
206
                                                    std::string_view stream, int part_num) = 0;
207
    // To combine the previously uploaded multiple file parts into a complete file, the file name is the name of the key passed in.
208
    // If it is an AWS-compatible system, the upload_id needs to be included.
209
    // After a successful execution, the large file can be accessed in the object storage
210
    virtual ObjectStorageResponse complete_multipart_upload(
211
            const ObjectStoragePathOptions& opts,
212
            const std::vector<ObjectCompleteMultiPart>& completed_parts) = 0;
213
    // According to the passed bucket and key, it will access whether the corresponding file exists in the object storage.
214
    // If it exists, it will return the corresponding file size
215
    virtual ObjectStorageHeadResponse head_object(const ObjectStoragePathOptions& opts) = 0;
216
    // According to the bucket and key, it finds the corresponding file in the object storage
217
    // and starting from the offset, it reads bytes_read bytes into the buffer, with size_return recording the actual number of bytes read
218
    virtual ObjectStorageResponse get_object(const ObjectStoragePathOptions& opts, void* buffer,
219
                                             size_t offset, size_t bytes_read,
220
                                             size_t* size_return) = 0;
221
    // Return at most one page of objects. One call corresponds to exactly one backend request.
222
    // **Notice**: The files returned by this function contain the full key in object storage.
223
    virtual ObjectStorageListPage list_objects(const ObjectStoragePathOptions& path,
224
                                               std::string_view continuation_token) = 0;
225
226
    // According to the bucket and prefix specified by the user, it performs batch deletion based on the object names in the object array.
227
    virtual ObjectStorageResponse delete_objects(const ObjectStoragePathOptions& opts,
228
                                                 std::vector<std::string> objs) = 0;
229
    // Delete the file named key in the object storage bucket.
230
    virtual ObjectStorageResponse delete_object(const ObjectStoragePathOptions& opts) = 0;
231
0
    virtual ObjStorageCapabilities capabilities() const { return {}; }
232
    // Return a presigned URL for users to access the object
233
    virtual std::string generate_presigned_url(const ObjectStoragePathOptions& opts,
234
                                               int64_t expiration_secs) = 0;
235
236
    // Get the objects' expiration time on the bucket
237
    virtual ObjectStorageResponse get_life_cycle(const std::string& /*bucket*/,
238
0
                                                 int64_t* /*expiration_days*/) {
239
0
        return {.status = {TStatusCode::NOT_IMPLEMENTED_ERROR,
240
0
                           "object storage lifecycle is not supported"},
241
0
                .http_code = 0};
242
0
    }
243
244
    // Check if the objects' versioning is on or off
245
    // returns 0 when versioning is on, otherwise versioning is off or check failed
246
0
    virtual ObjectStorageResponse check_versioning(const std::string& /*bucket*/) {
247
0
        return {.status = {TStatusCode::NOT_IMPLEMENTED_ERROR,
248
0
                           "object storage versioning is not supported"},
249
0
                .http_code = 0};
250
0
    }
251
252
    virtual ObjectStorageResponse abort_multipart_upload(const ObjectStoragePathOptions& /*path*/,
253
0
                                                         const std::string& /*upload_id*/) {
254
0
        return {.status = {TStatusCode::NOT_IMPLEMENTED_ERROR,
255
0
                           "aborting multipart uploads is not supported"},
256
0
                .http_code = 0};
257
0
    }
258
};
259
260
// The only object-storage interface exposed to upper layers. It combines a backend implementation
261
// with an optional runtime policy, so backends cannot accidentally bypass rate limiting.
262
class ObjStorageClient final {
263
public:
264
    explicit ObjStorageClient(
265
            std::shared_ptr<ObjStorageBackend> backend,
266
            std::shared_ptr<const ObjStorageRateLimitPolicy> rate_limit_policy = nullptr)
267
0
            : backend_(std::move(backend)), rate_limit_policy_(std::move(rate_limit_policy)) {}
268
269
    ObjectStorageUploadResponse create_multipart_upload(const ObjectStoragePathOptions& opts);
270
    ObjectStorageResponse put_object(const ObjectStoragePathOptions& opts, std::string_view stream);
271
    ObjectStorageUploadResponse upload_part(const ObjectStoragePathOptions& opts,
272
                                            std::string_view stream, int part_num);
273
    ObjectStorageResponse complete_multipart_upload(
274
            const ObjectStoragePathOptions& opts,
275
            const std::vector<ObjectCompleteMultiPart>& completed_parts);
276
    ObjectStorageHeadResponse head_object(const ObjectStoragePathOptions& opts);
277
    ObjectStorageResponse get_object(const ObjectStoragePathOptions& opts, void* buffer,
278
                                     size_t offset, size_t bytes_read, size_t* size_return);
279
    ObjectStorageListPage list_objects(const ObjectStoragePathOptions& opts,
280
                                       std::string_view continuation_token = {});
281
    ObjectStorageResponse delete_objects(const ObjectStoragePathOptions& opts,
282
                                         std::vector<std::string> objs);
283
    ObjectStorageResponse delete_object(const ObjectStoragePathOptions& opts);
284
    ObjectStorageResponse delete_objects_recursively(
285
            const ObjectStoragePathOptions& opts,
286
            const RecursiveDeleteOptions& options = RecursiveDeleteOptions {});
287
0
    ObjStorageCapabilities capabilities() const { return backend_->capabilities(); }
288
    std::string generate_presigned_url(const ObjectStoragePathOptions& opts,
289
                                       int64_t expiration_secs);
290
    ObjectStorageResponse get_life_cycle(const std::string& bucket, int64_t* expiration_days);
291
    ObjectStorageResponse check_versioning(const std::string& bucket);
292
    ObjectStorageResponse abort_multipart_upload(const ObjectStoragePathOptions& opts,
293
                                                 const std::string& upload_id);
294
295
private:
296
    ObjStorageRateLimitToken acquire(ObjStorageRequestType type, size_t estimated_bytes = 0) const;
297
298
    std::shared_ptr<ObjStorageBackend> backend_;
299
    std::shared_ptr<const ObjStorageRateLimitPolicy> rate_limit_policy_;
300
};
301
302
// A client-side iterator above ObjStorageClient. It requests one fixed-size page at a time, so
303
// every ObjStorageClient::list_objects call maps to one rate-limit admission and one SDK request.
304
class ObjectListIterator {
305
public:
306
    ObjectListIterator(std::shared_ptr<ObjStorageClient> client, ObjectStoragePathOptions opts)
307
0
            : client_(std::move(client)), opts_(std::move(opts)) {}
308
309
0
    bool is_valid() const { return is_valid_; }
310
    ObjectStorageResponse has_next();
311
    ObjectStorageListResponse next();
312
313
private:
314
    std::shared_ptr<ObjStorageClient> client_;
315
    ObjectStoragePathOptions opts_;
316
    std::vector<ObjectMeta> objects_;
317
    size_t next_index_ = 0;
318
    std::string continuation_token_;
319
    bool has_more_ = true;
320
    bool is_valid_ = true;
321
};
322
} // namespace doris
323
324
// Keep the BE namespace spelling source-compatible while the implementation is
325
// shared with Recycler in `doris`.
326
namespace doris::io {
327
using ::doris::ObjStorageCapabilities;
328
using ::doris::ObjStorageClient;
329
using ::doris::ObjStorageDeleteExecutor;
330
using ::doris::ObjStorageDeleteTask;
331
using ::doris::ObjStorageBackend;
332
using ::doris::ObjStorageRateLimitPolicy;
333
using ::doris::ObjStorageRateLimitToken;
334
using ::doris::ObjStorageRequestType;
335
using ::doris::ObjStorageType;
336
using ::doris::ObjectClientConfig;
337
using ::doris::ObjectCompleteMultiPart;
338
using ::doris::ObjectListIterator;
339
using ::doris::ObjectMeta;
340
using ::doris::ObjectStorageHeadResponse;
341
using ::doris::ObjectStorageListPage;
342
using ::doris::ObjectStorageListResponse;
343
using ::doris::ObjectStoragePathOptions;
344
using ::doris::ObjectStorageResponse;
345
using ::doris::ObjectStorageStatus;
346
using ::doris::ObjectStorageUploadResponse;
347
using ::doris::RecursiveDeleteOptions;
348
} // namespace doris::io