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