Skip to main content

operator/
request.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
49/// Region requester which processes flush, compact requests etc.
50pub 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    /// Handle the request to flush table.
72    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    /// Handle the request to build index for table.
106    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    /// Handle the request to compact table.
169    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    /// Handle the request to flush the region.
210    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    /// Handle the request to compact the region.
224    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, // todo(hl): maybe also support parameters in region compaction.
233            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    /// Cancels one identified compaction task through the region leader.
241    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    /// Discard all unflushed data from the region.
262    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    /// Discard all unflushed data from all regions of the table.
277    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        // Metadata regions are rejected before routing, even without a table route.
534        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}