1use std::collections::{HashMap, HashSet};
16use std::num::NonZeroUsize;
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19
20use api::v1::flow::{DirtyWindowRequest, DirtyWindowRequests};
21use api::v1::meta::Peer;
22use api::v1::region::{
23 BulkInsertRequest, RegionRequest, RegionRequestHeader, bulk_insert_request, region_request,
24};
25use api::v1::{ArrowIpc, ColumnSchema, RowInsertRequests, Rows};
26use arrow::array::Array;
27use arrow::compute::{concat_batches, filter_record_batch};
28use arrow::datatypes::{DataType as ArrowDataType, Schema as ArrowSchema, TimeUnit};
29use arrow::record_batch::RecordBatch;
30use async_trait::async_trait;
31use bytes::Bytes;
32use catalog::CatalogManagerRef;
33use common_grpc::flight::{FlightEncoder, FlightMessage};
34use common_meta::cache::TableFlownodeSetCacheRef;
35use common_meta::node_manager::NodeManagerRef;
36use common_query::prelude::{GREPTIME_PHYSICAL_TABLE, greptime_timestamp, greptime_value};
37use common_telemetry::tracing_context::TracingContext;
38use common_telemetry::{debug, error, warn};
39use dashmap::DashMap;
40use dashmap::mapref::entry::Entry;
41use futures::StreamExt;
42use metric_engine::batch_modifier::{TagColumnInfo, modify_batch_sparse};
43use partition::manager::PartitionRuleManagerRef;
44use partition::partition::PartitionRuleRef;
45use session::context::QueryContextRef;
46use smallvec::SmallVec;
47use snafu::{OptionExt, ResultExt, ensure};
48use store_api::storage::{RegionId, TableId};
49use table::metadata::{TableInfo, TableInfoRef};
50use tokio::sync::{OwnedSemaphorePermit, Semaphore, broadcast, mpsc, oneshot};
51
52use crate::error;
53use crate::error::{Error, Result};
54use crate::metrics::{
55 FLOW_NOTIFICATION_DROPPED, FLUSH_DROPPED_ROWS, FLUSH_ELAPSED, FLUSH_FAILURES, FLUSH_ROWS,
56 FLUSH_TOTAL, PENDING_BATCHES, PENDING_ROWS, PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED,
57 PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED, PENDING_WORKERS,
58};
59use crate::prom_row_builder::{
60 build_prom_create_table_schema_from_proto, identify_missing_columns_from_proto,
61 rows_to_aligned_record_batch,
62};
63
64const PHYSICAL_TABLE_KEY: &str = "physical_table";
65const PENDING_ROWS_BATCH_SYNC_ENV: &str = "PENDING_ROWS_BATCH_SYNC";
67const WORKER_IDLE_TIMEOUT_MULTIPLIER: u32 = 3;
68const PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT: usize = 3;
69const MAX_CONCURRENT_FLOW_NOTIFICATIONS: usize = 8;
70#[async_trait]
71pub trait PendingRowsSchemaAlterer: Send + Sync {
72 async fn create_tables_if_missing_batch(
75 &self,
76 catalog: &str,
77 schema: &str,
78 tables: &[(&str, &[ColumnSchema])],
79 with_metric_engine: bool,
80 ctx: QueryContextRef,
81 ) -> Result<()>;
82
83 async fn add_missing_prom_tag_columns_batch(
86 &self,
87 catalog: &str,
88 schema: &str,
89 tables: &[(&str, &[String])],
90 ctx: QueryContextRef,
91 ) -> Result<()>;
92}
93
94pub type PendingRowsSchemaAltererRef = Arc<dyn PendingRowsSchemaAlterer>;
95
96#[derive(Clone)]
97pub struct PhysicalTableMetadata {
98 pub table_info: TableInfoRef,
99 pub col_name_to_ids: Option<HashMap<String, u32>>,
101}
102
103#[async_trait]
104pub trait PhysicalFlushCatalogProvider: Send + Sync {
105 async fn physical_table(
106 &self,
107 catalog: &str,
108 schema: &str,
109 table_name: &str,
110 query_ctx: &session::context::QueryContext,
111 ) -> catalog::error::Result<Option<PhysicalTableMetadata>>;
112}
113
114#[async_trait]
115pub trait PhysicalFlushPartitionProvider: Send + Sync {
116 async fn find_table_partition_rule(
117 &self,
118 table_info: &TableInfo,
119 ) -> partition::error::Result<PartitionRuleRef>;
120
121 async fn find_region_leader(&self, region_id: RegionId) -> Result<Peer>;
122}
123
124#[async_trait]
125pub trait PhysicalFlushNodeRequester: Send + Sync {
126 async fn handle(
127 &self,
128 peer: &Peer,
129 request: RegionRequest,
130 ) -> Result<api::region::RegionResponse>;
131}
132
133#[derive(Clone)]
134struct CatalogManagerPhysicalFlushAdapter {
135 catalog_manager: CatalogManagerRef,
136}
137
138#[async_trait]
139impl PhysicalFlushCatalogProvider for CatalogManagerPhysicalFlushAdapter {
140 async fn physical_table(
141 &self,
142 catalog: &str,
143 schema: &str,
144 table_name: &str,
145 query_ctx: &session::context::QueryContext,
146 ) -> catalog::error::Result<Option<PhysicalTableMetadata>> {
147 self.catalog_manager
148 .table(catalog, schema, table_name, Some(query_ctx))
149 .await
150 .map(|table| {
151 table.map(|table| {
152 let table_info = table.table_info();
153 let name_to_ids = table_info.name_to_ids();
154 PhysicalTableMetadata {
155 table_info,
156 col_name_to_ids: name_to_ids,
157 }
158 })
159 })
160 }
161}
162
163#[derive(Clone)]
164struct PartitionManagerPhysicalFlushAdapter {
165 partition_manager: PartitionRuleManagerRef,
166}
167
168#[async_trait]
169impl PhysicalFlushPartitionProvider for PartitionManagerPhysicalFlushAdapter {
170 async fn find_table_partition_rule(
171 &self,
172 table_info: &TableInfo,
173 ) -> partition::error::Result<PartitionRuleRef> {
174 self.partition_manager
175 .find_table_partition_rule(table_info)
176 .await
177 .map(|(rule, _)| rule)
178 }
179
180 async fn find_region_leader(&self, region_id: RegionId) -> Result<Peer> {
181 let peer = self.partition_manager.find_region_leader(region_id).await?;
182 Ok(peer)
183 }
184}
185
186#[derive(Clone)]
187struct NodeManagerPhysicalFlushAdapter {
188 node_manager: NodeManagerRef,
189}
190
191#[async_trait]
192impl PhysicalFlushNodeRequester for NodeManagerPhysicalFlushAdapter {
193 async fn handle(
194 &self,
195 peer: &Peer,
196 request: RegionRequest,
197 ) -> error::Result<api::region::RegionResponse> {
198 let datanode = self.node_manager.datanode(peer).await;
199 datanode
200 .handle(request)
201 .await
202 .context(error::CommonMetaSnafu)
203 }
204}
205
206#[derive(Debug, Clone, Hash, Eq, PartialEq)]
207struct BatchKey {
208 catalog: String,
209 schema: String,
210 physical_table: String,
211}
212
213#[derive(Debug, Clone)]
215pub struct RecordBatchWithTsIdx {
216 batch: RecordBatch,
218 timestamp_index: usize,
220}
221
222impl RecordBatchWithTsIdx {
223 pub fn try_new(batch: RecordBatch, timestamp_index: usize) -> Result<Self> {
225 let schema = batch.schema();
226 let timestamp_field = schema.fields().get(timestamp_index).with_context(|| {
227 error::InvalidPromRemoteRequestSnafu {
228 msg: format!(
229 "Timestamp column index {} is out of bounds for record batch with {} columns",
230 timestamp_index,
231 batch.num_columns()
232 ),
233 }
234 })?;
235 ensure!(
236 matches!(timestamp_field.data_type(), ArrowDataType::Timestamp(_, _)),
237 error::InvalidPromRemoteRequestSnafu {
238 msg: format!(
239 "Column at index {} is not a timestamp column: {:?}",
240 timestamp_index,
241 timestamp_field.data_type()
242 ),
243 }
244 );
245
246 Ok(Self {
247 batch,
248 timestamp_index,
249 })
250 }
251
252 #[cfg(test)]
253 pub(crate) fn into_parts(self) -> (RecordBatch, usize) {
254 (self.batch, self.timestamp_index)
255 }
256}
257
258#[derive(Debug, Clone)]
259pub struct TableBatch {
260 pub table_name: String,
261 pub table_id: TableId,
262 pub batches: Vec<RecordBatchWithTsIdx>,
263 pub row_count: usize,
264}
265
266struct TableResolutionPlan {
269 region_schemas: HashMap<String, (Arc<ArrowSchema>, u32)>,
271 tables_to_create: Vec<(String, Vec<ColumnSchema>)>,
273 tables_to_alter: Vec<(String, Vec<String>)>,
275}
276
277struct PendingBatch {
278 tables: HashMap<TableId, TableBatch>,
279 created_at: Instant,
280 total_row_count: usize,
281 db_string: String,
282 ctx: QueryContextRef,
283 waiters: Vec<FlushWaiter>,
284}
285
286struct FlushWaiter {
287 response_tx: oneshot::Sender<std::result::Result<(), Arc<Error>>>,
288 _permit: OwnedSemaphorePermit,
289}
290
291struct FlushBatch {
292 table_batches: Vec<TableBatch>,
293 total_row_count: usize,
294 db_string: String,
295 ctx: QueryContextRef,
296 waiters: Vec<FlushWaiter>,
297}
298
299#[derive(Clone)]
300struct PendingWorker {
301 tx: mpsc::Sender<WorkerCommand>,
302}
303
304enum WorkerCommand {
305 Submit {
306 table_batches: Vec<(String, u32, RecordBatchWithTsIdx)>,
307 total_rows: usize,
308 ctx: QueryContextRef,
309 response_tx: oneshot::Sender<std::result::Result<(), Arc<Error>>>,
310 _permit: OwnedSemaphorePermit,
311 },
312}
313
314fn batch_key_from_ctx(ctx: &QueryContextRef) -> BatchKey {
317 let physical_table = ctx
318 .extension(PHYSICAL_TABLE_KEY)
319 .unwrap_or(GREPTIME_PHYSICAL_TABLE)
320 .to_string();
321 BatchKey {
322 catalog: ctx.current_catalog().to_string(),
323 schema: ctx.current_schema(),
324 physical_table,
325 }
326}
327
328pub struct PendingRowsBatcher {
330 workers: Arc<DashMap<BatchKey, PendingWorker>>,
331 flush_interval: Duration,
332 max_batch_rows: usize,
333 partition_manager: PartitionRuleManagerRef,
334 node_manager: NodeManagerRef,
335 catalog_manager: CatalogManagerRef,
336 flow_notification_tx: mpsc::Sender<FlowNotification>,
337 flush_semaphore: Arc<Semaphore>,
338 inflight_semaphore: Arc<Semaphore>,
339 worker_channel_capacity: usize,
340 prom_store_with_metric_engine: bool,
341 schema_alterer: PendingRowsSchemaAltererRef,
342 pending_rows_batch_sync: bool,
343 shutdown: broadcast::Sender<()>,
344}
345
346impl PendingRowsBatcher {
347 #[allow(clippy::too_many_arguments)]
348 pub fn try_new(
349 partition_manager: PartitionRuleManagerRef,
350 node_manager: NodeManagerRef,
351 catalog_manager: CatalogManagerRef,
352 table_flownode_set_cache: TableFlownodeSetCacheRef,
353 prom_store_with_metric_engine: bool,
354 schema_alterer: PendingRowsSchemaAltererRef,
355 flush_interval: Duration,
356 max_batch_rows: usize,
357 max_concurrent_flushes: usize,
358 worker_channel_capacity: usize,
359 max_inflight_requests: usize,
360 flow_notification_queue_capacity: NonZeroUsize,
361 ) -> Option<Arc<Self>> {
362 if flush_interval.is_zero()
366 || max_batch_rows == 0
367 || max_concurrent_flushes == 0
368 || worker_channel_capacity == 0
369 || max_inflight_requests == 0
370 {
371 return None;
372 }
373
374 let (shutdown, _) = broadcast::channel(1);
375 let pending_rows_batch_sync = std::env::var(PENDING_ROWS_BATCH_SYNC_ENV)
376 .ok()
377 .as_deref()
378 .and_then(|v| v.parse::<bool>().ok())
379 .unwrap_or(true);
380 let workers = Arc::new(DashMap::new());
381 PENDING_WORKERS.set(workers.len() as i64);
382 let (flow_notification_tx, flow_notification_rx) =
383 mpsc::channel(flow_notification_queue_capacity.get());
384 start_flow_notification_worker(
385 flow_notification_rx,
386 table_flownode_set_cache,
387 node_manager.clone(),
388 );
389
390 Some(Arc::new(Self {
391 workers,
392 flush_interval,
393 max_batch_rows,
394 partition_manager,
395 node_manager,
396 catalog_manager,
397 flow_notification_tx,
398 prom_store_with_metric_engine,
399 schema_alterer,
400 flush_semaphore: Arc::new(Semaphore::new(max_concurrent_flushes)),
401 inflight_semaphore: Arc::new(Semaphore::new(max_inflight_requests)),
402 worker_channel_capacity,
403 pending_rows_batch_sync,
404 shutdown,
405 }))
406 }
407
408 pub async fn submit(&self, requests: RowInsertRequests, ctx: QueryContextRef) -> Result<u64> {
409 let (table_batches, total_rows) = {
410 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
411 .with_label_values(&["submit_build_and_align"])
412 .start_timer();
413 self.build_and_align_table_batches(requests, &ctx).await?
414 };
415 if total_rows == 0 {
416 return Ok(0);
417 }
418
419 let permit = {
420 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
421 .with_label_values(&["submit_acquire_inflight_permit"])
422 .start_timer();
423 self.inflight_semaphore
424 .clone()
425 .acquire_owned()
426 .await
427 .map_err(|_| error::BatcherChannelClosedSnafu.build())?
428 };
429
430 let (response_tx, response_rx) = oneshot::channel();
431
432 let batch_key = batch_key_from_ctx(&ctx);
433 let mut cmd = Some(WorkerCommand::Submit {
434 table_batches,
435 total_rows,
436 ctx,
437 response_tx,
438 _permit: permit,
439 });
440
441 {
442 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
443 .with_label_values(&["submit_send_to_worker"])
444 .start_timer();
445
446 for _ in 0..2 {
447 let worker = self.get_or_spawn_worker(batch_key.clone());
448 let Some(worker_cmd) = cmd.take() else {
449 break;
450 };
451
452 match worker.tx.send(worker_cmd).await {
453 Ok(()) => break,
454 Err(err) => {
455 cmd = Some(err.0);
456 remove_worker_if_same_channel(
457 self.workers.as_ref(),
458 &batch_key,
459 &worker.tx,
460 );
461 }
462 }
463 }
464
465 if cmd.is_some() {
466 return Err(Error::BatcherChannelClosed);
467 }
468 }
469
470 if self.pending_rows_batch_sync {
471 let result = {
472 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
473 .with_label_values(&["submit_wait_flush_result"])
474 .start_timer();
475 response_rx
476 .await
477 .map_err(|_| error::BatcherChannelClosedSnafu.build())?
478 };
479 result
480 .context(error::SubmitBatchSnafu)
481 .map(|()| total_rows as u64)
482 } else {
483 Ok(total_rows as u64)
484 }
485 }
486
487 async fn build_and_align_table_batches(
492 &self,
493 requests: RowInsertRequests,
494 ctx: &QueryContextRef,
495 ) -> Result<(Vec<(String, u32, RecordBatchWithTsIdx)>, usize)> {
496 let catalog = ctx.current_catalog().to_string();
497 let schema = ctx.current_schema();
498
499 let (table_rows, total_rows) = Self::collect_non_empty_table_rows(requests);
500 if total_rows == 0 {
501 return Ok((Vec::new(), 0));
502 }
503
504 let unique_tables = Self::collect_unique_table_schemas(&table_rows)?;
505 let mut plan = self
506 .plan_table_resolution(&catalog, &schema, ctx, &unique_tables)
507 .await?;
508
509 self.create_missing_tables_and_refresh_schemas(
510 &catalog,
511 &schema,
512 ctx,
513 &table_rows,
514 &mut plan,
515 )
516 .await?;
517
518 self.alter_tables_and_refresh_schemas(&catalog, &schema, ctx, &mut plan)
519 .await?;
520
521 let aligned_batches = Self::build_aligned_batches(&table_rows, &plan.region_schemas)?;
522
523 Ok((aligned_batches, total_rows))
524 }
525
526 fn collect_non_empty_table_rows(requests: RowInsertRequests) -> (Vec<(String, Rows)>, usize) {
529 let mut table_rows: Vec<(String, Rows)> = Vec::with_capacity(requests.inserts.len());
530 let mut total_rows = 0;
531
532 for request in requests.inserts {
533 let Some(rows) = request.rows else {
534 continue;
535 };
536 if rows.rows.is_empty() {
537 continue;
538 }
539
540 total_rows += rows.rows.len();
541 table_rows.push((request.table_name, rows));
542 }
543
544 (table_rows, total_rows)
545 }
546
547 fn collect_unique_table_schemas(
550 table_rows: &[(String, Rows)],
551 ) -> Result<Vec<(&str, &[ColumnSchema])>> {
552 let mut unique_tables: Vec<(&str, &[ColumnSchema])> = Vec::with_capacity(table_rows.len());
553 let mut seen = HashSet::new();
554
555 for (table_name, rows) in table_rows {
556 if seen.insert(table_name.as_str()) {
557 unique_tables.push((table_name.as_str(), &rows.schema));
558 } else {
559 return error::InvalidPromRemoteRequestSnafu {
561 msg: format!(
562 "Found duplicated table name in RowInsertRequest: {}",
563 table_name
564 ),
565 }
566 .fail();
567 }
568 }
569
570 Ok(unique_tables)
571 }
572
573 async fn plan_table_resolution(
576 &self,
577 catalog: &str,
578 schema: &str,
579 ctx: &QueryContextRef,
580 unique_tables: &[(&str, &[ColumnSchema])],
581 ) -> Result<TableResolutionPlan> {
582 let mut plan = TableResolutionPlan {
583 region_schemas: HashMap::with_capacity(unique_tables.len()),
584 tables_to_create: Vec::new(),
585 tables_to_alter: Vec::new(),
586 };
587
588 let resolved_tables = {
589 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
590 .with_label_values(&["align_resolve_table"])
591 .start_timer();
592 futures::future::join_all(unique_tables.iter().map(|(table_name, _)| {
593 self.catalog_manager
594 .table(catalog, schema, table_name, Some(ctx.as_ref()))
595 }))
596 .await
597 };
598
599 for ((table_name, rows_schema), table_result) in unique_tables.iter().zip(resolved_tables) {
600 let table = table_result?;
601
602 if let Some(table) = table {
603 let table_info = table.table_info();
604 let table_id = table_info.ident.table_id;
605 let region_schema = table_info.meta.schema.arrow_schema().clone();
606
607 let missing_columns = {
608 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
609 .with_label_values(&["align_identify_missing_columns"])
610 .start_timer();
611 identify_missing_columns_from_proto(rows_schema, region_schema.as_ref())?
612 };
613 if !missing_columns.is_empty() {
614 plan.tables_to_alter
615 .push(((*table_name).to_string(), missing_columns));
616 }
617 plan.region_schemas
618 .insert((*table_name).to_string(), (region_schema, table_id));
619 } else {
620 let request_schema = {
621 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
622 .with_label_values(&["align_build_create_table_schema"])
623 .start_timer();
624 build_prom_create_table_schema_from_proto(rows_schema)?
625 };
626 plan.tables_to_create
627 .push(((*table_name).to_string(), request_schema));
628 }
629 }
630
631 Ok(plan)
632 }
633
634 async fn create_missing_tables_and_refresh_schemas(
637 &self,
638 catalog: &str,
639 schema: &str,
640 ctx: &QueryContextRef,
641 table_rows: &[(String, Rows)],
642 plan: &mut TableResolutionPlan,
643 ) -> Result<()> {
644 if plan.tables_to_create.is_empty() {
645 return Ok(());
646 }
647
648 let create_refs: Vec<(&str, &[ColumnSchema])> = plan
649 .tables_to_create
650 .iter()
651 .map(|(name, schema)| (name.as_str(), schema.as_slice()))
652 .collect();
653
654 {
655 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
656 .with_label_values(&["align_batch_create_tables"])
657 .start_timer();
658 self.schema_alterer
659 .create_tables_if_missing_batch(
660 catalog,
661 schema,
662 &create_refs,
663 self.prom_store_with_metric_engine,
664 ctx.clone(),
665 )
666 .await?;
667 }
668
669 let created_table_results = {
670 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
671 .with_label_values(&["align_resolve_table_after_create"])
672 .start_timer();
673 futures::future::join_all(plan.tables_to_create.iter().map(|(table_name, _)| {
674 self.catalog_manager
675 .table(catalog, schema, table_name, Some(ctx.as_ref()))
676 }))
677 .await
678 };
679
680 for ((table_name, _), table_result) in
681 plan.tables_to_create.iter().zip(created_table_results)
682 {
683 let table = table_result?.with_context(|| error::UnexpectedResultSnafu {
684 reason: format!(
685 "Table not found after pending batch create attempt: {}",
686 table_name
687 ),
688 })?;
689 let table_info = table.table_info();
690 let table_id = table_info.ident.table_id;
691 let region_schema = table_info.meta.schema.arrow_schema().clone();
692 plan.region_schemas
693 .insert(table_name.clone(), (region_schema, table_id));
694 }
695
696 Self::enqueue_alter_for_new_tables(table_rows, plan)?;
697
698 Ok(())
699 }
700
701 fn enqueue_alter_for_new_tables(
704 table_rows: &[(String, Rows)],
705 plan: &mut TableResolutionPlan,
706 ) -> Result<()> {
707 let created_tables: HashSet<&str> = plan
708 .tables_to_create
709 .iter()
710 .map(|(table_name, _)| table_name.as_str())
711 .collect();
712
713 for (table_name, rows) in table_rows {
714 if !created_tables.contains(table_name.as_str()) {
715 continue;
716 }
717
718 let Some((region_schema, _)) = plan.region_schemas.get(table_name) else {
719 continue;
720 };
721
722 let missing_columns = identify_missing_columns_from_proto(&rows.schema, region_schema)?;
723 if missing_columns.is_empty()
724 || plan
725 .tables_to_alter
726 .iter()
727 .any(|(existing_name, _)| existing_name == table_name)
728 {
729 continue;
730 }
731
732 plan.tables_to_alter
733 .push((table_name.clone(), missing_columns));
734 }
735
736 Ok(())
737 }
738
739 async fn alter_tables_and_refresh_schemas(
742 &self,
743 catalog: &str,
744 schema: &str,
745 ctx: &QueryContextRef,
746 plan: &mut TableResolutionPlan,
747 ) -> Result<()> {
748 if plan.tables_to_alter.is_empty() {
749 return Ok(());
750 }
751
752 let alter_refs: Vec<(&str, &[String])> = plan
753 .tables_to_alter
754 .iter()
755 .map(|(name, cols)| (name.as_str(), cols.as_slice()))
756 .collect();
757 {
758 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
759 .with_label_values(&["align_batch_add_missing_columns"])
760 .start_timer();
761 self.schema_alterer
762 .add_missing_prom_tag_columns_batch(catalog, schema, &alter_refs, ctx.clone())
763 .await?;
764 }
765
766 let altered_table_results = {
767 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
768 .with_label_values(&["align_resolve_table_after_schema_alter"])
769 .start_timer();
770 futures::future::join_all(plan.tables_to_alter.iter().map(|(table_name, _)| {
771 self.catalog_manager
772 .table(catalog, schema, table_name, Some(ctx.as_ref()))
773 }))
774 .await
775 };
776
777 for ((table_name, _), table_result) in
778 plan.tables_to_alter.iter().zip(altered_table_results)
779 {
780 let table = table_result?.with_context(|| error::UnexpectedResultSnafu {
781 reason: format!(
782 "Table not found after pending batch schema alter: {}",
783 table_name
784 ),
785 })?;
786 let table_info = table.table_info();
787 let table_id = table_info.ident.table_id;
788 let refreshed_region_schema = table_info.meta.schema.arrow_schema().clone();
789 plan.region_schemas
790 .insert(table_name.clone(), (refreshed_region_schema, table_id));
791 }
792
793 Ok(())
794 }
795
796 fn build_aligned_batches(
799 table_rows: &[(String, Rows)],
800 region_schemas: &HashMap<String, (Arc<ArrowSchema>, u32)>,
801 ) -> Result<Vec<(String, u32, RecordBatchWithTsIdx)>> {
802 let mut aligned_batches = Vec::with_capacity(table_rows.len());
803 for (table_name, rows) in table_rows {
804 let (region_schema, table_id) =
805 region_schemas.get(table_name).cloned().with_context(|| {
806 error::UnexpectedResultSnafu {
807 reason: format!("Region schema not resolved for table: {}", table_name),
808 }
809 })?;
810
811 let record_batch = {
812 let _timer = PENDING_ROWS_BATCH_INGEST_STAGE_ELAPSED
813 .with_label_values(&["align_rows_to_record_batch"])
814 .start_timer();
815 rows_to_aligned_record_batch(rows, region_schema.as_ref())?
816 };
817 aligned_batches.push((table_name.clone(), table_id, record_batch));
818 }
819
820 Ok(aligned_batches)
821 }
822
823 fn get_or_spawn_worker(&self, key: BatchKey) -> PendingWorker {
824 if let Some(worker) = self.workers.get(&key)
825 && !worker.tx.is_closed()
826 {
827 return worker.clone();
828 }
829
830 let entry = self.workers.entry(key.clone());
831 match entry {
832 Entry::Occupied(mut worker) => {
833 if worker.get().tx.is_closed() {
834 let new_worker = self.spawn_worker(key);
835 worker.insert(new_worker.clone());
836 PENDING_WORKERS.set(self.workers.len() as i64);
837 new_worker
838 } else {
839 worker.get().clone()
840 }
841 }
842 Entry::Vacant(vacant) => {
843 let worker = self.spawn_worker(key);
844
845 vacant.insert(worker.clone());
846 PENDING_WORKERS.set(self.workers.len() as i64);
847 worker
848 }
849 }
850 }
851
852 fn spawn_worker(&self, key: BatchKey) -> PendingWorker {
853 let (tx, rx) = mpsc::channel(self.worker_channel_capacity);
854 let worker = PendingWorker { tx: tx.clone() };
855 let worker_idle_timeout = self
856 .flush_interval
857 .checked_mul(WORKER_IDLE_TIMEOUT_MULTIPLIER)
858 .unwrap_or(self.flush_interval);
859
860 start_worker(
861 key,
862 worker.tx.clone(),
863 self.workers.clone(),
864 rx,
865 self.shutdown.clone(),
866 self.partition_manager.clone(),
867 self.node_manager.clone(),
868 self.catalog_manager.clone(),
869 self.flow_notification_tx.clone(),
870 self.flush_interval,
871 worker_idle_timeout,
872 self.max_batch_rows,
873 self.flush_semaphore.clone(),
874 );
875
876 worker
877 }
878}
879
880impl Drop for PendingRowsBatcher {
881 fn drop(&mut self) {
882 let _ = self.shutdown.send(());
883 }
884}
885
886impl PendingBatch {
887 fn new(ctx: QueryContextRef) -> Self {
888 let db_string = ctx.get_db_string();
889 Self {
890 tables: HashMap::new(),
891 created_at: Instant::now(),
892 total_row_count: 0,
893 db_string,
894 ctx,
895 waiters: Vec::new(),
896 }
897 }
898
899 fn add_table_batch(
900 &mut self,
901 table_name: String,
902 table_id: TableId,
903 record_batch: RecordBatchWithTsIdx,
904 ) {
905 let entry = self.tables.entry(table_id).or_insert_with(|| TableBatch {
906 table_name,
907 table_id,
908 batches: Vec::new(),
909 row_count: 0,
910 });
911 entry.row_count += record_batch.batch.num_rows();
912 entry.batches.push(record_batch);
913 }
914}
915
916#[allow(clippy::too_many_arguments)]
917fn start_worker(
918 key: BatchKey,
919 worker_tx: mpsc::Sender<WorkerCommand>,
920 workers: Arc<DashMap<BatchKey, PendingWorker>>,
921 mut rx: mpsc::Receiver<WorkerCommand>,
922 shutdown: broadcast::Sender<()>,
923 partition_manager: PartitionRuleManagerRef,
924 node_manager: NodeManagerRef,
925 catalog_manager: CatalogManagerRef,
926 flow_notification_tx: mpsc::Sender<FlowNotification>,
927 flush_interval: Duration,
928 worker_idle_timeout: Duration,
929 max_batch_rows: usize,
930 flush_semaphore: Arc<Semaphore>,
931) {
932 tokio::spawn(async move {
933 let mut batch = None;
934 let mut interval = tokio::time::interval(flush_interval);
935 let mut shutdown_rx = shutdown.subscribe();
936 let idle_deadline = tokio::time::Instant::now() + worker_idle_timeout;
937 let idle_timer = tokio::time::sleep_until(idle_deadline);
938 tokio::pin!(idle_timer);
939
940 loop {
941 tokio::select! {
942 cmd = rx.recv() => {
943 match cmd {
944 Some(WorkerCommand::Submit { table_batches, total_rows, ctx, response_tx, _permit }) => {
945 idle_timer.as_mut().reset(tokio::time::Instant::now() + worker_idle_timeout);
946
947 let pending_batch = batch.get_or_insert_with(||{
948 PENDING_BATCHES.inc();
949 PendingBatch::new(ctx)
950 });
951
952 pending_batch.waiters.push(FlushWaiter { response_tx, _permit });
953
954 for (table_name, table_id, record_batch) in table_batches {
955 pending_batch.add_table_batch(table_name, table_id, record_batch);
956 }
957
958 pending_batch.total_row_count += total_rows;
959 PENDING_ROWS.add(total_rows as i64);
960
961 if pending_batch.total_row_count >= max_batch_rows
962 && let Some(flush) = drain_batch(&mut batch) {
963 spawn_flush(
964 flush,
965 partition_manager.clone(),
966 node_manager.clone(),
967 catalog_manager.clone(),
968 flow_notification_tx.clone(),
969 flush_semaphore.clone(),
970 ).await;
971 }
972 }
973 None => {
974 if let Some(flush) = drain_batch(&mut batch) {
975 flush_batch_with_managers(
976 flush,
977 partition_manager.clone(),
978 node_manager.clone(),
979 catalog_manager.clone(),
980 flow_notification_tx.clone(),
981 ).await;
982 }
983 break;
984 }
985 }
986 }
987 _ = &mut idle_timer => {
988 if !should_close_worker_on_idle_timeout(
989 batch.as_ref().map_or(0, |batch| batch.total_row_count),
990 rx.len(),
991 ) {
992 idle_timer
993 .as_mut()
994 .reset(tokio::time::Instant::now() + worker_idle_timeout);
995 continue;
996 }
997
998 debug!(
999 "Closing idle pending rows worker due to timeout: catalog={}, schema={}, physical_table={}",
1000 key.catalog,
1001 key.schema,
1002 key.physical_table
1003 );
1004 break;
1005 }
1006 _ = interval.tick() => {
1007 if batch
1008 .as_ref()
1009 .is_some_and(|batch| batch.created_at.elapsed() >= flush_interval)
1010 && let Some(flush) = drain_batch(&mut batch) {
1011 spawn_flush(
1012 flush,
1013 partition_manager.clone(),
1014 node_manager.clone(),
1015 catalog_manager.clone(),
1016 flow_notification_tx.clone(),
1017 flush_semaphore.clone(),
1018 ).await;
1019 }
1020 }
1021 _ = shutdown_rx.recv() => {
1022 if let Some(flush) = drain_batch(&mut batch) {
1023 flush_batch_with_managers(
1024 flush,
1025 partition_manager.clone(),
1026 node_manager.clone(),
1027 catalog_manager.clone(),
1028 flow_notification_tx.clone(),
1029 ).await;
1030 }
1031 break;
1032 }
1033 }
1034 }
1035
1036 remove_worker_if_same_channel(workers.as_ref(), &key, &worker_tx);
1037 });
1038}
1039
1040fn remove_worker_if_same_channel(
1041 workers: &DashMap<BatchKey, PendingWorker>,
1042 key: &BatchKey,
1043 worker_tx: &mpsc::Sender<WorkerCommand>,
1044) -> bool {
1045 if let Some(worker) = workers.get(key)
1046 && worker.tx.same_channel(worker_tx)
1047 {
1048 drop(worker);
1049 workers.remove(key);
1050 PENDING_WORKERS.set(workers.len() as i64);
1051 return true;
1052 }
1053
1054 false
1055}
1056
1057fn should_close_worker_on_idle_timeout(total_row_count: usize, queued_requests: usize) -> bool {
1058 total_row_count == 0 && queued_requests == 0
1059}
1060
1061fn drain_batch(batch: &mut Option<PendingBatch>) -> Option<FlushBatch> {
1062 let batch = batch.take()?;
1063 let total_row_count = batch.total_row_count;
1064
1065 if total_row_count == 0 {
1066 return None;
1067 }
1068
1069 let table_batches = batch.tables.into_values().collect();
1070 let waiters = batch.waiters;
1071
1072 PENDING_ROWS.sub(total_row_count as i64);
1073 PENDING_BATCHES.dec();
1074
1075 Some(FlushBatch {
1076 table_batches,
1077 total_row_count,
1078 db_string: batch.db_string,
1079 ctx: batch.ctx,
1080 waiters,
1081 })
1082}
1083
1084async fn spawn_flush(
1085 flush: FlushBatch,
1086 partition_manager: PartitionRuleManagerRef,
1087 node_manager: NodeManagerRef,
1088 catalog_manager: CatalogManagerRef,
1089 flow_notification_tx: mpsc::Sender<FlowNotification>,
1090 semaphore: Arc<Semaphore>,
1091) {
1092 match semaphore.acquire_owned().await {
1093 Ok(permit) => {
1094 tokio::spawn(async move {
1095 let _permit = permit;
1096 flush_batch_with_managers(
1097 flush,
1098 partition_manager,
1099 node_manager,
1100 catalog_manager,
1101 flow_notification_tx,
1102 )
1103 .await;
1104 });
1105 }
1106 Err(err) => {
1107 warn!(err; "Flush semaphore closed, flushing inline");
1108 flush_batch_with_managers(
1109 flush,
1110 partition_manager,
1111 node_manager,
1112 catalog_manager,
1113 flow_notification_tx,
1114 )
1115 .await;
1116 }
1117 }
1118}
1119
1120struct FlushRegionWrite {
1121 datanode: Peer,
1122 request: RegionRequest,
1123}
1124
1125struct PlannedRegionBatch {
1126 region_id: RegionId,
1127 batch: RecordBatch,
1128}
1129
1130#[cfg(test)]
1131impl PlannedRegionBatch {
1132 fn num_rows(&self) -> usize {
1133 self.batch.num_rows()
1134 }
1135}
1136
1137struct ResolvedRegionBatch {
1138 planned: PlannedRegionBatch,
1139 datanode: Peer,
1140}
1141
1142fn should_dispatch_concurrently(region_write_count: usize) -> bool {
1143 region_write_count > 1
1144}
1145
1146fn columns_taxonomy(
1155 batch_schema: &Arc<ArrowSchema>,
1156 table_name: &str,
1157 name_to_ids: &HashMap<String, u32>,
1158 partition_columns: &HashSet<&str>,
1159) -> Result<(Vec<TagColumnInfo>, SmallVec<[usize; 3]>)> {
1160 let mut tag_columns = Vec::new();
1161 let mut essential_column_indices =
1162 SmallVec::<[usize; 3]>::with_capacity(2 + partition_columns.len());
1163 essential_column_indices.push(0);
1165 essential_column_indices.push(0);
1166
1167 let mut timestamp_index = None;
1168 let mut value_index = None;
1169
1170 for (index, field) in batch_schema.fields().iter().enumerate() {
1171 match field.data_type() {
1172 ArrowDataType::Utf8 => {
1173 let column_id = name_to_ids.get(field.name()).copied().with_context(|| {
1174 error::InvalidPromRemoteRequestSnafu {
1175 msg: format!(
1176 "Column '{}' from logical table '{}' not found in physical table column IDs",
1177 field.name(),
1178 table_name
1179 ),
1180 }
1181 })?;
1182 tag_columns.push(TagColumnInfo {
1183 name: field.name().clone(),
1184 index,
1185 column_id,
1186 });
1187
1188 if partition_columns.contains(field.name().as_str()) {
1189 essential_column_indices.push(index);
1190 }
1191 }
1192 ArrowDataType::Timestamp(TimeUnit::Millisecond, _) => {
1193 ensure!(
1194 timestamp_index.replace(index).is_none(),
1195 error::InvalidPromRemoteRequestSnafu {
1196 msg: format!(
1197 "Duplicated timestamp column in logical table '{}' batch schema",
1198 table_name
1199 ),
1200 }
1201 );
1202 }
1203 ArrowDataType::Float64 => {
1204 ensure!(
1205 value_index.replace(index).is_none(),
1206 error::InvalidPromRemoteRequestSnafu {
1207 msg: format!(
1208 "Duplicated value column in logical table '{}' batch schema",
1209 table_name
1210 ),
1211 }
1212 );
1213 }
1214 datatype => {
1215 return error::InvalidPromRemoteRequestSnafu {
1216 msg: format!(
1217 "Unexpected data type '{datatype:?}' in logical table '{}' batch schema",
1218 table_name
1219 ),
1220 }
1221 .fail();
1222 }
1223 }
1224 }
1225
1226 let timestamp_index =
1227 timestamp_index.with_context(|| error::InvalidPromRemoteRequestSnafu {
1228 msg: format!(
1229 "Missing essential column '{}' in logical table '{}' batch schema",
1230 greptime_timestamp(),
1231 table_name
1232 ),
1233 })?;
1234 let value_index = value_index.with_context(|| error::InvalidPromRemoteRequestSnafu {
1235 msg: format!(
1236 "Missing essential column '{}' in logical table '{}' batch schema",
1237 greptime_value(),
1238 table_name
1239 ),
1240 })?;
1241
1242 tag_columns.sort_by(|a, b| a.name.cmp(&b.name));
1243
1244 essential_column_indices[0] = timestamp_index;
1245 essential_column_indices[1] = value_index;
1246
1247 Ok((tag_columns, essential_column_indices))
1248}
1249
1250fn strip_partition_columns_from_batch(batch: RecordBatch) -> Result<RecordBatch> {
1251 ensure!(
1252 batch.num_columns() >= PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT,
1253 error::InternalSnafu {
1254 err_msg: format!(
1255 "Expected at least {} columns in physical batch, got {}",
1256 PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT,
1257 batch.num_columns()
1258 ),
1259 }
1260 );
1261 let essential_indices: Vec<usize> = (0..PHYSICAL_REGION_ESSENTIAL_COLUMN_COUNT).collect();
1262 batch.project(&essential_indices).context(error::ArrowSnafu)
1263}
1264
1265async fn flush_region_writes_concurrently(
1266 node_manager: &(impl PhysicalFlushNodeRequester + ?Sized),
1267 writes: Vec<FlushRegionWrite>,
1268) -> Result<usize> {
1269 let mut affected_rows = 0;
1270 if !should_dispatch_concurrently(writes.len()) {
1271 for write in writes {
1272 let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1273 .with_label_values(&["flush_write_region"])
1274 .start_timer();
1275 affected_rows += node_manager
1276 .handle(&write.datanode, write.request)
1277 .await?
1278 .affected_rows;
1279 }
1280 return Ok(affected_rows);
1281 }
1282
1283 let write_futures = writes.into_iter().map(|write| async move {
1284 let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1285 .with_label_values(&["flush_write_region"])
1286 .start_timer();
1287
1288 let response = node_manager.handle(&write.datanode, write.request).await?;
1289 Ok::<_, Error>(response.affected_rows)
1290 });
1291
1292 let affected_rows = futures::future::try_join_all(write_futures)
1294 .await?
1295 .into_iter()
1296 .sum();
1297 Ok(affected_rows)
1298}
1299
1300async fn flush_batch_with_managers(
1301 flush: FlushBatch,
1302 partition_manager: PartitionRuleManagerRef,
1303 node_manager: NodeManagerRef,
1304 catalog_manager: CatalogManagerRef,
1305 flow_notification_tx: mpsc::Sender<FlowNotification>,
1306) {
1307 let partition_provider = PartitionManagerPhysicalFlushAdapter { partition_manager };
1308 let node_requester = NodeManagerPhysicalFlushAdapter {
1309 node_manager: node_manager.clone(),
1310 };
1311 let catalog_provider = CatalogManagerPhysicalFlushAdapter { catalog_manager };
1312 flush_batch(
1313 flush,
1314 &partition_provider,
1315 &node_requester,
1316 &catalog_provider,
1317 flow_notification_tx,
1318 )
1319 .await;
1320}
1321
1322async fn flush_batch(
1323 flush: FlushBatch,
1324 partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
1325 node_manager: &(impl PhysicalFlushNodeRequester + ?Sized),
1326 catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
1327 flow_notification_tx: mpsc::Sender<FlowNotification>,
1328) {
1329 let FlushBatch {
1330 table_batches,
1331 total_row_count,
1332 db_string,
1333 ctx,
1334 waiters,
1335 } = flush;
1336 let start = Instant::now();
1337
1338 let physical_table_name = ctx
1341 .extension(PHYSICAL_TABLE_KEY)
1342 .unwrap_or(GREPTIME_PHYSICAL_TABLE)
1343 .to_string();
1344 let result = flush_batch_physical(
1345 &table_batches,
1346 &physical_table_name,
1347 &ctx,
1348 partition_manager,
1349 node_manager,
1350 catalog_manager,
1351 )
1352 .await;
1353
1354 let elapsed = start.elapsed().as_secs_f64();
1355 FLUSH_ELAPSED.observe(elapsed);
1356
1357 debug!(
1358 "Pending rows batch flushed, total rows: {}, elapsed time: {}s",
1359 total_row_count, elapsed
1360 );
1361
1362 match result {
1363 Ok(affected_rows) => {
1364 FLUSH_TOTAL.inc();
1365 FLUSH_ROWS.observe(total_row_count as f64);
1366 operator::metrics::DIST_INGEST_ROW_COUNT
1367 .with_label_values(&[db_string.as_str()])
1368 .inc_by(affected_rows as u64);
1369
1370 notify_waiters(waiters, Ok(()));
1371 enqueue_flow_notifications(table_batches, &flow_notification_tx);
1372 }
1373 Err(err) => {
1374 FLUSH_FAILURES.inc();
1375 FLUSH_DROPPED_ROWS.inc_by(total_row_count as u64);
1376 notify_waiters(waiters, Err(err));
1377 }
1378 }
1379}
1380
1381fn extract_timestamps(table_batch: &TableBatch) -> Vec<i64> {
1382 let mut timestamps = Vec::with_capacity(table_batch.row_count);
1383 for batch in &table_batch.batches {
1384 let timestamp_column = batch.batch.column(batch.timestamp_index);
1385 let Some((timestamp_values, _)) =
1386 datatypes::timestamp::timestamp_array_to_primitive(timestamp_column)
1387 else {
1388 error!(
1389 "Failed to extract timestamps from record batch, table_id: {}, timestamp_index: {}",
1390 table_batch.table_id, batch.timestamp_index
1391 );
1392 continue;
1393 };
1394
1395 if timestamp_values.null_count() == 0 {
1396 timestamps.extend_from_slice(timestamp_values.values());
1397 } else {
1398 timestamps.extend(timestamp_values.iter().flatten());
1399 }
1400 }
1401 timestamps
1402}
1403
1404struct FlowNotification {
1405 table_id: TableId,
1406 timestamps: Vec<i64>,
1407}
1408
1409fn try_enqueue_flow_notification(
1410 tx: &mpsc::Sender<FlowNotification>,
1411 notification: FlowNotification,
1412) -> bool {
1413 match tx.try_send(notification) {
1414 Ok(()) => true,
1415 Err(mpsc::error::TrySendError::Full(notification)) => {
1416 FLOW_NOTIFICATION_DROPPED.with_label_values(&["full"]).inc();
1417 warn!(
1418 "Dropping flow notification because queue is full, table_id: {}, queue_capacity: {}",
1419 notification.table_id,
1420 tx.max_capacity()
1421 );
1422 false
1423 }
1424 Err(mpsc::error::TrySendError::Closed(notification)) => {
1425 FLOW_NOTIFICATION_DROPPED
1426 .with_label_values(&["closed"])
1427 .inc();
1428 error!(
1429 "Dropping flow notification because queue is closed, table_id: {}, queue_capacity: {}",
1430 notification.table_id,
1431 tx.max_capacity()
1432 );
1433 false
1434 }
1435 }
1436}
1437
1438fn enqueue_flow_notifications(table_batches: Vec<TableBatch>, tx: &mpsc::Sender<FlowNotification>) {
1439 for table_batch in table_batches {
1440 let timestamps = extract_timestamps(&table_batch);
1441 if timestamps.is_empty() {
1442 continue;
1443 }
1444 try_enqueue_flow_notification(
1445 tx,
1446 FlowNotification {
1447 table_id: table_batch.table_id,
1448 timestamps,
1449 },
1450 );
1451 }
1452}
1453
1454async fn handle_flow_notification(
1455 notification: FlowNotification,
1456 table_flownode_set_cache: TableFlownodeSetCacheRef,
1457 node_manager: NodeManagerRef,
1458) {
1459 let table_id = notification.table_id;
1460 let flownodes = match table_flownode_set_cache.get(table_id).await {
1461 Ok(Some(flownodes)) => flownodes,
1462 Ok(None) => return,
1463 Err(e) => {
1464 error!(e; "Failed to get flownodes for table id: {}", table_id);
1465 return;
1466 }
1467 };
1468 let peers = flownodes.values().cloned().collect::<HashSet<_>>();
1469
1470 for peer in peers {
1471 if let Err(e) = node_manager
1472 .flownode(&peer)
1473 .await
1474 .handle_mark_window_dirty(DirtyWindowRequests {
1475 requests: vec![DirtyWindowRequest {
1476 table_id,
1477 timestamps: notification.timestamps.clone(),
1478 time_ranges: Vec::new(),
1479 }],
1480 })
1481 .await
1482 {
1483 error!(
1484 e;
1485 "Failed to mark timestamps as dirty, table_id: {}, peer_id: {}, peer_addr: {}",
1486 table_id,
1487 peer.id,
1488 peer.addr
1489 );
1490 }
1491 }
1492}
1493
1494fn start_flow_notification_worker(
1495 notification_rx: mpsc::Receiver<FlowNotification>,
1496 table_flownode_set_cache: TableFlownodeSetCacheRef,
1497 node_manager: NodeManagerRef,
1498) {
1499 common_runtime::spawn_global(async move {
1500 tokio_stream::wrappers::ReceiverStream::new(notification_rx)
1501 .for_each_concurrent(MAX_CONCURRENT_FLOW_NOTIFICATIONS, |notification| {
1502 let table_flownode_set_cache = table_flownode_set_cache.clone();
1503 let node_manager = node_manager.clone();
1504 handle_flow_notification(notification, table_flownode_set_cache, node_manager)
1505 })
1506 .await;
1507 });
1508}
1509
1510#[cfg(test)]
1511fn notify_flow_dirty_windows_after_flush(
1512 table_batches: Vec<TableBatch>,
1513 table_flownode_set_cache: TableFlownodeSetCacheRef,
1514 node_manager: NodeManagerRef,
1515) {
1516 let (tx, rx) = mpsc::channel(table_batches.len().max(1));
1517 start_flow_notification_worker(rx, table_flownode_set_cache, node_manager);
1518 enqueue_flow_notifications(table_batches, &tx);
1519}
1520
1521pub async fn flush_batch_physical(
1532 table_batches: &[TableBatch],
1533 physical_table_name: &str,
1534 ctx: &QueryContextRef,
1535 partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
1536 node_manager: &(impl PhysicalFlushNodeRequester + ?Sized),
1537 catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
1538) -> Result<usize> {
1539 let physical_table = {
1541 let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1542 .with_label_values(&["flush_physical_resolve_table"])
1543 .start_timer();
1544 catalog_manager
1545 .physical_table(
1546 ctx.current_catalog(),
1547 &ctx.current_schema(),
1548 physical_table_name,
1549 ctx.as_ref(),
1550 )
1551 .await?
1552 .with_context(|| error::InternalSnafu {
1553 err_msg: format!(
1554 "Physical table '{}' not found during pending flush",
1555 physical_table_name
1556 ),
1557 })?
1558 };
1559
1560 let physical_table_info = physical_table.table_info;
1561 let name_to_ids = physical_table
1562 .col_name_to_ids
1563 .with_context(|| error::InternalSnafu {
1564 err_msg: format!(
1565 "Physical table '{}' has no column IDs for pending flush",
1566 physical_table_name
1567 ),
1568 })?;
1569
1570 let partition_rule = {
1572 let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1573 .with_label_values(&["flush_physical_fetch_partition_rule"])
1574 .start_timer();
1575 partition_manager
1576 .find_table_partition_rule(physical_table_info.as_ref())
1577 .await?
1578 };
1579 let partition_columns = partition_rule.partition_columns();
1580 let partition_columns_set: HashSet<&str> =
1581 partition_columns.iter().map(String::as_str).collect();
1582
1583 let modified_batches =
1585 transform_logical_batches_to_physical(table_batches, &name_to_ids, &partition_columns_set)?;
1586
1587 let combined_batch = concat_modified_batches(&modified_batches)?;
1589
1590 let physical_table_id = physical_table_info.table_id();
1592 let planned_batches = plan_region_batches(
1593 combined_batch,
1594 physical_table_id,
1595 partition_rule.as_ref(),
1596 partition_columns,
1597 )?;
1598
1599 let resolved_batches = resolve_region_targets(planned_batches, partition_manager).await?;
1600 let region_writes = encode_region_write_requests(resolved_batches)?;
1601 flush_region_writes_concurrently(node_manager, region_writes).await
1602}
1603
1604fn transform_logical_batches_to_physical(
1609 table_batches: &[TableBatch],
1610 name_to_ids: &HashMap<String, u32>,
1611 partition_columns_set: &HashSet<&str>,
1612) -> Result<Vec<RecordBatch>> {
1613 let mut modified_batches: Vec<RecordBatch> =
1614 Vec::with_capacity(table_batches.iter().map(|b| b.batches.len()).sum());
1615
1616 let mut modify_elapsed = Duration::ZERO;
1617 let mut columns_taxonomy_elapsed = Duration::ZERO;
1618
1619 for table_batch in table_batches {
1620 let table_id = table_batch.table_id;
1621
1622 for batch in &table_batch.batches {
1623 let batch = &batch.batch;
1624 let batch_schema = batch.schema();
1625 let start = Instant::now();
1626 let (tag_columns, essential_col_indices) = columns_taxonomy(
1627 &batch_schema,
1628 &table_batch.table_name,
1629 name_to_ids,
1630 partition_columns_set,
1631 )?;
1632
1633 columns_taxonomy_elapsed += start.elapsed();
1634 if tag_columns.is_empty() && essential_col_indices.is_empty() {
1635 continue;
1636 }
1637
1638 let modified = {
1639 let start = Instant::now();
1640 let batch = modify_batch_sparse(
1642 batch.clone(),
1643 table_id,
1644 &tag_columns,
1645 &essential_col_indices,
1646 )?;
1647 modify_elapsed += start.elapsed();
1648 batch
1649 };
1650
1651 modified_batches.push(modified);
1652 }
1653 }
1654
1655 PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1656 .with_label_values(&["flush_physical_modify_batch"])
1657 .observe(modify_elapsed.as_secs_f64());
1658 PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1659 .with_label_values(&["flush_physical_columns_taxonomy"])
1660 .observe(columns_taxonomy_elapsed.as_secs_f64());
1661
1662 ensure!(
1663 !modified_batches.is_empty(),
1664 error::InternalSnafu {
1665 err_msg: "No batches can be transformed during pending flush",
1666 }
1667 );
1668 Ok(modified_batches)
1669}
1670
1671fn concat_modified_batches(modified_batches: &[RecordBatch]) -> Result<RecordBatch> {
1675 let combined_schema = modified_batches[0].schema();
1676 let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1677 .with_label_values(&["flush_physical_concat_all"])
1678 .start_timer();
1679 concat_batches(&combined_schema, modified_batches).context(error::ArrowSnafu)
1680}
1681
1682fn split_combined_batch_by_region(
1683 combined_batch: &RecordBatch,
1684 partition_rule: &dyn partition::partition::PartitionRule,
1685) -> Result<HashMap<u32, partition::partition::RegionMask>> {
1686 let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1687 .with_label_values(&["flush_physical_split_record_batch"])
1688 .start_timer();
1689 let map = partition_rule.split_record_batch(combined_batch)?;
1690 Ok(map)
1691}
1692
1693fn prepare_physical_region_routing_batch(
1694 combined_batch: RecordBatch,
1695 partition_columns: &[String],
1696) -> Result<RecordBatch> {
1697 if partition_columns.is_empty() {
1698 return Ok(combined_batch);
1699 }
1700 strip_partition_columns_from_batch(combined_batch)
1701}
1702
1703fn plan_region_batch(
1704 stripped_batch: &RecordBatch,
1705 physical_table_id: TableId,
1706 region_number: u32,
1707 mask: &partition::partition::RegionMask,
1708) -> Result<Option<PlannedRegionBatch>> {
1709 if mask.select_none() {
1710 return Ok(None);
1711 }
1712
1713 let region_batch = if mask.select_all() {
1714 stripped_batch.clone()
1715 } else {
1716 let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1717 .with_label_values(&["flush_physical_filter_record_batch"])
1718 .start_timer();
1719 filter_record_batch(stripped_batch, mask.array()).context(error::ArrowSnafu)?
1720 };
1721
1722 let row_count = region_batch.num_rows();
1723 if row_count == 0 {
1724 return Ok(None);
1725 }
1726
1727 Ok(Some(PlannedRegionBatch {
1728 region_id: RegionId::new(physical_table_id, region_number),
1729 batch: region_batch,
1730 }))
1731}
1732
1733fn plan_region_batches(
1734 combined_batch: RecordBatch,
1735 physical_table_id: TableId,
1736 partition_rule: &dyn partition::partition::PartitionRule,
1737 partition_columns: &[String],
1738) -> Result<Vec<PlannedRegionBatch>> {
1739 let region_masks = split_combined_batch_by_region(&combined_batch, partition_rule)?;
1740 let stripped_batch = prepare_physical_region_routing_batch(combined_batch, partition_columns)?;
1741
1742 let mut planned_batches = Vec::new();
1743 for (region_number, mask) in region_masks {
1744 if let Some(planned_batch) =
1745 plan_region_batch(&stripped_batch, physical_table_id, region_number, &mask)?
1746 {
1747 planned_batches.push(planned_batch);
1748 }
1749 }
1750
1751 Ok(planned_batches)
1752}
1753
1754async fn resolve_region_targets(
1755 planned_batches: Vec<PlannedRegionBatch>,
1756 partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
1757) -> Result<Vec<ResolvedRegionBatch>> {
1758 let mut resolved_batches = Vec::with_capacity(planned_batches.len());
1759 for planned in planned_batches {
1760 let datanode = {
1761 let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1762 .with_label_values(&["flush_physical_resolve_region_leader"])
1763 .start_timer();
1764 partition_manager
1765 .find_region_leader(planned.region_id)
1766 .await?
1767 };
1768
1769 resolved_batches.push(ResolvedRegionBatch { planned, datanode });
1770 }
1771
1772 Ok(resolved_batches)
1773}
1774
1775fn encode_region_write_requests(
1776 resolved_batches: Vec<ResolvedRegionBatch>,
1777) -> Result<Vec<FlushRegionWrite>> {
1778 let mut region_writes = Vec::with_capacity(resolved_batches.len());
1779 for resolved in resolved_batches {
1780 let region_id = resolved.planned.region_id;
1781 let (schema_bytes, data_header, payload) = {
1782 let _timer = PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
1783 .with_label_values(&["flush_physical_encode_ipc"])
1784 .start_timer();
1785 record_batch_to_ipc(resolved.planned.batch)?
1786 };
1787
1788 let request = RegionRequest {
1789 header: Some(RegionRequestHeader {
1790 tracing_context: TracingContext::from_current_span().to_w3c(),
1791 ..Default::default()
1792 }),
1793 body: Some(region_request::Body::BulkInsert(BulkInsertRequest {
1794 region_id: region_id.as_u64(),
1795 partition_expr_version: None,
1796 aligned_schema_version: None,
1799 body: Some(bulk_insert_request::Body::ArrowIpc(ArrowIpc {
1800 schema: schema_bytes,
1801 data_header,
1802 payload,
1803 })),
1804 })),
1805 };
1806
1807 region_writes.push(FlushRegionWrite {
1808 datanode: resolved.datanode,
1809 request,
1810 });
1811 }
1812
1813 Ok(region_writes)
1814}
1815
1816fn notify_waiters(waiters: Vec<FlushWaiter>, result: Result<()>) {
1817 let shared_result = result.map_err(Arc::new);
1818 for waiter in waiters {
1819 let _ = waiter.response_tx.send(match &shared_result {
1820 Ok(()) => Ok(()),
1821 Err(error) => Err(Arc::clone(error)),
1822 });
1823 }
1825}
1826
1827fn record_batch_to_ipc(record_batch: RecordBatch) -> Result<(Bytes, Bytes, Bytes)> {
1828 let mut encoder = FlightEncoder::default();
1829 let schema = encoder.encode_schema(record_batch.schema().as_ref());
1830 let mut iter = encoder
1831 .encode(FlightMessage::RecordBatch(record_batch))
1832 .into_iter();
1833 let Some(flight_data) = iter.next() else {
1834 return Err(Error::Internal {
1835 err_msg: "Failed to encode empty flight data".to_string(),
1836 });
1837 };
1838 if iter.next().is_some() {
1839 return Err(Error::NotSupported {
1840 feat: "bulk insert RecordBatch with dictionary arrays".to_string(),
1841 });
1842 }
1843
1844 Ok((
1845 schema.data_header,
1846 flight_data.data_header,
1847 flight_data.data_body,
1848 ))
1849}
1850
1851#[cfg(test)]
1852mod tests {
1853 use std::any::Any;
1854 use std::collections::{HashMap, HashSet};
1855 use std::sync::atomic::{AtomicUsize, Ordering};
1856 use std::sync::{Arc, Mutex};
1857 use std::time::{Duration, Instant};
1858
1859 use api::region::RegionResponse;
1860 use api::v1::flow::{DirtyWindowRequests, FlowRequest, FlowResponse};
1861 use api::v1::meta::Peer;
1862 use api::v1::region::{InsertRequests, RegionRequest, region_request};
1863 use api::v1::value::ValueData;
1864 use api::v1::{
1865 ColumnDataType, ColumnSchema, Row, RowInsertRequest, RowInsertRequests, Rows, SemanticType,
1866 Value,
1867 };
1868 use arrow::array::{BinaryArray, BooleanArray, StringArray, TimestampMillisecondArray};
1869 use arrow::datatypes::{DataType as ArrowDataType, Field, Schema as ArrowSchema};
1870 use arrow::record_batch::RecordBatch;
1871 use async_trait::async_trait;
1872 use catalog::error::Result as CatalogResult;
1873 use common_meta::cache::{TableFlownodeSetCacheRef, new_table_flownode_set_cache};
1874 use common_meta::error::Result as MetaResult;
1875 use common_meta::instruction::{CacheIdent, CreateFlow};
1876 use common_meta::kv_backend::memory::MemoryKvBackend;
1877 use common_meta::kv_backend::{KvBackend, TxnService};
1878 use common_meta::node_manager::{
1879 Datanode, DatanodeManager, DatanodeRef, Flownode, FlownodeManager, FlownodeRef,
1880 NodeManagerRef,
1881 };
1882 use common_meta::rpc::store::{
1883 BatchDeleteRequest, BatchDeleteResponse, BatchGetRequest, BatchGetResponse,
1884 BatchPutRequest, BatchPutResponse, DeleteRangeRequest, DeleteRangeResponse, PutRequest,
1885 PutResponse, RangeRequest, RangeResponse,
1886 };
1887 use common_query::request::QueryRequest;
1888 use common_recordbatch::SendableRecordBatchStream;
1889 use dashmap::DashMap;
1890 use datatypes::schema::{ColumnSchema as DtColumnSchema, Schema as DtSchema};
1891 use moka::future::CacheBuilder;
1892 use partition::error::Result as PartitionResult;
1893 use partition::partition::{PartitionRule, PartitionRuleRef, RegionMask};
1894 use smallvec::SmallVec;
1895 use snafu::ResultExt;
1896 use store_api::storage::RegionId;
1897 use table::metadata::TableId;
1898 use table::test_util::table_info::test_table_info;
1899 use tokio::sync::{Notify, Semaphore, mpsc, oneshot};
1900 use tokio::time::sleep;
1901
1902 use super::{
1903 BatchKey, Error, FlushBatch, FlushRegionWrite, FlushWaiter, PendingBatch,
1904 PendingRowsBatcher, PendingWorker, PhysicalFlushCatalogProvider,
1905 PhysicalFlushNodeRequester, PhysicalFlushPartitionProvider, PhysicalTableMetadata,
1906 PlannedRegionBatch, RecordBatchWithTsIdx, ResolvedRegionBatch, TableBatch, WorkerCommand,
1907 columns_taxonomy, drain_batch, encode_region_write_requests, extract_timestamps,
1908 flush_batch, flush_batch_physical, flush_region_writes_concurrently, greptime_timestamp,
1909 notify_flow_dirty_windows_after_flush, plan_region_batches, remove_worker_if_same_channel,
1910 should_close_worker_on_idle_timeout, should_dispatch_concurrently,
1911 start_flow_notification_worker, strip_partition_columns_from_batch,
1912 transform_logical_batches_to_physical, try_enqueue_flow_notification,
1913 };
1914 use crate::error;
1915 use crate::metrics::FLOW_NOTIFICATION_DROPPED;
1916 use crate::prom_row_builder::rows_to_aligned_record_batch;
1917
1918 fn mock_rows(row_count: usize, schema_name: &str) -> Rows {
1919 Rows {
1920 schema: vec![ColumnSchema {
1921 column_name: schema_name.to_string(),
1922 ..Default::default()
1923 }],
1924 rows: (0..row_count).map(|_| Row { values: vec![] }).collect(),
1925 }
1926 }
1927
1928 fn mock_tag_batch(tag_name: &str, tag_value: &str, ts: i64, val: f64) -> RecordBatch {
1929 let schema = Arc::new(ArrowSchema::new(vec![
1930 Field::new(
1931 "greptime_timestamp",
1932 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
1933 false,
1934 ),
1935 Field::new("greptime_value", ArrowDataType::Float64, true),
1936 Field::new(tag_name, ArrowDataType::Utf8, true),
1937 ]));
1938
1939 RecordBatch::try_new(
1940 schema,
1941 vec![
1942 Arc::new(TimestampMillisecondArray::from(vec![ts])),
1943 Arc::new(arrow::array::Float64Array::from(vec![val])),
1944 Arc::new(StringArray::from(vec![tag_value])),
1945 ],
1946 )
1947 .unwrap()
1948 }
1949
1950 fn mock_aligned_tag_batch(
1951 tag_name: &str,
1952 tag_value: &str,
1953 ts: i64,
1954 val: f64,
1955 ) -> RecordBatchWithTsIdx {
1956 RecordBatchWithTsIdx::try_new(mock_tag_batch(tag_name, tag_value, ts, val), 0).unwrap()
1957 }
1958
1959 fn mock_timestamp_batch(timestamps: Vec<Option<i64>>) -> RecordBatchWithTsIdx {
1960 let batch = RecordBatch::try_new(
1961 Arc::new(ArrowSchema::new(vec![Field::new(
1962 greptime_timestamp(),
1963 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
1964 true,
1965 )])),
1966 vec![Arc::new(TimestampMillisecondArray::from(timestamps))],
1967 )
1968 .unwrap();
1969 RecordBatchWithTsIdx::try_new(batch, 0).unwrap()
1970 }
1971
1972 #[test]
1973 fn test_extract_timestamps_appends_non_null_batches_in_order() {
1974 let table_batch = TableBatch {
1975 table_name: "cpu".to_string(),
1976 table_id: 42,
1977 batches: vec![
1978 mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
1979 mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
1980 ],
1981 row_count: 2,
1982 };
1983
1984 assert_eq!(vec![1000, 2000], extract_timestamps(&table_batch));
1985 }
1986
1987 #[test]
1988 fn test_extract_timestamps_omits_nulls_and_retains_order() {
1989 let table_batch = TableBatch {
1990 table_name: "cpu".to_string(),
1991 table_id: 42,
1992 batches: vec![
1993 mock_timestamp_batch(vec![Some(1000), None, Some(3000)]),
1994 mock_timestamp_batch(vec![None, Some(5000)]),
1995 ],
1996 row_count: 5,
1997 };
1998
1999 assert_eq!(vec![1000, 3000, 5000], extract_timestamps(&table_batch));
2000 }
2001
2002 #[test]
2003 fn test_record_batch_with_ts_idx_rejects_out_of_bounds_index() {
2004 let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
2005
2006 assert!(RecordBatchWithTsIdx::try_new(batch, 3).is_err());
2007 }
2008
2009 #[test]
2010 fn test_record_batch_with_ts_idx_rejects_non_timestamp_column() {
2011 let batch = mock_tag_batch("tag1", "host-1", 1000, 1.0);
2012
2013 assert!(RecordBatchWithTsIdx::try_new(batch, 1).is_err());
2014 }
2015
2016 #[test]
2017 fn test_extract_timestamps_supports_per_batch_timestamp_indices() {
2018 let timestamp_first = RecordBatch::try_new(
2019 Arc::new(ArrowSchema::new(vec![
2020 Field::new(
2021 "ts",
2022 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2023 false,
2024 ),
2025 Field::new("host", ArrowDataType::Utf8, true),
2026 ])),
2027 vec![
2028 Arc::new(TimestampMillisecondArray::from(vec![1000, 2000])),
2029 Arc::new(StringArray::from(vec!["host-1", "host-2"])),
2030 ],
2031 )
2032 .unwrap();
2033 let timestamp_second = RecordBatch::try_new(
2034 Arc::new(ArrowSchema::new(vec![
2035 Field::new("host", ArrowDataType::Utf8, true),
2036 Field::new(
2037 "timestamp",
2038 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2039 false,
2040 ),
2041 ])),
2042 vec![
2043 Arc::new(StringArray::from(vec!["host-3", "host-4"])),
2044 Arc::new(TimestampMillisecondArray::from(vec![3000, 4000])),
2045 ],
2046 )
2047 .unwrap();
2048 let table_batch = TableBatch {
2049 table_name: "cpu".to_string(),
2050 table_id: 42,
2051 batches: vec![
2052 RecordBatchWithTsIdx::try_new(timestamp_first, 0).unwrap(),
2053 RecordBatchWithTsIdx::try_new(timestamp_second, 1).unwrap(),
2054 ],
2055 row_count: 4,
2056 };
2057
2058 assert_eq!(
2059 vec![1000, 2000, 3000, 4000],
2060 extract_timestamps(&table_batch)
2061 );
2062 }
2063
2064 #[test]
2065 fn test_extract_timestamps_uses_aligned_custom_timestamp_index() {
2066 let rows = Rows {
2067 schema: vec![
2068 ColumnSchema {
2069 column_name: greptime_timestamp().to_string(),
2070 datatype: ColumnDataType::TimestampMillisecond as i32,
2071 semantic_type: SemanticType::Timestamp as i32,
2072 ..Default::default()
2073 },
2074 ColumnSchema {
2075 column_name: "host".to_string(),
2076 datatype: ColumnDataType::String as i32,
2077 semantic_type: SemanticType::Tag as i32,
2078 ..Default::default()
2079 },
2080 ColumnSchema {
2081 column_name: "greptime_value".to_string(),
2082 datatype: ColumnDataType::Float64 as i32,
2083 semantic_type: SemanticType::Field as i32,
2084 ..Default::default()
2085 },
2086 ],
2087 rows: vec![
2088 Row {
2089 values: vec![
2090 Value {
2091 value_data: Some(ValueData::TimestampMillisecondValue(1000)),
2092 },
2093 Value {
2094 value_data: Some(ValueData::StringValue("host-1".to_string())),
2095 },
2096 Value {
2097 value_data: Some(ValueData::F64Value(1.0)),
2098 },
2099 ],
2100 },
2101 Row {
2102 values: vec![
2103 Value {
2104 value_data: Some(ValueData::TimestampMillisecondValue(2000)),
2105 },
2106 Value {
2107 value_data: Some(ValueData::StringValue("host-2".to_string())),
2108 },
2109 Value {
2110 value_data: Some(ValueData::F64Value(2.0)),
2111 },
2112 ],
2113 },
2114 ],
2115 };
2116 let target_schema = ArrowSchema::new(vec![
2117 Field::new("host", ArrowDataType::Utf8, true),
2118 Field::new(
2119 "timestamp",
2120 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2121 false,
2122 ),
2123 Field::new("greptime_value", ArrowDataType::Float64, true),
2124 ]);
2125 let batch = rows_to_aligned_record_batch(&rows, &target_schema).unwrap();
2126 assert_eq!(1, batch.timestamp_index);
2127 let table_batch = TableBatch {
2128 table_name: "cpu".to_string(),
2129 table_id: 42,
2130 row_count: batch.batch.num_rows(),
2131 batches: vec![batch],
2132 };
2133
2134 assert_eq!(vec![1000, 2000], extract_timestamps(&table_batch));
2135 }
2136
2137 #[test]
2138 fn test_flow_notification_queue_drops_when_full() {
2139 let (tx, mut rx) = mpsc::channel(1);
2140 let notification = |table_id| super::FlowNotification {
2141 table_id,
2142 timestamps: vec![table_id as i64],
2143 };
2144 let dropped = FLOW_NOTIFICATION_DROPPED.with_label_values(&["full"]);
2145 let dropped_before = dropped.get();
2146
2147 assert!(try_enqueue_flow_notification(&tx, notification(1)));
2148 assert!(!try_enqueue_flow_notification(&tx, notification(2)));
2149
2150 assert_eq!(1, rx.try_recv().unwrap().table_id);
2151 assert_eq!(dropped_before + 1, dropped.get());
2152 }
2153
2154 fn mock_physical_table_metadata(table_id: TableId) -> PhysicalTableMetadata {
2155 let schema = Arc::new(
2156 DtSchema::try_new(vec![
2157 DtColumnSchema::new(
2158 "__primary_key",
2159 datatypes::prelude::ConcreteDataType::binary_datatype(),
2160 false,
2161 ),
2162 DtColumnSchema::new(
2163 "greptime_timestamp",
2164 datatypes::prelude::ConcreteDataType::timestamp_millisecond_datatype(),
2165 false,
2166 ),
2167 DtColumnSchema::new(
2168 "greptime_value",
2169 datatypes::prelude::ConcreteDataType::float64_datatype(),
2170 true,
2171 ),
2172 DtColumnSchema::new(
2173 "tag1",
2174 datatypes::prelude::ConcreteDataType::string_datatype(),
2175 true,
2176 ),
2177 ])
2178 .unwrap(),
2179 );
2180 let mut table_info = test_table_info(table_id, "phy", "public", "greptime", schema);
2181 table_info.meta.column_ids = vec![0, 1, 2, 3];
2182
2183 PhysicalTableMetadata {
2184 table_info: Arc::new(table_info),
2185 col_name_to_ids: Some(HashMap::from([("tag1".to_string(), 3)])),
2186 }
2187 }
2188
2189 struct MockFlushCatalogProvider {
2190 table: Option<PhysicalTableMetadata>,
2191 }
2192
2193 #[async_trait]
2194 impl PhysicalFlushCatalogProvider for MockFlushCatalogProvider {
2195 async fn physical_table(
2196 &self,
2197 _catalog: &str,
2198 _schema: &str,
2199 _table_name: &str,
2200 _query_ctx: &session::context::QueryContext,
2201 ) -> CatalogResult<Option<PhysicalTableMetadata>> {
2202 Ok(self.table.clone())
2203 }
2204 }
2205
2206 struct SingleRegionPartitionRule;
2207
2208 impl PartitionRule for SingleRegionPartitionRule {
2209 fn as_any(&self) -> &dyn std::any::Any {
2210 self
2211 }
2212
2213 fn partition_columns(&self) -> &[String] {
2214 &[]
2215 }
2216
2217 fn find_region(
2218 &self,
2219 _values: &[datatypes::prelude::Value],
2220 ) -> partition::error::Result<store_api::storage::RegionNumber> {
2221 unimplemented!()
2222 }
2223
2224 fn split_record_batch(
2225 &self,
2226 record_batch: &RecordBatch,
2227 ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
2228 {
2229 Ok(HashMap::from([(
2230 1,
2231 RegionMask::new(
2232 arrow::array::BooleanArray::from(vec![true; record_batch.num_rows()]),
2233 record_batch.num_rows(),
2234 ),
2235 )]))
2236 }
2237 }
2238
2239 struct TwoRegionPartitionRule {
2240 partition_columns: Vec<String>,
2241 }
2242
2243 impl PartitionRule for TwoRegionPartitionRule {
2244 fn as_any(&self) -> &dyn std::any::Any {
2245 self
2246 }
2247
2248 fn partition_columns(&self) -> &[String] {
2249 &self.partition_columns
2250 }
2251
2252 fn find_region(
2253 &self,
2254 _values: &[datatypes::prelude::Value],
2255 ) -> partition::error::Result<store_api::storage::RegionNumber> {
2256 unimplemented!()
2257 }
2258
2259 fn split_record_batch(
2260 &self,
2261 _record_batch: &RecordBatch,
2262 ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
2263 {
2264 Ok(HashMap::from([
2265 (1, RegionMask::new(BooleanArray::from(vec![true, false]), 1)),
2266 (2, RegionMask::new(BooleanArray::from(vec![false, true]), 1)),
2267 (
2268 3,
2269 RegionMask::new(BooleanArray::from(vec![false, false]), 0),
2270 ),
2271 ]))
2272 }
2273 }
2274
2275 struct MockFlushPartitionProvider {
2276 partition_rule_calls: Arc<AtomicUsize>,
2277 region_leader_calls: Arc<AtomicUsize>,
2278 }
2279
2280 #[async_trait]
2281 impl PhysicalFlushPartitionProvider for MockFlushPartitionProvider {
2282 async fn find_table_partition_rule(
2283 &self,
2284 _table_info: &table::metadata::TableInfo,
2285 ) -> PartitionResult<PartitionRuleRef> {
2286 self.partition_rule_calls.fetch_add(1, Ordering::SeqCst);
2287 Ok(Arc::new(SingleRegionPartitionRule))
2288 }
2289
2290 async fn find_region_leader(&self, _region_id: RegionId) -> error::Result<Peer> {
2291 self.region_leader_calls.fetch_add(1, Ordering::SeqCst);
2292 Ok(Peer {
2293 id: 1,
2294 addr: "node-1".to_string(),
2295 })
2296 }
2297 }
2298
2299 #[derive(Default)]
2300 struct MockFlushNodeRequester {
2301 writes: Arc<AtomicUsize>,
2302 fail: bool,
2303 }
2304
2305 #[async_trait]
2306 impl PhysicalFlushNodeRequester for MockFlushNodeRequester {
2307 async fn handle(
2308 &self,
2309 _peer: &Peer,
2310 _request: RegionRequest,
2311 ) -> error::Result<RegionResponse> {
2312 self.writes.fetch_add(1, Ordering::SeqCst);
2313 if self.fail {
2314 return Err(Error::Internal {
2315 err_msg: "physical write failed".to_string(),
2316 });
2317 }
2318 Ok(RegionResponse::new(0))
2319 }
2320 }
2321
2322 #[test]
2323 fn test_collect_non_empty_table_rows_filters_empty_payloads() {
2324 let requests = RowInsertRequests {
2325 inserts: vec![
2326 RowInsertRequest {
2327 table_name: "cpu".to_string(),
2328 rows: Some(mock_rows(2, "host")),
2329 },
2330 RowInsertRequest {
2331 table_name: "mem".to_string(),
2332 rows: Some(mock_rows(0, "host")),
2333 },
2334 RowInsertRequest {
2335 table_name: "disk".to_string(),
2336 rows: None,
2337 },
2338 ],
2339 };
2340
2341 let (table_rows, total_rows) = PendingRowsBatcher::collect_non_empty_table_rows(requests);
2342
2343 assert_eq!(2, total_rows);
2344 assert_eq!(1, table_rows.len());
2345 assert_eq!("cpu", table_rows[0].0);
2346 assert_eq!(2, table_rows[0].1.rows.len());
2347 }
2348
2349 #[test]
2350 fn test_drain_batch_takes_initialized_pending_batch_from_option() {
2351 let ctx = session::context::QueryContext::arc();
2352 let (response_tx, _response_rx) = oneshot::channel();
2353 let permit = Arc::new(Semaphore::new(1)).try_acquire_owned().unwrap();
2354 let mut batch = Some(PendingBatch {
2355 tables: HashMap::from([(
2356 42,
2357 TableBatch {
2358 table_name: "cpu".to_string(),
2359 table_id: 42,
2360 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2361 row_count: 1,
2362 },
2363 )]),
2364 created_at: Instant::now(),
2365 total_row_count: 1,
2366 db_string: ctx.get_db_string(),
2367 ctx: ctx.clone(),
2368 waiters: vec![FlushWaiter {
2369 response_tx,
2370 _permit: permit,
2371 }],
2372 });
2373
2374 let flush = drain_batch(&mut batch).unwrap();
2375
2376 assert!(batch.is_none());
2377 assert_eq!(1, flush.total_row_count);
2378 assert_eq!(1, flush.table_batches.len());
2379 assert_eq!(ctx.get_db_string(), flush.db_string);
2380 assert_eq!(ctx.current_catalog(), flush.ctx.current_catalog());
2381 }
2382
2383 #[test]
2384 fn test_pending_batch_keeps_same_name_batches_with_distinct_table_ids() {
2385 let ctx = session::context::QueryContext::arc();
2386 let mut pending_batch = PendingBatch::new(ctx);
2387
2388 pending_batch.add_table_batch(
2389 "cpu".to_string(),
2390 42,
2391 mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
2392 );
2393 pending_batch.add_table_batch(
2394 "cpu".to_string(),
2395 43,
2396 mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
2397 );
2398
2399 assert_eq!(2, pending_batch.tables.len());
2400 assert_eq!(42, pending_batch.tables[&42].table_id);
2401 assert_eq!(43, pending_batch.tables[&43].table_id);
2402 assert_eq!("cpu", pending_batch.tables[&42].table_name);
2403 assert_eq!("cpu", pending_batch.tables[&43].table_name);
2404 }
2405
2406 #[derive(Clone)]
2407 struct ConcurrentMockDatanode {
2408 delay: Duration,
2409 inflight: Arc<AtomicUsize>,
2410 max_inflight: Arc<AtomicUsize>,
2411 }
2412
2413 #[async_trait]
2414 impl Datanode for ConcurrentMockDatanode {
2415 async fn handle(&self, _request: RegionRequest) -> MetaResult<RegionResponse> {
2416 let now = self.inflight.fetch_add(1, Ordering::SeqCst) + 1;
2417 loop {
2418 let max = self.max_inflight.load(Ordering::SeqCst);
2419 if now <= max {
2420 break;
2421 }
2422 if self
2423 .max_inflight
2424 .compare_exchange(max, now, Ordering::SeqCst, Ordering::SeqCst)
2425 .is_ok()
2426 {
2427 break;
2428 }
2429 }
2430
2431 sleep(self.delay).await;
2432 self.inflight.fetch_sub(1, Ordering::SeqCst);
2433 Ok(RegionResponse::new(0))
2434 }
2435
2436 async fn handle_query(
2437 &self,
2438 _request: QueryRequest,
2439 ) -> MetaResult<SendableRecordBatchStream> {
2440 unimplemented!()
2441 }
2442 }
2443
2444 #[derive(Clone)]
2445 struct ConcurrentMockNodeManager {
2446 datanodes: Arc<HashMap<u64, DatanodeRef>>,
2447 }
2448
2449 #[async_trait]
2450 impl DatanodeManager for ConcurrentMockNodeManager {
2451 async fn datanode(&self, node: &Peer) -> DatanodeRef {
2452 self.datanodes
2453 .get(&node.id)
2454 .expect("datanode not found")
2455 .clone()
2456 }
2457 }
2458
2459 struct NoopFlownode;
2460
2461 #[async_trait]
2462 impl Flownode for NoopFlownode {
2463 async fn handle(&self, _request: FlowRequest) -> MetaResult<FlowResponse> {
2464 unimplemented!()
2465 }
2466
2467 async fn handle_inserts(&self, _request: InsertRequests) -> MetaResult<FlowResponse> {
2468 unimplemented!()
2469 }
2470
2471 async fn handle_mark_window_dirty(
2472 &self,
2473 _req: DirtyWindowRequests,
2474 ) -> MetaResult<FlowResponse> {
2475 unimplemented!()
2476 }
2477 }
2478
2479 #[async_trait]
2480 impl FlownodeManager for ConcurrentMockNodeManager {
2481 async fn flownode(&self, _node: &Peer) -> FlownodeRef {
2482 Arc::new(NoopFlownode)
2483 }
2484 }
2485
2486 struct RecordingFlownode {
2487 requests_tx: mpsc::UnboundedSender<DirtyWindowRequests>,
2488 }
2489
2490 #[async_trait]
2491 impl Flownode for RecordingFlownode {
2492 async fn handle(&self, _request: FlowRequest) -> MetaResult<FlowResponse> {
2493 unimplemented!()
2494 }
2495
2496 async fn handle_inserts(&self, _request: InsertRequests) -> MetaResult<FlowResponse> {
2497 unimplemented!()
2498 }
2499
2500 async fn handle_mark_window_dirty(
2501 &self,
2502 req: DirtyWindowRequests,
2503 ) -> MetaResult<FlowResponse> {
2504 self.requests_tx.send(req).unwrap();
2505 Ok(FlowResponse::default())
2506 }
2507 }
2508
2509 struct FlowNotificationMockNodeManager {
2510 flownode: FlownodeRef,
2511 }
2512
2513 #[async_trait]
2514 impl DatanodeManager for FlowNotificationMockNodeManager {
2515 async fn datanode(&self, _node: &Peer) -> DatanodeRef {
2516 unimplemented!()
2517 }
2518 }
2519
2520 #[async_trait]
2521 impl FlownodeManager for FlowNotificationMockNodeManager {
2522 async fn flownode(&self, _node: &Peer) -> FlownodeRef {
2523 self.flownode.clone()
2524 }
2525 }
2526
2527 async fn mock_table_flownode_cache(table_id: TableId, peer: Peer) -> TableFlownodeSetCacheRef {
2528 let cache = Arc::new(new_table_flownode_set_cache(
2529 "test".to_string(),
2530 CacheBuilder::new(1).build(),
2531 Arc::new(MemoryKvBackend::default()),
2532 ));
2533 cache
2534 .invalidate(&[CacheIdent::CreateFlow(CreateFlow {
2535 flow_id: 1,
2536 source_table_ids: vec![table_id],
2537 partition_to_peer_mapping: vec![(0, peer.clone()), (1, peer)],
2538 })])
2539 .await
2540 .unwrap();
2541 cache
2542 }
2543
2544 fn mock_flow_notification_sender(
2545 cache: TableFlownodeSetCacheRef,
2546 node_manager: NodeManagerRef,
2547 ) -> mpsc::Sender<super::FlowNotification> {
2548 let (tx, rx) = mpsc::channel(16);
2549 start_flow_notification_worker(rx, cache, node_manager);
2550 tx
2551 }
2552
2553 struct BlockingRangeKvBackend {
2554 range_started: Mutex<Option<oneshot::Sender<()>>>,
2555 range_release: Arc<Notify>,
2556 }
2557
2558 impl TxnService for BlockingRangeKvBackend {
2559 type Error = common_meta::error::Error;
2560 }
2561
2562 #[async_trait]
2563 impl KvBackend for BlockingRangeKvBackend {
2564 fn name(&self) -> &str {
2565 "blocking_range"
2566 }
2567
2568 fn as_any(&self) -> &dyn Any {
2569 self
2570 }
2571
2572 async fn range(&self, _req: RangeRequest) -> MetaResult<RangeResponse> {
2573 let range_started = self.range_started.lock().unwrap().take();
2574 if let Some(range_started) = range_started {
2575 let _ = range_started.send(());
2576 self.range_release.notified().await;
2577 }
2578 Ok(RangeResponse {
2579 kvs: Vec::new(),
2580 more: false,
2581 })
2582 }
2583
2584 async fn put(&self, _req: PutRequest) -> MetaResult<PutResponse> {
2585 unimplemented!()
2586 }
2587
2588 async fn batch_put(&self, _req: BatchPutRequest) -> MetaResult<BatchPutResponse> {
2589 unimplemented!()
2590 }
2591
2592 async fn batch_get(&self, _req: BatchGetRequest) -> MetaResult<BatchGetResponse> {
2593 unimplemented!()
2594 }
2595
2596 async fn delete_range(&self, _req: DeleteRangeRequest) -> MetaResult<DeleteRangeResponse> {
2597 unimplemented!()
2598 }
2599
2600 async fn batch_delete(&self, _req: BatchDeleteRequest) -> MetaResult<BatchDeleteResponse> {
2601 unimplemented!()
2602 }
2603 }
2604
2605 #[tokio::test]
2606 async fn test_flow_notifications_do_not_block_on_previous_table_cache_lookup() {
2607 let blocked_table_id = 41;
2608 let cached_table_id = 42;
2609 let peer = Peer {
2610 id: 7,
2611 addr: "flow-7".to_string(),
2612 };
2613 let (range_started_tx, range_started_rx) = oneshot::channel();
2614 let range_release = Arc::new(Notify::new());
2615 let cache = Arc::new(new_table_flownode_set_cache(
2616 "test".to_string(),
2617 CacheBuilder::new(2).build(),
2618 Arc::new(BlockingRangeKvBackend {
2619 range_started: Mutex::new(Some(range_started_tx)),
2620 range_release: range_release.clone(),
2621 }),
2622 ));
2623 cache
2624 .invalidate(&[CacheIdent::CreateFlow(CreateFlow {
2625 flow_id: 1,
2626 source_table_ids: vec![cached_table_id],
2627 partition_to_peer_mapping: vec![(0, peer)],
2628 })])
2629 .await
2630 .unwrap();
2631 let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2632 let _requests_tx = requests_tx.clone();
2633 let node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2634 flownode: Arc::new(RecordingFlownode { requests_tx }),
2635 });
2636 let table_batches = vec![
2637 TableBatch {
2638 table_name: "blocked".to_string(),
2639 table_id: blocked_table_id,
2640 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2641 row_count: 1,
2642 },
2643 TableBatch {
2644 table_name: "cached".to_string(),
2645 table_id: cached_table_id,
2646 batches: vec![mock_aligned_tag_batch("tag1", "host-2", 2000, 2.0)],
2647 row_count: 1,
2648 },
2649 ];
2650
2651 notify_flow_dirty_windows_after_flush(table_batches, cache, node_manager);
2652
2653 tokio::time::timeout(Duration::from_secs(1), range_started_rx)
2654 .await
2655 .unwrap()
2656 .unwrap();
2657 let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
2658 .await
2659 .unwrap()
2660 .unwrap();
2661 assert_eq!(cached_table_id, requests.requests[0].table_id);
2662 range_release.notify_one();
2663 }
2664
2665 #[tokio::test]
2666 async fn test_successful_flush_notifies_flownode_with_logical_table_timestamps() {
2667 let table_id = 42;
2668 let peer = Peer {
2669 id: 7,
2670 addr: "flow-7".to_string(),
2671 };
2672 let cache = mock_table_flownode_cache(table_id, peer).await;
2673 let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2674 let _requests_tx = requests_tx.clone();
2675 let node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2676 flownode: Arc::new(RecordingFlownode { requests_tx }),
2677 });
2678 let table_batches = vec![TableBatch {
2679 table_name: "cpu".to_string(),
2680 table_id,
2681 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2682 row_count: 1,
2683 }];
2684
2685 notify_flow_dirty_windows_after_flush(table_batches, cache, node_manager);
2686
2687 let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
2688 .await
2689 .unwrap()
2690 .unwrap();
2691 assert_eq!(
2692 vec![api::v1::flow::DirtyWindowRequest {
2693 table_id,
2694 timestamps: vec![1000],
2695 time_ranges: Vec::new(),
2696 }],
2697 requests.requests
2698 );
2699 assert!(
2700 tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
2701 .await
2702 .is_err()
2703 );
2704 }
2705
2706 #[tokio::test]
2707 async fn test_successful_flush_coalesces_logical_batches_per_flownode() {
2708 let table_id = 42;
2709 let peer = Peer {
2710 id: 7,
2711 addr: "flow-7".to_string(),
2712 };
2713 let cache = mock_table_flownode_cache(table_id, peer).await;
2714 let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2715 let _requests_tx = requests_tx.clone();
2716 let node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2717 flownode: Arc::new(RecordingFlownode { requests_tx }),
2718 });
2719 let table_batches = vec![TableBatch {
2720 table_name: "cpu".to_string(),
2721 table_id,
2722 batches: vec![
2723 mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
2724 mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
2725 ],
2726 row_count: 2,
2727 }];
2728
2729 notify_flow_dirty_windows_after_flush(table_batches, cache, node_manager);
2730
2731 let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
2732 .await
2733 .unwrap()
2734 .unwrap();
2735 assert_eq!(
2736 vec![api::v1::flow::DirtyWindowRequest {
2737 table_id,
2738 timestamps: vec![1000, 2000],
2739 time_ranges: Vec::new(),
2740 }],
2741 requests.requests
2742 );
2743 assert!(
2744 tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
2745 .await
2746 .is_err()
2747 );
2748 }
2749
2750 #[tokio::test]
2751 async fn test_flush_batch_notifies_flownode_after_successful_physical_write() {
2752 let table_id = 42;
2753 let peer = Peer {
2754 id: 7,
2755 addr: "flow-7".to_string(),
2756 };
2757 let cache = mock_table_flownode_cache(table_id, peer).await;
2758 let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2759 let _requests_tx = requests_tx.clone();
2760 let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2761 flownode: Arc::new(RecordingFlownode { requests_tx }),
2762 });
2763 let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
2764 let ctx = session::context::QueryContext::arc();
2765 let table_batches = vec![TableBatch {
2766 table_name: "cpu".to_string(),
2767 table_id,
2768 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2769 row_count: 1,
2770 }];
2771 let writes = Arc::new(AtomicUsize::new(0));
2772
2773 flush_batch(
2774 FlushBatch {
2775 table_batches,
2776 total_row_count: 1,
2777 db_string: ctx.get_db_string(),
2778 ctx,
2779 waiters: Vec::new(),
2780 },
2781 &MockFlushPartitionProvider {
2782 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
2783 region_leader_calls: Arc::new(AtomicUsize::new(0)),
2784 },
2785 &MockFlushNodeRequester {
2786 writes: writes.clone(),
2787 fail: false,
2788 },
2789 &MockFlushCatalogProvider {
2790 table: Some(mock_physical_table_metadata(1024)),
2791 },
2792 flow_notification_tx,
2793 )
2794 .await;
2795
2796 assert_eq!(1, writes.load(Ordering::SeqCst));
2797 let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
2798 .await
2799 .unwrap()
2800 .unwrap();
2801 assert_eq!(
2802 vec![api::v1::flow::DirtyWindowRequest {
2803 table_id,
2804 timestamps: vec![1000],
2805 time_ranges: Vec::new(),
2806 }],
2807 requests.requests
2808 );
2809 }
2810
2811 #[tokio::test]
2812 async fn test_flush_batch_does_not_notify_flownode_after_physical_write_error() {
2813 let table_id = 42;
2814 let peer = Peer {
2815 id: 7,
2816 addr: "flow-7".to_string(),
2817 };
2818 let cache = mock_table_flownode_cache(table_id, peer).await;
2819 let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
2820 let _requests_tx = requests_tx.clone();
2821 let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
2822 flownode: Arc::new(RecordingFlownode { requests_tx }),
2823 });
2824 let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
2825 let ctx = session::context::QueryContext::arc();
2826 let table_batches = vec![TableBatch {
2827 table_name: "cpu".to_string(),
2828 table_id,
2829 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
2830 row_count: 1,
2831 }];
2832 let writes = Arc::new(AtomicUsize::new(0));
2833
2834 flush_batch(
2835 FlushBatch {
2836 table_batches,
2837 total_row_count: 1,
2838 db_string: ctx.get_db_string(),
2839 ctx,
2840 waiters: Vec::new(),
2841 },
2842 &MockFlushPartitionProvider {
2843 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
2844 region_leader_calls: Arc::new(AtomicUsize::new(0)),
2845 },
2846 &MockFlushNodeRequester {
2847 writes: writes.clone(),
2848 fail: true,
2849 },
2850 &MockFlushCatalogProvider {
2851 table: Some(mock_physical_table_metadata(1024)),
2852 },
2853 flow_notification_tx,
2854 )
2855 .await;
2856
2857 assert_eq!(1, writes.load(Ordering::SeqCst));
2858 assert!(
2859 tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
2860 .await
2861 .is_err()
2862 );
2863 }
2864
2865 #[async_trait]
2866 impl PhysicalFlushNodeRequester for ConcurrentMockNodeManager {
2867 async fn handle(
2868 &self,
2869 peer: &Peer,
2870 request: RegionRequest,
2871 ) -> error::Result<RegionResponse> {
2872 let datanode = self.datanode(peer).await;
2873 datanode
2874 .handle(request)
2875 .await
2876 .context(error::CommonMetaSnafu)
2877 }
2878 }
2879
2880 #[test]
2881 fn test_remove_worker_if_same_channel_removes_matching_entry() {
2882 let workers = DashMap::new();
2883 let key = BatchKey {
2884 catalog: "greptime".to_string(),
2885 schema: "public".to_string(),
2886 physical_table: "phy".to_string(),
2887 };
2888
2889 let (tx, _rx) = mpsc::channel::<WorkerCommand>(1);
2890 workers.insert(key.clone(), PendingWorker { tx: tx.clone() });
2891
2892 assert!(remove_worker_if_same_channel(&workers, &key, &tx));
2893 assert!(!workers.contains_key(&key));
2894 }
2895
2896 #[test]
2897 fn test_remove_worker_if_same_channel_keeps_newer_entry() {
2898 let workers = DashMap::new();
2899 let key = BatchKey {
2900 catalog: "greptime".to_string(),
2901 schema: "public".to_string(),
2902 physical_table: "phy".to_string(),
2903 };
2904
2905 let (stale_tx, _stale_rx) = mpsc::channel::<WorkerCommand>(1);
2906 let (fresh_tx, _fresh_rx) = mpsc::channel::<WorkerCommand>(1);
2907 workers.insert(
2908 key.clone(),
2909 PendingWorker {
2910 tx: fresh_tx.clone(),
2911 },
2912 );
2913
2914 assert!(!remove_worker_if_same_channel(&workers, &key, &stale_tx));
2915 assert!(workers.contains_key(&key));
2916 assert!(workers.get(&key).unwrap().tx.same_channel(&fresh_tx));
2917 }
2918
2919 #[test]
2920 fn test_worker_idle_timeout_close_decision() {
2921 assert!(should_close_worker_on_idle_timeout(0, 0));
2922 assert!(!should_close_worker_on_idle_timeout(1, 0));
2923 assert!(!should_close_worker_on_idle_timeout(0, 1));
2924 }
2925
2926 #[tokio::test]
2927 async fn test_flush_region_writes_concurrently_dispatches_multiple_datanodes() {
2928 let inflight = Arc::new(AtomicUsize::new(0));
2929 let max_inflight = Arc::new(AtomicUsize::new(0));
2930 let datanode1: DatanodeRef = Arc::new(ConcurrentMockDatanode {
2931 delay: Duration::from_millis(100),
2932 inflight: inflight.clone(),
2933 max_inflight: max_inflight.clone(),
2934 });
2935 let datanode2: DatanodeRef = Arc::new(ConcurrentMockDatanode {
2936 delay: Duration::from_millis(100),
2937 inflight,
2938 max_inflight: max_inflight.clone(),
2939 });
2940
2941 let mut datanodes = HashMap::new();
2942 datanodes.insert(1, datanode1);
2943 datanodes.insert(2, datanode2);
2944 let node_manager = Arc::new(ConcurrentMockNodeManager {
2945 datanodes: Arc::new(datanodes),
2946 });
2947
2948 let writes = vec![
2949 FlushRegionWrite {
2950 datanode: Peer {
2951 id: 1,
2952 addr: "node1".to_string(),
2953 },
2954 request: RegionRequest::default(),
2955 },
2956 FlushRegionWrite {
2957 datanode: Peer {
2958 id: 2,
2959 addr: "node2".to_string(),
2960 },
2961 request: RegionRequest::default(),
2962 },
2963 ];
2964
2965 flush_region_writes_concurrently(node_manager.as_ref(), writes)
2966 .await
2967 .unwrap();
2968 assert!(max_inflight.load(Ordering::SeqCst) >= 2);
2969 }
2970
2971 #[test]
2972 fn test_should_dispatch_concurrently_by_region_count() {
2973 assert!(!should_dispatch_concurrently(0));
2974 assert!(!should_dispatch_concurrently(1));
2975 assert!(should_dispatch_concurrently(2));
2976 }
2977
2978 #[test]
2979 fn test_strip_partition_columns_from_batch_removes_partition_tags() {
2980 let batch = RecordBatch::try_new(
2981 Arc::new(ArrowSchema::new(vec![
2982 Field::new("__primary_key", ArrowDataType::Binary, false),
2983 Field::new(
2984 "greptime_timestamp",
2985 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
2986 false,
2987 ),
2988 Field::new("greptime_value", ArrowDataType::Float64, true),
2989 Field::new("host", ArrowDataType::Utf8, true),
2990 ])),
2991 vec![
2992 Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
2993 Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
2994 Arc::new(arrow::array::Float64Array::from(vec![42.0_f64])),
2995 Arc::new(StringArray::from(vec!["node-1"])),
2996 ],
2997 )
2998 .unwrap();
2999
3000 let stripped = strip_partition_columns_from_batch(batch).unwrap();
3001
3002 assert_eq!(3, stripped.num_columns());
3003 assert_eq!("__primary_key", stripped.schema().field(0).name());
3004 assert_eq!("greptime_timestamp", stripped.schema().field(1).name());
3005 assert_eq!("greptime_value", stripped.schema().field(2).name());
3006 }
3007
3008 #[test]
3009 fn test_strip_partition_columns_from_batch_projects_essential_columns_without_lookup() {
3010 let batch = RecordBatch::try_new(
3011 Arc::new(ArrowSchema::new(vec![
3012 Field::new("__primary_key", ArrowDataType::Binary, false),
3013 Field::new(
3014 "greptime_timestamp",
3015 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3016 false,
3017 ),
3018 Field::new("greptime_value", ArrowDataType::Float64, true),
3019 Field::new("host", ArrowDataType::Utf8, true),
3020 ])),
3021 vec![
3022 Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
3023 Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
3024 Arc::new(arrow::array::Float64Array::from(vec![42.0_f64])),
3025 Arc::new(StringArray::from(vec!["node-1"])),
3026 ],
3027 )
3028 .unwrap();
3029
3030 let stripped = strip_partition_columns_from_batch(batch).unwrap();
3031
3032 assert_eq!(3, stripped.num_columns());
3033 assert_eq!("__primary_key", stripped.schema().field(0).name());
3034 assert_eq!("greptime_timestamp", stripped.schema().field(1).name());
3035 assert_eq!("greptime_value", stripped.schema().field(2).name());
3036 }
3037
3038 #[test]
3039 fn test_collect_tag_columns_and_non_tag_indices_keeps_partition_tag_column() {
3040 let schema = Arc::new(ArrowSchema::new(vec![
3041 Field::new(
3042 "greptime_timestamp",
3043 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3044 false,
3045 ),
3046 Field::new("greptime_value", ArrowDataType::Float64, true),
3047 Field::new("host", ArrowDataType::Utf8, true),
3048 Field::new("region", ArrowDataType::Utf8, true),
3049 ]));
3050 let name_to_ids =
3051 HashMap::from([("host".to_string(), 1_u32), ("region".to_string(), 2_u32)]);
3052 let partition_columns = HashSet::from(["host"]);
3053
3054 let (tag_columns, non_tag_indices) =
3055 columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns).unwrap();
3056
3057 assert_eq!(2, tag_columns.len());
3058 assert_eq!(&[0, 1, 2], non_tag_indices.as_slice());
3059 }
3060
3061 #[test]
3062 fn test_collect_tag_columns_and_non_tag_indices_prioritizes_essential_columns() {
3063 let schema = Arc::new(ArrowSchema::new(vec![
3064 Field::new("host", ArrowDataType::Utf8, true),
3065 Field::new("greptime_value", ArrowDataType::Float64, true),
3066 Field::new(
3067 "greptime_timestamp",
3068 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3069 false,
3070 ),
3071 Field::new("region", ArrowDataType::Utf8, true),
3072 ]));
3073 let name_to_ids =
3074 HashMap::from([("host".to_string(), 1_u32), ("region".to_string(), 2_u32)]);
3075 let partition_columns = HashSet::from(["host", "region"]);
3076
3077 let (_tag_columns, non_tag_indices): (_, SmallVec<[usize; 3]>) =
3078 columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns).unwrap();
3079
3080 assert_eq!(&[2, 1, 0, 3], non_tag_indices.as_slice());
3081 }
3082
3083 #[test]
3084 fn test_collect_tag_columns_and_non_tag_indices_rejects_unexpected_data_type() {
3085 let schema = Arc::new(ArrowSchema::new(vec![
3086 Field::new(
3087 "greptime_timestamp",
3088 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3089 false,
3090 ),
3091 Field::new("greptime_value", ArrowDataType::Float64, true),
3092 Field::new("host", ArrowDataType::Utf8, true),
3093 Field::new("invalid", ArrowDataType::Boolean, true),
3094 ]));
3095 let name_to_ids = HashMap::from([("host".to_string(), 1_u32)]);
3096 let partition_columns = HashSet::from(["host"]);
3097
3098 let result = columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns);
3099
3100 assert!(matches!(
3101 result,
3102 Err(Error::InvalidPromRemoteRequest { .. })
3103 ));
3104 }
3105
3106 #[test]
3107 fn test_collect_tag_columns_and_non_tag_indices_rejects_int64_timestamp_column() {
3108 let schema = Arc::new(ArrowSchema::new(vec![
3109 Field::new("greptime_timestamp", ArrowDataType::Int64, false),
3110 Field::new("greptime_value", ArrowDataType::Float64, true),
3111 Field::new("host", ArrowDataType::Utf8, true),
3112 ]));
3113 let name_to_ids = HashMap::from([("host".to_string(), 1_u32)]);
3114 let partition_columns = HashSet::from(["host"]);
3115
3116 let result = columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns);
3117
3118 assert!(matches!(
3119 result,
3120 Err(Error::InvalidPromRemoteRequest { .. })
3121 ));
3122 }
3123
3124 #[test]
3125 fn test_collect_tag_columns_and_non_tag_indices_rejects_duplicated_timestamp_column() {
3126 let schema = Arc::new(ArrowSchema::new(vec![
3127 Field::new(
3128 "ts1",
3129 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3130 false,
3131 ),
3132 Field::new(
3133 "ts2",
3134 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3135 false,
3136 ),
3137 Field::new("greptime_value", ArrowDataType::Float64, true),
3138 Field::new("host", ArrowDataType::Utf8, true),
3139 ]));
3140 let name_to_ids = HashMap::from([("host".to_string(), 1_u32)]);
3141 let partition_columns = HashSet::from(["host"]);
3142
3143 let result = columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns);
3144
3145 assert!(matches!(
3146 result,
3147 Err(Error::InvalidPromRemoteRequest { .. })
3148 ));
3149 }
3150
3151 #[test]
3152 fn test_collect_tag_columns_and_non_tag_indices_rejects_duplicated_value_column() {
3153 let schema = Arc::new(ArrowSchema::new(vec![
3154 Field::new(
3155 "greptime_timestamp",
3156 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3157 false,
3158 ),
3159 Field::new("value1", ArrowDataType::Float64, true),
3160 Field::new("value2", ArrowDataType::Float64, true),
3161 Field::new("host", ArrowDataType::Utf8, true),
3162 ]));
3163 let name_to_ids = HashMap::from([("host".to_string(), 1_u32)]);
3164 let partition_columns = HashSet::from(["host"]);
3165
3166 let result = columns_taxonomy(&schema, "cpu", &name_to_ids, &partition_columns);
3167
3168 assert!(matches!(
3169 result,
3170 Err(Error::InvalidPromRemoteRequest { .. })
3171 ));
3172 }
3173
3174 #[test]
3175 fn test_modify_batch_sparse_with_taxonomy_per_batch() {
3176 use arrow::array::BinaryArray;
3177 use metric_engine::batch_modifier::modify_batch_sparse;
3178
3179 let schema1 = Arc::new(ArrowSchema::new(vec![
3180 Field::new(
3181 "greptime_timestamp",
3182 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3183 false,
3184 ),
3185 Field::new("greptime_value", ArrowDataType::Float64, true),
3186 Field::new("tag1", ArrowDataType::Utf8, true),
3187 ]));
3188
3189 let schema2 = Arc::new(ArrowSchema::new(vec![
3190 Field::new(
3191 "greptime_timestamp",
3192 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3193 false,
3194 ),
3195 Field::new("greptime_value", ArrowDataType::Float64, true),
3196 Field::new("tag1", ArrowDataType::Utf8, true),
3197 Field::new("tag2", ArrowDataType::Utf8, true),
3198 ]));
3199 let batch2 = RecordBatch::try_new(
3200 schema2.clone(),
3201 vec![
3202 Arc::new(TimestampMillisecondArray::from(vec![2000])),
3203 Arc::new(arrow::array::Float64Array::from(vec![2.0])),
3204 Arc::new(StringArray::from(vec!["v1"])),
3205 Arc::new(StringArray::from(vec!["v2"])),
3206 ],
3207 )
3208 .unwrap();
3209
3210 let name_to_ids = HashMap::from([("tag1".to_string(), 1), ("tag2".to_string(), 2)]);
3211 let partition_columns = HashSet::new();
3212
3213 let batch3 = RecordBatch::try_new(
3215 schema1.clone(),
3216 vec![
3217 Arc::new(TimestampMillisecondArray::from(vec![2000])),
3218 Arc::new(arrow::array::Float64Array::from(vec![2.0])),
3219 Arc::new(StringArray::from(vec!["v1"])),
3220 ],
3221 )
3222 .unwrap();
3223
3224 let (tag_columns2, indices2) =
3227 columns_taxonomy(&batch2.schema(), "table", &name_to_ids, &partition_columns).unwrap();
3228 let modified2 = modify_batch_sparse(batch2, 123, &tag_columns2, &indices2).unwrap();
3229
3230 let (tag_columns3, indices3) =
3231 columns_taxonomy(&batch3.schema(), "table", &name_to_ids, &partition_columns).unwrap();
3232 let modified3 = modify_batch_sparse(batch3, 123, &tag_columns3, &indices3).unwrap();
3233
3234 let pk2 = modified2
3235 .column(0)
3236 .as_any()
3237 .downcast_ref::<BinaryArray>()
3238 .unwrap();
3239 let pk3 = modified3
3240 .column(0)
3241 .as_any()
3242 .downcast_ref::<BinaryArray>()
3243 .unwrap();
3244
3245 assert_ne!(
3247 pk2.value(0),
3248 pk3.value(0),
3249 "PK should be different because batch2 has tag2!"
3250 );
3251 }
3252
3253 #[test]
3254 fn test_transform_logical_batches_to_physical_success() {
3255 let batch = mock_aligned_tag_batch("tag1", "v1", 1000, 1.0);
3256
3257 let table_batches = vec![TableBatch {
3258 table_name: "t1".to_string(),
3259 table_id: 1,
3260 batches: vec![batch],
3261 row_count: 1,
3262 }];
3263
3264 let name_to_ids = HashMap::from([("tag1".to_string(), 1)]);
3265 let partition_columns = HashSet::new();
3266 let modified =
3267 transform_logical_batches_to_physical(&table_batches, &name_to_ids, &partition_columns)
3268 .unwrap();
3269
3270 assert_eq!(1, modified.len());
3271 assert_eq!(3, modified[0].num_columns());
3272 assert_eq!("__primary_key", modified[0].schema().field(0).name());
3273 assert_eq!("greptime_timestamp", modified[0].schema().field(1).name());
3274 assert_eq!("greptime_value", modified[0].schema().field(2).name());
3275 }
3276
3277 #[test]
3278 fn test_transform_logical_batches_to_physical_taxonomy_failure() {
3279 let batch = mock_aligned_tag_batch("tag1", "v1", 1000, 1.0);
3280
3281 let table_batches = vec![TableBatch {
3282 table_name: "t1".to_string(),
3283 table_id: 1,
3284 batches: vec![batch],
3285 row_count: 1,
3286 }];
3287
3288 let name_to_ids = HashMap::new();
3290 let partition_columns = HashSet::new();
3291 let err =
3292 transform_logical_batches_to_physical(&table_batches, &name_to_ids, &partition_columns)
3293 .unwrap_err();
3294
3295 assert!(
3296 err.to_string()
3297 .contains("not found in physical table column IDs")
3298 );
3299 }
3300
3301 #[test]
3302 fn test_transform_logical_batches_to_physical_multiple_batches() {
3303 let batch1 = mock_aligned_tag_batch("tag1", "v1", 1000, 1.0);
3304 let batch2 = mock_aligned_tag_batch("tag2", "v2", 2000, 2.0);
3305
3306 let table_batches = vec![
3307 TableBatch {
3308 table_name: "t1".to_string(),
3309 table_id: 1,
3310 batches: vec![batch1],
3311 row_count: 1,
3312 },
3313 TableBatch {
3314 table_name: "t2".to_string(),
3315 table_id: 2,
3316 batches: vec![batch2],
3317 row_count: 1,
3318 },
3319 ];
3320
3321 let name_to_ids = HashMap::from([("tag1".to_string(), 1), ("tag2".to_string(), 2)]);
3322 let partition_columns = HashSet::new();
3323 let modified =
3324 transform_logical_batches_to_physical(&table_batches, &name_to_ids, &partition_columns)
3325 .unwrap();
3326
3327 assert_eq!(2, modified.len());
3328 }
3329
3330 #[test]
3331 fn test_transform_logical_batches_to_physical_mixed_success_failure() {
3332 let batch1 = mock_aligned_tag_batch("tag1", "v1", 1000, 1.0);
3333 let batch2 = mock_aligned_tag_batch("tag2", "v2", 2000, 2.0);
3334
3335 let table_batches = vec![
3336 TableBatch {
3337 table_name: "t1".to_string(),
3338 table_id: 1,
3339 batches: vec![batch1],
3340 row_count: 1,
3341 },
3342 TableBatch {
3343 table_name: "t2".to_string(),
3344 table_id: 2,
3345 batches: vec![batch2],
3346 row_count: 1,
3347 },
3348 ];
3349
3350 let name_to_ids = HashMap::from([("tag2".to_string(), 2)]);
3352 let partition_columns = HashSet::new();
3353 let err =
3354 transform_logical_batches_to_physical(&table_batches, &name_to_ids, &partition_columns)
3355 .unwrap_err();
3356
3357 assert!(err.to_string().contains("tag1"));
3358 }
3359
3360 #[tokio::test]
3361 async fn test_flush_batch_physical_uses_mockable_trait_dependencies() {
3362 let table_batches = vec![TableBatch {
3363 table_name: "t1".to_string(),
3364 table_id: 11,
3365 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
3366 row_count: 1,
3367 }];
3368 let partition_calls = Arc::new(AtomicUsize::new(0));
3369 let leader_calls = Arc::new(AtomicUsize::new(0));
3370 let node = MockFlushNodeRequester::default();
3371 let ctx = session::context::QueryContext::arc();
3372
3373 flush_batch_physical(
3374 &table_batches,
3375 "phy",
3376 &ctx,
3377 &MockFlushPartitionProvider {
3378 partition_rule_calls: partition_calls.clone(),
3379 region_leader_calls: leader_calls.clone(),
3380 },
3381 &node,
3382 &MockFlushCatalogProvider {
3383 table: Some(mock_physical_table_metadata(1024)),
3384 },
3385 )
3386 .await
3387 .unwrap();
3388
3389 assert_eq!(1, partition_calls.load(Ordering::SeqCst));
3390 assert_eq!(1, leader_calls.load(Ordering::SeqCst));
3391 assert_eq!(1, node.writes.load(Ordering::SeqCst));
3392 }
3393
3394 #[derive(Default)]
3395 struct AffectedRowsFlushNodeRequester {
3396 affected_rows: usize,
3397 }
3398
3399 #[async_trait]
3400 impl PhysicalFlushNodeRequester for AffectedRowsFlushNodeRequester {
3401 async fn handle(
3402 &self,
3403 _peer: &Peer,
3404 _request: RegionRequest,
3405 ) -> error::Result<RegionResponse> {
3406 Ok(RegionResponse::new(self.affected_rows))
3407 }
3408 }
3409
3410 #[tokio::test]
3411 async fn test_flush_batch_physical_returns_actual_affected_rows() {
3412 let table_batches = vec![TableBatch {
3413 table_name: "t1".to_string(),
3414 table_id: 11,
3415 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
3416 row_count: 1,
3417 }];
3418 let ctx = session::context::QueryContext::arc();
3419
3420 let affected_rows = flush_batch_physical(
3421 &table_batches,
3422 "phy",
3423 &ctx,
3424 &MockFlushPartitionProvider {
3425 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
3426 region_leader_calls: Arc::new(AtomicUsize::new(0)),
3427 },
3428 &AffectedRowsFlushNodeRequester { affected_rows: 7 },
3429 &MockFlushCatalogProvider {
3430 table: Some(mock_physical_table_metadata(1024)),
3431 },
3432 )
3433 .await
3434 .unwrap();
3435
3436 assert_eq!(7, affected_rows);
3437 }
3438
3439 #[tokio::test]
3440 async fn test_flush_batch_physical_stops_before_partition_and_node_when_table_missing() {
3441 let table_batches = vec![TableBatch {
3442 table_name: "t1".to_string(),
3443 table_id: 11,
3444 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
3445 row_count: 1,
3446 }];
3447 let partition_calls = Arc::new(AtomicUsize::new(0));
3448 let leader_calls = Arc::new(AtomicUsize::new(0));
3449 let node = MockFlushNodeRequester::default();
3450 let ctx = session::context::QueryContext::arc();
3451
3452 let err = flush_batch_physical(
3453 &table_batches,
3454 "missing_phy",
3455 &ctx,
3456 &MockFlushPartitionProvider {
3457 partition_rule_calls: partition_calls.clone(),
3458 region_leader_calls: leader_calls.clone(),
3459 },
3460 &node,
3461 &MockFlushCatalogProvider { table: None },
3462 )
3463 .await
3464 .unwrap_err();
3465
3466 assert!(
3467 err.to_string()
3468 .contains("Physical table 'missing_phy' not found")
3469 );
3470 assert_eq!(0, partition_calls.load(Ordering::SeqCst));
3471 assert_eq!(0, leader_calls.load(Ordering::SeqCst));
3472 assert_eq!(0, node.writes.load(Ordering::SeqCst));
3473 }
3474
3475 #[tokio::test]
3476 async fn test_flush_batch_physical_aborts_immediately_on_transform_error() {
3477 let table_batches = vec![
3478 TableBatch {
3479 table_name: "broken".to_string(),
3480 table_id: 11,
3481 batches: vec![mock_aligned_tag_batch("unknown_tag", "host-1", 1000, 1.0)],
3482 row_count: 1,
3483 },
3484 TableBatch {
3485 table_name: "healthy".to_string(),
3486 table_id: 12,
3487 batches: vec![mock_aligned_tag_batch("tag1", "host-2", 2000, 2.0)],
3488 row_count: 1,
3489 },
3490 ];
3491 let partition_calls = Arc::new(AtomicUsize::new(0));
3492 let leader_calls = Arc::new(AtomicUsize::new(0));
3493 let node = MockFlushNodeRequester::default();
3494 let ctx = session::context::QueryContext::arc();
3495
3496 let err = flush_batch_physical(
3497 &table_batches,
3498 "phy",
3499 &ctx,
3500 &MockFlushPartitionProvider {
3501 partition_rule_calls: partition_calls.clone(),
3502 region_leader_calls: leader_calls.clone(),
3503 },
3504 &node,
3505 &MockFlushCatalogProvider {
3506 table: Some(mock_physical_table_metadata(1024)),
3507 },
3508 )
3509 .await
3510 .unwrap_err();
3511
3512 assert!(err.to_string().contains("unknown_tag"));
3513 assert_eq!(1, partition_calls.load(Ordering::SeqCst));
3514 assert_eq!(0, leader_calls.load(Ordering::SeqCst));
3515 assert_eq!(0, node.writes.load(Ordering::SeqCst));
3516 }
3517
3518 #[test]
3519 fn test_plan_region_batches_splits_and_strips_partition_columns() {
3520 let combined_batch = RecordBatch::try_new(
3521 Arc::new(ArrowSchema::new(vec![
3522 Field::new("__primary_key", ArrowDataType::Binary, false),
3523 Field::new(
3524 "greptime_timestamp",
3525 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3526 false,
3527 ),
3528 Field::new("greptime_value", ArrowDataType::Float64, true),
3529 Field::new("host", ArrowDataType::Utf8, true),
3530 ])),
3531 vec![
3532 Arc::new(BinaryArray::from(vec![b"k1".as_slice(), b"k2".as_slice()])),
3533 Arc::new(TimestampMillisecondArray::from(vec![1000_i64, 2000_i64])),
3534 Arc::new(arrow::array::Float64Array::from(vec![1.0_f64, 2.0_f64])),
3535 Arc::new(StringArray::from(vec!["node-1", "node-2"])),
3536 ],
3537 )
3538 .unwrap();
3539 let mut planned_batches = plan_region_batches(
3540 combined_batch,
3541 1024,
3542 &TwoRegionPartitionRule {
3543 partition_columns: vec!["host".to_string()],
3544 },
3545 &["host".to_string()],
3546 )
3547 .unwrap();
3548 planned_batches.sort_by_key(|planned| planned.region_id.region_number());
3549
3550 assert_eq!(2, planned_batches.len());
3551 assert_eq!(RegionId::new(1024, 1), planned_batches[0].region_id);
3552 assert_eq!(1, planned_batches[0].num_rows());
3553 assert_eq!(3, planned_batches[0].batch.num_columns());
3554 assert_eq!(RegionId::new(1024, 2), planned_batches[1].region_id);
3555 assert_eq!(1, planned_batches[1].num_rows());
3556 assert_eq!(3, planned_batches[1].batch.num_columns());
3557 }
3558
3559 #[test]
3560 fn test_encode_region_write_requests_builds_bulk_insert_requests() {
3561 let planned_batch = PlannedRegionBatch {
3562 region_id: RegionId::new(1024, 1),
3563 batch: RecordBatch::try_new(
3564 Arc::new(ArrowSchema::new(vec![
3565 Field::new("__primary_key", ArrowDataType::Binary, false),
3566 Field::new(
3567 "greptime_timestamp",
3568 ArrowDataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None),
3569 false,
3570 ),
3571 Field::new("greptime_value", ArrowDataType::Float64, true),
3572 ])),
3573 vec![
3574 Arc::new(BinaryArray::from(vec![b"k1".as_slice()])),
3575 Arc::new(TimestampMillisecondArray::from(vec![1000_i64])),
3576 Arc::new(arrow::array::Float64Array::from(vec![1.0_f64])),
3577 ],
3578 )
3579 .unwrap(),
3580 };
3581 let resolved_batch = ResolvedRegionBatch {
3582 planned: planned_batch,
3583 datanode: Peer {
3584 id: 1,
3585 addr: "node-1".to_string(),
3586 },
3587 };
3588 let writes = encode_region_write_requests(vec![resolved_batch]).unwrap();
3589
3590 assert_eq!(1, writes.len());
3591 assert_eq!(1, writes[0].datanode.id);
3592 let Some(region_request::Body::BulkInsert(request)) = &writes[0].request.body else {
3593 panic!("expected bulk insert request");
3594 };
3595 assert_eq!(RegionId::new(1024, 1).as_u64(), request.region_id);
3596 }
3597}