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, 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
48/// Region requester which processes flush, compact requests etc.
49pub 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    /// Handle the request to flush table.
71    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    /// Handle the request to build index for table.
105    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    /// Handle the request to compact table.
143    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    /// Handle the request to flush the region.
184    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    /// Handle the request to compact the region.
198    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, // todo(hl): maybe also support parameters in region compaction.
207            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    /// Discard all unflushed data from the region.
215    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    /// Discard all unflushed data from all regions of the table.
230    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}