1use std::collections::HashSet;
16use std::sync::Arc;
17use std::time::Instant;
18
19use catalog::CatalogManagerRef;
20use common_batcher::flush_limiter::FlushLimiter;
21use common_query::prelude;
22use common_telemetry::{debug, warn};
23use operator::write_dispatcher::WriteDispatcherRef;
24use partition::manager::PartitionRuleManagerRef;
25use session::context::QueryContextRef;
26use snafu::OptionExt;
27
28use crate::batcher::flow_notifier::FlowNotifier;
29use crate::batcher::logical_table;
30use crate::batcher::logical_table::batch_convert::{self, TableBatch};
31use crate::batcher::logical_table::flow_notifier;
32use crate::batcher::logical_table::pending_worker::FlushWaiter;
33use crate::batcher::logical_table::region_write::{
34 self, CatalogManagerPhysicalFlushAdapter, PartitionManagerPhysicalFlushAdapter,
35 PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
36 WriteDispatcherPhysicalFlushAdapter,
37};
38use crate::error;
39use crate::error::Result;
40use crate::metrics;
41
42pub(crate) struct Batch {
44 pub(crate) table_batches: Vec<TableBatch>,
45 pub(crate) total_row_count: usize,
46 pub(crate) db_string: String,
47 pub(crate) ctx: QueryContextRef,
48 pub(crate) waiters: Vec<FlushWaiter>,
49}
50
51pub(crate) async fn spawn_flush(
53 flush: Batch,
54 partition_manager: PartitionRuleManagerRef,
55 write_dispatcher: WriteDispatcherRef,
56 catalog_manager: CatalogManagerRef,
57 flow_notification_tx: FlowNotifier,
58 flush_limiter: FlushLimiter,
59) {
60 match flush_limiter.acquire().await {
61 Ok(permit) => {
62 tokio::spawn(async move {
63 let _permit = permit;
64 flush_batch_with_managers(
65 flush,
66 partition_manager,
67 write_dispatcher,
68 catalog_manager,
69 flow_notification_tx,
70 )
71 .await;
72 });
73 }
74 Err(err) => {
75 warn!(err; "Flush semaphore closed, flushing inline");
76 flush_batch_with_managers(
77 flush,
78 partition_manager,
79 write_dispatcher,
80 catalog_manager,
81 flow_notification_tx,
82 )
83 .await;
84 }
85 }
86}
87
88pub(crate) async fn flush_batch_with_managers(
90 flush: Batch,
91 partition_manager: PartitionRuleManagerRef,
92 write_dispatcher: WriteDispatcherRef,
93 catalog_manager: CatalogManagerRef,
94 flow_notification_tx: FlowNotifier,
95) {
96 let partition_provider = PartitionManagerPhysicalFlushAdapter { partition_manager };
97 let write_dispatcher = WriteDispatcherPhysicalFlushAdapter { write_dispatcher };
98 let catalog_provider = CatalogManagerPhysicalFlushAdapter { catalog_manager };
99 flush_batch(
100 flush,
101 &partition_provider,
102 &write_dispatcher,
103 &catalog_provider,
104 flow_notification_tx,
105 )
106 .await;
107}
108
109pub(crate) async fn flush_batch(
111 flush: Batch,
112 partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
113 write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
114 catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
115 flow_notification_tx: FlowNotifier,
116) {
117 let Batch {
118 table_batches,
119 total_row_count,
120 db_string,
121 ctx,
122 waiters,
123 } = flush;
124 let start = Instant::now();
125
126 let physical_table_name = ctx
129 .extension(logical_table::PHYSICAL_TABLE_KEY)
130 .unwrap_or(prelude::GREPTIME_PHYSICAL_TABLE)
131 .to_string();
132 let result = flush_batch_physical(
133 &table_batches,
134 &physical_table_name,
135 &ctx,
136 partition_manager,
137 write_dispatcher,
138 catalog_manager,
139 )
140 .await;
141
142 let elapsed = start.elapsed().as_secs_f64();
143 metrics::FLUSH_ELAPSED.observe(elapsed);
144
145 debug!(
146 "Pending rows batch flushed, total rows: {}, elapsed time: {}s",
147 total_row_count, elapsed
148 );
149
150 match result {
151 Ok(affected_rows) => {
152 metrics::FLUSH_TOTAL.inc();
153 metrics::FLUSH_ROWS.observe(total_row_count as f64);
154 operator::metrics::DIST_INGEST_ROW_COUNT
155 .with_label_values(&[db_string.as_str()])
156 .inc_by(affected_rows as u64);
157
158 notify_waiters(waiters, Ok(()));
159 flow_notifier::enqueue_flow_notifications(table_batches, &flow_notification_tx);
160 }
161 Err(err) => {
162 metrics::FLUSH_FAILURES.inc();
163 metrics::FLUSH_DROPPED_ROWS.inc_by(total_row_count as u64);
164 notify_waiters(waiters, Err(err));
165 }
166 }
167}
168
169pub async fn flush_batch_physical(
180 table_batches: &[TableBatch],
181 physical_table_name: &str,
182 ctx: &QueryContextRef,
183 partition_manager: &(impl PhysicalFlushPartitionProvider + ?Sized),
184 write_dispatcher: &(impl PhysicalFlushWriteDispatcher + ?Sized),
185 catalog_manager: &(impl PhysicalFlushCatalogProvider + ?Sized),
186) -> Result<usize> {
187 let physical_table = {
189 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
190 .with_label_values(&["flush_physical_resolve_table"])
191 .start_timer();
192 catalog_manager
193 .physical_table(
194 ctx.current_catalog(),
195 &ctx.current_schema(),
196 physical_table_name,
197 ctx.as_ref(),
198 )
199 .await?
200 .with_context(|| error::InternalSnafu {
201 err_msg: format!(
202 "Physical table '{}' not found during pending flush",
203 physical_table_name
204 ),
205 })?
206 };
207
208 let physical_table_info = physical_table.table_info;
209 let name_to_ids = physical_table
210 .col_name_to_ids
211 .with_context(|| error::InternalSnafu {
212 err_msg: format!(
213 "Physical table '{}' has no column IDs for pending flush",
214 physical_table_name
215 ),
216 })?;
217
218 let (partition_rule, partition_versions) = {
220 let _timer = metrics::PENDING_ROWS_BATCH_FLUSH_STAGE_ELAPSED
221 .with_label_values(&["flush_physical_fetch_partition_rule"])
222 .start_timer();
223 partition_manager
224 .find_table_partition_rule(physical_table_info.as_ref())
225 .await?
226 };
227 let partition_columns = partition_rule.partition_columns();
228 let partition_columns_set: HashSet<&str> =
229 partition_columns.iter().map(String::as_str).collect();
230
231 let modified_batches = batch_convert::transform_logical_batches_to_physical(
233 table_batches,
234 &name_to_ids,
235 &partition_columns_set,
236 )?;
237
238 let combined_batch = batch_convert::concat_modified_batches(&modified_batches)?;
240
241 let physical_table_id = physical_table_info.table_id();
243 let planned_batches = region_write::plan_region_batches(
244 combined_batch,
245 physical_table_id,
246 partition_rule.as_ref(),
247 )?;
248
249 let region_writes = region_write::encode_region_write_requests(
250 planned_batches,
251 ctx.skip_wal(),
252 &partition_versions,
253 )?;
254 write_dispatcher.dispatch(region_writes).await
255}
256
257pub(in crate::batcher::logical_table) fn notify_waiters(
258 waiters: Vec<FlushWaiter>,
259 result: Result<()>,
260) {
261 let shared_result = result.map_err(Arc::new);
262 for waiter in waiters {
263 let _ = waiter.response_tx.send(match &shared_result {
264 Ok(()) => Ok(()),
265 Err(error) => Err(Arc::clone(error)),
266 });
267 }
269}
270
271#[cfg(test)]
272mod tests {
273 use std::collections::HashMap;
274 use std::future::poll_fn;
275 use std::sync::Arc;
276 use std::sync::atomic::{AtomicUsize, Ordering};
277 use std::task::Poll;
278 use std::time::Duration;
279
280 use api::v1::meta::Peer;
281 use api::v1::region::RegionRequest;
282 use arrow::record_batch::RecordBatch;
283 use async_trait::async_trait;
284 use catalog::error::Result as CatalogResult;
285 use common_batcher::request_limiter::RequestLimiter;
286 use common_meta::cache::TableFlownodeSetCacheRef;
287 use common_meta::node_manager::NodeManagerRef;
288 use datatypes::schema::{ColumnSchema as DtColumnSchema, Schema as DtSchema};
289 use partition::error::Result as PartitionResult;
290 use partition::partition::{PartitionRule, PartitionRuleRef, RegionMask};
291 use table::metadata::TableId;
292 use table::test_util::table_info::test_table_info;
293 use tokio::sync::{mpsc, oneshot};
294
295 use crate::batcher::flow_notifier::{FlowNotifier, start_flow_notification_worker};
296 use crate::batcher::logical_table::batch::{
297 Batch, flush_batch, flush_batch_physical, notify_waiters,
298 };
299 use crate::batcher::logical_table::batch_convert::TableBatch;
300 use crate::batcher::logical_table::pending_worker::FlushWaiter;
301 use crate::batcher::logical_table::region_write::{
302 PhysicalFlushCatalogProvider, PhysicalFlushPartitionProvider, PhysicalFlushWriteDispatcher,
303 PhysicalTableMetadata,
304 };
305 use crate::batcher::logical_table::test_util::{
306 FlowNotificationMockNodeManager, RecordingFlownode, mock_aligned_tag_batch,
307 };
308 use crate::batcher::test_util::mock_table_flownode_cache;
309 use crate::error;
310 use crate::error::Error;
311 use crate::metrics::FLOW_NOTIFICATION_DROPPED;
312
313 #[tokio::test]
314 async fn test_flush_batch_notifies_flownode_after_successful_physical_write() {
315 let table_id = 42;
316 let peer = Peer {
317 id: 7,
318 addr: "flow-7".to_string(),
319 };
320 let cache = mock_table_flownode_cache(table_id, vec![(0, peer.clone()), (1, peer)]).await;
321 let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
322 let _requests_tx = requests_tx.clone();
323 let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
324 flownode: Arc::new(RecordingFlownode { requests_tx }),
325 });
326 let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
327 let ctx = session::context::QueryContext::arc();
328 let table_batches = vec![TableBatch {
329 table_name: "cpu".to_string(),
330 table_id,
331 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
332 row_count: 1,
333 }];
334 let writes = Arc::new(AtomicUsize::new(0));
335
336 flush_batch(
337 Batch {
338 table_batches,
339 total_row_count: 1,
340 db_string: ctx.get_db_string(),
341 ctx,
342 waiters: Vec::new(),
343 },
344 &MockFlushPartitionProvider {
345 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
346 },
347 &MockFlushWriteDispatcher {
348 writes: writes.clone(),
349 fail: false,
350 },
351 &MockFlushCatalogProvider {
352 table: Some(mock_physical_table_metadata(1024)),
353 },
354 flow_notification_tx,
355 )
356 .await;
357
358 assert_eq!(1, writes.load(Ordering::SeqCst));
359 let requests = tokio::time::timeout(Duration::from_secs(1), requests_rx.recv())
360 .await
361 .unwrap()
362 .unwrap();
363 assert_eq!(
364 vec![api::v1::flow::DirtyWindowRequest {
365 table_id,
366 timestamps: vec![1000],
367 time_ranges: Vec::new(),
368 }],
369 requests.requests
370 );
371 }
372
373 #[tokio::test]
374 async fn test_flush_batch_does_not_notify_flownode_after_physical_write_error() {
375 let table_id = 42;
376 let peer = Peer {
377 id: 7,
378 addr: "flow-7".to_string(),
379 };
380 let cache = mock_table_flownode_cache(table_id, vec![(0, peer.clone()), (1, peer)]).await;
381 let (requests_tx, mut requests_rx) = mpsc::unbounded_channel();
382 let _requests_tx = requests_tx.clone();
383 let flow_node_manager: NodeManagerRef = Arc::new(FlowNotificationMockNodeManager {
384 flownode: Arc::new(RecordingFlownode { requests_tx }),
385 });
386 let flow_notification_tx = mock_flow_notification_sender(cache, flow_node_manager.clone());
387 let ctx = session::context::QueryContext::arc();
388 let table_batches = vec![TableBatch {
389 table_name: "cpu".to_string(),
390 table_id,
391 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
392 row_count: 1,
393 }];
394 let writes = Arc::new(AtomicUsize::new(0));
395
396 flush_batch(
397 Batch {
398 table_batches,
399 total_row_count: 1,
400 db_string: ctx.get_db_string(),
401 ctx,
402 waiters: Vec::new(),
403 },
404 &MockFlushPartitionProvider {
405 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
406 },
407 &MockFlushWriteDispatcher {
408 writes: writes.clone(),
409 fail: true,
410 },
411 &MockFlushCatalogProvider {
412 table: Some(mock_physical_table_metadata(1024)),
413 },
414 flow_notification_tx,
415 )
416 .await;
417
418 assert_eq!(1, writes.load(Ordering::SeqCst));
419 assert!(
420 tokio::time::timeout(Duration::from_millis(50), requests_rx.recv())
421 .await
422 .is_err()
423 );
424 }
425
426 #[tokio::test]
427 async fn test_cancelled_waiter_retains_request_slot_until_notification() {
428 let limiter = RequestLimiter::try_new(1).unwrap();
429 let (response_tx, response_rx) = oneshot::channel();
430 let waiter = FlushWaiter {
431 response_tx,
432 _permit: limiter.acquire().await.unwrap(),
433 };
434 drop(response_rx);
435 let next = limiter.acquire();
436 tokio::pin!(next);
437 assert!(poll_fn(|cx| Poll::Ready(next.as_mut().poll(cx).is_pending())).await);
438 notify_waiters(vec![waiter], Ok(()));
439 let _permit = next.await.unwrap();
440 }
441
442 #[tokio::test]
443 async fn test_flush_batch_physical_uses_mockable_trait_dependencies() {
444 let table_batches = vec![TableBatch {
445 table_name: "t1".to_string(),
446 table_id: 11,
447 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
448 row_count: 1,
449 }];
450 let partition_calls = Arc::new(AtomicUsize::new(0));
451 let dispatcher = MockFlushWriteDispatcher::default();
452 let ctx = session::context::QueryContext::arc();
453
454 flush_batch_physical(
455 &table_batches,
456 "phy",
457 &ctx,
458 &MockFlushPartitionProvider {
459 partition_rule_calls: partition_calls.clone(),
460 },
461 &dispatcher,
462 &MockFlushCatalogProvider {
463 table: Some(mock_physical_table_metadata(1024)),
464 },
465 )
466 .await
467 .unwrap();
468
469 assert_eq!(1, partition_calls.load(Ordering::SeqCst));
470 assert_eq!(1, dispatcher.writes.load(Ordering::SeqCst));
471 }
472
473 #[tokio::test]
474 async fn test_flush_batch_physical_returns_actual_affected_rows() {
475 let table_batches = vec![TableBatch {
476 table_name: "t1".to_string(),
477 table_id: 11,
478 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
479 row_count: 1,
480 }];
481 let ctx = session::context::QueryContext::arc();
482
483 let affected_rows = flush_batch_physical(
484 &table_batches,
485 "phy",
486 &ctx,
487 &MockFlushPartitionProvider {
488 partition_rule_calls: Arc::new(AtomicUsize::new(0)),
489 },
490 &AffectedRowsFlushWriteDispatcher { affected_rows: 7 },
491 &MockFlushCatalogProvider {
492 table: Some(mock_physical_table_metadata(1024)),
493 },
494 )
495 .await
496 .unwrap();
497
498 assert_eq!(7, affected_rows);
499 }
500
501 #[tokio::test]
502 async fn test_flush_batch_physical_stops_before_partition_and_dispatch_when_table_missing() {
503 let table_batches = vec![TableBatch {
504 table_name: "t1".to_string(),
505 table_id: 11,
506 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
507 row_count: 1,
508 }];
509 let partition_calls = Arc::new(AtomicUsize::new(0));
510 let dispatcher = MockFlushWriteDispatcher::default();
511 let ctx = session::context::QueryContext::arc();
512
513 let err = flush_batch_physical(
514 &table_batches,
515 "missing_phy",
516 &ctx,
517 &MockFlushPartitionProvider {
518 partition_rule_calls: partition_calls.clone(),
519 },
520 &dispatcher,
521 &MockFlushCatalogProvider { table: None },
522 )
523 .await
524 .unwrap_err();
525
526 assert!(
527 err.to_string()
528 .contains("Physical table 'missing_phy' not found")
529 );
530 assert_eq!(0, partition_calls.load(Ordering::SeqCst));
531 assert_eq!(0, dispatcher.writes.load(Ordering::SeqCst));
532 }
533
534 #[tokio::test]
535 async fn test_flush_batch_physical_aborts_immediately_on_transform_error() {
536 let table_batches = vec![
537 TableBatch {
538 table_name: "broken".to_string(),
539 table_id: 11,
540 batches: vec![mock_aligned_tag_batch("unknown_tag", "host-1", 1000, 1.0)],
541 row_count: 1,
542 },
543 TableBatch {
544 table_name: "healthy".to_string(),
545 table_id: 12,
546 batches: vec![mock_aligned_tag_batch("tag1", "host-2", 2000, 2.0)],
547 row_count: 1,
548 },
549 ];
550 let partition_calls = Arc::new(AtomicUsize::new(0));
551 let dispatcher = MockFlushWriteDispatcher::default();
552 let ctx = session::context::QueryContext::arc();
553
554 let err = flush_batch_physical(
555 &table_batches,
556 "phy",
557 &ctx,
558 &MockFlushPartitionProvider {
559 partition_rule_calls: partition_calls.clone(),
560 },
561 &dispatcher,
562 &MockFlushCatalogProvider {
563 table: Some(mock_physical_table_metadata(1024)),
564 },
565 )
566 .await
567 .unwrap_err();
568
569 assert!(err.to_string().contains("unknown_tag"));
570 assert_eq!(1, partition_calls.load(Ordering::SeqCst));
571 assert_eq!(0, dispatcher.writes.load(Ordering::SeqCst));
572 }
573
574 fn mock_physical_table_metadata(table_id: TableId) -> PhysicalTableMetadata {
575 let schema = Arc::new(
576 DtSchema::try_new(vec![
577 DtColumnSchema::new(
578 "__primary_key",
579 datatypes::prelude::ConcreteDataType::binary_datatype(),
580 false,
581 ),
582 DtColumnSchema::new(
583 "greptime_timestamp",
584 datatypes::prelude::ConcreteDataType::timestamp_millisecond_datatype(),
585 false,
586 ),
587 DtColumnSchema::new(
588 "greptime_value",
589 datatypes::prelude::ConcreteDataType::float64_datatype(),
590 true,
591 ),
592 DtColumnSchema::new(
593 "tag1",
594 datatypes::prelude::ConcreteDataType::string_datatype(),
595 true,
596 ),
597 ])
598 .unwrap(),
599 );
600 let mut table_info = test_table_info(table_id, "phy", "public", "greptime", schema);
601 table_info.meta.column_ids = vec![0, 1, 2, 3];
602
603 PhysicalTableMetadata {
604 table_info: Arc::new(table_info),
605 col_name_to_ids: Some(HashMap::from([("tag1".to_string(), 3)])),
606 }
607 }
608
609 struct MockFlushCatalogProvider {
610 table: Option<PhysicalTableMetadata>,
611 }
612
613 #[async_trait]
614 impl PhysicalFlushCatalogProvider for MockFlushCatalogProvider {
615 async fn physical_table(
616 &self,
617 _catalog: &str,
618 _schema: &str,
619 _table_name: &str,
620 _query_ctx: &session::context::QueryContext,
621 ) -> CatalogResult<Option<PhysicalTableMetadata>> {
622 Ok(self.table.clone())
623 }
624 }
625
626 struct SingleRegionPartitionRule;
627
628 impl PartitionRule for SingleRegionPartitionRule {
629 fn as_any(&self) -> &dyn std::any::Any {
630 self
631 }
632
633 fn partition_columns(&self) -> &[String] {
634 &[]
635 }
636
637 fn find_region(
638 &self,
639 _values: &[datatypes::prelude::Value],
640 ) -> partition::error::Result<store_api::storage::RegionNumber> {
641 unimplemented!()
642 }
643
644 fn split_record_batch(
645 &self,
646 record_batch: &RecordBatch,
647 ) -> partition::error::Result<HashMap<store_api::storage::RegionNumber, RegionMask>>
648 {
649 Ok(HashMap::from([(
650 1,
651 RegionMask::new(
652 arrow::array::BooleanArray::from(vec![true; record_batch.num_rows()]),
653 record_batch.num_rows(),
654 ),
655 )]))
656 }
657 }
658
659 struct MockFlushPartitionProvider {
660 partition_rule_calls: Arc<AtomicUsize>,
661 }
662
663 #[async_trait]
664 impl PhysicalFlushPartitionProvider for MockFlushPartitionProvider {
665 async fn find_table_partition_rule(
666 &self,
667 _table_info: &table::metadata::TableInfo,
668 ) -> PartitionResult<(PartitionRuleRef, HashMap<u32, Option<u64>>)> {
669 self.partition_rule_calls.fetch_add(1, Ordering::SeqCst);
670 Ok((Arc::new(SingleRegionPartitionRule), HashMap::new()))
671 }
672 }
673
674 #[derive(Default)]
675 struct MockFlushWriteDispatcher {
676 writes: Arc<AtomicUsize>,
677 fail: bool,
678 }
679
680 #[async_trait]
681 impl PhysicalFlushWriteDispatcher for MockFlushWriteDispatcher {
682 async fn dispatch(&self, _requests: Vec<RegionRequest>) -> error::Result<usize> {
683 self.writes.fetch_add(1, Ordering::SeqCst);
684 if self.fail {
685 return Err(Error::Internal {
686 err_msg: "physical write failed".to_string(),
687 });
688 }
689 Ok(0)
690 }
691 }
692
693 fn mock_flow_notification_sender(
694 cache: TableFlownodeSetCacheRef,
695 node_manager: NodeManagerRef,
696 ) -> FlowNotifier {
697 let (tx, rx) = FlowNotifier::try_new(16, FLOW_NOTIFICATION_DROPPED.clone()).unwrap();
698 start_flow_notification_worker(rx, cache, node_manager);
699 tx
700 }
701
702 #[derive(Default)]
703 struct AffectedRowsFlushWriteDispatcher {
704 affected_rows: usize,
705 }
706
707 #[async_trait]
708 impl PhysicalFlushWriteDispatcher for AffectedRowsFlushWriteDispatcher {
709 async fn dispatch(&self, _requests: Vec<RegionRequest>) -> error::Result<usize> {
710 Ok(self.affected_rows)
711 }
712 }
713}