Skip to main content

servers/batcher/logical_table/
region_write.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::collections::HashMap;
16
17use api::v1::region::{
18    BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
19};
20use api::v1::{ArrowIpc, PartitionExprVersion};
21use arrow::compute;
22use arrow::record_batch::RecordBatch;
23use async_trait::async_trait;
24use catalog::CatalogManagerRef;
25use common_grpc::error::Error as GrpcError;
26use common_grpc::flight;
27use common_telemetry::tracing_context::TracingContext;
28use operator::write_dispatcher::WriteDispatcherRef;
29use partition::manager::PartitionRuleManagerRef;
30use partition::partition::PartitionRuleRef;
31use snafu::ResultExt;
32use store_api::storage::RegionId;
33use table::metadata::{TableId, TableInfo, TableInfoRef};
34
35use crate::error;
36use crate::error::{Error, Result};
37use crate::metrics;
38
39#[derive(Clone)]
40pub struct PhysicalTableMetadata {
41    pub table_info: TableInfoRef,
42    /// Mapping from column name to column id
43    pub col_name_to_ids: Option<HashMap<String, u32>>,
44}
45
46#[async_trait]
47pub trait PhysicalFlushCatalogProvider: Send + Sync {
48    async fn physical_table(
49        &self,
50        catalog: &str,
51        schema: &str,
52        table_name: &str,
53        query_ctx: &session::context::QueryContext,
54    ) -> catalog::error::Result<Option<PhysicalTableMetadata>>;
55}
56
57/// Loads the physical partition rule and its expression versions.
58#[async_trait]
59pub trait PhysicalFlushPartitionProvider: Send + Sync {
60    /// Loads a routing snapshot for the physical table.
61    async fn find_table_partition_rule(
62        &self,
63        table_info: &TableInfo,
64    ) -> partition::error::Result<(PartitionRuleRef, HashMap<u32, Option<u64>>)>;
65}
66
67/// Dispatches the region requests produced by a physical flush.
68#[async_trait]
69pub trait PhysicalFlushWriteDispatcher: Send + Sync {
70    /// Dispatches one physical flush together, sharing routing refreshes across its writes.
71    async fn dispatch(&self, requests: Vec<RegionRequest>) -> Result<usize>;
72}
73
74#[derive(Clone)]
75pub(in crate::batcher::logical_table) struct CatalogManagerPhysicalFlushAdapter {
76    pub(in crate::batcher::logical_table) catalog_manager: CatalogManagerRef,
77}
78
79#[async_trait]
80impl PhysicalFlushCatalogProvider for CatalogManagerPhysicalFlushAdapter {
81    async fn physical_table(
82        &self,
83        catalog: &str,
84        schema: &str,
85        table_name: &str,
86        query_ctx: &session::context::QueryContext,
87    ) -> catalog::error::Result<Option<PhysicalTableMetadata>> {
88        self.catalog_manager
89            .table(catalog, schema, table_name, Some(query_ctx))
90            .await
91            .map(|table| {
92                table.map(|table| {
93                    let table_info = table.table_info();
94                    let name_to_ids = table_info.name_to_ids();
95                    PhysicalTableMetadata {
96                        table_info,
97                        col_name_to_ids: name_to_ids,
98                    }
99                })
100            })
101    }
102}
103
104#[derive(Clone)]
105pub(in crate::batcher::logical_table) struct PartitionManagerPhysicalFlushAdapter {
106    pub(in crate::batcher::logical_table) partition_manager: PartitionRuleManagerRef,
107}
108
109#[async_trait]
110impl PhysicalFlushPartitionProvider for PartitionManagerPhysicalFlushAdapter {
111    async fn find_table_partition_rule(
112        &self,
113        table_info: &TableInfo,
114    ) -> partition::error::Result<(PartitionRuleRef, HashMap<u32, Option<u64>>)> {
115        self.partition_manager
116            .find_table_partition_rule(table_info)
117            .await
118    }
119}
120
121/// Sends a physical flush through the shared write dispatcher.
122#[derive(Clone)]
123pub(crate) struct WriteDispatcherPhysicalFlushAdapter {
124    pub(crate) write_dispatcher: WriteDispatcherRef,
125}
126
127#[async_trait]
128impl PhysicalFlushWriteDispatcher for WriteDispatcherPhysicalFlushAdapter {
129    async fn dispatch(&self, requests: Vec<RegionRequest>) -> Result<usize> {
130        self.write_dispatcher
131            .dispatch_with_metrics(
132                requests,
133                metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
134                    .with_label_values(&["flush_write_region"]),
135            )
136            .await
137            .context(error::CommonMetaSnafu)
138            .map(|response| response.affected_rows)
139    }
140}
141
142/// Arrow rows selected for one physical region.
143pub(crate) struct PlannedRegionBatch {
144    pub(crate) region_id: RegionId,
145    pub(crate) batch: RecordBatch,
146}
147
148#[cfg(test)]
149impl PlannedRegionBatch {
150    pub(crate) fn num_rows(&self) -> usize {
151        self.batch.num_rows()
152    }
153}
154
155pub(in crate::batcher::logical_table) fn split_combined_batch_by_region(
156    combined_batch: &RecordBatch,
157    partition_rule: &dyn partition::partition::PartitionRule,
158) -> Result<HashMap<u32, partition::partition::RegionMask>> {
159    let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
160        .with_label_values(&["flush_physical_split_record_batch"])
161        .start_timer();
162    let map = partition_rule.split_record_batch(combined_batch)?;
163    Ok(map)
164}
165
166/// Selects the rows assigned to one physical region.
167pub(crate) fn plan_region_batch(
168    combined_batch: &RecordBatch,
169    physical_table_id: TableId,
170    region_number: u32,
171    mask: &partition::partition::RegionMask,
172) -> Result<Option<PlannedRegionBatch>> {
173    if mask.select_none() {
174        return Ok(None);
175    }
176
177    let region_batch = if mask.select_all() {
178        combined_batch.clone()
179    } else {
180        let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
181            .with_label_values(&["flush_physical_filter_record_batch"])
182            .start_timer();
183        compute::filter_record_batch(combined_batch, mask.array()).context(error::ArrowSnafu)?
184    };
185
186    let row_count = region_batch.num_rows();
187    if row_count == 0 {
188        return Ok(None);
189    }
190
191    Ok(Some(PlannedRegionBatch {
192        region_id: RegionId::new(physical_table_id, region_number),
193        batch: region_batch,
194    }))
195}
196
197/// Partitions a physical flush while retaining columns needed for rerouting.
198pub(crate) fn plan_region_batches(
199    combined_batch: RecordBatch,
200    physical_table_id: TableId,
201    partition_rule: &dyn partition::partition::PartitionRule,
202) -> Result<Vec<PlannedRegionBatch>> {
203    let region_masks = split_combined_batch_by_region(&combined_batch, partition_rule)?;
204
205    // Retain partition columns in the RPC payload so routing can be rebuilt after repartition.
206    // Mito's sparse bulk conversion projects the stored fields and encoded primary key.
207    let mut planned_batches = Vec::new();
208    for (region_number, mask) in region_masks {
209        if let Some(planned_batch) =
210            plan_region_batch(&combined_batch, physical_table_id, region_number, &mask)?
211        {
212            planned_batches.push(planned_batch);
213        }
214    }
215
216    Ok(planned_batches)
217}
218
219/// Encodes each region batch with the partition version used to select its rows.
220pub(crate) fn encode_region_write_requests(
221    planned_batches: Vec<PlannedRegionBatch>,
222    skip_wal: bool,
223    partition_versions: &HashMap<u32, Option<u64>>,
224) -> Result<Vec<RegionRequest>> {
225    let mut region_writes = Vec::with_capacity(planned_batches.len());
226    for planned in planned_batches {
227        let region_id = planned.region_id;
228        let (schema_bytes, data_header, payload) = {
229            let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
230                .with_label_values(&["flush_physical_encode_ipc"])
231                .start_timer();
232            flight::record_batch_to_ipc(planned.batch).map_err(map_ipc_error)?
233        };
234
235        let request = RegionRequest {
236            header: Some(RegionRequestHeader {
237                tracing_context: TracingContext::from_current_span().to_w3c(),
238                ..Default::default()
239            }),
240            body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
241                skip_wal,
242                region_id: region_id.as_u64(),
243                partition_expr_version: partition_versions
244                    .get(&region_id.region_number())
245                    .copied()
246                    .flatten()
247                    .map(|value| PartitionExprVersion { value }),
248                // Set aligned_schema_version to None so that datanode will check the batch schema again to see if any
249                // column is missing.
250                aligned_schema_version: None,
251                body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
252                    schema: schema_bytes,
253                    data_header,
254                    payload,
255                })),
256            })),
257        };
258
259        region_writes.push(request);
260    }
261
262    Ok(region_writes)
263}
264
265pub(in crate::batcher::logical_table) fn map_ipc_error(error: GrpcError) -> Error {
266    match error {
267        GrpcError::NotSupported { feat } => Error::NotSupported { feat },
268        GrpcError::InvalidFlightData { reason, .. } => Error::Internal { err_msg: reason },
269        error => Error::Internal {
270            err_msg: error.to_string(),
271        },
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use std::collections::{HashMap, HashSet};
278    use std::sync::Arc;
279
280    use api::v1::PartitionExprVersion;
281    use api::v1::region::{bulk_insert_request, region_request};
282    use arrow::array::{Array, BinaryArray, BooleanArray, StringArray, TimestampMillisecondArray};
283    use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
284    use arrow::record_batch::RecordBatch;
285    use common_grpc::flight::FlightDecoder;
286    use partition::partition::{PartitionRule, RegionMask};
287    use store_api::storage::RegionId;
288
289    use crate::batcher::logical_table::batch_convert::{self, RecordBatchWithTsIdx, TableBatch};
290    use crate::batcher::logical_table::region_write::{
291        PlannedRegionBatch, encode_region_write_requests, plan_region_batches,
292    };
293
294    #[test]
295    fn test_plan_region_batches_preserves_partition_columns_through_encoding() {
296        let logical_batch = RecordBatch::try_new(
297            Arc::new(ArrowSchema::new(vec![
298                Field::new(
299                    "greptime_timestamp",
300                    ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
301                    false,
302                ),
303                Field::new("greptime_value", ArrowDataType::Float64, true),
304                Field::new("host", ArrowDataType::Utf8, true),
305                Field::new("zone", ArrowDataType::Utf8, true),
306            ])),
307            vec![
308                Arc::new(TimestampMillisecondArray::from(vec![1000_i64, 2000_i64])),
309                Arc::new(arrow::array::Float64Array::from(vec![1.0_f64, 2.0_f64])),
310                Arc::new(StringArray::from(vec!["node-1", "node-2"])),
311                Arc::new(StringArray::from(vec!["east", "west"])),
312            ],
313        )
314        .unwrap();
315        let table_batches = [TableBatch {
316            table_name: "metric".to_string(),
317            table_id: 2048,
318            batches: vec![RecordBatchWithTsIdx::try_new(logical_batch, 0).unwrap()],
319            row_count: 2,
320        }];
321        let modified = batch_convert::transform_logical_batches_to_physical(
322            &table_batches,
323            &HashMap::from([("host".to_string(), 1), ("zone".to_string(), 2)]),
324            &HashSet::from(["host"]),
325        )
326        .unwrap();
327        let combined_batch = batch_convert::concat_modified_batches(&modified).unwrap();
328        assert!(combined_batch.column_by_name("host").is_some());
329        assert!(combined_batch.column_by_name("zone").is_none());
330        let expected = combined_batch.clone();
331        let mut planned_batches = plan_region_batches(
332            combined_batch,
333            1024,
334            &TwoRegionPartitionRule {
335                partition_columns: vec!["host".to_string()],
336            },
337        )
338        .unwrap();
339        planned_batches.sort_by_key(|planned| planned.region_id.region_number());
340
341        assert_eq!(2, planned_batches.len());
342        assert_eq!(RegionId::new(1024, 1), planned_batches[0].region_id);
343        assert_eq!(1, planned_batches[0].num_rows());
344        assert_eq!(4, planned_batches[0].batch.num_columns());
345        assert_eq!(RegionId::new(1024, 2), planned_batches[1].region_id);
346        assert_eq!(1, planned_batches[1].num_rows());
347        assert_eq!(4, planned_batches[1].batch.num_columns());
348
349        let writes = encode_region_write_requests(
350            planned_batches,
351            false,
352            &HashMap::from([(1, Some(7)), (2, Some(8))]),
353        )
354        .unwrap();
355        for (index, write) in writes.into_iter().enumerate() {
356            let Some(region_request::Body::BulkInsert(request)) = write.body else {
357                panic!("expected bulk")
358            };
359            assert_eq!(
360                request.partition_expr_version.unwrap().value,
361                7 + index as u64
362            );
363            let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = request.body else {
364                panic!("expected IPC")
365            };
366            let batch = FlightDecoder::try_from_schema_bytes(&ipc.schema)
367                .unwrap()
368                .try_decode_record_batch(&ipc.data_header, &ipc.payload)
369                .unwrap();
370            // The partition-only Arrow projection must retain the complete sparse key,
371            // including the zone tag that a later partition rule can require.
372            assert!(batch.column_by_name("zone").is_none());
373            assert_eq!(batch, expected.slice(index, 1));
374            let tags = batch
375                .column_by_name("host")
376                .unwrap()
377                .as_any()
378                .downcast_ref::<StringArray>()
379                .unwrap();
380            assert_eq!(tags.len(), 1);
381            assert_eq!(tags.value(0), if index == 0 { "node-1" } else { "node-2" });
382        }
383    }
384
385    #[test]
386    fn test_encode_region_write_requests_builds_bulk_insert_requests() {
387        check_encode_region_write_requests(false);
388        check_encode_region_write_requests(true);
389    }
390
391    struct TwoRegionPartitionRule {
392        partition_columns: Vec<String>,
393    }
394
395    impl PartitionRule for TwoRegionPartitionRule {
396        fn as_any(&self) -> &dyn std::any::Any {
397            self
398        }
399
400        fn partition_columns(&self) -> &[String] {
401            &self.partition_columns
402        }
403
404        fn find_region(
405            &self,
406            _values: &[datatypes::prelude::Value],
407        ) -> partition::error::Result<store_api::storage::RegionNumber> {
408            unimplemented!()
409        }
410
411        fn split_record_batch(
412            &self,
413            _record_batch: &RecordBatch,
414        ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
415        {
416            Ok(HashMap::from([
417                (1, RegionMask::new(BooleanArray::from(vec![true, false]), 1)),
418                (2, RegionMask::new(BooleanArray::from(vec![false, true]), 1)),
419                (
420                    3,
421                    RegionMask::new(BooleanArray::from(vec![false, false]), 0),
422                ),
423            ]))
424        }
425    }
426
427    fn check_encode_region_write_requests(skip_wal: bool) {
428        let planned_batch = PlannedRegionBatch {
429            region_id: RegionId::new(1024, 1),
430            batch: RecordBatch::try_new(
431                Arc::new(ArrowSchema::new(vec![
432                    Field::new("__primary_key", ArrowDataType::Binary, false),
433                    Field::new(
434                        "greptime_timestamp",
435                        ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
436                        false,
437                    ),
438                    Field::new("greptime_value", ArrowDataType::Float64, true),
439                ])),
440                vec![
441                    Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
442                    Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
443                    Arc::new(arrow::array::Float64Array::from(vec![1.0_f64])),
444                ],
445            )
446            .unwrap(),
447        };
448        let writes = encode_region_write_requests(
449            vec![planned_batch],
450            skip_wal,
451            &HashMap::from([(1, Some(7))]),
452        )
453        .unwrap();
454
455        assert_eq!(1, writes.len());
456        let Some(region_request::Body::BulkInsert(request)) = &writes[0].body else {
457            panic!("expected bulk insert request");
458        };
459        assert_eq!(RegionId::new(1024, 1).as_u64(), request.region_id);
460        assert_eq!(skip_wal, request.skip_wal);
461        assert_eq!(
462            Some(PartitionExprVersion { value: 7 }),
463            request.partition_expr_version
464        );
465    }
466}