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, CancelCompactionRequest, CompactRequest, CompactionTimeRange, FlushRequest,
21 RegionRequestHeader, TruncateRequest, Unflushed, build_index_request, 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::metric_engine_consts;
38use store_api::storage::RegionId;
39use table::requests::{BuildIndexTableRequest, CompactTableRequest, FlushTableRequest};
40use table::table_name::TableName;
41
42use crate::error::{
43 CatalogSnafu, FindRegionLeaderSnafu, FindTablePartitionRuleSnafu, JoinTaskSnafu,
44 NotSupportedSnafu, RequestRegionSnafu, Result, TableNotFoundSnafu,
45 UnsupportedRegionRequestSnafu,
46};
47use crate::region_req_factory::RegionRequestFactory;
48
49pub struct Requester {
51 catalog_manager: CatalogManagerRef,
52 partition_manager: PartitionRuleManagerRef,
53 node_manager: NodeManagerRef,
54}
55
56pub type RequesterRef = Arc<Requester>;
57
58impl Requester {
59 pub fn new(
60 catalog_manager: CatalogManagerRef,
61 partition_manager: PartitionRuleManagerRef,
62 node_manager: NodeManagerRef,
63 ) -> Self {
64 Self {
65 catalog_manager,
66 partition_manager,
67 node_manager,
68 }
69 }
70
71 pub async fn handle_table_flush(
73 &self,
74 request: FlushTableRequest,
75 ctx: QueryContextRef,
76 ) -> Result<AffectedRows> {
77 let partitions = &self
78 .get_table_partition_info(
79 &request.catalog_name,
80 &request.schema_name,
81 &request.table_name,
82 )
83 .await?
84 .partitions;
85
86 let requests = partitions
87 .iter()
88 .map(|partition| {
89 RegionRequestBody::Flush(FlushRequest {
90 region_id: partition.id.into(),
91 })
92 })
93 .collect();
94
95 info!("Handle table manual flush request: {:?}", request);
96
97 self.do_request(
98 requests,
99 Some(build_db_string(&request.catalog_name, &request.schema_name)),
100 &ctx,
101 )
102 .await
103 }
104
105 pub async fn handle_table_build_index(
107 &self,
108 request: BuildIndexTableRequest,
109 ctx: QueryContextRef,
110 ) -> Result<AffectedRows> {
111 if matches!(
112 request.options,
113 Some(build_index_request::Options::SeriesIndex(_))
114 ) {
115 let table = self
116 .catalog_manager
117 .table(
118 &request.catalog_name,
119 &request.schema_name,
120 &request.table_name,
121 None,
122 )
123 .await
124 .context(CatalogSnafu)?;
125 let table = table.with_context(|| TableNotFoundSnafu {
126 table_name: common_catalog::format_full_table_name(
127 &request.catalog_name,
128 &request.schema_name,
129 &request.table_name,
130 ),
131 })?;
132 let info = table.table_info();
133 ensure_build_series_index_supported(&info.meta.engine, info.is_physical_table())?;
134 }
135 let partitions = &self
136 .get_table_partition_info(
137 &request.catalog_name,
138 &request.schema_name,
139 &request.table_name,
140 )
141 .await?
142 .partitions;
143
144 let requests = partitions
145 .iter()
146 .map(|partition| {
147 RegionRequestBody::BuildIndex(BuildIndexRequest {
148 region_id: partition.id.into(),
149 options: request.options,
150 })
151 })
152 .collect();
153
154 info!(
155 "Handle table manual build index for table {}",
156 request.table_name
157 );
158 debug!("Request details: {:?}", request);
159
160 self.do_request(
161 requests,
162 Some(build_db_string(&request.catalog_name, &request.schema_name)),
163 &ctx,
164 )
165 .await
166 }
167
168 pub async fn handle_table_compaction(
170 &self,
171 request: CompactTableRequest,
172 ctx: QueryContextRef,
173 ) -> Result<AffectedRows> {
174 let partitions = &self
175 .get_table_partition_info(
176 &request.catalog_name,
177 &request.schema_name,
178 &request.table_name,
179 )
180 .await?
181 .partitions;
182
183 let time_range = request
184 .time_range
185 .map(to_pb_compaction_time_range)
186 .transpose()?;
187 let requests = partitions
188 .iter()
189 .map(|partition| {
190 RegionRequestBody::Compact(CompactRequest {
191 region_id: partition.id.into(),
192 parallelism: request.parallelism,
193 options: Some(request.compact_options),
194 time_range,
195 })
196 })
197 .collect();
198
199 info!("Handle table manual compaction request: {:?}", request);
200
201 self.do_request(
202 requests,
203 Some(build_db_string(&request.catalog_name, &request.schema_name)),
204 &ctx,
205 )
206 .await
207 }
208
209 pub async fn handle_region_flush(
211 &self,
212 region_id: RegionId,
213 ctx: QueryContextRef,
214 ) -> Result<AffectedRows> {
215 let request = RegionRequestBody::Flush(FlushRequest {
216 region_id: region_id.into(),
217 });
218
219 info!("Handle region manual flush request: {region_id}");
220 self.do_request(vec![request], None, &ctx).await
221 }
222
223 pub async fn handle_region_compaction(
225 &self,
226 region_id: RegionId,
227 ctx: QueryContextRef,
228 ) -> Result<AffectedRows> {
229 let request = RegionRequestBody::Compact(CompactRequest {
230 region_id: region_id.into(),
231 parallelism: 1,
232 options: None, time_range: None,
234 });
235
236 info!("Handle region manual compaction request: {region_id}");
237 self.do_request(vec![request], None, &ctx).await
238 }
239
240 pub async fn handle_cancel_region_compaction(
242 &self,
243 region_id: RegionId,
244 task_id: String,
245 ctx: QueryContextRef,
246 ) -> Result<AffectedRows> {
247 ensure!(
248 region_id.region_group() != metric_engine_consts::METRIC_METADATA_REGION_GROUP,
249 NotSupportedSnafu {
250 feat: "cancelling compaction for an internal metric metadata region",
251 }
252 );
253
254 let request = RegionRequestBody::CancelCompaction(CancelCompactionRequest {
255 region_id: region_id.into(),
256 task_id,
257 });
258 self.do_request(vec![request], None, &ctx).await
259 }
260
261 pub async fn handle_discard_unflushed_data(
263 &self,
264 region_id: RegionId,
265 ctx: QueryContextRef,
266 ) -> Result<AffectedRows> {
267 let request = RegionRequestBody::Truncate(TruncateRequest {
268 region_id: region_id.into(),
269 kind: Some(truncate_request::Kind::Unflushed(Unflushed {})),
270 });
271
272 info!("Handle region discard unflushed data request: {region_id}");
273 self.do_request(vec![request], None, &ctx).await
274 }
275
276 pub async fn handle_discard_unflushed_data_by_table(
278 &self,
279 table_name: TableName,
280 ctx: QueryContextRef,
281 ) -> Result<AffectedRows> {
282 let table = self
283 .catalog_manager
284 .table(
285 &table_name.catalog_name,
286 &table_name.schema_name,
287 &table_name.table_name,
288 None,
289 )
290 .await
291 .context(CatalogSnafu)?;
292 let table = table.with_context(|| TableNotFoundSnafu {
293 table_name: table_name.to_string(),
294 })?;
295 let table_info = table.table_info();
296 ensure_discard_unflushed_supported(
297 &table_info.meta.engine,
298 table_info.is_physical_table(),
299 )?;
300
301 let partitions = &self
302 .partition_manager
303 .find_physical_partition_info(table_info.ident.table_id)
304 .await
305 .with_context(|_| FindTablePartitionRuleSnafu {
306 table_name: table_name.to_string(),
307 })?
308 .partitions;
309 let requests = partitions
310 .iter()
311 .map(|partition| {
312 RegionRequestBody::Truncate(TruncateRequest {
313 region_id: partition.id.into(),
314 kind: Some(truncate_request::Kind::Unflushed(Unflushed {})),
315 })
316 })
317 .collect();
318
319 info!("Handle table discard unflushed data request: {table_name}");
320 self.do_request(
321 requests,
322 Some(build_db_string(
323 &table_name.catalog_name,
324 &table_name.schema_name,
325 )),
326 &ctx,
327 )
328 .await
329 }
330}
331
332fn to_pb_compaction_time_range(range: TimestampRange) -> Result<CompactionTimeRange> {
333 let (Some(start), Some(end)) = (*range.start(), *range.end()) else {
334 return crate::error::InvalidTimestampRangeSnafu {
335 start: format!("{:?}", range.start()),
336 end: format!("{:?}", range.end()),
337 }
338 .fail();
339 };
340 let original_start = start;
341 let original_end = end;
342 let start = start.convert_to(TimestampUnit::Second).with_context(|| {
343 crate::error::InvalidTimestampRangeSnafu {
344 start: format!("{original_start:?}"),
345 end: format!("{original_end:?}"),
346 }
347 })?;
348 let end = end
349 .convert_to_ceil(TimestampUnit::Second)
350 .with_context(|| crate::error::InvalidTimestampRangeSnafu {
351 start: format!("{original_start:?}"),
352 end: format!("{original_end:?}"),
353 })?;
354 ensure!(
355 start < end,
356 crate::error::InvalidTimestampRangeSnafu {
357 start: format!("{original_start:?}"),
358 end: format!("{original_end:?}"),
359 }
360 );
361
362 Ok(CompactionTimeRange {
363 start: start.value(),
364 end: end.value(),
365 time_unit: to_pb_time_unit(TimestampUnit::Second) as i32,
366 })
367}
368
369impl Requester {
370 async fn do_request(
371 &self,
372 requests: Vec<RegionRequestBody>,
373 db_string: Option<String>,
374 ctx: &QueryContextRef,
375 ) -> Result<AffectedRows> {
376 let request_factory = RegionRequestFactory::new(RegionRequestHeader {
377 tracing_context: TracingContext::from_current_span().to_w3c(),
378 dbname: db_string.unwrap_or_else(|| ctx.get_db_string()),
379 ..Default::default()
380 });
381
382 let tasks = requests.into_iter().map(|req_body| {
383 let request = request_factory.build_request(req_body.clone());
384 let partition_manager = self.partition_manager.clone();
385 let node_manager = self.node_manager.clone();
386 common_runtime::spawn_global(async move {
387 let peer =
388 Self::find_region_leader_by_request(partition_manager, &req_body).await?;
389 node_manager
390 .datanode(&peer)
391 .await
392 .handle(request)
393 .await
394 .context(RequestRegionSnafu)
395 })
396 });
397 let results = future::try_join_all(tasks).await.context(JoinTaskSnafu)?;
398
399 let affected_rows = results
400 .into_iter()
401 .map(|resp| resp.map(|r| r.affected_rows))
402 .sum::<Result<AffectedRows>>()?;
403
404 Ok(affected_rows)
405 }
406
407 async fn find_region_leader_by_request(
408 partition_manager: PartitionRuleManagerRef,
409 req: &RegionRequestBody,
410 ) -> Result<Peer> {
411 let region_id = match req {
412 RegionRequestBody::Flush(req) => req.region_id,
413 RegionRequestBody::Compact(req) => req.region_id,
414 RegionRequestBody::CancelCompaction(req) => req.region_id,
415 RegionRequestBody::BuildIndex(req) => req.region_id,
416 RegionRequestBody::Truncate(req) => req.region_id,
417 _ => {
418 error!("Unsupported region request: {:?}", req);
419 return UnsupportedRegionRequestSnafu {}.fail();
420 }
421 };
422
423 partition_manager
424 .find_region_leader(region_id.into())
425 .await
426 .context(FindRegionLeaderSnafu)
427 }
428
429 async fn get_table_partition_info(
430 &self,
431 catalog: &str,
432 schema: &str,
433 table_name: &str,
434 ) -> Result<Arc<PhysicalPartitionInfo>> {
435 let table = self
436 .catalog_manager
437 .table(catalog, schema, table_name, None)
438 .await
439 .context(CatalogSnafu)?;
440
441 let table = table.with_context(|| TableNotFoundSnafu {
442 table_name: common_catalog::format_full_table_name(catalog, schema, table_name),
443 })?;
444 let table_info = table.table_info();
445
446 self.partition_manager
447 .find_physical_partition_info(table_info.ident.table_id)
448 .await
449 .with_context(|_| FindTablePartitionRuleSnafu {
450 table_name: common_catalog::format_full_table_name(catalog, schema, table_name),
451 })
452 }
453}
454
455fn ensure_build_series_index_supported(engine: &str, is_physical_table: bool) -> Result<()> {
456 ensure!(
457 engine == METRIC_ENGINE && is_physical_table,
458 NotSupportedSnafu {
459 feat: "building series indexes requires a physical metric table",
460 }
461 );
462 Ok(())
463}
464
465fn ensure_discard_unflushed_supported(engine: &str, is_physical_table: bool) -> Result<()> {
466 ensure!(
467 engine != METRIC_ENGINE || is_physical_table,
468 NotSupportedSnafu {
469 feat: "discarding unflushed data from a Metric Engine logical table"
470 }
471 );
472 Ok(())
473}
474
475#[cfg(test)]
476mod tests {
477 use api::region::RegionResponse;
478 use api::v1::TimeUnit;
479 use common_error::ext::ErrorExt;
480 use common_error::status_code::StatusCode;
481 use common_meta::ddl::test_util::datanode_handler::DatanodeWatcher;
482 use common_meta::test_util::MockDatanodeManager;
483 use common_time::Timestamp;
484 use common_time::range::TimestampRange;
485 use session::context::QueryContext;
486
487 use super::*;
488 use crate::test_util;
489
490 #[tokio::test]
491 async fn test_cancel_region_compaction_routes_to_region_leader() {
492 let backend = test_util::prepare_mocked_backend().await;
493 let partition_manager = test_util::create_partition_rule_manager(backend).await;
494 let (sender, mut requests) = tokio::sync::mpsc::channel(4);
495 let watcher = DatanodeWatcher::new(sender).with_handler(|_, request| {
496 let Some(RegionRequestBody::CancelCompaction(request)) = request.body else {
497 panic!("expected cancellation through Handle");
498 };
499 Ok(RegionResponse::new(usize::from(
500 request.task_id == "live-cycle",
501 )))
502 });
503 let requester = Requester::new(
504 catalog::memory::MemoryCatalogManager::new(),
505 partition_manager,
506 Arc::new(MockDatanodeManager::new(watcher)),
507 );
508 let ctx = Arc::new(QueryContext::with("greptime", "cancel_test"));
509 for (region_number, task_id, affected_rows) in
510 [(1, "live-cycle", 1), (3, "absent-cycle", 0)]
511 {
512 let region_id = RegionId::new(1, region_number);
513 assert_eq!(
514 requester
515 .handle_cancel_region_compaction(region_id, task_id.into(), ctx.clone())
516 .await
517 .unwrap(),
518 affected_rows
519 );
520 let (peer, request) = requests.try_recv().unwrap();
521 assert_eq!(peer.id, u64::from(region_number));
522 assert_eq!(request.header.unwrap().dbname, ctx.get_db_string());
523 assert_eq!(
524 request.body,
525 Some(RegionRequestBody::CancelCompaction(
526 CancelCompactionRequest {
527 region_id: region_id.as_u64(),
528 task_id: task_id.into(),
529 }
530 ))
531 );
532 }
533 for table_id in [1, u32::MAX] {
535 let region_id = RegionId::with_group_and_seq(
536 table_id,
537 metric_engine_consts::METRIC_METADATA_REGION_GROUP,
538 1,
539 );
540 let error = requester
541 .handle_cancel_region_compaction(region_id, "live-cycle".into(), ctx.clone())
542 .await
543 .unwrap_err();
544 assert_eq!(error.status_code(), StatusCode::Unsupported);
545 assert!(matches!(error, crate::error::Error::NotSupported { .. }));
546 assert!(
547 error
548 .to_string()
549 .contains("internal metric metadata region")
550 );
551 }
552 assert!(requests.try_recv().is_err());
553 }
554
555 #[test]
556 fn test_build_series_index_requires_physical_metric_table() {
557 ensure_build_series_index_supported(METRIC_ENGINE, true).unwrap();
558 assert!(ensure_build_series_index_supported(METRIC_ENGINE, false).is_err());
559 assert!(ensure_build_series_index_supported("mito", false).is_err());
560 assert!(ensure_build_series_index_supported("mito", true).is_err());
561 }
562
563 #[test]
564 fn test_to_pb_compaction_time_range_normalizes_mixed_units() {
565 let range = TimestampRange::new(
566 Timestamp::new_millisecond(1_500),
567 Timestamp::new_microsecond(2_500_000),
568 )
569 .unwrap();
570
571 let pb_range = to_pb_compaction_time_range(range).unwrap();
572 assert_eq!(1, pb_range.start);
573 assert_eq!(3, pb_range.end);
574 assert_eq!(TimeUnit::Second as i32, pb_range.time_unit);
575 }
576
577 #[test]
578 fn test_discard_unflushed_rejects_metric_logical_table() {
579 let error = ensure_discard_unflushed_supported(METRIC_ENGINE, false).unwrap_err();
580 assert!(matches!(error, crate::error::Error::NotSupported { .. }));
581
582 ensure_discard_unflushed_supported(METRIC_ENGINE, true).unwrap();
583 ensure_discard_unflushed_supported("mito", false).unwrap();
584 }
585
586 #[test]
587 fn test_to_pb_compaction_time_range() {
588 let range = TimestampRange::new(
589 Timestamp::new_microsecond(1_000),
590 Timestamp::new_microsecond(2_000),
591 )
592 .unwrap();
593
594 let pb_range = to_pb_compaction_time_range(range).unwrap();
595 assert_eq!(0, pb_range.start);
596 assert_eq!(1, pb_range.end);
597 assert_eq!(TimeUnit::Second as i32, pb_range.time_unit);
598 }
599}