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