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