1use 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 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 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 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}