Skip to main content

sui_indexer_alt_framework/pipeline/concurrent/
collector.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeMap;
5use std::sync::Arc;
6use std::sync::atomic::AtomicU64;
7use std::sync::atomic::Ordering;
8
9use sui_futures::service::Service;
10use tokio::sync::SetOnce;
11use tokio::sync::mpsc;
12use tokio::time::MissedTickBehavior;
13use tokio::time::interval;
14use tracing::debug;
15use tracing::info;
16
17use crate::metrics::CheckpointLagMetricReporter;
18use crate::metrics::IndexerMetrics;
19use crate::pipeline::CommitterConfig;
20use crate::pipeline::IndexedCheckpoint;
21use crate::pipeline::WatermarkPart;
22use crate::pipeline::concurrent::BatchStatus;
23use crate::pipeline::concurrent::BatchedRows;
24use crate::pipeline::concurrent::Handler;
25
26/// Processed values that are waiting to be written to the database. This is an internal type used
27/// by the concurrent collector to hold data it is waiting to send to the committer.
28struct PendingCheckpoint<H: Handler> {
29    /// Iterator over values to be inserted into the database from this checkpoint
30    values: std::vec::IntoIter<H::Value>,
31    /// The watermark associated with this checkpoint and the part of it that is left to commit
32    watermark: WatermarkPart,
33}
34
35impl<H: Handler> PendingCheckpoint<H> {
36    /// Whether there are values left to commit from this indexed checkpoint.
37    fn is_empty(&self) -> bool {
38        let empty = self.values.len() == 0;
39        debug_assert!(!empty || self.watermark.batch_rows == 0);
40        empty
41    }
42}
43
44impl<H: Handler> From<IndexedCheckpoint<H>> for PendingCheckpoint<H> {
45    fn from(indexed: IndexedCheckpoint<H>) -> Self {
46        let total_rows = indexed.values.len();
47        Self {
48            watermark: WatermarkPart {
49                watermark: indexed.watermark,
50                batch_rows: total_rows,
51                total_rows,
52            },
53            values: indexed.values.into_iter(),
54        }
55    }
56}
57
58/// The collector task is responsible for gathering rows into batches which it then sends to a
59/// committer task to write to the database. The task publishes batches in the following
60/// circumstances:
61///
62/// - If `H::BATCH_SIZE` rows are pending, it will immediately schedule a batch to be gathered.
63///
64/// - If after sending one batch there is more data to be sent, it will immediately schedule the
65///   next batch to be gathered (Each batch will contain at most `H::CHUNK_SIZE` rows).
66///
67/// - Otherwise, it will check for any data to write out at a regular interval (controlled by
68///   `config.collect_interval()`).
69///
70/// The `main_reader_lo` tracks the lowest checkpoint that can be committed by this pipeline.
71///
72/// This task will shutdown if any of its channels are closed.
73pub(super) fn collector<H: Handler>(
74    handler: Arc<H>,
75    config: CommitterConfig,
76    mut rx: mpsc::Receiver<IndexedCheckpoint<H>>,
77    tx: mpsc::Sender<BatchedRows<H>>,
78    main_reader_lo: Arc<SetOnce<AtomicU64>>,
79    metrics: Arc<IndexerMetrics>,
80    min_eager_rows: usize,
81    max_pending_rows: usize,
82    max_watermark_updates: usize,
83) -> Service {
84    Service::new().spawn_aborting(async move {
85        // The `poll` interval controls the maximum time to wait between collecting batches,
86        // regardless of number of rows pending.
87        let mut poll = interval(config.collect_interval());
88        poll.set_missed_tick_behavior(MissedTickBehavior::Delay);
89
90        let checkpoint_lag_reporter = CheckpointLagMetricReporter::new_for_pipeline::<H>(
91            &metrics.collected_checkpoint_timestamp_lag,
92            &metrics.latest_collected_checkpoint_timestamp_lag_ms,
93            &metrics.latest_collected_checkpoint,
94        );
95
96        // Data for checkpoints that are ready to be sent but haven't been written yet.
97        let mut pending: BTreeMap<u64, PendingCheckpoint<H>> = BTreeMap::new();
98        let mut pending_rows = 0;
99
100        info!(pipeline = H::NAME, "Starting collector");
101
102        // Wait for main_reader_lo to be initialized before processing any checkpoints.
103        let reader_lo_atomic = main_reader_lo.wait().await;
104
105        loop {
106            // === IDLE: block until timer fires or enough data accumulates ===
107            tokio::select! {
108                biased;
109
110                // docs::#collector (see docs/content/guides/developer/advanced/custom-indexer.mdx)
111                Some(mut indexed) = rx.recv(), if pending_rows < max_pending_rows => {
112                    let reader_lo = reader_lo_atomic.load(Ordering::Relaxed);
113
114                    metrics
115                        .collector_reader_lo
116                        .with_label_values(&[H::NAME])
117                        .set(reader_lo as i64);
118
119                    let mut recv_cps = 0usize;
120                    let mut recv_rows = 0usize;
121                    loop {
122                        if indexed.checkpoint() < reader_lo {
123                            indexed.values.clear();
124                            metrics
125                                .total_collector_skipped_checkpoints
126                                .with_label_values(&[H::NAME])
127                                .inc();
128                        }
129
130                        recv_cps += 1;
131                        recv_rows += indexed.len();
132                        pending_rows += indexed.len();
133                        pending.insert(indexed.checkpoint(), indexed.into());
134
135                        if pending_rows >= max_pending_rows {
136                            break;
137                        }
138
139                        match rx.try_recv() {
140                            Ok(next) => indexed = next,
141                            Err(_) => break,
142                        }
143                    }
144
145                    metrics
146                        .total_collector_rows_received
147                        .with_label_values(&[H::NAME])
148                        .inc_by(recv_rows as u64);
149                    metrics
150                        .total_collector_checkpoints_received
151                        .with_label_values(&[H::NAME])
152                        .inc_by(recv_cps as u64);
153
154                    if pending_rows < min_eager_rows {
155                        continue;
156                    }
157                }
158                // docs::/#collector
159
160                // Timer: always flush (even if empty, for watermark progress)
161                _ = poll.tick() => {}
162            }
163
164            // === FLUSHING: send batches until pending is drained ===
165            //
166            // Always executes at least once — on timer ticks this sends an empty
167            // heartbeat batch so watermarks make progress.
168            loop {
169                let guard = metrics
170                    .collector_gather_latency
171                    .with_label_values(&[H::NAME])
172                    .start_timer();
173
174                let mut batch = H::Batch::default();
175                let mut watermark = Vec::new();
176                let mut batch_len = 0;
177
178                while let Some(mut entry) = pending.first_entry() {
179                    if watermark.len() >= max_watermark_updates {
180                        break;
181                    }
182
183                    let indexed = entry.get_mut();
184                    let before = indexed.values.len();
185                    let status = handler.batch(&mut batch, &mut indexed.values);
186                    let taken = before - indexed.values.len();
187
188                    batch_len += taken;
189                    watermark.push(indexed.watermark.take(taken));
190                    if indexed.is_empty() {
191                        checkpoint_lag_reporter.report_lag(
192                            indexed.watermark.checkpoint(),
193                            indexed.watermark.timestamp_ms(),
194                        );
195                        entry.remove();
196                    }
197
198                    if status == BatchStatus::Ready {
199                        break;
200                    }
201                }
202
203                let elapsed = guard.stop_and_record();
204                debug!(
205                    pipeline = H::NAME,
206                    elapsed_ms = elapsed * 1000.0,
207                    rows = batch_len,
208                    "Gathered batch",
209                );
210
211                metrics
212                    .total_collector_batches_created
213                    .with_label_values(&[H::NAME])
214                    .inc();
215
216                metrics
217                    .collector_batch_size
218                    .with_label_values(&[H::NAME])
219                    .observe(batch_len as f64);
220
221                pending_rows -= batch_len;
222
223                let batched_rows = BatchedRows {
224                    batch,
225                    batch_len,
226                    watermark,
227                };
228                if tx.send(batched_rows).await.is_err() {
229                    info!(
230                        pipeline = H::NAME,
231                        "Committer closed channel, stopping collector"
232                    );
233                    return Ok(());
234                }
235
236                if pending.is_empty() {
237                    break;
238                }
239            }
240
241            if rx.is_closed() && rx.is_empty() && pending_rows == 0 {
242                info!(
243                    pipeline = H::NAME,
244                    "Processor closed channel, pending rows empty, stopping collector",
245                );
246                break;
247            }
248        }
249
250        Ok(())
251    })
252}
253
254#[cfg(test)]
255mod tests {
256    use std::time::Duration;
257
258    use async_trait::async_trait;
259    use tokio::sync::mpsc;
260
261    use crate::metrics::tests::test_metrics;
262    use crate::mocks::store::FallibleMockConnection;
263    use crate::mocks::store::FallibleMockStore;
264    use crate::pipeline::Processor;
265    use crate::pipeline::concurrent::BatchStatus;
266    use crate::types::full_checkpoint_content::Checkpoint;
267
268    use super::*;
269
270    #[derive(Clone)]
271    struct Entry;
272
273    struct TestHandler;
274
275    // Max chunk rows for testing - simulates postgres bind parameter limit
276    const TEST_MAX_CHUNK_ROWS: usize = 1024;
277
278    #[async_trait]
279    impl Processor for TestHandler {
280        type Value = Entry;
281        const NAME: &'static str = "test_handler";
282
283        async fn process(&self, _checkpoint: &Arc<Checkpoint>) -> anyhow::Result<Vec<Self::Value>> {
284            Ok(vec![])
285        }
286    }
287
288    #[async_trait]
289    impl Handler for TestHandler {
290        type Store = FallibleMockStore;
291        type Batch = Vec<Entry>;
292
293        const MIN_EAGER_ROWS: usize = 10;
294        const MAX_PENDING_ROWS: usize = 10000;
295
296        fn batch(
297            &self,
298            batch: &mut Self::Batch,
299            values: &mut std::vec::IntoIter<Self::Value>,
300        ) -> BatchStatus {
301            // Simulate batch size limit
302            let remaining_capacity = TEST_MAX_CHUNK_ROWS.saturating_sub(batch.len());
303            let to_take = remaining_capacity.min(values.len());
304            batch.extend(values.take(to_take));
305
306            if batch.len() >= TEST_MAX_CHUNK_ROWS {
307                BatchStatus::Ready
308            } else {
309                BatchStatus::Pending
310            }
311        }
312
313        async fn commit<'a>(
314            &self,
315            _batch: &Self::Batch,
316            _conn: &mut FallibleMockConnection<'a>,
317        ) -> anyhow::Result<usize> {
318            tokio::time::sleep(Duration::from_millis(1000)).await;
319            Ok(0)
320        }
321    }
322
323    /// Wait for a timeout on the channel, expecting this operation to timeout.
324    async fn expect_timeout<H: Handler>(
325        rx: &mut mpsc::Receiver<BatchedRows<H>>,
326        duration: Duration,
327    ) {
328        match tokio::time::timeout(duration, rx.recv()).await {
329            Err(_) => (), // Expected timeout - test passes
330            Ok(_) => panic!("Expected timeout but received data instead"),
331        }
332    }
333
334    /// Receive from the channel with a given timeout, panicking if the timeout is reached or the
335    /// channel is closed.
336    async fn recv_with_timeout<H: Handler>(
337        rx: &mut mpsc::Receiver<BatchedRows<H>>,
338        timeout: Duration,
339    ) -> BatchedRows<H> {
340        match tokio::time::timeout(timeout, rx.recv()).await {
341            Ok(Some(batch)) => batch,
342            Ok(None) => panic!("Collector channel was closed unexpectedly"),
343            Err(_) => panic!("Test timed out waiting for batch from collector"),
344        }
345    }
346
347    #[tokio::test]
348    async fn test_collector_batches_data() {
349        let (processor_tx, processor_rx) = mpsc::channel(10);
350        let (collector_tx, mut collector_rx) = mpsc::channel(10);
351        let main_reader_lo = Arc::new(SetOnce::new_with(Some(AtomicU64::new(0))));
352
353        let handler = Arc::new(TestHandler);
354        let _collector = collector::<TestHandler>(
355            handler,
356            CommitterConfig::default(),
357            processor_rx,
358            collector_tx,
359            main_reader_lo.clone(),
360            test_metrics(),
361            TestHandler::MIN_EAGER_ROWS,
362            TestHandler::MAX_PENDING_ROWS,
363            TestHandler::MAX_WATERMARK_UPDATES,
364        );
365
366        let part1_length = TEST_MAX_CHUNK_ROWS / 2;
367        let part2_length = TEST_MAX_CHUNK_ROWS - part1_length - 1;
368
369        // Send test data
370        let test_data = vec![
371            IndexedCheckpoint::new(0, 1, 10, 1000, vec![Entry; part1_length]),
372            IndexedCheckpoint::new(0, 2, 20, 2000, vec![Entry; part2_length]),
373            IndexedCheckpoint::new(0, 3, 30, 3000, vec![Entry, Entry]),
374        ];
375
376        for data in test_data {
377            processor_tx.send(data).await.unwrap();
378        }
379
380        let batch1 = recv_with_timeout(&mut collector_rx, Duration::from_secs(1)).await;
381        assert_eq!(batch1.batch_len, TEST_MAX_CHUNK_ROWS);
382
383        let batch2 = recv_with_timeout(&mut collector_rx, Duration::from_secs(1)).await;
384        assert_eq!(batch2.batch_len, 1);
385    }
386
387    #[tokio::test]
388    async fn test_collector_shutdown() {
389        let (processor_tx, processor_rx) = mpsc::channel(10);
390        let (collector_tx, mut collector_rx) = mpsc::channel(10);
391        let main_reader_lo = Arc::new(SetOnce::new_with(Some(AtomicU64::new(0))));
392
393        let handler = Arc::new(TestHandler);
394        let mut collector = collector::<TestHandler>(
395            handler,
396            CommitterConfig::default(),
397            processor_rx,
398            collector_tx,
399            main_reader_lo,
400            test_metrics(),
401            TestHandler::MIN_EAGER_ROWS,
402            TestHandler::MAX_PENDING_ROWS,
403            TestHandler::MAX_WATERMARK_UPDATES,
404        );
405
406        processor_tx
407            .send(IndexedCheckpoint::new(0, 1, 10, 1000, vec![Entry, Entry]))
408            .await
409            .unwrap();
410
411        tokio::time::sleep(Duration::from_millis(200)).await;
412
413        let batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(1)).await;
414        assert_eq!(batch.batch_len, 2);
415
416        // Drop processor sender to simulate shutdown
417        drop(processor_tx);
418
419        // After a short delay, collector should shut down
420        tokio::time::timeout(Duration::from_millis(500), collector.join())
421            .await
422            .expect("collector shutdown timeout")
423            .expect("collector shutdown failed");
424    }
425
426    #[tokio::test]
427    async fn test_collector_respects_max_pending() {
428        let processor_channel_size = 5; // unit is checkpoint
429        let collector_channel_size = 2; // unit is batch, aka rows / MAX_CHUNK_ROWS
430        let (processor_tx, processor_rx) = mpsc::channel(processor_channel_size);
431        let (collector_tx, _collector_rx) = mpsc::channel(collector_channel_size);
432        let main_reader_lo = Arc::new(SetOnce::new_with(Some(AtomicU64::new(0))));
433
434        let metrics = test_metrics();
435
436        let handler = Arc::new(TestHandler);
437        let _collector = collector::<TestHandler>(
438            handler,
439            CommitterConfig::default(),
440            processor_rx,
441            collector_tx,
442            main_reader_lo.clone(),
443            metrics.clone(),
444            TestHandler::MIN_EAGER_ROWS,
445            TestHandler::MAX_PENDING_ROWS,
446            TestHandler::MAX_WATERMARK_UPDATES,
447        );
448
449        // Send more data than MAX_PENDING_ROWS plus collector channel buffer
450        let data = IndexedCheckpoint::new(
451            0,
452            1,
453            10,
454            1000,
455            vec![
456                Entry;
457                // Decreasing this number by even 1 would make the test fail.
458                TestHandler::MAX_PENDING_ROWS
459                    + TEST_MAX_CHUNK_ROWS * collector_channel_size
460            ],
461        );
462        processor_tx.send(data).await.unwrap();
463
464        tokio::time::sleep(Duration::from_millis(200)).await;
465
466        // Now fill up the processor channel with minimum data to trigger send blocking
467        for _ in 0..processor_channel_size {
468            let more_data = IndexedCheckpoint::new(0, 2, 11, 1000, vec![Entry]);
469            processor_tx.send(more_data).await.unwrap();
470        }
471
472        // Now sending even more data should block because of MAX_PENDING_ROWS limit.
473        let even_more_data = IndexedCheckpoint::new(0, 3, 12, 1000, vec![Entry]);
474
475        let send_result = processor_tx.try_send(even_more_data);
476        assert!(matches!(
477            send_result,
478            Err(mpsc::error::TrySendError::Full(_))
479        ));
480    }
481
482    #[tokio::test]
483    async fn test_collector_accumulates_across_checkpoints_until_eager_threshold() {
484        let (processor_tx, processor_rx) = mpsc::channel(10);
485        let (collector_tx, mut collector_rx) = mpsc::channel(10);
486        let main_reader_lo = Arc::new(SetOnce::new_with(Some(AtomicU64::new(0))));
487
488        // Set a very long collect interval (60 seconds) to ensure timing doesn't trigger batching
489        let config = CommitterConfig {
490            collect_interval_ms: 60_000,
491            ..CommitterConfig::default()
492        };
493        let handler = Arc::new(TestHandler);
494        let _collector = collector::<TestHandler>(
495            handler,
496            config,
497            processor_rx,
498            collector_tx,
499            main_reader_lo.clone(),
500            test_metrics(),
501            TestHandler::MIN_EAGER_ROWS,
502            TestHandler::MAX_PENDING_ROWS,
503            TestHandler::MAX_WATERMARK_UPDATES,
504        );
505
506        let start_time = std::time::Instant::now();
507
508        // The collector starts with an immediate poll tick, creating an empty batch
509        let initial_batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(1)).await;
510        assert_eq!(initial_batch.batch_len, 0);
511
512        // Send data that's just below MIN_EAGER_ROWS threshold.
513        let below_threshold =
514            IndexedCheckpoint::new(0, 1, 10, 1000, vec![Entry; TestHandler::MIN_EAGER_ROWS - 1]);
515        processor_tx.send(below_threshold).await.unwrap();
516
517        // Try to receive with timeout - should timeout since we're below threshold
518        expect_timeout(&mut collector_rx, Duration::from_secs(1)).await;
519
520        // Now send one more entry to cross the MIN_EAGER_ROWS threshold
521        let threshold_trigger = IndexedCheckpoint::new(
522            0,
523            2,
524            20,
525            2000,
526            vec![Entry; 1], // Just 1 more entry to reach 10 total
527        );
528        processor_tx.send(threshold_trigger).await.unwrap();
529
530        // Should immediately get a batch without waiting for the long interval
531        let eager_batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(1)).await;
532        assert_eq!(eager_batch.batch_len, TestHandler::MIN_EAGER_ROWS);
533
534        // Verify batch was created quickly (much less than 60 seconds)
535        let elapsed = start_time.elapsed();
536        assert!(elapsed < Duration::from_secs(10));
537    }
538
539    #[tokio::test]
540    async fn test_immediate_batch_on_min_eager_rows() {
541        let (processor_tx, processor_rx) = mpsc::channel(10);
542        let (collector_tx, mut collector_rx) = mpsc::channel(10);
543        let main_reader_lo = Arc::new(SetOnce::new_with(Some(AtomicU64::new(0))));
544
545        // Set a very long collect interval (60 seconds) to ensure timing doesn't trigger batching
546        let config = CommitterConfig {
547            collect_interval_ms: 60_000,
548            ..CommitterConfig::default()
549        };
550        let handler = Arc::new(TestHandler);
551        let _collector = collector::<TestHandler>(
552            handler,
553            config,
554            processor_rx,
555            collector_tx,
556            main_reader_lo.clone(),
557            test_metrics(),
558            TestHandler::MIN_EAGER_ROWS,
559            TestHandler::MAX_PENDING_ROWS,
560            TestHandler::MAX_WATERMARK_UPDATES,
561        );
562
563        // The collector starts with an immediate poll tick, creating an empty batch
564        let initial_batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(1)).await;
565        assert_eq!(initial_batch.batch_len, 0);
566        // The collector will then just wait for the next poll as there is no new data yet.
567        expect_timeout(&mut collector_rx, Duration::from_secs(1)).await;
568
569        let start_time = std::time::Instant::now();
570
571        // Send exactly MIN_EAGER_ROWS in one checkpoint
572        let exact_threshold =
573            IndexedCheckpoint::new(0, 1, 10, 1000, vec![Entry; TestHandler::MIN_EAGER_ROWS]);
574        processor_tx.send(exact_threshold).await.unwrap();
575
576        // Should trigger immediately since pending_rows >= MIN_EAGER_ROWS.
577        let batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(1)).await;
578        assert_eq!(batch.batch_len, TestHandler::MIN_EAGER_ROWS);
579
580        // Verify batch was created quickly (much less than 60 seconds)
581        let elapsed = start_time.elapsed();
582        assert!(elapsed < Duration::from_secs(10));
583    }
584
585    #[tokio::test]
586    async fn test_collector_waits_for_timer_when_below_eager_threshold() {
587        let (processor_tx, processor_rx) = mpsc::channel(10);
588        let (collector_tx, mut collector_rx) = mpsc::channel(10);
589        let main_reader_lo = Arc::new(SetOnce::new_with(Some(AtomicU64::new(0))));
590
591        // Set a reasonable collect interval for this test (3 seconds).
592        let config = CommitterConfig {
593            collect_interval_ms: 3000,
594            ..CommitterConfig::default()
595        };
596        let handler = Arc::new(TestHandler);
597        let _collector = collector::<TestHandler>(
598            handler,
599            config,
600            processor_rx,
601            collector_tx,
602            main_reader_lo.clone(),
603            test_metrics(),
604            TestHandler::MIN_EAGER_ROWS,
605            TestHandler::MAX_PENDING_ROWS,
606            TestHandler::MAX_WATERMARK_UPDATES,
607        );
608
609        // Consume initial empty batch
610        let initial_batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(1)).await;
611        assert_eq!(initial_batch.batch_len, 0);
612
613        // Send MIN_EAGER_ROWS - 1 entries (below threshold)
614        let below_threshold =
615            IndexedCheckpoint::new(0, 1, 10, 1000, vec![Entry; TestHandler::MIN_EAGER_ROWS - 1]);
616        processor_tx.send(below_threshold).await.unwrap();
617
618        // Try to receive with timeout - should timeout since we're below threshold
619        expect_timeout(&mut collector_rx, Duration::from_secs(1)).await;
620
621        // Should eventually get batch when timer triggers
622        let timer_batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(4)).await;
623        assert_eq!(timer_batch.batch_len, TestHandler::MIN_EAGER_ROWS - 1);
624    }
625
626    /// The collector must wait for `main_reader_lo` to be initialized before attempting to prepare
627    /// checkpoints for commit.
628    #[tokio::test(start_paused = true)]
629    async fn test_collector_waits_for_main_reader_lo_init() {
630        let (processor_tx, processor_rx) = mpsc::channel(10);
631        let (collector_tx, mut collector_rx) = mpsc::channel(10);
632        let main_reader_lo = Arc::new(SetOnce::new());
633
634        let handler = Arc::new(TestHandler);
635        let collector = collector(
636            handler,
637            CommitterConfig {
638                // Collect interval longer than time to advance to ensure timing doesn't trigger
639                // batching.
640                collect_interval_ms: 200_000,
641                ..CommitterConfig::default()
642            },
643            processor_rx,
644            collector_tx,
645            main_reader_lo.clone(),
646            test_metrics(),
647            TestHandler::MIN_EAGER_ROWS,
648            TestHandler::MAX_PENDING_ROWS,
649            TestHandler::MAX_WATERMARK_UPDATES,
650        );
651
652        // Send enough data to trigger batching.
653        let test_data =
654            IndexedCheckpoint::new(0, 1, 10, 1000, vec![Entry; TestHandler::MIN_EAGER_ROWS + 1]);
655        processor_tx.send(test_data).await.unwrap();
656
657        // Advance time significantly - collector should still be blocked waiting for
658        // main_reader_lo.
659        tokio::time::advance(Duration::from_secs(100)).await;
660
661        assert!(collector_rx.try_recv().is_err());
662
663        // Now initialize the main reader lo to 0, unblocking the collector.
664        main_reader_lo.set(AtomicU64::new(0)).ok();
665
666        tokio::time::advance(Duration::from_secs(1)).await;
667
668        let batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(2)).await;
669
670        assert_eq!(batch.batch_len, TestHandler::MIN_EAGER_ROWS + 1);
671
672        collector.shutdown().await.unwrap();
673    }
674
675    /// When receiving checkpoints, if they are below the main reader lo, they should be dropped
676    /// immediately.
677    #[tokio::test]
678    async fn test_collector_drops_checkpoints_immediately_if_le_main_reader_lo() {
679        let (processor_tx, processor_rx) = mpsc::channel(10);
680        let (collector_tx, mut collector_rx) = mpsc::channel(10);
681        let main_reader_lo = Arc::new(SetOnce::new_with(Some(AtomicU64::new(5))));
682        let metrics = test_metrics();
683
684        let collector = collector(
685            Arc::new(TestHandler),
686            CommitterConfig {
687                // Collect interval longer than time to advance to ensure timing doesn't trigger
688                // batching.
689                collect_interval_ms: 200_000,
690                ..CommitterConfig::default()
691            },
692            processor_rx,
693            collector_tx,
694            main_reader_lo.clone(),
695            metrics.clone(),
696            TestHandler::MIN_EAGER_ROWS,
697            TestHandler::MAX_PENDING_ROWS,
698            TestHandler::MAX_WATERMARK_UPDATES,
699        );
700
701        let eager_rows_plus_one = TestHandler::MIN_EAGER_ROWS + 1;
702
703        let test_data: Vec<_> = [1, 5, 2, 6, 4, 3]
704            .into_iter()
705            .map(|cp| IndexedCheckpoint::new(0, cp, 10, 1000, vec![Entry; eager_rows_plus_one]))
706            .collect();
707        for data in test_data {
708            processor_tx.send(data).await.unwrap();
709        }
710        let batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(2)).await;
711
712        // Make sure that we are advancing watermarks.
713        assert_eq!(batch.watermark.len(), 6);
714        // And reporting the checkpoints as received.
715        assert_eq!(
716            metrics
717                .total_collector_checkpoints_received
718                .with_label_values(&[TestHandler::NAME])
719                .get(),
720            6
721        );
722        // But the collector should filter out four checkpoints: (1, 2, 3, 4)
723        assert_eq!(
724            metrics
725                .total_collector_skipped_checkpoints
726                .with_label_values(&[TestHandler::NAME])
727                .get(),
728            4
729        );
730        // And that we only have values from two checkpoints (5, 6)
731        assert_eq!(batch.batch_len, eager_rows_plus_one * 2);
732
733        collector.shutdown().await.unwrap();
734    }
735
736    /// Because a checkpoint may be partially batched before the main reader lo advances past it,
737    /// the collector must ensure that it fully writes out the checkpoint. Otherwise, this will
738    /// essentially stall the commit_watermark task indefinitely as the latter waits for the
739    /// remaining checkpoint parts.
740    #[tokio::test(start_paused = true)]
741    async fn test_collector_only_filters_whole_checkpoints() {
742        let (processor_tx, processor_rx) = mpsc::channel(10);
743        let (collector_tx, mut collector_rx) = mpsc::channel(10);
744        let main_reader_lo = Arc::new(SetOnce::new_with(Some(AtomicU64::new(0))));
745
746        let metrics = test_metrics();
747
748        let collector = collector(
749            Arc::new(TestHandler),
750            CommitterConfig::default(),
751            processor_rx,
752            collector_tx,
753            main_reader_lo.clone(),
754            metrics.clone(),
755            TestHandler::MIN_EAGER_ROWS,
756            TestHandler::MAX_PENDING_ROWS,
757            TestHandler::MAX_WATERMARK_UPDATES,
758        );
759
760        let more_than_max_chunk_rows = TEST_MAX_CHUNK_ROWS + 10;
761
762        let test_data =
763            IndexedCheckpoint::new(0, 1, 10, 1000, vec![Entry; more_than_max_chunk_rows]);
764        processor_tx.send(test_data).await.unwrap();
765        tokio::time::advance(Duration::from_secs(1)).await;
766        let batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(2)).await;
767
768        // There are still 10 rows left to be sent in the next batch.
769        assert_eq!(batch.batch_len, TEST_MAX_CHUNK_ROWS);
770
771        // Send indexed checkpoints 2 through 5 inclusive, but also bump the main reader lo to 4.
772        let test_data: Vec<_> = (2..=5)
773            .map(|cp| {
774                IndexedCheckpoint::new(
775                    0,
776                    cp,
777                    10,
778                    1000,
779                    vec![Entry; TestHandler::MIN_EAGER_ROWS + 1],
780                )
781            })
782            .collect();
783        for data in test_data {
784            processor_tx.send(data).await.unwrap();
785        }
786        let atomic = main_reader_lo.get().unwrap();
787        atomic.store(4, Ordering::Relaxed);
788        tokio::time::advance(Duration::from_secs(10)).await;
789
790        let batch = recv_with_timeout(&mut collector_rx, Duration::from_secs(2)).await;
791
792        // The next batch should still be the remaining 10 rows from checkpoint 1.
793        assert_eq!(batch.batch_len, 10);
794        assert_eq!(batch.watermark[0].watermark.checkpoint_hi_inclusive, 1);
795
796        recv_with_timeout(&mut collector_rx, Duration::from_secs(2)).await;
797
798        assert_eq!(
799            metrics
800                .total_collector_skipped_checkpoints
801                .with_label_values(&[TestHandler::NAME])
802                .get(),
803            2
804        );
805        assert_eq!(
806            metrics
807                .total_collector_checkpoints_received
808                .with_label_values(&[TestHandler::NAME])
809                .get(),
810            5
811        );
812
813        collector.shutdown().await.unwrap();
814    }
815}