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::ArrowIpc;
18use api::v1::meta::Peer;
19use api::v1::region::{
20    BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
21};
22use arrow::compute::filter_record_batch;
23use arrow::record_batch::RecordBatch;
24use async_trait::async_trait;
25use catalog::CatalogManagerRef;
26use common_grpc::error::Error as GrpcError;
27use common_grpc::flight::record_batch_to_ipc;
28use common_meta::node_manager::NodeManagerRef;
29use common_telemetry::tracing_context::TracingContext;
30use partition::manager::PartitionRuleManagerRef;
31use partition::partition::PartitionRuleRef;
32use snafu::ResultExt;
33use store_api::storage::RegionId;
34use table::metadata::{TableId, TableInfo, TableInfoRef};
35
36use crate::batcher::logical_table::batch_convert::strip_partition_columns_from_batch;
37use crate::error;
38use crate::error::{Error, Result};
39use crate::metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED;
40
41#[derive(Clone)]
42pub struct PhysicalTableMetadata {
43    pub table_info: TableInfoRef,
44    /// Mapping from column name to column id
45    pub col_name_to_ids: Option<HashMap<String, u32>>,
46}
47
48#[async_trait]
49pub trait PhysicalFlushCatalogProvider: Send + Sync {
50    async fn physical_table(
51        &self,
52        catalog: &str,
53        schema: &str,
54        table_name: &str,
55        query_ctx: &session::context::QueryContext,
56    ) -> catalog::error::Result<Option<PhysicalTableMetadata>>;
57}
58
59#[async_trait]
60pub trait PhysicalFlushPartitionProvider: Send + Sync {
61    async fn find_table_partition_rule(
62        &self,
63        table_info: &TableInfo,
64    ) -> partition::error::Result<PartitionRuleRef>;
65
66    async fn find_region_leader(&self, region_id: RegionId) -> Result<Peer>;
67}
68
69#[async_trait]
70pub trait PhysicalFlushNodeRequester: Send + Sync {
71    async fn handle(
72        &self,
73        peer: &Peer,
74        request: RegionRequest,
75    ) -> Result<api::region::RegionResponse>;
76}
77
78#[derive(Clone)]
79pub(in crate::batcher::logical_table) struct CatalogManagerPhysicalFlushAdapter {
80    pub(in crate::batcher::logical_table) catalog_manager: CatalogManagerRef,
81}
82
83#[async_trait]
84impl PhysicalFlushCatalogProvider for CatalogManagerPhysicalFlushAdapter {
85    async fn physical_table(
86        &self,
87        catalog: &str,
88        schema: &str,
89        table_name: &str,
90        query_ctx: &session::context::QueryContext,
91    ) -> catalog::error::Result<Option<PhysicalTableMetadata>> {
92        self.catalog_manager
93            .table(catalog, schema, table_name, Some(query_ctx))
94            .await
95            .map(|table| {
96                table.map(|table| {
97                    let table_info = table.table_info();
98                    let name_to_ids = table_info.name_to_ids();
99                    PhysicalTableMetadata {
100                        table_info,
101                        col_name_to_ids: name_to_ids,
102                    }
103                })
104            })
105    }
106}
107
108#[derive(Clone)]
109pub(in crate::batcher::logical_table) struct PartitionManagerPhysicalFlushAdapter {
110    pub(in crate::batcher::logical_table) partition_manager: PartitionRuleManagerRef,
111}
112
113#[async_trait]
114impl PhysicalFlushPartitionProvider for PartitionManagerPhysicalFlushAdapter {
115    async fn find_table_partition_rule(
116        &self,
117        table_info: &TableInfo,
118    ) -> partition::error::Result<PartitionRuleRef> {
119        self.partition_manager
120            .find_table_partition_rule(table_info)
121            .await
122            .map(|(rule, _)| rule)
123    }
124
125    async fn find_region_leader(&self, region_id: RegionId) -> Result<Peer> {
126        let peer = self.partition_manager.find_region_leader(region_id).await?;
127        Ok(peer)
128    }
129}
130
131#[derive(Clone)]
132pub(in crate::batcher::logical_table) struct NodeManagerPhysicalFlushAdapter {
133    pub(in crate::batcher::logical_table) node_manager: NodeManagerRef,
134}
135
136#[async_trait]
137impl PhysicalFlushNodeRequester for NodeManagerPhysicalFlushAdapter {
138    async fn handle(
139        &self,
140        peer: &Peer,
141        request: RegionRequest,
142    ) -> error::Result<api::region::RegionResponse> {
143        let datanode = self.node_manager.datanode(peer).await;
144        datanode
145            .handle(request)
146            .await
147            .context(error::CommonMetaSnafu)
148    }
149}
150
151pub(in crate::batcher::logical_table) struct FlushRegionWrite {
152    pub(in crate::batcher::logical_table) datanode: Peer,
153    pub(in crate::batcher::logical_table) request: RegionRequest,
154}
155
156pub(in crate::batcher::logical_table) struct PlannedRegionBatch {
157    pub(in crate::batcher::logical_table) region_id: RegionId,
158    pub(in crate::batcher::logical_table) batch: RecordBatch,
159}
160
161#[cfg(test)]
162impl PlannedRegionBatch {
163    pub(in crate::batcher::logical_table) fn num_rows(&self) -> usize {
164        self.batch.num_rows()
165    }
166}
167
168pub(in crate::batcher::logical_table) struct ResolvedRegionBatch {
169    pub(in crate::batcher::logical_table) planned: PlannedRegionBatch,
170    pub(in crate::batcher::logical_table) datanode: Peer,
171}
172
173pub(in crate::batcher::logical_table) fn should_dispatch_concurrently(
174    region_write_count: usize,
175) -> bool {
176    region_write_count > 1
177}
178
179pub(in crate::batcher::logical_table) async fn flush_region_writes_concurrently(
180    node_manager: &(impl PhysicalFlushNodeRequester + ?Sized),
181    writes: Vec<FlushRegionWrite>,
182) -> Result<usize> {
183    let mut affected_rows = 0;
184    if !should_dispatch_concurrently(writes.len()) {
185        for write in writes {
186            let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
187                .with_label_values(&["flush_write_region"])
188                .start_timer();
189            affected_rows += node_manager
190                .handle(&write.datanode, write.request)
191                .await?
192                .affected_rows;
193        }
194        return Ok(affected_rows);
195    }
196
197    let write_futures = writes.into_iter().map(|write| async move {
198        let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
199            .with_label_values(&["flush_write_region"])
200            .start_timer();
201
202        let response = node_manager.handle(&write.datanode, write.request).await?;
203        Ok::<_, Error>(response.affected_rows)
204    });
205
206    // todo(hl): should be bounded.
207    let affected_rows = futures::future::try_join_all(write_futures)
208        .await?
209        .into_iter()
210        .sum();
211    Ok(affected_rows)
212}
213
214pub(in crate::batcher::logical_table) fn split_combined_batch_by_region(
215    combined_batch: &RecordBatch,
216    partition_rule: &dyn partition::partition::PartitionRule,
217) -> Result<HashMap<u32, partition::partition::RegionMask>> {
218    let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
219        .with_label_values(&["flush_physical_split_record_batch"])
220        .start_timer();
221    let map = partition_rule.split_record_batch(combined_batch)?;
222    Ok(map)
223}
224
225pub(in crate::batcher::logical_table) fn prepare_physical_region_routing_batch(
226    combined_batch: RecordBatch,
227    partition_columns: &[String],
228) -> Result<RecordBatch> {
229    if partition_columns.is_empty() {
230        return Ok(combined_batch);
231    }
232    strip_partition_columns_from_batch(combined_batch)
233}
234
235pub(in crate::batcher::logical_table) fn plan_region_batch(
236    stripped_batch: &RecordBatch,
237    physical_table_id: TableId,
238    region_number: u32,
239    mask: &partition::partition::RegionMask,
240) -> Result<Option<PlannedRegionBatch>> {
241    if mask.select_none() {
242        return Ok(None);
243    }
244
245    let region_batch = if mask.select_all() {
246        stripped_batch.clone()
247    } else {
248        let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
249            .with_label_values(&["flush_physical_filter_record_batch"])
250            .start_timer();
251        filter_record_batch(stripped_batch, mask.array()).context(error::ArrowSnafu)?
252    };
253
254    let row_count = region_batch.num_rows();
255    if row_count == 0 {
256        return Ok(None);
257    }
258
259    Ok(Some(PlannedRegionBatch {
260        region_id: RegionId::new(physical_table_id, region_number),
261        batch: region_batch,
262    }))
263}
264
265pub(in crate::batcher::logical_table) fn plan_region_batches(
266    combined_batch: RecordBatch,
267    physical_table_id: TableId,
268    partition_rule: &dyn partition::partition::PartitionRule,
269    partition_columns: &[String],
270) -> Result<Vec<PlannedRegionBatch>> {
271    let region_masks = split_combined_batch_by_region(&combined_batch, partition_rule)?;
272    let stripped_batch = prepare_physical_region_routing_batch(combined_batch, partition_columns)?;
273
274    let mut planned_batches = Vec::new();
275    for (region_number, mask) in region_masks {
276        if let Some(planned_batch) =
277            plan_region_batch(&stripped_batch, physical_table_id, region_number, &mask)?
278        {
279            planned_batches.push(planned_batch);
280        }
281    }
282
283    Ok(planned_batches)
284}
285
286pub(in crate::batcher::logical_table) async fn resolve_region_targets(
287    planned_batches: Vec<PlannedRegionBatch>,
288    partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
289) -> Result<Vec<ResolvedRegionBatch>> {
290    let mut resolved_batches = Vec::with_capacity(planned_batches.len());
291    for planned in planned_batches {
292        let datanode = {
293            let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
294                .with_label_values(&["flush_physical_resolve_region_leader"])
295                .start_timer();
296            partition_manager
297                .find_region_leader(planned.region_id)
298                .await?
299        };
300
301        resolved_batches.push(ResolvedRegionBatch { planned, datanode });
302    }
303
304    Ok(resolved_batches)
305}
306
307pub(in crate::batcher::logical_table) fn encode_region_write_requests(
308    resolved_batches: Vec<ResolvedRegionBatch>,
309    skip_wal: bool,
310) -> Result<Vec<FlushRegionWrite>> {
311    let mut region_writes = Vec::with_capacity(resolved_batches.len());
312    for resolved in resolved_batches {
313        let region_id = resolved.planned.region_id;
314        let (schema_bytes, data_header, payload) = {
315            let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
316                .with_label_values(&["flush_physical_encode_ipc"])
317                .start_timer();
318            record_batch_to_ipc(resolved.planned.batch).map_err(map_ipc_error)?
319        };
320
321        let request = RegionRequest {
322            header: Some(RegionRequestHeader {
323                tracing_context: TracingContext::from_current_span().to_w3c(),
324                ..Default::default()
325            }),
326            body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
327                skip_wal,
328                region_id: region_id.as_u64(),
329                partition_expr_version: None,
330                // Set aligned_schema_version to None so that datanode will check the batch schema again to see if any
331                // column is missing.
332                aligned_schema_version: None,
333                body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
334                    schema: schema_bytes,
335                    data_header,
336                    payload,
337                })),
338            })),
339        };
340
341        region_writes.push(FlushRegionWrite {
342            datanode: resolved.datanode,
343            request,
344        });
345    }
346
347    Ok(region_writes)
348}
349
350pub(in crate::batcher::logical_table) fn map_ipc_error(error: GrpcError) -> Error {
351    match error {
352        GrpcError::NotSupported { feat } => Error::NotSupported { feat },
353        GrpcError::InvalidFlightData { reason, .. } => Error::Internal { err_msg: reason },
354        error => Error::Internal {
355            err_msg: error.to_string(),
356        },
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use std::collections::HashMap;
363    use std::sync::Arc;
364    use std::sync::atomic::{AtomicUsize, Ordering};
365    use std::time::Duration;
366
367    use api::region::RegionResponse;
368    use api::v1::meta::Peer;
369    use api::v1::region::{RegionRequest, region_request};
370    use arrow::array::{BinaryArray, BooleanArray, StringArray, TimestampMillisecondArray};
371    use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
372    use arrow::record_batch::RecordBatch;
373    use async_trait::async_trait;
374    use common_meta::error::Result as MetaResult;
375    use common_meta::node_manager::{Datanode, DatanodeRef};
376    use common_query::request::QueryRequest;
377    use common_recordbatch::SendableRecordBatchStream;
378    use partition::partition::{PartitionRule, RegionMask};
379    use store_api::storage::RegionId;
380    use tokio::time::sleep;
381
382    use crate::batcher::logical_table::region_write::{
383        FlushRegionWrite, PlannedRegionBatch, ResolvedRegionBatch, encode_region_write_requests,
384        flush_region_writes_concurrently, plan_region_batches, should_dispatch_concurrently,
385    };
386    use crate::batcher::logical_table::test_util::ConcurrentMockNodeManager;
387
388    #[tokio::test]
389    async fn test_flush_region_writes_concurrently_dispatches_multiple_datanodes() {
390        let inflight = Arc::new(AtomicUsize::new(0));
391        let max_inflight = Arc::new(AtomicUsize::new(0));
392        let datanode1: DatanodeRef = Arc::new(ConcurrentMockDatanode {
393            delay: Duration::from_millis(100),
394            inflight: inflight.clone(),
395            max_inflight: max_inflight.clone(),
396        });
397        let datanode2: DatanodeRef = Arc::new(ConcurrentMockDatanode {
398            delay: Duration::from_millis(100),
399            inflight,
400            max_inflight: max_inflight.clone(),
401        });
402
403        let mut datanodes = HashMap::new();
404        datanodes.insert(1, datanode1);
405        datanodes.insert(2, datanode2);
406        let node_manager = Arc::new(ConcurrentMockNodeManager {
407            datanodes: Arc::new(datanodes),
408        });
409
410        let writes = vec![
411            FlushRegionWrite {
412                datanode: Peer {
413                    id: 1,
414                    addr: "node1".to_string(),
415                },
416                request: RegionRequest::default(),
417            },
418            FlushRegionWrite {
419                datanode: Peer {
420                    id: 2,
421                    addr: "node2".to_string(),
422                },
423                request: RegionRequest::default(),
424            },
425        ];
426
427        flush_region_writes_concurrently(node_manager.as_ref(), writes)
428            .await
429            .unwrap();
430        assert!(max_inflight.load(Ordering::SeqCst) >= 2);
431    }
432
433    #[test]
434    fn test_should_dispatch_concurrently_by_region_count() {
435        assert!(!should_dispatch_concurrently(0));
436        assert!(!should_dispatch_concurrently(1));
437        assert!(should_dispatch_concurrently(2));
438    }
439
440    #[test]
441    fn test_plan_region_batches_splits_and_strips_partition_columns() {
442        let combined_batch = RecordBatch::try_new(
443            Arc::new(ArrowSchema::new(vec![
444                Field::new("__primary_key", ArrowDataType::Binary, false),
445                Field::new(
446                    "greptime_timestamp",
447                    ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
448                    false,
449                ),
450                Field::new("greptime_value", ArrowDataType::Float64, true),
451                Field::new("host", ArrowDataType::Utf8, true),
452            ])),
453            vec![
454                Arc::new(BinaryArray::from(vec![b"k1".as_slice(), b"k2".as_slice()])),
455                Arc::new(TimestampMillisecondArray::from(vec![1000_i64, 2000_i64])),
456                Arc::new(arrow::array::Float64Array::from(vec![1.0_f64, 2.0_f64])),
457                Arc::new(StringArray::from(vec!["node-1", "node-2"])),
458            ],
459        )
460        .unwrap();
461        let mut planned_batches = plan_region_batches(
462            combined_batch,
463            1024,
464            &TwoRegionPartitionRule {
465                partition_columns: vec!["host".to_string()],
466            },
467            &["host".to_string()],
468        )
469        .unwrap();
470        planned_batches.sort_by_key(|planned| planned.region_id.region_number());
471
472        assert_eq!(2, planned_batches.len());
473        assert_eq!(RegionId::new(1024, 1), planned_batches[0].region_id);
474        assert_eq!(1, planned_batches[0].num_rows());
475        assert_eq!(3, planned_batches[0].batch.num_columns());
476        assert_eq!(RegionId::new(1024, 2), planned_batches[1].region_id);
477        assert_eq!(1, planned_batches[1].num_rows());
478        assert_eq!(3, planned_batches[1].batch.num_columns());
479    }
480
481    #[test]
482    fn test_encode_region_write_requests_builds_bulk_insert_requests() {
483        check_encode_region_write_requests(false);
484        check_encode_region_write_requests(true);
485    }
486
487    struct TwoRegionPartitionRule {
488        partition_columns: Vec<String>,
489    }
490
491    impl PartitionRule for TwoRegionPartitionRule {
492        fn as_any(&self) -> &dyn std::any::Any {
493            self
494        }
495
496        fn partition_columns(&self) -> &[String] {
497            &self.partition_columns
498        }
499
500        fn find_region(
501            &self,
502            _values: &[datatypes::prelude::Value],
503        ) -> partition::error::Result<store_api::storage::RegionNumber> {
504            unimplemented!()
505        }
506
507        fn split_record_batch(
508            &self,
509            _record_batch: &RecordBatch,
510        ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
511        {
512            Ok(HashMap::from([
513                (1, RegionMask::new(BooleanArray::from(vec![true, false]), 1)),
514                (2, RegionMask::new(BooleanArray::from(vec![false, true]), 1)),
515                (
516                    3,
517                    RegionMask::new(BooleanArray::from(vec![false, false]), 0),
518                ),
519            ]))
520        }
521    }
522
523    #[derive(Clone)]
524    struct ConcurrentMockDatanode {
525        delay: Duration,
526        inflight: Arc<AtomicUsize>,
527        max_inflight: Arc<AtomicUsize>,
528    }
529
530    #[async_trait]
531    impl Datanode for ConcurrentMockDatanode {
532        async fn handle(&self, _request: RegionRequest) -> MetaResult<RegionResponse> {
533            let now = self.inflight.fetch_add(1, Ordering::SeqCst) + 1;
534            loop {
535                let max = self.max_inflight.load(Ordering::SeqCst);
536                if now <= max {
537                    break;
538                }
539                if self
540                    .max_inflight
541                    .compare_exchange(max, now, Ordering::SeqCst, Ordering::SeqCst)
542                    .is_ok()
543                {
544                    break;
545                }
546            }
547
548            sleep(self.delay).await;
549            self.inflight.fetch_sub(1, Ordering::SeqCst);
550            Ok(RegionResponse::new(0))
551        }
552
553        async fn handle_query(
554            &self,
555            _request: QueryRequest,
556        ) -> MetaResult<SendableRecordBatchStream> {
557            unimplemented!()
558        }
559    }
560
561    fn check_encode_region_write_requests(skip_wal: bool) {
562        let planned_batch = PlannedRegionBatch {
563            region_id: RegionId::new(1024, 1),
564            batch: RecordBatch::try_new(
565                Arc::new(ArrowSchema::new(vec![
566                    Field::new("__primary_key", ArrowDataType::Binary, false),
567                    Field::new(
568                        "greptime_timestamp",
569                        ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
570                        false,
571                    ),
572                    Field::new("greptime_value", ArrowDataType::Float64, true),
573                ])),
574                vec![
575                    Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
576                    Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
577                    Arc::new(arrow::array::Float64Array::from(vec![1.0_f64])),
578                ],
579            )
580            .unwrap(),
581        };
582        let resolved_batch = ResolvedRegionBatch {
583            planned: planned_batch,
584            datanode: Peer {
585                id: 1,
586                addr: "node-1".to_string(),
587            },
588        };
589        let writes = encode_region_write_requests(vec![resolved_batch], skip_wal).unwrap();
590
591        assert_eq!(1, writes.len());
592        assert_eq!(1, writes[0].datanode.id);
593        let Some(region_request::Body::BulkInsert(request)) = &writes[0].request.body else {
594            panic!("expected bulk insert request");
595        };
596        assert_eq!(RegionId::new(1024, 1).as_u64(), request.region_id);
597        assert_eq!(skip_wal, request.skip_wal);
598    }
599}