be/src/io/fs/packed_file_manager.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 <butil/macros.h> |
21 | | #include <gen_cpp/cloud.pb.h> |
22 | | #include <glog/logging.h> |
23 | | |
24 | | #include <atomic> |
25 | | #include <chrono> |
26 | | #include <condition_variable> |
27 | | #include <map> |
28 | | #include <memory> |
29 | | #include <mutex> |
30 | | #include <optional> |
31 | | #include <string> |
32 | | #include <thread> |
33 | | #include <unordered_map> |
34 | | #include <vector> |
35 | | |
36 | | #include "common/status.h" |
37 | | #include "io/fs/file_system.h" |
38 | | #include "io/fs/file_writer.h" |
39 | | #include "io/fs/path.h" |
40 | | #include "util/slice.h" |
41 | | |
42 | | namespace doris::io { |
43 | | |
44 | | struct PackedSliceLocation { |
45 | | std::string packed_file_path; |
46 | | int64_t offset; |
47 | | int64_t size; |
48 | | int64_t create_time = 0; |
49 | | int64_t tablet_id = 0; |
50 | | std::string rowset_id; |
51 | | std::string resource_id; |
52 | | int64_t txn_id = 0; |
53 | | int64_t packed_file_size = -1; // Total size of the packed file, -1 means not set |
54 | | }; |
55 | | |
56 | | // Upload state of the packed file a slice belongs to |
57 | | enum class PackedSliceUploadState : uint8_t { |
58 | | PENDING = 0, |
59 | | UPLOADED, |
60 | | FAILED, |
61 | | }; |
62 | | |
63 | | // A slice of a packed file, shared by PackedFileManager and the PackedFileWriter that |
64 | | // produced it. The writer holds its handle for as long as it lives, so it can wait for the |
65 | | // upload and read the location back at the end of the load, whatever the manager recycled |
66 | | // from its by-path index in the meantime. |
67 | | class PackedSliceHandle { |
68 | | public: |
69 | 1.05k | explicit PackedSliceHandle(PackedSliceLocation location) : _location(std::move(location)) {} |
70 | | |
71 | 1.00k | const std::string& packed_file_path() const { return _location.packed_file_path; } |
72 | | |
73 | 2 | int64_t create_time() const { return _location.create_time; } |
74 | | |
75 | 5.12k | PackedSliceUploadState upload_state() const { |
76 | 5.12k | return _upload_state.load(std::memory_order_acquire); |
77 | 5.12k | } |
78 | | |
79 | | // Called once the packed file this slice belongs to reaches a terminal state |
80 | 1.00k | void set_upload_result(PackedSliceUploadState state, int64_t packed_file_size) { |
81 | 1.00k | if (state == PackedSliceUploadState::UPLOADED) { |
82 | 1.00k | _packed_file_size.store(packed_file_size, std::memory_order_relaxed); |
83 | 1.00k | } |
84 | 1.00k | _upload_state.store(state, std::memory_order_release); |
85 | 1.00k | } |
86 | | |
87 | 4.11k | PackedSliceLocation location() const { |
88 | 4.11k | PackedSliceLocation location = _location; |
89 | 4.11k | if (upload_state() == PackedSliceUploadState::UPLOADED) { |
90 | 1.00k | location.packed_file_size = _packed_file_size.load(std::memory_order_relaxed); |
91 | 1.00k | } |
92 | 4.11k | return location; |
93 | 4.11k | } |
94 | | |
95 | | private: |
96 | | const PackedSliceLocation _location; // Immutable once the slice has been appended |
97 | | std::atomic<int64_t> _packed_file_size {-1}; |
98 | | std::atomic<PackedSliceUploadState> _upload_state {PackedSliceUploadState::PENDING}; |
99 | | }; |
100 | | |
101 | | using PackedSliceHandlePtr = std::shared_ptr<PackedSliceHandle>; |
102 | | |
103 | | struct PackedAppendContext { |
104 | | std::string resource_id; |
105 | | int64_t tablet_id = 0; |
106 | | std::string rowset_id; |
107 | | int64_t first_segment_id = 0; |
108 | | int64_t txn_id = 0; |
109 | | uint64_t expiration_time = 0; // TTL expiration time in seconds since epoch, 0 means no TTL |
110 | | bool write_file_cache = true; // Whether to write data to file cache |
111 | | }; |
112 | | |
113 | | // Global object that manages packing small files into larger files for S3 optimization |
114 | | class PackedFileManager { |
115 | | struct PackedFileContext; |
116 | | |
117 | | public: |
118 | | static PackedFileManager* instance(); |
119 | | |
120 | | // Initialize manager state; file system will be resolved lazily |
121 | | Status init(); |
122 | | |
123 | | // Write a small file to the current packed file. On success `handle` receives a handle |
124 | | // to the new slice, or nullptr if `data` was too large to be packed. |
125 | | Status append_small_file(const std::string& path, const Slice& data, |
126 | | const PackedAppendContext& info, PackedSliceHandlePtr* handle); |
127 | | |
128 | | // Block until the packed file holding `handle` is uploaded to S3 |
129 | | Status wait_upload_done(const PackedSliceHandlePtr& handle); |
130 | | |
131 | | // Look a slice location up by small file path, for readers that have no handle to the |
132 | | // slice. The entry is subject to the retention based cleanup, so this can fail for a |
133 | | // file written long ago. |
134 | | Status get_packed_slice_location(const std::string& path, PackedSliceLocation* location); |
135 | | |
136 | | // Start the background management thread |
137 | | void start_background_manager(); |
138 | | |
139 | | // Stop the background management thread |
140 | | void stop_background_manager(); |
141 | | |
142 | | // Mark current packed file for upload and create new one |
143 | | Status mark_current_packed_file_for_upload(const std::string& resource_id); |
144 | | |
145 | | // Internal helper; expects caller holds _current_packed_file_mutex |
146 | | Status mark_current_packed_file_for_upload_locked(const std::string& resource_id); |
147 | | |
148 | | void record_packed_file_metrics(const PackedFileContext& packed_file); |
149 | | |
150 | | private: |
151 | 43 | PackedFileManager() = default; |
152 | | ~PackedFileManager(); |
153 | | |
154 | | DISALLOW_COPY_AND_ASSIGN(PackedFileManager); |
155 | | |
156 | | // Background thread function for managing packed file lifecycle |
157 | | void background_manager(); |
158 | | |
159 | | // Upload packed file to S3 and update meta service |
160 | | Status finalize_packed_file_upload(const std::string& packed_file_path, FileWriter* writer); |
161 | | |
162 | | // Update meta service with packed file information |
163 | | // table_id is used for rate limiting; -1 means no specific table (cross-table operation) |
164 | | Status update_meta_service(const std::string& packed_file_path, |
165 | | const cloud::PackedFileInfoPB& packed_file_info, |
166 | | int64_t table_id = -1); |
167 | | |
168 | | // Process uploading files |
169 | | void process_uploading_packed_files(); |
170 | | |
171 | | // Clean up expired data |
172 | | void cleanup_expired_data(); |
173 | | |
174 | | // Record the terminal upload state of `packed_file` on the slices it contains |
175 | | void mark_slices_upload_result(const PackedFileContext& packed_file, |
176 | | PackedSliceUploadState state); |
177 | | |
178 | | // Internal structure to track packed file state |
179 | | enum class PackedFileState { |
180 | | INIT, // Initial state, no files written yet |
181 | | ACTIVE, // Has files but doesn't meet upload conditions |
182 | | READY_TO_UPLOAD, // Ready for upload, metadata still being prepared |
183 | | UPLOADING, // Upload triggered, waiting for writer close to finish |
184 | | UPLOADED, // Upload completed |
185 | | FAILED // Upload failed |
186 | | }; |
187 | | |
188 | | struct PackedFileContext { |
189 | | std::string packed_file_path; |
190 | | std::unique_ptr<FileWriter> writer; |
191 | | std::unordered_map<std::string, PackedSliceHandlePtr> slice_locations; |
192 | | int64_t current_offset = 0; |
193 | | int64_t total_size = 0; |
194 | | int64_t create_time; |
195 | | int64_t upload_time = 0; |
196 | | std::chrono::steady_clock::time_point create_timestamp; |
197 | | std::optional<std::chrono::steady_clock::time_point> first_append_timestamp; |
198 | | std::optional<std::chrono::steady_clock::time_point> ready_to_upload_timestamp; |
199 | | std::optional<std::chrono::steady_clock::time_point> uploading_timestamp; |
200 | | std::atomic<PackedFileState> state {PackedFileState::INIT}; |
201 | | std::condition_variable upload_cv; |
202 | | std::mutex upload_mutex; |
203 | | std::string last_error; |
204 | | std::string resource_id; |
205 | | FileSystemSPtr file_system; |
206 | | }; |
207 | | |
208 | | // Create a new packed file state with file writer |
209 | | Status create_new_packed_file_context(const std::string& resource_id, |
210 | | std::unique_ptr<PackedFileContext>& packed_file_ctx); |
211 | | |
212 | | Status ensure_file_system(const std::string& resource_id, FileSystemSPtr* file_system); |
213 | | |
214 | | // Helper function to wait for packed file upload completion |
215 | | Status wait_for_packed_file_upload(PackedFileContext* packed_file_ptr); |
216 | | |
217 | | // Thread management |
218 | | std::atomic<bool> _stop_background_thread {false}; |
219 | | std::unique_ptr<std::thread> _background_thread; |
220 | | |
221 | | // File system |
222 | | FileSystemSPtr _default_file_system; |
223 | | std::unordered_map<std::string, FileSystemSPtr> _file_systems; |
224 | | std::mutex _file_system_mutex; |
225 | | |
226 | | // Current active packed file |
227 | | std::unordered_map<std::string, std::unique_ptr<PackedFileContext>> _current_packed_files; |
228 | | std::timed_mutex _current_packed_file_mutex; |
229 | | |
230 | | // Merge files ready for upload or being processed |
231 | | std::unordered_map<std::string, std::shared_ptr<PackedFileContext>> _uploading_packed_files; |
232 | | |
233 | | // Uploaded packed files (kept for some time for wait_write_done) |
234 | | std::unordered_map<std::string, std::shared_ptr<PackedFileContext>> _uploaded_packed_files; |
235 | | std::mutex _packed_files_mutex; |
236 | | |
237 | | // Global index mapping small file path to packed file index. It only serves readers |
238 | | // that look a file up by path; writers hold their own handle to the slice, so |
239 | | // recycling an entry here never invalidates a writer. |
240 | | std::unordered_map<std::string, PackedSliceHandlePtr> _global_slice_locations; |
241 | | std::mutex _global_index_mutex; |
242 | | |
243 | | #ifdef BE_TEST |
244 | | public: |
245 | | void reset_packed_file_bvars_for_test() const; |
246 | | int64_t packed_file_total_count_for_test() const; |
247 | | int64_t packed_file_total_small_file_num_for_test() const; |
248 | | int64_t packed_file_total_size_bytes_for_test() const; |
249 | | double packed_file_avg_small_file_num_for_test() const; |
250 | | double packed_file_avg_file_size_for_test() const; |
251 | | void record_packed_file_metrics_for_test(const PackedFileContext* packed_file); |
252 | | |
253 | | // Test-only helpers to introspect/clear internal state |
254 | | void clear_state_for_test(); |
255 | | auto& current_packed_files_for_test() { return _current_packed_files; } |
256 | | auto& uploading_packed_files_for_test() { return _uploading_packed_files; } |
257 | | auto& uploaded_packed_files_for_test() { return _uploaded_packed_files; } |
258 | | auto& global_slice_locations_for_test() { return _global_slice_locations; } |
259 | | auto& file_systems_for_test() { return _file_systems; } |
260 | | FileSystemSPtr& default_file_system_for_test() { return _default_file_system; } |
261 | | Status create_new_packed_file_state_for_test(const std::string& resource_id, |
262 | | std::unique_ptr<PackedFileContext>& ctx) { |
263 | | return create_new_packed_file_context(resource_id, ctx); |
264 | | } |
265 | | #endif |
266 | | }; |
267 | | |
268 | | } // namespace doris::io |