1use std::sync::Arc;
16
17use api::helper::to_pb_time_unit;
18use api::v1::region::region_request::Body as RegionRequestBody;
19use api::v1::region::{
20 BuildIndexRequest, CompactRequest, CompactionTimeRange, FlushRequest, RegionRequestHeader,
21 TruncateRequest, Unflushed, truncate_request,
22};
23use catalog::CatalogManagerRef;
24use common_catalog::build_db_string;
25use common_catalog::consts::METRIC_ENGINE;
26use common_meta::node_manager::{AffectedRows, NodeManagerRef};
27use common_meta::peer::Peer;
28use common_telemetry::tracing_context::TracingContext;
29use common_telemetry::{debug, error, info};
30use common_time::range::TimestampRange;
31use common_time::timestamp::TimeUnit as TimestampUnit;
32use futures_util::future;
33use partition::cache::PhysicalPartitionInfo;
34use partition::manager::PartitionRuleManagerRef;
35use session::context::QueryContextRef;
36use snafu::prelude::*;
37use store_api::storage::RegionId;
38use table::requests::{BuildIndexTableRequest, CompactTableRequest, FlushTableRequest};
39use table::table_name::TableName;
40
41use crate::error::{
42 CatalogSnafu, FindRegionLeaderSnafu, FindTablePartitionRuleSnafu, JoinTaskSnafu,
43 NotSupportedSnafu, RequestRegionSnafu, Result, TableNotFoundSnafu,
44 UnsupportedRegionRequestSnafu,
45};
46use crate::region_req_factory::RegionRequestFactory;
47
48pub struct Requester {
50 catalog_manager: CatalogManagerRef,
51 partition_manager: PartitionRuleManagerRef,
52 node_manager: NodeManagerRef,
53}
54
55pub type RequesterRef = Arc<Requester>;
56
57impl Requester {
58 pub fn new(
59 catalog_manager: CatalogManagerRef,
60 partition_manager: PartitionRuleManagerRef,
61 node_manager: NodeManagerRef,
62 ) -> Self {
63 Self {
64 catalog_manager,
65 partition_manager,
66 node_manager,
67 }
68 }
69
70 pub async fn handle_table_flush(
72 &self,
73 request: FlushTableRequest,
74 ctx: QueryContextRef,
75 ) -> Result<AffectedRows> {
76 let partitions = &self
77 .get_table_partition_info(
78 &request.catalog_name,
79 &request.schema_name,
80 &request.table_name,
81 )
82 .await?
83 .partitions;
84
85 let requests = partitions
86 .iter()
87 .map(|partition| {
88 RegionRequestBody::Flush(FlushRequest {
89 region_id: partition.id.into(),
90 })
91 })
92 .collect();
93
94 info!("Handle table manual flush request: {:?}", request);
95
96 self.do_request(
97 requests,
98 Some(build_db_string(&request.catalog_name, &request.schema_name)),
99 &ctx,
100 )
101 .await
102 }
103
104 pub async fn handle_table_build_index(
106 &self,
107 request: BuildIndexTableRequest,
108 ctx: QueryContextRef,
109 ) -> Result<AffectedRows> {
110 let partitions = &self
111 .get_table_partition_info(
112 &request.catalog_name,
113 &request.schema_name,
114 &request.table_name,
115 )
116 .await?
117 .partitions;
118
119 let requests = partitions
120 .iter()
121 .map(|partition| {
122 RegionRequestBody::BuildIndex(BuildIndexRequest {
123 region_id: partition.id.into(),
124 })
125 })
126 .collect();
127
128 info!(
129 "Handle table manual build index for table {}",
130 request.table_name
131 );
132 debug!("Request details: {:?}", request);
133
134 self.do_request(
135 requests,
136 Some(build_db_string(&request.catalog_name, &request.schema_name)),
137 &ctx,
138 )
139 .await
140 }
141
142 pub async fn handle_table_compaction(
144 &self,
145 request: CompactTableRequest,
146 ctx: QueryContextRef,
147 ) -> Result<AffectedRows> {
148 let partitions = &self
149 .get_table_partition_info(
150 &request.catalog_name,
151 &request.schema_name,
152 &request.table_name,
153 )
154 .await?
155 .partitions;
156
157 let time_range = request
158 .time_range
159 .map(to_pb_compaction_time_range)
160 .transpose()?;
161 let requests = partitions
162 .iter()
163 .map(|partition| {
164 RegionRequestBody::Compact(CompactRequest {
165 region_id: partition.id.into(),
166 parallelism: request.parallelism,
167 options: Some(request.compact_options),
168 time_range,
169 })
170 })
171 .collect();
172
173 info!("Handle table manual compaction request: {:?}", request);
174
175 self.do_request(
176 requests,
177 Some(build_db_string(&request.catalog_name, &request.schema_name)),
178 &ctx,
179 )
180 .await
181 }
182
183 pub async fn handle_region_flush(
185 &self,
186 region_id: RegionId,
187 ctx: QueryContextRef,
188 ) -> Result<AffectedRows> {
189 let request = RegionRequestBody::Flush(FlushRequest {
190 region_id: region_id.into(),
191 });
192
193 info!("Handle region manual flush request: {region_id}");
194 self.do_request(vec![request], None, &ctx).await
195 }
196
197 pub async fn handle_region_compaction(
199 &self,
200 region_id: RegionId,
201 ctx: QueryContextRef,
202 ) -> Result<AffectedRows> {
203 let request = RegionRequestBody::Compact(CompactRequest {
204 region_id: region_id.into(),
205 parallelism: 1,
206 options: None, time_range: None,
208 });
209
210 info!("Handle region manual compaction request: {region_id}");
211 self.do_request(vec![request], None, &ctx).await
212 }
213
214 pub async fn handle_discard_unflushed_data(
216 &self,
217 region_id: RegionId,
218 ctx: QueryContextRef,
219 ) -> Result<AffectedRows> {
220 let request = RegionRequestBody::Truncate(TruncateRequest {
221 region_id: region_id.into(),
222 kind: Some(truncate_request::Kind::Unflushed(Unflushed {})),
223 });
224
225 info!("Handle region discard unflushed data request: {region_id}");
226 self.do_request(vec![request], None, &ctx).await
227 }
228
229 pub async fn handle_discard_unflushed_data_by_table(
231 &self,
232 table_name: TableName,
233 ctx: QueryContextRef,
234 ) -> Result<AffectedRows> {
235 let table = self
236 .catalog_manager
237 .table(
238 &table_name.catalog_name,
239 &table_name.schema_name,
240 &table_name.table_name,
241 None,
242 )
243 .await
244 .context(CatalogSnafu)?;
245 let table = table.with_context(|| TableNotFoundSnafu {
246 table_name: table_name.to_string(),
247 })?;
248 let table_info = table.table_info();
249 ensure_discard_unflushed_supported(
250 &table_info.meta.engine,
251 table_info.is_physical_table(),
252 )?;
253
254 let partitions = &self
255 .partition_manager
256 .find_physical_partition_info(table_info.ident.table_id)
257 .await
258 .with_context(|_| FindTablePartitionRuleSnafu {
259 table_name: table_name.to_string(),
260 })?
261 .partitions;
262 let requests = partitions
263 .iter()
264 .map(|partition| {
265 RegionRequestBody::Truncate(TruncateRequest {
266 region_id: partition.id.into(),
267 kind: Some(truncate_request::Kind::Unflushed(Unflushed {})),
268 })
269 })
270 .collect();
271
272 info!("Handle table discard unflushed data request: {table_name}");
273 self.do_request(
274 requests,
275 Some(build_db_string(
276 &table_name.catalog_name,
277 &table_name.schema_name,
278 )),
279 &ctx,
280 )
281 .await
282 }
283}
284
285fn to_pb_compaction_time_range(range: TimestampRange) -> Result<CompactionTimeRange> {
286 let (Some(start), Some(end)) = (*range.start(), *range.end()) else {
287 return crate::error::InvalidTimestampRangeSnafu {
288 start: format!("{:?}", range.start()),
289 end: format!("{:?}", range.end()),
290 }
291 .fail();
292 };
293 let original_start = start;
294 let original_end = end;
295 let start = start.convert_to(TimestampUnit::Second).with_context(|| {
296 crate::error::InvalidTimestampRangeSnafu {
297 start: format!("{original_start:?}"),
298 end: format!("{original_end:?}"),
299 }
300 })?;
301 let end = end
302 .convert_to_ceil(TimestampUnit::Second)
303 .with_context(|| crate::error::InvalidTimestampRangeSnafu {
304 start: format!("{original_start:?}"),
305 end: format!("{original_end:?}"),
306 })?;
307 ensure!(
308 start < end,
309 crate::error::InvalidTimestampRangeSnafu {
310 start: format!("{original_start:?}"),
311 end: format!("{original_end:?}"),
312 }
313 );
314
315 Ok(CompactionTimeRange {
316 start: start.value(),
317 end: end.value(),
318 time_unit: to_pb_time_unit(TimestampUnit::Second) as i32,
319 })
320}
321
322impl Requester {
323 async fn do_request(
324 &self,
325 requests: Vec<RegionRequestBody>,
326 db_string: Option<String>,
327 ctx: &QueryContextRef,
328 ) -> Result<AffectedRows> {
329 let request_factory = RegionRequestFactory::new(RegionRequestHeader {
330 tracing_context: TracingContext::from_current_span().to_w3c(),
331 dbname: db_string.unwrap_or_else(|| ctx.get_db_string()),
332 ..Default::default()
333 });
334
335 let tasks = requests.into_iter().map(|req_body| {
336 let request = request_factory.build_request(req_body.clone());
337 let partition_manager = self.partition_manager.clone();
338 let node_manager = self.node_manager.clone();
339 common_runtime::spawn_global(async move {
340 let peer =
341 Self::find_region_leader_by_request(partition_manager, &req_body).await?;
342 node_manager
343 .datanode(&peer)
344 .await
345 .handle(request)
346 .await
347 .context(RequestRegionSnafu)
348 })
349 });
350 let results = future::try_join_all(tasks).await.context(JoinTaskSnafu)?;
351
352 let affected_rows = results
353 .into_iter()
354 .map(|resp| resp.map(|r| r.affected_rows))
355 .sum::<Result<AffectedRows>>()?;
356
357 Ok(affected_rows)
358 }
359
360 async fn find_region_leader_by_request(
361 partition_manager: PartitionRuleManagerRef,
362 req: &RegionRequestBody,
363 ) -> Result<Peer> {
364 let region_id = match req {
365 RegionRequestBody::Flush(req) => req.region_id,
366 RegionRequestBody::Compact(req) => req.region_id,
367 RegionRequestBody::BuildIndex(req) => req.region_id,
368 RegionRequestBody::Truncate(req) => req.region_id,
369 _ => {
370 error!("Unsupported region request: {:?}", req);
371 return UnsupportedRegionRequestSnafu {}.fail();
372 }
373 };
374
375 partition_manager
376 .find_region_leader(region_id.into())
377 .await
378 .context(FindRegionLeaderSnafu)
379 }
380
381 async fn get_table_partition_info(
382 &self,
383 catalog: &str,
384 schema: &str,
385 table_name: &str,
386 ) -> Result<Arc<PhysicalPartitionInfo>> {
387 let table = self
388 .catalog_manager
389 .table(catalog, schema, table_name, None)
390 .await
391 .context(CatalogSnafu)?;
392
393 let table = table.with_context(|| TableNotFoundSnafu {
394 table_name: common_catalog::format_full_table_name(catalog, schema, table_name),
395 })?;
396 let table_info = table.table_info();
397
398 self.partition_manager
399 .find_physical_partition_info(table_info.ident.table_id)
400 .await
401 .with_context(|_| FindTablePartitionRuleSnafu {
402 table_name: common_catalog::format_full_table_name(catalog, schema, table_name),
403 })
404 }
405}
406
407fn ensure_discard_unflushed_supported(engine: &str, is_physical_table: bool) -> Result<()> {
408 ensure!(
409 engine != METRIC_ENGINE || is_physical_table,
410 NotSupportedSnafu {
411 feat: "discarding unflushed data from a Metric Engine logical table"
412 }
413 );
414 Ok(())
415}
416
417#[cfg(test)]
418mod tests {
419 use api::v1::TimeUnit;
420 use common_time::Timestamp;
421 use common_time::range::TimestampRange;
422
423 use super::*;
424
425 #[test]
426 fn test_to_pb_compaction_time_range_normalizes_mixed_units() {
427 let range = TimestampRange::new(
428 Timestamp::new_millisecond(1_500),
429 Timestamp::new_microsecond(2_500_000),
430 )
431 .unwrap();
432
433 let pb_range = to_pb_compaction_time_range(range).unwrap();
434 assert_eq!(1, pb_range.start);
435 assert_eq!(3, pb_range.end);
436 assert_eq!(TimeUnit::Second as i32, pb_range.time_unit);
437 }
438
439 #[test]
440 fn test_discard_unflushed_rejects_metric_logical_table() {
441 let error = ensure_discard_unflushed_supported(METRIC_ENGINE, false).unwrap_err();
442 assert!(matches!(error, crate::error::Error::NotSupported { .. }));
443
444 ensure_discard_unflushed_supported(METRIC_ENGINE, true).unwrap();
445 ensure_discard_unflushed_supported("mito", false).unwrap();
446 }
447
448 #[test]
449 fn test_to_pb_compaction_time_range() {
450 let range = TimestampRange::new(
451 Timestamp::new_microsecond(1_000),
452 Timestamp::new_microsecond(2_000),
453 )
454 .unwrap();
455
456 let pb_range = to_pb_compaction_time_range(range).unwrap();
457 assert_eq!(0, pb_range.start);
458 assert_eq!(1, pb_range.end);
459 assert_eq!(TimeUnit::Second as i32, pb_range.time_unit);
460 }
461}