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 |