1use std::collections::HashMap;
16use std::sync::Arc;
17use std::time::Duration;
18
19use catalog::CatalogManagerRef;
20use common_batcher::flush_limiter::FlushLimiter;
21use common_batcher::flush_policy::FlushTrigger;
22use common_batcher::flush_policy::timing::TimingFlushPolicy;
23use common_batcher::pending_worker::PendingWorker as PendingCore;
24use common_batcher::worker_registry::WorkerRegistry;
25use common_query::WriteProgress;
26use common_telemetry::debug;
27use operator::write_dispatcher::WriteDispatcherRef;
28use partition::manager::PartitionRuleManagerRef;
29use session::context::QueryContextRef;
30use table::metadata::TableId;
31use tokio::sync::{OwnedSemaphorePermit, broadcast, mpsc, oneshot};
32
33use crate::batcher::flow_notifier::FlowNotifier;
34use crate::batcher::logical_table::BatchKey;
35use crate::batcher::logical_table::batch::{self, Batch};
36use crate::batcher::logical_table::batch_convert::{RecordBatchWithTsIdx, TableBatch};
37use crate::metrics::{PENDING_BATCHES, PENDING_ROWS, PENDING_WORKERS};
38
39pub(in crate::batcher::logical_table) struct PendingBatch {
40 inputs: usize,
41 pub(in crate::batcher::logical_table) tables: HashMap<TableId, TableBatch>,
42 pub(in crate::batcher::logical_table) db_string: String,
43 pub(in crate::batcher::logical_table) ctx: QueryContextRef,
44}
45
46pub(in crate::batcher::logical_table) struct FlushWaiter {
47 pub(in crate::batcher::logical_table) _completion:
48 Option<session::write_progress::PendingWrite>,
49 pub(in crate::batcher::logical_table) response_tx: oneshot::Sender<WriteProgress>,
50 pub(in crate::batcher::logical_table) _permit: Arc<OwnedSemaphorePermit>,
51}
52
53#[derive(Clone)]
54pub(in crate::batcher::logical_table) struct PendingWorker {
55 pub(in crate::batcher::logical_table) tx: mpsc::Sender<WorkerCommand>,
56}
57
58pub(crate) enum WorkerCommand {
60 Submit {
61 _completion: Option<session::write_progress::PendingWrite>,
62 table_batches: Vec<(String, u32, RecordBatchWithTsIdx)>,
63 total_rows: usize,
64 ctx: QueryContextRef,
65 response_tx: oneshot::Sender<WriteProgress>,
66 _permit: Arc<OwnedSemaphorePermit>,
67 },
68 #[cfg(test)]
69 Ack { ack_tx: oneshot::Sender<()> },
70}
71
72impl PendingBatch {
73 pub(in crate::batcher::logical_table) fn new(ctx: QueryContextRef) -> Self {
74 let db_string = ctx.get_db_string();
75 Self {
76 inputs: 0,
77 tables: HashMap::new(),
78 db_string,
79 ctx,
80 }
81 }
82
83 pub(in crate::batcher::logical_table) fn add_table_batch(
84 &mut self,
85 table_name: String,
86 table_id: TableId,
87 record_batch: RecordBatchWithTsIdx,
88 ) {
89 let entry = self.tables.entry(table_id).or_insert_with(|| TableBatch {
90 table_name,
91 table_id,
92 batches: Vec::new(),
93 row_count: 0,
94 });
95 entry.row_count += record_batch.batch.num_rows();
96 entry.batches.push(record_batch);
97 }
98}
99
100#[allow(clippy::too_many_arguments)]
102pub(crate) fn start_worker(
103 key: BatchKey,
104 worker_tx: mpsc::Sender<WorkerCommand>,
105 workers: Arc<WorkerRegistry<BatchKey, WorkerCommand>>,
106 mut rx: mpsc::Receiver<WorkerCommand>,
107 shutdown: broadcast::Sender<()>,
108 partition_manager: PartitionRuleManagerRef,
109 write_dispatcher: WriteDispatcherRef,
110 catalog_manager: CatalogManagerRef,
111 flow_notification_tx: FlowNotifier,
112 worker_idle_timeout: Duration,
113 flush_policy: TimingFlushPolicy,
114 flush_limiter: FlushLimiter,
115) {
116 tokio::spawn(async move {
117 let mut batch = None;
119 let mut pending_flush = PendingCore::new(flush_policy);
120 let mut shutdown_rx = shutdown.subscribe();
121 let idle_deadline = tokio::time::Instant::now() + worker_idle_timeout;
122 let idle_timer = tokio::time::sleep_until(idle_deadline);
123 tokio::pin!(idle_timer);
124
125 loop {
126 tokio::select! {
127 cmd = rx.recv() => {
128 match cmd {
129 Some(WorkerCommand::Submit { table_batches, total_rows, ctx, response_tx, _permit, _completion }) => {
130 let submitted_at = tokio::time::Instant::now();
131 idle_timer.as_mut().reset(submitted_at + worker_idle_timeout);
132
133 pending_flush.submit(
134 FlushWaiter { response_tx, _permit, _completion },
135 total_rows,
136 );
137 let progress = ctx.write_progress().cloned();
138 let pending_batch = batch.get_or_insert_with(||{
139 PENDING_BATCHES.inc();
140 PendingBatch::new(ctx)
141 });
142
143 for (table_name, table_id, mut record_batch) in table_batches {
144 record_batch.progress = progress.clone();
145 record_batch.owner = pending_batch.inputs;
146 pending_batch.add_table_batch(table_name, table_id, record_batch);
147 }
148
149 pending_batch.inputs += 1;
150 PENDING_ROWS.add(total_rows as i64);
151
152 if let Some(flush) = drain_batch(&mut batch, &mut pending_flush, Some(FlushTrigger::Submission)) {
153 batch::spawn_flush(
154 flush,
155 partition_manager.clone(),
156 write_dispatcher.clone(),
157 catalog_manager.clone(),
158 flow_notification_tx.clone(),
159 flush_limiter.clone(),
160 ).await;
161 }
162 }
163 None => {
164 if let Some(flush) = drain_batch(&mut batch, &mut pending_flush, None) {
165 batch::flush_batch_with_managers(
166 flush,
167 partition_manager.clone(),
168 write_dispatcher.clone(),
169 catalog_manager.clone(),
170 flow_notification_tx.clone(),
171 ).await;
172 }
173 break;
174 }
175 #[cfg(test)]
176 Some(WorkerCommand::Ack { ack_tx }) => {
177 let _ = ack_tx.send(());
178 }
179 }
180 }
181 _ = &mut idle_timer => {
182 if !should_close_worker_on_idle_timeout(
183 pending_flush.total_rows(),
184 rx.len(),
185 ) {
186 idle_timer
187 .as_mut()
188 .reset(tokio::time::Instant::now() + worker_idle_timeout);
189 continue;
190 }
191
192 debug!(
193 "Closing idle pending rows worker due to timeout: catalog={}, schema={}, physical_table={}",
194 key.catalog,
195 key.schema,
196 key.physical_table
197 );
198 break;
199 }
200 _ = pending_flush.wait_flush() => {
201 if let Some(flush) = drain_batch(&mut batch, &mut pending_flush, Some(FlushTrigger::Deadline)) {
202 batch::spawn_flush(
203 flush,
204 partition_manager.clone(),
205 write_dispatcher.clone(),
206 catalog_manager.clone(),
207 flow_notification_tx.clone(),
208 flush_limiter.clone(),
209 ).await;
210 }
211 }
212 _ = shutdown_rx.recv() => {
213 if let Some(flush) = drain_batch(&mut batch, &mut pending_flush, None) {
214 batch::flush_batch_with_managers(
215 flush,
216 partition_manager.clone(),
217 write_dispatcher.clone(),
218 catalog_manager.clone(),
219 flow_notification_tx.clone(),
220 ).await;
221 }
222 break;
223 }
224 }
225 }
226
227 remove_worker_if_same_channel(workers.as_ref(), &key, &worker_tx).await;
228 });
229}
230
231pub(in crate::batcher::logical_table) async fn remove_worker_if_same_channel(
232 workers: &WorkerRegistry<BatchKey, WorkerCommand>,
233 key: &BatchKey,
234 worker_tx: &mpsc::Sender<WorkerCommand>,
235) -> bool {
236 if workers.remove_if_same(key, worker_tx).await {
237 PENDING_WORKERS.set(workers.len().await as i64);
238 true
239 } else {
240 false
241 }
242}
243
244pub(in crate::batcher::logical_table) fn should_close_worker_on_idle_timeout(
245 total_row_count: usize,
246 queued_requests: usize,
247) -> bool {
248 total_row_count == 0 && queued_requests == 0
249}
250
251pub(in crate::batcher::logical_table) fn drain_batch(
254 batch: &mut Option<PendingBatch>,
255 pending_flush: &mut PendingCore<FlushWaiter, TimingFlushPolicy>,
256 trigger: Option<FlushTrigger>,
257) -> Option<Batch> {
258 let total_row_count = pending_flush.total_rows();
259 let waiters = match trigger {
260 Some(trigger) => pending_flush.take_ready(trigger)?,
261 None => pending_flush.take_pending()?,
262 };
263 let batch = batch.take()?;
264
265 if total_row_count == 0 {
266 return None;
267 }
268
269 let table_batches = batch.tables.into_values().collect();
270
271 PENDING_ROWS.sub(total_row_count as i64);
272 PENDING_BATCHES.dec();
273
274 Some(Batch {
275 table_batches,
276 total_row_count,
277 db_string: batch.db_string,
278 ctx: batch.ctx,
279 waiters,
280 })
281}
282
283#[cfg(test)]
284mod tests {
285 use common_query::WriteProgress;
286 use std::collections::HashMap;
287 use std::sync::Arc;
288 use std::time::Duration;
289
290 use catalog::memory::MemoryCatalogManager;
291 use common_batcher::flush_limiter::FlushLimiter;
292 use common_batcher::flush_policy::FlushTrigger;
293 use common_batcher::flush_policy::timing::TimingFlushPolicy;
294 use common_batcher::pending_worker::PendingWorker as PendingCore;
295 use common_batcher::worker_registry::WorkerRegistry;
296 use common_meta::cache::new_table_route_cache;
297 use common_meta::kv_backend::memory::MemoryKvBackend;
298 use common_meta::node_manager::NodeManagerRef;
299 use moka::future::CacheBuilder;
300 use operator::write_dispatcher::WriteDispatcher;
301 use partition::cache::new_partition_info_cache;
302 use partition::manager::PartitionRuleManager;
303 use tokio::sync::{Semaphore, broadcast, mpsc, oneshot};
304 use tokio::time::advance;
305
306 use crate::batcher::flow_notifier::FlowNotifier;
307 use crate::batcher::logical_table::BatchKey;
308 use crate::batcher::logical_table::batch_convert::TableBatch;
309 use crate::batcher::logical_table::pending_worker::{
310 FlushWaiter, PendingBatch, WorkerCommand, drain_batch, remove_worker_if_same_channel,
311 should_close_worker_on_idle_timeout, start_worker,
312 };
313 use crate::batcher::logical_table::test_util::{
314 ConcurrentMockNodeManager, mock_aligned_tag_batch,
315 };
316 use crate::metrics::FLOW_NOTIFICATION_DROPPED;
317
318 #[tokio::test]
319 async fn test_drain_batch_takes_initialized_pending_batch_from_option() {
320 let ctx = session::context::QueryContext::arc();
321 let (response_tx, _response_rx) = oneshot::channel();
322 let permit = Arc::new(Semaphore::new(1)).try_acquire_owned().unwrap();
323 let mut pending_flush =
324 PendingCore::new(TimingFlushPolicy::try_new(Duration::from_secs(10), 1).unwrap());
325 pending_flush.submit(
326 FlushWaiter {
327 _completion: None,
328 response_tx,
329 _permit: Arc::new(permit),
330 },
331 1,
332 );
333 let mut batch = Some(PendingBatch {
334 inputs: 1,
335 tables: HashMap::from([(
336 42,
337 TableBatch {
338 table_name: "cpu".to_string(),
339 table_id: 42,
340 batches: vec![mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0)],
341 row_count: 1,
342 },
343 )]),
344 db_string: ctx.get_db_string(),
345 ctx: ctx.clone(),
346 });
347
348 let flush = drain_batch(
349 &mut batch,
350 &mut pending_flush,
351 Some(FlushTrigger::Submission),
352 )
353 .unwrap();
354
355 assert!(batch.is_none());
356 assert!(pending_flush.is_empty());
357 assert_eq!(0, pending_flush.total_rows());
358 assert_eq!(1, flush.waiters.len());
359 assert_eq!(1, flush.total_row_count);
360 assert_eq!(1, flush.table_batches.len());
361 assert_eq!(ctx.get_db_string(), flush.db_string);
362 assert_eq!(ctx.current_catalog(), flush.ctx.current_catalog());
363 }
364
365 #[tokio::test]
366 async fn test_drain_batch_preserves_unready_state_and_clears_zero_rows() {
367 for total_rows in [0, 1] {
368 let mut pending_flush =
369 PendingCore::new(TimingFlushPolicy::try_new(Duration::from_secs(10), 2).unwrap());
370 let mut batch = Some(PendingBatch::new(session::context::QueryContext::arc()));
371 let semaphore = Arc::new(Semaphore::new(1));
372 let (response_tx, mut response_rx) = oneshot::channel();
373 pending_flush.submit(
374 FlushWaiter {
375 _completion: None,
376 response_tx,
377 _permit: Arc::new(semaphore.clone().acquire_owned().await.unwrap()),
378 },
379 total_rows,
380 );
381 assert!(
382 drain_batch(
383 &mut batch,
384 &mut pending_flush,
385 Some(FlushTrigger::Submission)
386 )
387 .is_none()
388 );
389 assert!(batch.is_some());
390 assert!(!pending_flush.is_empty());
391 assert_eq!(total_rows, pending_flush.total_rows());
392 assert_eq!(0, semaphore.available_permits());
393
394 let drained = drain_batch(&mut batch, &mut pending_flush, None);
395 assert!(batch.is_none());
396 assert!(pending_flush.is_empty());
397 assert_eq!(0, pending_flush.total_rows());
398 if total_rows == 0 {
399 assert!(drained.is_none());
400 assert_eq!(1, semaphore.available_permits());
401 assert!(matches!(
402 response_rx.try_recv(),
403 Err(oneshot::error::TryRecvError::Closed)
404 ));
405 } else {
406 let drained = drained.unwrap();
407 assert_eq!(1, drained.total_row_count);
408 assert_eq!(1, drained.waiters.len());
409 assert_eq!(0, semaphore.available_permits());
410 drop(drained);
411 assert_eq!(1, semaphore.available_permits());
412 }
413 }
414 }
415
416 #[test]
417 fn test_pending_batch_keeps_same_name_batches_with_distinct_table_ids() {
418 let ctx = session::context::QueryContext::arc();
419 let mut pending_batch = PendingBatch::new(ctx);
420
421 pending_batch.add_table_batch(
422 "cpu".to_string(),
423 42,
424 mock_aligned_tag_batch("tag1", "host-1", 1000, 1.0),
425 );
426 pending_batch.add_table_batch(
427 "cpu".to_string(),
428 43,
429 mock_aligned_tag_batch("tag1", "host-1", 2000, 2.0),
430 );
431
432 assert_eq!(2, pending_batch.tables.len());
433 assert_eq!(42, pending_batch.tables[&42].table_id);
434 assert_eq!(43, pending_batch.tables[&43].table_id);
435 assert_eq!("cpu", pending_batch.tables[&42].table_name);
436 assert_eq!("cpu", pending_batch.tables[&43].table_name);
437 }
438
439 #[tokio::test]
440 async fn test_remove_worker_if_same_channel_removes_matching_entry() {
441 let workers = WorkerRegistry::new();
442 let key = BatchKey {
443 catalog: "greptime".to_string(),
444 schema: "public".to_string(),
445 physical_table: "phy".to_string(),
446 skip_wal: false,
447 };
448
449 let (tx, _rx) = mpsc::channel::<WorkerCommand>(1);
450 workers.get_or_insert_with(key.clone(), || tx.clone()).await;
451
452 assert!(remove_worker_if_same_channel(&workers, &key, &tx).await);
453 assert!(workers.is_empty().await);
454 }
455
456 #[tokio::test]
457 async fn test_remove_worker_if_same_channel_keeps_newer_entry() {
458 let workers = WorkerRegistry::new();
459 let key = BatchKey {
460 catalog: "greptime".to_string(),
461 schema: "public".to_string(),
462 physical_table: "phy".to_string(),
463 skip_wal: false,
464 };
465
466 let (stale_tx, _stale_rx) = mpsc::channel::<WorkerCommand>(1);
467 let (fresh_tx, _fresh_rx) = mpsc::channel::<WorkerCommand>(1);
468 workers
469 .get_or_insert_with(key.clone(), || fresh_tx.clone())
470 .await;
471
472 assert!(!remove_worker_if_same_channel(&workers, &key, &stale_tx).await);
473 assert!(workers.get(&key).await.is_some());
474 assert!(workers.get(&key).await.unwrap().same_channel(&fresh_tx));
475 }
476
477 #[test]
478 fn test_worker_idle_timeout_close_decision() {
479 assert!(should_close_worker_on_idle_timeout(0, 0));
480 assert!(!should_close_worker_on_idle_timeout(1, 0));
481 assert!(!should_close_worker_on_idle_timeout(0, 1));
482 }
483
484 #[tokio::test(start_paused = true)]
485 async fn test_worker_preserves_first_deadline_and_inline_shutdown() {
486 let flush_interval = Duration::from_secs(10);
487 let worker_idle_timeout = Duration::from_secs(30);
488 let key = BatchKey {
489 catalog: "greptime".to_string(),
490 schema: "public".to_string(),
491 physical_table: "phy".to_string(),
492 skip_wal: false,
493 };
494 let workers = Arc::new(WorkerRegistry::new());
495 let (worker_tx, worker_rx) = mpsc::channel(1);
496 workers
497 .get_or_insert_with(key.clone(), || worker_tx.clone())
498 .await;
499
500 let backend = Arc::new(MemoryKvBackend::default());
501 let table_route_cache = Arc::new(new_table_route_cache(
502 "pending-rows-flush-deadline-routes".to_string(),
503 CacheBuilder::new(1).build(),
504 backend.clone(),
505 ));
506 let partition_info_cache = Arc::new(new_partition_info_cache(
507 "pending-rows-flush-deadline-partitions".to_string(),
508 CacheBuilder::new(1).build(),
509 table_route_cache.clone(),
510 ));
511 let partition_manager = Arc::new(PartitionRuleManager::new(
512 backend,
513 table_route_cache,
514 partition_info_cache,
515 ));
516 let node_manager: NodeManagerRef = Arc::new(ConcurrentMockNodeManager {
517 datanodes: Arc::new(HashMap::new()),
518 });
519 let write_dispatcher = Arc::new(WriteDispatcher::new(
520 node_manager,
521 partition_manager.clone(),
522 ));
523 let catalog_manager = MemoryCatalogManager::with_default_setup();
524 let (flow_notification_tx, _flow_notification_rx) =
525 FlowNotifier::try_new(1, FLOW_NOTIFICATION_DROPPED.clone()).unwrap();
526 let (shutdown, _) = broadcast::channel(1);
527
528 let flush_limiter = FlushLimiter::try_new(1).unwrap();
529 start_worker(
530 key.clone(),
531 worker_tx.clone(),
532 workers.clone(),
533 worker_rx,
534 shutdown.clone(),
535 partition_manager,
536 write_dispatcher,
537 catalog_manager,
538 flow_notification_tx,
539 worker_idle_timeout,
540 TimingFlushPolicy::try_new(flush_interval, 3).unwrap(),
541 flush_limiter.clone(),
542 );
543
544 tokio::task::yield_now().await;
548 advance(flush_interval / 2).await;
549 let size_flush_rx = submit_mock_worker_batch(&worker_tx, 3, 1000).await;
550 let size_flush_result =
551 receive_mock_flush_result(size_flush_rx, "row threshold did not flush the first batch")
552 .await;
553 assert_missing_physical_table(size_flush_result);
554
555 advance(flush_interval / 5).await;
558 let mut timed_flush_rx = submit_mock_worker_batch(&worker_tx, 1, 2000).await;
559
560 let first_submission = tokio::time::Instant::now();
561 advance(flush_interval / 2).await;
562 let later_flush_rx = submit_mock_worker_batch(&worker_tx, 1, 3000).await;
563 advance(flush_interval / 2 - Duration::from_millis(1)).await;
564 for _ in 0..10 {
565 tokio::task::yield_now().await;
566 }
567 assert!(matches!(
568 timed_flush_rx.try_recv(),
569 Err(oneshot::error::TryRecvError::Empty)
570 ));
571
572 advance(Duration::from_millis(1)).await;
573 let timed_flush_result = receive_mock_flush_result(
574 timed_flush_rx,
575 "batch was not flushed one interval after its creation",
576 )
577 .await;
578 assert_missing_physical_table(timed_flush_result);
579
580 assert_eq!(
581 first_submission + flush_interval,
582 tokio::time::Instant::now()
583 );
584 assert_missing_physical_table(
585 receive_mock_flush_result(
586 later_flush_rx,
587 "later submission was not included in the timed flush",
588 )
589 .await,
590 );
591
592 let _held_permit = flush_limiter.acquire().await.unwrap();
594 let shutdown_flush_rx = submit_mock_worker_batch(&worker_tx, 1, 4000).await;
595 let shutdown_at = tokio::time::Instant::now();
596 let _ = shutdown.send(());
597 assert_missing_physical_table(
598 receive_mock_flush_result(
599 shutdown_flush_rx,
600 "shutdown incorrectly waited for a flush permit",
601 )
602 .await,
603 );
604 assert_eq!(shutdown_at, tokio::time::Instant::now());
605 for _ in 0..10 {
606 if workers.is_empty().await {
607 break;
608 }
609 tokio::task::yield_now().await;
610 }
611 assert!(
612 workers.is_empty().await,
613 "worker did not exit after shutdown"
614 );
615 }
616
617 const WORKER_TEST_TIMEOUT: Duration = Duration::from_secs(30);
618
619 async fn submit_mock_worker_batch(
620 worker_tx: &mpsc::Sender<WorkerCommand>,
621 total_rows: usize,
622 timestamp: i64,
623 ) -> oneshot::Receiver<WriteProgress> {
624 let (response_tx, response_rx) = oneshot::channel();
625 let permit = Arc::new(Semaphore::new(1)).acquire_owned().await.unwrap();
626 worker_tx
627 .send(WorkerCommand::Submit {
628 _completion: None,
629 table_batches: vec![(
630 "cpu".to_string(),
631 42,
632 mock_aligned_tag_batch("tag1", "host-1", timestamp, 1.0),
633 )],
634 total_rows,
635 ctx: session::context::QueryContext::arc(),
636 response_tx,
637 _permit: Arc::new(permit),
638 })
639 .await
640 .unwrap();
641
642 let (ack_tx, ack_rx) = oneshot::channel();
646 worker_tx.send(WorkerCommand::Ack { ack_tx }).await.unwrap();
647 ack_rx
648 .await
649 .expect("worker exited before acking the submitted batch");
650
651 response_rx
652 }
653
654 async fn receive_mock_flush_result(
655 response_rx: oneshot::Receiver<WriteProgress>,
656 context: &str,
657 ) -> WriteProgress {
658 tokio::time::timeout(WORKER_TEST_TIMEOUT, response_rx)
661 .await
662 .unwrap_or_else(|_| panic!("{context}"))
663 .expect("flush result channel closed without a result")
664 }
665
666 fn assert_missing_physical_table(result: WriteProgress) {
667 let err = result
668 .error
669 .expect("the empty catalog should make the flush fail");
670 assert!(
671 err.to_string().contains("not found during pending flush"),
672 "unexpected flush error: {err}"
673 );
674 }
675}