be/src/service/http/action/compaction_profile_action.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 "service/http/action/compaction_profile_action.h" |
19 | | |
20 | | #include <rapidjson/document.h> |
21 | | #include <rapidjson/prettywriter.h> |
22 | | #include <rapidjson/stringbuffer.h> |
23 | | |
24 | | #include <ctime> |
25 | | #include <exception> |
26 | | #include <string> |
27 | | |
28 | | #include "common/logging.h" |
29 | | #include "service/http/http_channel.h" |
30 | | #include "service/http/http_headers.h" |
31 | | #include "service/http/http_request.h" |
32 | | #include "service/http/http_status.h" |
33 | | #include "storage/compaction_task_tracker.h" |
34 | | |
35 | | namespace doris { |
36 | | |
37 | | #include "common/compile_check_begin.h" |
38 | | |
39 | | namespace { |
40 | | |
41 | | // Format millisecond timestamp to "YYYY-MM-DD HH:MM:SS" string. |
42 | | // Returns empty string for 0 timestamps. |
43 | 0 | std::string format_timestamp_ms(int64_t timestamp_ms) { |
44 | 0 | if (timestamp_ms <= 0) { |
45 | 0 | return ""; |
46 | 0 | } |
47 | 0 | time_t ts = static_cast<time_t>(timestamp_ms / 1000); |
48 | 0 | struct tm local_tm; |
49 | 0 | if (localtime_r(&ts, &local_tm) == nullptr) { |
50 | 0 | return ""; |
51 | 0 | } |
52 | 0 | char buf[64]; |
53 | 0 | strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &local_tm); |
54 | 0 | return std::string(buf); |
55 | 0 | } |
56 | | |
57 | | } // namespace |
58 | | |
59 | | CompactionProfileAction::CompactionProfileAction(ExecEnv* exec_env, TPrivilegeHier::type hier, |
60 | | TPrivilegeType::type ptype) |
61 | 0 | : HttpHandlerWithAuth(exec_env, hier, ptype) {} |
62 | | |
63 | 0 | void CompactionProfileAction::handle(HttpRequest* req) { |
64 | 0 | req->add_output_header(HttpHeaders::CONTENT_TYPE, HttpHeaders::JSON_TYPE.data()); |
65 | | |
66 | | // Parse optional parameters |
67 | 0 | int64_t tablet_id = 0; |
68 | 0 | int64_t top_n = 0; |
69 | 0 | std::string compact_type; |
70 | 0 | int success_filter = -1; // -1 = no filter, 0 = failed only, 1 = success only |
71 | | |
72 | | // tablet_id |
73 | 0 | const auto& tablet_id_str = req->param("tablet_id"); |
74 | 0 | if (!tablet_id_str.empty()) { |
75 | 0 | try { |
76 | 0 | tablet_id = std::stoll(tablet_id_str); |
77 | 0 | if (tablet_id < 0) { |
78 | 0 | HttpChannel::send_reply(req, HttpStatus::BAD_REQUEST, |
79 | 0 | R"({"status":"Error","msg":"tablet_id must be >= 0"})"); |
80 | 0 | return; |
81 | 0 | } |
82 | 0 | } catch (const std::exception& e) { |
83 | 0 | auto msg = R"({"status":"Error","msg":"invalid tablet_id: )" + std::string(e.what()) + |
84 | 0 | "\"}"; |
85 | 0 | HttpChannel::send_reply(req, HttpStatus::BAD_REQUEST, msg); |
86 | 0 | return; |
87 | 0 | } |
88 | 0 | } |
89 | | |
90 | | // top_n |
91 | 0 | const auto& top_n_str = req->param("top_n"); |
92 | 0 | if (!top_n_str.empty()) { |
93 | 0 | try { |
94 | 0 | top_n = std::stoll(top_n_str); |
95 | 0 | if (top_n < 0) { |
96 | 0 | HttpChannel::send_reply(req, HttpStatus::BAD_REQUEST, |
97 | 0 | R"({"status":"Error","msg":"top_n must be >= 0"})"); |
98 | 0 | return; |
99 | 0 | } |
100 | 0 | } catch (const std::exception& e) { |
101 | 0 | auto msg = |
102 | 0 | R"({"status":"Error","msg":"invalid top_n: )" + std::string(e.what()) + "\"}"; |
103 | 0 | HttpChannel::send_reply(req, HttpStatus::BAD_REQUEST, msg); |
104 | 0 | return; |
105 | 0 | } |
106 | 0 | } |
107 | | |
108 | | // compact_type |
109 | 0 | compact_type = req->param("compact_type"); |
110 | 0 | if (!compact_type.empty() && compact_type != "base" && compact_type != "cumulative" && |
111 | 0 | compact_type != "full") { |
112 | 0 | HttpChannel::send_reply( |
113 | 0 | req, HttpStatus::BAD_REQUEST, |
114 | 0 | R"({"status":"Error","msg":"compact_type must be one of: base, cumulative, full"})"); |
115 | 0 | return; |
116 | 0 | } |
117 | | |
118 | | // success |
119 | 0 | const auto& success_str = req->param("success"); |
120 | 0 | if (!success_str.empty()) { |
121 | 0 | if (success_str == "true") { |
122 | 0 | success_filter = 1; |
123 | 0 | } else if (success_str == "false") { |
124 | 0 | success_filter = 0; |
125 | 0 | } else { |
126 | 0 | HttpChannel::send_reply( |
127 | 0 | req, HttpStatus::BAD_REQUEST, |
128 | 0 | R"({"status":"Error","msg":"success must be 'true' or 'false'"})"); |
129 | 0 | return; |
130 | 0 | } |
131 | 0 | } |
132 | | |
133 | | // Get completed tasks from tracker |
134 | 0 | auto tasks = CompactionTaskTracker::instance()->get_completed_tasks( |
135 | 0 | tablet_id, top_n, compact_type, success_filter); |
136 | | |
137 | | // Build JSON response |
138 | 0 | rapidjson::Document root; |
139 | 0 | root.SetObject(); |
140 | 0 | auto& allocator = root.GetAllocator(); |
141 | |
|
142 | 0 | root.AddMember("status", "Success", allocator); |
143 | |
|
144 | 0 | rapidjson::Value profiles(rapidjson::kArrayType); |
145 | |
|
146 | 0 | for (const auto& task : tasks) { |
147 | 0 | rapidjson::Value profile(rapidjson::kObjectType); |
148 | |
|
149 | 0 | profile.AddMember("compaction_id", task.compaction_id, allocator); |
150 | |
|
151 | 0 | { |
152 | 0 | rapidjson::Value v; |
153 | 0 | v.SetString(to_string(task.compaction_type), allocator); |
154 | 0 | profile.AddMember("compaction_type", v, allocator); |
155 | 0 | } |
156 | |
|
157 | 0 | profile.AddMember("tablet_id", task.tablet_id, allocator); |
158 | 0 | profile.AddMember("table_id", task.table_id, allocator); |
159 | 0 | profile.AddMember("partition_id", task.partition_id, allocator); |
160 | |
|
161 | 0 | { |
162 | 0 | rapidjson::Value v; |
163 | 0 | v.SetString(to_string(task.trigger_method), allocator); |
164 | 0 | profile.AddMember("trigger_method", v, allocator); |
165 | 0 | } |
166 | |
|
167 | 0 | profile.AddMember("compaction_score", task.compaction_score, allocator); |
168 | | |
169 | | // Datetime fields |
170 | 0 | { |
171 | 0 | auto s = format_timestamp_ms(task.scheduled_time_ms); |
172 | 0 | rapidjson::Value v; |
173 | 0 | v.SetString(s.c_str(), static_cast<rapidjson::SizeType>(s.size()), allocator); |
174 | 0 | profile.AddMember("scheduled_time", v, allocator); |
175 | 0 | } |
176 | 0 | { |
177 | 0 | auto s = format_timestamp_ms(task.start_time_ms); |
178 | 0 | rapidjson::Value v; |
179 | 0 | v.SetString(s.c_str(), static_cast<rapidjson::SizeType>(s.size()), allocator); |
180 | 0 | profile.AddMember("start_time", v, allocator); |
181 | 0 | } |
182 | 0 | { |
183 | 0 | auto s = format_timestamp_ms(task.end_time_ms); |
184 | 0 | rapidjson::Value v; |
185 | 0 | v.SetString(s.c_str(), static_cast<rapidjson::SizeType>(s.size()), allocator); |
186 | 0 | profile.AddMember("end_time", v, allocator); |
187 | 0 | } |
188 | | |
189 | | // Derived: cost_time_ms = end_time_ms - start_time_ms |
190 | 0 | int64_t cost_time_ms = 0; |
191 | 0 | if (task.start_time_ms > 0 && task.end_time_ms > 0) { |
192 | 0 | cost_time_ms = task.end_time_ms - task.start_time_ms; |
193 | 0 | } |
194 | 0 | profile.AddMember("cost_time_ms", cost_time_ms, allocator); |
195 | | |
196 | | // Derived: success = (status == FINISHED) |
197 | 0 | bool success = (task.status == CompactionTaskStatus::FINISHED); |
198 | 0 | profile.AddMember("success", success, allocator); |
199 | | |
200 | | // Input statistics |
201 | 0 | profile.AddMember("input_rowsets_count", task.input_rowsets_count, allocator); |
202 | 0 | profile.AddMember("input_row_num", task.input_row_num, allocator); |
203 | 0 | profile.AddMember("input_data_size", task.input_data_size, allocator); |
204 | 0 | profile.AddMember("input_index_size", task.input_index_size, allocator); |
205 | 0 | profile.AddMember("input_total_size", task.input_total_size, allocator); |
206 | 0 | profile.AddMember("input_segments_num", task.input_segments_num, allocator); |
207 | |
|
208 | 0 | { |
209 | 0 | rapidjson::Value v; |
210 | 0 | v.SetString(task.input_version_range.c_str(), |
211 | 0 | static_cast<rapidjson::SizeType>(task.input_version_range.size()), |
212 | 0 | allocator); |
213 | 0 | profile.AddMember("input_version_range", v, allocator); |
214 | 0 | } |
215 | | |
216 | | // Output statistics |
217 | 0 | profile.AddMember("merged_rows", task.merged_rows, allocator); |
218 | 0 | profile.AddMember("filtered_rows", task.filtered_rows, allocator); |
219 | 0 | profile.AddMember("output_rows", task.output_rows, allocator); |
220 | 0 | profile.AddMember("output_row_num", task.output_row_num, allocator); |
221 | 0 | profile.AddMember("output_data_size", task.output_data_size, allocator); |
222 | 0 | profile.AddMember("output_index_size", task.output_index_size, allocator); |
223 | 0 | profile.AddMember("output_total_size", task.output_total_size, allocator); |
224 | 0 | profile.AddMember("output_segments_num", task.output_segments_num, allocator); |
225 | |
|
226 | 0 | { |
227 | 0 | rapidjson::Value v; |
228 | 0 | v.SetString(task.output_version.c_str(), |
229 | 0 | static_cast<rapidjson::SizeType>(task.output_version.size()), allocator); |
230 | 0 | profile.AddMember("output_version", v, allocator); |
231 | 0 | } |
232 | | |
233 | | // Merge performance |
234 | 0 | profile.AddMember("merge_latency_ms", task.merge_latency_ms, allocator); |
235 | | |
236 | | // IO statistics |
237 | 0 | profile.AddMember("bytes_read_from_local", task.bytes_read_from_local, allocator); |
238 | 0 | profile.AddMember("bytes_read_from_remote", task.bytes_read_from_remote, allocator); |
239 | | |
240 | | // Resources |
241 | 0 | profile.AddMember("peak_memory_bytes", task.peak_memory_bytes, allocator); |
242 | 0 | profile.AddMember("is_vertical", task.is_vertical, allocator); |
243 | 0 | profile.AddMember("permits", task.permits, allocator); |
244 | | |
245 | | // Vertical compaction progress |
246 | 0 | profile.AddMember("vertical_total_groups", task.vertical_total_groups, allocator); |
247 | 0 | profile.AddMember("vertical_completed_groups", task.vertical_completed_groups, allocator); |
248 | | |
249 | | // Status message (only for failed tasks) |
250 | 0 | if (!task.status_msg.empty()) { |
251 | 0 | rapidjson::Value v; |
252 | 0 | v.SetString(task.status_msg.c_str(), |
253 | 0 | static_cast<rapidjson::SizeType>(task.status_msg.size()), allocator); |
254 | 0 | profile.AddMember("status_msg", v, allocator); |
255 | 0 | } |
256 | |
|
257 | 0 | profiles.PushBack(profile, allocator); |
258 | 0 | } |
259 | |
|
260 | 0 | root.AddMember("compaction_profiles", profiles, allocator); |
261 | |
|
262 | 0 | rapidjson::StringBuffer str_buf; |
263 | 0 | rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(str_buf); |
264 | 0 | root.Accept(writer); |
265 | |
|
266 | 0 | HttpChannel::send_reply(req, HttpStatus::OK, str_buf.GetString()); |
267 | 0 | } |
268 | | |
269 | | #include "common/compile_check_end.h" |
270 | | } // namespace doris |