1use std::collections::HashSet;
16use std::time::Instant;
17
18use catalog::CatalogManagerRef;
19use common_batcher::flush_limiter::FlushLimiter;
20use common_error::ext::BoxedError;
21use common_query::{WriteProgress, prelude};
22use common_telemetry::{debug, warn};
23use operator::write_dispatcher::TrackedWrite;
24use operator::write_dispatcher::WriteDispatcherRef;
25use partition::manager::PartitionRuleManagerRef;
26use session::context::QueryContextRef;
27use snafu::{IntoError, OptionExt, ensure};
28
29use crate::batcher::flow_notifier::FlowNotifier;
30use crate::batcher::logical_table;
31use crate::batcher::logical_table::batch_convert::{self, TableBatch};
32use crate::batcher::logical_table::flow_notifier;
33use crate::batcher::logical_table::pending_worker::FlushWaiter;
34use crate::batcher::logical_table::region_write::{
35 self, CatalogManagerPhysicalFlushAdapter, PartitionManagerPhysicalFlushAdapter,
36 PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
37 WriteDispatcherPhysicalFlushAdapter,
38};
39use crate::error;
40use crate::error::Result;
41use crate::metrics;
42
43pub(crate) struct Batch {
45 pub(crate) table_batches: Vec<TableBatch>,
46 pub(crate) total_row_count: usize,
47 pub(crate) db_string: String,
48 pub(crate) ctx: QueryContextRef,
49 pub(crate) waiters: Vec<FlushWaiter>,
50}
51
52pub(crate) async fn spawn_flush(
54 flush: Batch,
55 partition_manager: PartitionRuleManagerRef,
56 write_dispatcher: WriteDispatcherRef,
57 catalog_manager: CatalogManagerRef,
58 flow_notification_tx: FlowNotifier,
59 flush_limiter: FlushLimiter,
60) {
61 match flush_limiter.acquire().await {
62 Ok(permit) => {
63 tokio::spawn(async move {
64 let _permit = permit;
65 flush_batch_with_managers(
66 flush,
67 partition_manager,
68 write_dispatcher,
69 catalog_manager,
70 flow_notification_tx,
71 )
72 .await;
73 });
74 }
75 Err(err) => {
76 warn!(err; "Flush semaphore closed, flushing inline");
77 flush_batch_with_managers(
78 flush,
79 partition_manager,
80 write_dispatcher,
81 catalog_manager,
82 flow_notification_tx,
83 )
84 .await;
85 }
86 }
87}
88
89pub(crate) async fn flush_batch_with_managers(
91 flush: Batch,
92 partition_manager: PartitionRuleManagerRef,
93 write_dispatcher: WriteDispatcherRef,
94 catalog_manager: CatalogManagerRef,
95 flow_notification_tx: FlowNotifier,
96) {
97 let partition_provider = PartitionManagerPhysicalFlushAdapter { partition_manager };
98 let write_dispatcher = WriteDispatcherPhysicalFlushAdapter { write_dispatcher };
99 let catalog_provider = CatalogManagerPhysicalFlushAdapter { catalog_manager };
100 flush_batch(
101 flush,
102 &partition_provider,
103 &write_dispatcher,
104 &catalog_provider,
105 flow_notification_tx,
106 )
107 .await;
108}
109
110pub(crate) async fn flush_batch(
112 flush: Batch,
113 partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
114 write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
115 catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
116 flow_notification_tx: FlowNotifier,
117) {
118 let Batch {
119 table_batches,
120 total_row_count,
121 db_string,
122 ctx,
123 waiters,
124 } = flush;
125 let start = Instant::now();
126
127 let physical_table_name = ctx
130 .extension(logical_table::PHYSICAL_TABLE_KEY)
131 .unwrap_or(prelude::GREPTIME_PHYSICAL_TABLE)
132 .to_string();
133 let result = flush_batch_physical_tracked(
134 &table_batches,
135 &physical_table_name,
136 &ctx,
137 partition_manager,
138 write_dispatcher,
139 catalog_manager,
140 waiters.len().max(
141 table_batches
142 .iter()
143 .flat_map(|table| &table.batches)
144 .map(|batch| batch.owner + 1)
145 .max()
146 .unwrap_or(0),
147 ),
148 )
149 .await;
150
151 let elapsed = start.elapsed().as_secs_f64();
152 metrics::FLUSH_ELAPSED.observe(elapsed);
153
154 debug!(
155 "Pending rows batch flushed, total rows: {}, elapsed time: {}s",
156 total_row_count, elapsed
157 );
158
159 match result {
160 Ok(progress) => {
161 if progress.iter().any(|result| result.error.is_some()) {
162 metrics::FLUSH_FAILURES.inc();
163 } else {
164 metrics::FLUSH_TOTAL.inc();
165 metrics::FLUSH_ROWS.observe(total_row_count as f64);
166 }
167 let affected_rows = progress.iter().map(|result| result.affected_rows).sum();
168 metrics::FLUSH_DROPPED_ROWS
169 .inc_by(total_row_count.saturating_sub(affected_rows) as u64);
170 let successful = progress
171 .iter()
172 .enumerate()
173 .filter_map(|(owner, result)| result.error.is_none().then_some(owner))
174 .collect::<HashSet<_>>();
175 for (waiter, result) in waiters.into_iter().zip(progress) {
176 operator::metrics::DIST_INGEST_ROW_COUNT
177 .with_label_values(&[db_string.as_str()])
178 .inc_by(result.affected_rows as u64);
179 let _ = waiter.response_tx.send(result);
180 }
181 let accepted = table_batches
182 .into_iter()
183 .filter_map(|mut table| {
184 table
185 .batches
186 .retain(|batch| successful.contains(&batch.owner));
187 table.row_count = table
188 .batches
189 .iter()
190 .map(|batch| batch.batch.num_rows())
191 .sum();
192 (!table.batches.is_empty()).then_some(table)
193 })
194 .collect();
195 flow_notifier::enqueue_flow_notifications(accepted, &flow_notification_tx);
196 }
197 Err(err) => {
198 metrics::FLUSH_FAILURES.inc();
199 metrics::FLUSH_DROPPED_ROWS.inc_by(total_row_count as u64);
200 notify_waiters(waiters, Err(err));
201 }
202 }
203}
204
205pub async fn flush_batch_physical(
216 table_batches: &[TableBatch],
217 physical_table_name: &str,
218 ctx: &QueryContextRef,
219 partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
220 write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
221 catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
222) -> Result<usize> {
223 let inputs = table_batches
224 .iter()
225 .flat_map(|table| &table.batches)
226 .map(|batch| batch.owner + 1)
227 .max()
228 .unwrap_or(1);
229 let progress = flush_batch_physical_tracked(
230 table_batches,
231 physical_table_name,
232 ctx,
233 partition_manager,
234 write_dispatcher,
235 catalog_manager,
236 inputs,
237 )
238 .await?;
239 let mut result = WriteProgress::default();
240 for part in progress {
241 result.merge(part);
242 }
243 result
244 .into_result()
245 .map_err(|source| error::ExecuteGrpcQuerySnafu.into_error(source))
246}
247
248async fn flush_batch_physical_tracked(
249 table_batches: &[TableBatch],
250 physical_table_name: &str,
251 ctx: &QueryContextRef,
252 partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
253 write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
254 catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
255 inputs: usize,
256) -> Result<Vec<WriteProgress>> {
257 let physical_table = {
259 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
260 .with_label_values(&["flush_physical_resolve_table"])
261 .start_timer();
262 catalog_manager
263 .physical_table(
264 ctx.current_catalog(),
265 &ctx.current_schema(),
266 physical_table_name,
267 ctx.as_ref(),
268 )
269 .await?
270 .with_context(|| error::InternalSnafu {
271 err_msg: format!(
272 "Physical table '{}' not found during pending flush",
273 physical_table_name
274 ),
275 })?
276 };
277
278 let physical_table_info = physical_table.table_info;
279 let name_to_ids = physical_table
280 .col_name_to_ids
281 .with_context(|| error::InternalSnafu {
282 err_msg: format!(
283 "Physical table '{}' has no column IDs for pending flush",
284 physical_table_name
285 ),
286 })?;
287
288 let (partition_rule, partition_versions) = {
290 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
291 .with_label_values(&["flush_physical_fetch_partition_rule"])
292 .start_timer();
293 partition_manager
294 .find_table_partition_rule(physical_table_info.as_ref())
295 .await?
296 };
297 let partition_columns = partition_rule.partition_columns();
298 let partition_columns_set: HashSet<&str> =
299 partition_columns.iter().map(String::as_str).collect();
300
301 let modified_batches = batch_convert::transform_logical_batches_to_physical(
303 table_batches,
304 &name_to_ids,
305 &partition_columns_set,
306 )?;
307
308 let combined_batch = batch_convert::concat_modified_batches(&modified_batches)?;
310 let physical_table_id = physical_table_info.table_id();
312 let row_count = table_batches
313 .iter()
314 .flat_map(|table| &table.batches)
315 .map(|batch| batch.batch.num_rows())
316 .sum::<usize>();
317 ensure!(
318 row_count == combined_batch.num_rows(),
319 error::InternalSnafu {
320 err_msg: "Physical transformation changed row ownership"
321 }
322 );
323 let masks =
324 region_write::split_combined_batch_by_region(&combined_batch, partition_rule.as_ref())?;
325 let mut writes = Vec::new();
326 for (region, mask) in masks {
327 let mut selected = Vec::with_capacity(mask.selected_rows());
328 let bits = mask.array().values();
329 let mut offset = 0;
330 for batch in table_batches.iter().flat_map(|table| &table.batches) {
332 let num_rows = batch.batch.num_rows();
333 let selected_rows = if num_rows == 1 {
334 usize::from(bits.value(offset))
336 } else {
337 bits.inner()
339 .count_set_bits_offset(bits.offset() + offset, num_rows)
340 };
341 selected.extend(std::iter::repeat_n(batch.owner, selected_rows));
342 offset += num_rows;
343 }
344 if let Some(batch) =
345 region_write::plan_region_batch(&combined_batch, physical_table_id, region, &mask)?
346 {
347 let request = region_write::encode_region_write_request(
348 batch,
349 ctx.skip_wal(),
350 &partition_versions,
351 )?;
352 writes.push(TrackedWrite {
353 request,
354 owners: selected,
355 });
356 }
357 }
358 let progress = write_dispatcher.dispatch_tracked(writes, inputs).await;
359 let counters = table_batches
361 .iter()
362 .flat_map(|table| &table.batches)
363 .filter_map(|batch| {
364 batch
365 .progress
366 .as_ref()
367 .map(|counter| (batch.owner, counter))
368 })
369 .collect::<std::collections::HashMap<_, _>>();
370 for (owner, counter) in counters {
371 counter.add_written(progress[owner].affected_rows);
372 }
373 Ok(progress)
374}
375
376pub(in crate::batcher::logical_table) fn notify_waiters(
377 waiters: Vec<FlushWaiter>,
378 result: Result<()>,
379) {
380 let progress = match result {
381 Ok(()) => WriteProgress::default(),
382 Err(error) => WriteProgress::failed(BoxedError::new(error)),
383 };
384 for waiter in waiters {
385 let _ = waiter.response_tx.send(progress.clone());
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use std::collections::HashMap;
392 use std::future::poll_fn;
393 use std::sync::Arc;
394 use std::sync::atomic::{AtomicUsize, Ordering};
395 use std::task::Poll;
396 use std::time::Duration;
397
398 use api::v1::meta::Peer;
399 use api::v1::region::RegionRequest;
400 use arrow::record_batch::RecordBatch;
401 use async_trait::async_trait;
402 use catalog::error::Result as CatalogResult;
403 use common_batcher::request_limiter::RequestLimiter;
404 use common_meta::cache::TableFlownodeSetCacheRef;
405 use common_meta::node_manager::NodeManagerRef;
406 use datatypes::schema::{ColumnSchema as DtColumnSchema, Schema as DtSchema};
407 use partition::error::Result as PartitionResult;
408 use partition::partition::{PartitionRule, PartitionRuleRef, RegionMask};
409 use table::metadata::TableId;
410 use table::test_util::table_info::test_table_info;
411 use tokio::sync::{mpsc, oneshot};
412
413 use crate::batcher::flow_notifier::{FlowNotifier, start_flow_notification_worker};
414 use crate::batcher::logical_table::batch::{
415 Batch, flush_batch, flush_batch_physical, notify_waiters,
416 };
417 use crate::batcher::logical_table::batch_convert::TableBatch;
418 use crate::batcher::logical_table::pending_worker::FlushWaiter;
419 use crate::batcher::logical_table::region_write::{
420 PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
421 PhysicalTableMetadata,
422 };
423 use crate::batcher::logical_table::test_util::{
424 FlowNotificationMockNodeManager, RecordingFlownode, mock_aligned_tag_batch,
425 };
426 use crate::batcher::test_util::mock_table_flownode_cache;
427 use crate::error;
428 use crate::error::Error;
429 use crate::metrics::{self, FLOW_NOTIFICATION_DROPPED};
430
431 #[tokio::test]
432 async fn test_flush_batch_notifies_flownode_after_successful_physical_write() {
433 let table_id = 42;
434 let peer = Peer {
435 id: 7,
436 addr: "flow-7".to_string(),
437 };
438 let cache = mock_table_flownode_cache(table_id, vec![(0, peer.clone()), (1, peer)]).await;
439 let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
440 let _requests_tx = requests_tx.clone();
441 let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
442 flownode: Arc::new(RecordingFlownode { requests_tx }),
443 });
444 let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
445 let ctx = session::context::QueryContext::arc();
446 let table_batches = vec![TableBatch {
447 table_name: "cpu".to_string(),
448 table_id,
449 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
450 row_count: 1,
451 }];
452 let writes = Arc::new(AtomicUsize::new(0));
453
454 flush_batch(
455 Batch {
456 table_batches,
457 total_row_count: 1,
458 db_string: ctx.get_db_string(),
459 ctx,
460 waiters: Vec::new(),
461 },
462 &MockFlushPartitionProvider {
463 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
464 },
465 &MockFlushWriteDispatcher {
466 writes: writes.clone(),
467 fail: false,
468 },
469 &MockFlushCatalogProvider {
470 table: Some(mock_physical_table_metadata(1024)),
471 },
472 flow_notification_tx,
473 )
474 .await;
475
476 assert_eq!(1, writes.load(Ordering::SeqCst));
477 let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
478 .await
479 .unwrap()
480 .unwrap();
481 assert_eq!(
482 vec![api::v1::flow::DirtyWindowRequest {
483 table_id,
484 timestamps: vec![1000],
485 time_ranges: Vec::new(),
486 }],
487 requests.requests
488 );
489 }
490
491 #[tokio::test]
492 async fn test_flush_batch_does_not_notify_flownode_after_physical_write_error() {
493 let table_id = 42;
494 let peer = Peer {
495 id: 7,
496 addr: "flow-7".to_string(),
497 };
498 let cache = mock_table_flownode_cache(table_id, vec![(0, peer.clone()), (1, peer)]).await;
499 let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
500 let _requests_tx = requests_tx.clone();
501 let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
502 flownode: Arc::new(RecordingFlownode { requests_tx }),
503 });
504 let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
505 let ctx = session::context::QueryContext::arc();
506 let table_batches = vec![TableBatch {
507 table_name: "cpu".to_string(),
508 table_id,
509 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
510 row_count: 1,
511 }];
512 let writes = Arc::new(AtomicUsize::new(0));
513
514 flush_batch(
515 Batch {
516 table_batches,
517 total_row_count: 1,
518 db_string: ctx.get_db_string(),
519 ctx,
520 waiters: Vec::new(),
521 },
522 &MockFlushPartitionProvider {
523 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
524 },
525 &MockFlushWriteDispatcher {
526 writes: writes.clone(),
527 fail: true,
528 },
529 &MockFlushCatalogProvider {
530 table: Some(mock_physical_table_metadata(1024)),
531 },
532 flow_notification_tx,
533 )
534 .await;
535
536 assert_eq!(1, writes.load(Ordering::SeqCst));
537 assert!(
538 tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
539 .await
540 .is_err()
541 );
542 }
543
544 #[tokio::test]
545 async fn test_cancelled_waiter_retains_request_slot_until_notification() {
546 let limiter = RequestLimiter::try_new(1).unwrap();
547 let (response_tx, response_rx) = oneshot::channel();
548 let waiter = FlushWaiter {
549 _completion: None,
550 response_tx,
551 _permit: limiter.acquire().await.unwrap(),
552 };
553 drop(response_rx);
554 let next = limiter.acquire();
555 tokio::pin!(next);
556 assert!(poll_fn(|cx| Poll::Ready(next.as_mut().poll(cx).is_pending())).await);
557 notify_waiters(vec![waiter], Ok(()));
558 let _permit = next.await.unwrap();
559 }
560
561 #[tokio::test]
562 async fn test_flush_batch_physical_uses_mockable_trait_dependencies() {
563 let table_batches = vec![TableBatch {
564 table_name: "t1".to_string(),
565 table_id: 11,
566 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
567 row_count: 1,
568 }];
569 let partition_calls = Arc::new(AtomicUsize::new(0));
570 let dispatcher = MockFlushWriteDispatcher::default();
571 let ctx = session::context::QueryContext::arc();
572
573 flush_batch_physical(
574 &table_batches,
575 "phy",
576 &ctx,
577 &MockFlushPartitionProvider {
578 partition_rule_calls: partition_calls.clone(),
579 },
580 &dispatcher,
581 &MockFlushCatalogProvider {
582 table: Some(mock_physical_table_metadata(1024)),
583 },
584 )
585 .await
586 .unwrap();
587
588 assert_eq!(1, partition_calls.load(Ordering::SeqCst));
589 assert_eq!(1, dispatcher.writes.load(Ordering::SeqCst));
590 }
591
592 #[tokio::test]
593 async fn test_flush_batch_physical_rejects_unattributable_affected_rows() {
594 let table_batches = vec![TableBatch {
595 table_name: "t1".to_string(),
596 table_id: 11,
597 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
598 row_count: 1,
599 }];
600 let ctx = session::context::QueryContext::arc();
601
602 let error = flush_batch_physical(
603 &table_batches,
604 "phy",
605 &ctx,
606 &MockFlushPartitionProvider {
607 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
608 },
609 &AffectedRowsFlushWriteDispatcher { affected_rows: 7 },
610 &MockFlushCatalogProvider {
611 table: Some(mock_physical_table_metadata(1024)),
612 },
613 )
614 .await
615 .unwrap_err();
616
617 assert!(format!("{error:?}").contains("Unattributable"));
618 }
619
620 #[tokio::test]
621 async fn test_exemplar_coalesced_logical_flush_retains_each_requests_progress() {
622 use common_error::ext::BoxedError;
623 use common_query::WriteProgress;
624 use operator::write_dispatcher::TrackedWrite;
625
626 struct ProgressDispatcher([(usize, bool); 2]);
627 #[async_trait]
628 impl PhysicalFlushWriteDispatcher for ProgressDispatcher {
629 async fn dispatch(&self, _: Vec<RegionRequest>) -> error::Result<usize> {
630 unreachable!()
631 }
632 async fn dispatch_tracked(
633 &self,
634 requests: Vec<TrackedWrite>,
635 inputs: usize,
636 ) -> Vec<WriteProgress> {
637 assert_eq!(inputs, 2);
638 assert_eq!(
639 requests
640 .iter()
641 .flat_map(|r| &r.owners)
642 .copied()
643 .collect::<Vec<_>>(),
644 vec![0, 1, 1]
645 );
646 self.0
647 .iter()
648 .map(|&(affected_rows, failed)| WriteProgress {
649 affected_rows,
650 error: failed.then(|| {
651 Arc::new(BoxedError::new(
652 error::InternalSnafu {
653 err_msg: "lost acknowledgement",
654 }
655 .build(),
656 ))
657 }),
658 })
659 .collect()
660 }
661 }
662 let ctx = session::context::QueryContext::arc();
663 let ingested = operator::metrics::DIST_INGEST_ROW_COUNT
664 .with_label_values(&[ctx.get_db_string().as_str()]);
665 for (expected_progress, successful_flush, expected_timestamps) in [
666 ([(1, false), (2, false)], true, vec![1000; 3]),
667 ([(1, false), (1, true)], false, vec![1000]),
668 ([(0, true), (0, true)], false, vec![]),
669 ] {
670 let limiter = RequestLimiter::try_new(2).unwrap();
671 let mut waiters = Vec::new();
672 let mut receivers = Vec::new();
673 let counters =
674 [0, 1].map(|_| Arc::new(session::write_progress::WriteProgress::default()));
675 for counter in &counters {
676 let (response_tx, response_rx) = oneshot::channel();
677 waiters.push(FlushWaiter {
678 _completion: Some(counter.start()),
679 response_tx,
680 _permit: limiter.acquire().await.unwrap(),
681 });
682 receivers.push(response_rx);
683 }
684 let batches = [0, 1, 1]
685 .into_iter()
686 .map(|owner| {
687 let mut batch = mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0);
688 batch.owner = owner;
689 batch.progress = Some(counters[owner].clone());
690 batch
691 })
692 .collect();
693 let (notifier, mut notifications) =
694 FlowNotifier::try_new(1, FLOW_NOTIFICATION_DROPPED.clone()).unwrap();
695 let successes = metrics::FLUSH_TOTAL.get();
696 let failures = metrics::FLUSH_FAILURES.get();
697 let samples = metrics::FLUSH_ROWS.get_sample_count();
698 let sum = metrics::FLUSH_ROWS.get_sample_sum();
699 let dropped = metrics::FLUSH_DROPPED_ROWS.get();
700 let written = ingested.get();
701 flush_batch(
702 Batch {
703 table_batches: vec![TableBatch {
704 table_name: "cpu".into(),
705 table_id: 42,
706 batches,
707 row_count: 3,
708 }],
709 total_row_count: 3,
710 db_string: ctx.get_db_string(),
711 ctx: ctx.clone(),
712 waiters,
713 },
714 &MockFlushPartitionProvider {
715 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
716 },
717 &ProgressDispatcher(expected_progress),
718 &MockFlushCatalogProvider {
719 table: Some(mock_physical_table_metadata(1024)),
720 },
721 notifier,
722 )
723 .await;
724 let expected_written: usize = expected_progress.iter().map(|(rows, _)| rows).sum();
725 assert_eq!(
726 metrics::FLUSH_TOTAL.get(),
727 successes + u64::from(successful_flush)
728 );
729 assert_eq!(
730 metrics::FLUSH_FAILURES.get(),
731 failures + u64::from(!successful_flush)
732 );
733 assert_eq!(
734 metrics::FLUSH_ROWS.get_sample_count(),
735 samples + u64::from(successful_flush)
736 );
737 assert_eq!(
738 metrics::FLUSH_ROWS.get_sample_sum(),
739 sum + if successful_flush { 3.0 } else { 0.0 }
740 );
741 assert_eq!(
742 metrics::FLUSH_DROPPED_ROWS.get(),
743 dropped + (3 - expected_written) as u64
744 );
745 assert_eq!(ingested.get(), written + expected_written as u64);
746 for (counter, (affected_rows, _)) in counters.iter().zip(expected_progress) {
747 counter.wait().await;
748 assert_eq!(counter.written(), affected_rows as u64);
749 }
750 for (receiver, (affected_rows, failed)) in receivers.into_iter().zip(expected_progress)
751 {
752 let result = receiver.await.unwrap();
753 assert_eq!(result.affected_rows, affected_rows);
754 assert_eq!(result.error.is_some(), failed);
755 }
756 if !expected_timestamps.is_empty() {
757 let notification = notifications.try_recv().unwrap();
758 assert_eq!(notification.table_id, 42);
759 assert_eq!(notification.timestamps, expected_timestamps);
760 }
761 assert!(notifications.try_recv().is_err());
762 }
763 }
764
765 #[tokio::test]
766 async fn test_flush_batch_physical_preserves_owners_with_sliced_masks() {
767 use std::sync::Mutex;
768
769 use api::v1::region::{bulk_insert_request, region_request};
770 use arrow::array::{BooleanArray, Float64Array, StringArray, TimestampMillisecondArray};
771 use common_grpc::flight::FlightDecoder;
772 use common_query::WriteProgress;
773 use operator::write_dispatcher::TrackedWrite;
774
775 #[derive(Clone)]
776 struct MaskPartitions {
777 offset: usize,
778 pattern: usize,
779 }
780
781 impl PartitionRule for MaskPartitions {
782 fn as_any(&self) -> &dyn std::any::Any {
783 self
784 }
785
786 fn partition_columns(&self) -> &[String] {
787 &[]
788 }
789
790 fn find_region(&self, _: &[datatypes::prelude::Value]) -> PartitionResult<u32> {
791 unreachable!()
792 }
793
794 fn split_record_batch(
795 &self,
796 batch: &RecordBatch,
797 ) -> PartitionResult<HashMap<u32, RegionMask>> {
798 Ok([false, true]
799 .into_iter()
800 .enumerate()
801 .map(|(region, selected)| {
802 let values = std::iter::repeat_n(true, self.offset)
803 .chain((0..batch.num_rows()).map(|row| {
804 let matches = match self.pattern {
805 0 => false,
806 1 => true,
807 _ => row % 3 != 0,
808 };
809 matches == selected
810 }))
811 .chain([true; 8])
812 .collect::<Vec<_>>();
813 let mask = BooleanArray::from(values).slice(self.offset, batch.num_rows());
814 (region as u32, RegionMask::from(mask))
815 })
816 .collect())
817 }
818 }
819
820 #[async_trait]
821 impl PhysicalFlushPartitionProvider for MaskPartitions {
822 async fn find_table_partition_rule(
823 &self,
824 _: &table::metadata::TableInfo,
825 ) -> PartitionResult<(PartitionRuleRef, HashMap<u32, Option<u64>>)> {
826 Ok((Arc::new(self.clone()), HashMap::new()))
827 }
828 }
829
830 #[derive(Default)]
831 struct RecordingDispatcher(Mutex<Vec<TrackedWrite>>);
832
833 #[async_trait]
834 impl PhysicalFlushWriteDispatcher for RecordingDispatcher {
835 async fn dispatch(&self, _: Vec<RegionRequest>) -> error::Result<usize> {
836 unreachable!()
837 }
838
839 async fn dispatch_tracked(
840 &self,
841 requests: Vec<TrackedWrite>,
842 inputs: usize,
843 ) -> Vec<WriteProgress> {
844 let mut progress = vec![WriteProgress::default(); inputs];
845 for owner in requests.iter().flat_map(|request| &request.owners) {
846 progress[*owner].affected_rows += 1;
847 }
848 *self.0.lock().unwrap() = requests;
849 progress
850 }
851 }
852
853 let ctx = session::context::QueryContext::arc();
854 let catalog = MockFlushCatalogProvider {
855 table: Some(mock_physical_table_metadata(1024)),
856 };
857 for lengths in [[0; 7], [1; 7], [0, 3, 65, 1, 127, 0, 64]] {
858 let mut expected = Vec::new();
859 let tables = lengths
860 .chunks(4)
861 .enumerate()
862 .map(|(table, lengths)| {
863 let batches = lengths
864 .iter()
865 .enumerate()
866 .map(|(index, &rows)| {
867 let owner = [2, 0, 1, 0, 2, 1, 0][table * 4 + index];
868 let start = expected.len();
869 expected.extend(std::iter::repeat_n(owner, rows));
870 let mut batch = mock_aligned_tag_batch("tag1", "host-1", 0, 1.0);
871 batch.batch = RecordBatch::try_new(
872 batch.batch.schema(),
873 vec![
874 Arc::new(TimestampMillisecondArray::from_iter_values(
875 (start..start + rows).map(|row| row as i64),
876 )),
877 Arc::new(Float64Array::from(vec![1.0; rows])),
878 Arc::new(StringArray::from(vec!["host-1"; rows])),
879 ],
880 )
881 .unwrap();
882 batch.owner = owner;
883 batch
884 })
885 .collect();
886 TableBatch {
887 table_name: format!("logical_{table}"),
888 table_id: 100 + table as u32,
889 batches,
890 row_count: lengths.iter().sum(),
891 }
892 })
893 .collect::<Vec<_>>();
894 for offset in [0, 1, 7, 65] {
895 for pattern in 0..3 {
896 let dispatcher = RecordingDispatcher::default();
897 let affected = flush_batch_physical(
898 &tables,
899 "phy",
900 &ctx,
901 &MaskPartitions { offset, pattern },
902 &dispatcher,
903 &catalog,
904 )
905 .await
906 .unwrap();
907 assert_eq!(affected, expected.len());
908 let mut actual = Vec::new();
909 for write in dispatcher.0.into_inner().unwrap() {
910 let Some(region_request::Body::BulkInsert(request)) = write.request.body
911 else {
912 panic!("expected bulk request")
913 };
914 let Some(bulk_insert_request::Body::ArrowIpc(ipc)) = request.body else {
915 panic!("expected Arrow IPC")
916 };
917 let batch = FlightDecoder::try_from_schema_bytes(&ipc.schema)
918 .unwrap()
919 .try_decode_record_batch(&ipc.data_header, &ipc.payload)
920 .unwrap();
921 assert_eq!(batch.num_rows(), write.owners.len());
922 let timestamps = batch
923 .column_by_name("greptime_timestamp")
924 .unwrap()
925 .as_any()
926 .downcast_ref::<TimestampMillisecondArray>()
927 .unwrap();
928 assert!(timestamps.values().windows(2).all(|pair| pair[0] < pair[1]));
929 actual.extend(timestamps.values().iter().copied().zip(write.owners));
930 }
931 actual.sort_unstable();
932 assert_eq!(
933 actual,
934 expected
935 .iter()
936 .enumerate()
937 .map(|(row, owner)| (row as i64, *owner))
938 .collect::<Vec<_>>(),
939 );
940 }
941 }
942 }
943 }
944
945 #[tokio::test]
946 async fn test_flush_batch_physical_stops_before_partition_and_dispatch_when_table_missing() {
947 let table_batches = vec![TableBatch {
948 table_name: "t1".to_string(),
949 table_id: 11,
950 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
951 row_count: 1,
952 }];
953 let partition_calls = Arc::new(AtomicUsize::new(0));
954 let dispatcher = MockFlushWriteDispatcher::default();
955 let ctx = session::context::QueryContext::arc();
956
957 let err = flush_batch_physical(
958 &table_batches,
959 "missing_phy",
960 &ctx,
961 &MockFlushPartitionProvider {
962 partition_rule_calls: partition_calls.clone(),
963 },
964 &dispatcher,
965 &MockFlushCatalogProvider { table: None },
966 )
967 .await
968 .unwrap_err();
969
970 assert!(
971 err.to_string()
972 .contains("Physical table 'missing_phy' not found")
973 );
974 assert_eq!(0, partition_calls.load(Ordering::SeqCst));
975 assert_eq!(0, dispatcher.writes.load(Ordering::SeqCst));
976 }
977
978 #[tokio::test]
979 async fn test_flush_batch_physical_aborts_immediately_on_transform_error() {
980 let table_batches = vec![
981 TableBatch {
982 table_name: "broken".to_string(),
983 table_id: 11,
984 batches: vec![mock_aligned_tag_batch("unknown_tag", "host-1", 1000, 1.0)],
985 row_count: 1,
986 },
987 TableBatch {
988 table_name: "healthy".to_string(),
989 table_id: 12,
990 batches: vec![mock_aligned_tag_batch("tag1", "host-2", 2000, 2.0)],
991 row_count: 1,
992 },
993 ];
994 let partition_calls = Arc::new(AtomicUsize::new(0));
995 let dispatcher = MockFlushWriteDispatcher::default();
996 let ctx = session::context::QueryContext::arc();
997
998 let err = flush_batch_physical(
999 &table_batches,
1000 "phy",
1001 &ctx,
1002 &MockFlushPartitionProvider {
1003 partition_rule_calls: partition_calls.clone(),
1004 },
1005 &dispatcher,
1006 &MockFlushCatalogProvider {
1007 table: Some(mock_physical_table_metadata(1024)),
1008 },
1009 )
1010 .await
1011 .unwrap_err();
1012
1013 assert!(err.to_string().contains("unknown_tag"));
1014 assert_eq!(1, partition_calls.load(Ordering::SeqCst));
1015 assert_eq!(0, dispatcher.writes.load(Ordering::SeqCst));
1016 }
1017
1018 fn mock_physical_table_metadata(table_id: TableId) -> PhysicalTableMetadata {
1019 let schema = Arc::new(
1020 DtSchema::try_new(vec![
1021 DtColumnSchema::new(
1022 "__primary_key",
1023 datatypes::prelude::ConcreteDataType::binary_datatype(),
1024 false,
1025 ),
1026 DtColumnSchema::new(
1027 "greptime_timestamp",
1028 datatypes::prelude::ConcreteDataType::timestamp_millisecond_datatype(),
1029 false,
1030 ),
1031 DtColumnSchema::new(
1032 "greptime_value",
1033 datatypes::prelude::ConcreteDataType::float64_datatype(),
1034 true,
1035 ),
1036 DtColumnSchema::new(
1037 "tag1",
1038 datatypes::prelude::ConcreteDataType::string_datatype(),
1039 true,
1040 ),
1041 ])
1042 .unwrap(),
1043 );
1044 let mut table_info = test_table_info(table_id, "phy", "public", "greptime", schema);
1045 table_info.meta.column_ids = vec![0, 1, 2, 3];
1046
1047 PhysicalTableMetadata {
1048 table_info: Arc::new(table_info),
1049 col_name_to_ids: Some(HashMap::from([("tag1".to_string(), 3)])),
1050 }
1051 }
1052
1053 struct MockFlushCatalogProvider {
1054 table: Option<PhysicalTableMetadata>,
1055 }
1056
1057 #[async_trait]
1058 impl PhysicalFlushCatalogProvider for MockFlushCatalogProvider {
1059 async fn physical_table(
1060 &self,
1061 _catalog: &str,
1062 _schema: &str,
1063 _table_name: &str,
1064 _query_ctx: &session::context::QueryContext,
1065 ) -> CatalogResult<Option<PhysicalTableMetadata>> {
1066 Ok(self.table.clone())
1067 }
1068 }
1069
1070 struct SingleRegionPartitionRule;
1071
1072 impl PartitionRule for SingleRegionPartitionRule {
1073 fn as_any(&self) -> &dyn std::any::Any {
1074 self
1075 }
1076
1077 fn partition_columns(&self) -> &[String] {
1078 &[]
1079 }
1080
1081 fn find_region(
1082 &self,
1083 _values: &[datatypes::prelude::Value],
1084 ) -> partition::error::Result<store_api::storage::RegionNumber> {
1085 unimplemented!()
1086 }
1087
1088 fn split_record_batch(
1089 &self,
1090 record_batch: &RecordBatch,
1091 ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
1092 {
1093 Ok(HashMap::from([(
1094 1,
1095 RegionMask::new(
1096 arrow::array::BooleanArray::from(vec![true; record_batch.num_rows()]),
1097 record_batch.num_rows(),
1098 ),
1099 )]))
1100 }
1101 }
1102
1103 struct MockFlushPartitionProvider {
1104 partition_rule_calls: Arc<AtomicUsize>,
1105 }
1106
1107 #[async_trait]
1108 impl PhysicalFlushPartitionProvider for MockFlushPartitionProvider {
1109 async fn find_table_partition_rule(
1110 &self,
1111 _table_info: &table::metadata::TableInfo,
1112 ) -> PartitionResult<(PartitionRuleRef, HashMap<u32, Option<u64>>)> {
1113 self.partition_rule_calls.fetch_add(1, Ordering::SeqCst);
1114 Ok((Arc::new(SingleRegionPartitionRule), HashMap::new()))
1115 }
1116 }
1117
1118 #[derive(Default)]
1119 struct MockFlushWriteDispatcher {
1120 writes: Arc<AtomicUsize>,
1121 fail: bool,
1122 }
1123
1124 #[async_trait]
1125 impl PhysicalFlushWriteDispatcher for MockFlushWriteDispatcher {
1126 async fn dispatch(&self, _requests: Vec<RegionRequest>) -> error::Result<usize> {
1127 self.writes.fetch_add(1, Ordering::SeqCst);
1128 if self.fail {
1129 return Err(Error::Internal {
1130 err_msg: "physical write failed".to_string(),
1131 });
1132 }
1133 Ok(1)
1134 }
1135 }
1136
1137 fn mock_flow_notification_sender(
1138 cache: TableFlownodeSetCacheRef,
1139 node_manager: NodeManagerRef,
1140 ) -> FlowNotifier {
1141 let (tx, rx) = FlowNotifier::try_new(16, FLOW_NOTIFICATION_DROPPED.clone()).unwrap();
1142 start_flow_notification_worker(rx, cache, node_manager);
1143 tx
1144 }
1145
1146 #[derive(Default)]
1147 struct AffectedRowsFlushWriteDispatcher {
1148 affected_rows: usize,
1149 }
1150
1151 #[async_trait]
1152 impl PhysicalFlushWriteDispatcher for AffectedRowsFlushWriteDispatcher {
1153 async fn dispatch(&self, _requests: Vec<RegionRequest>) -> error::Result<usize> {
1154 Ok(self.affected_rows)
1155 }
1156 }
1157}