Skip to main content

sui_indexer_alt_framework/pipeline/sequential/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use serde::Deserialize;
8use serde::Serialize;
9use sui_futures::service::Service;
10use tokio::sync::mpsc;
11use tracing::info;
12
13use crate::config::ConcurrencyConfig;
14use crate::ingestion::ingestion_client::CheckpointEnvelope;
15use crate::metrics::IndexerMetrics;
16use crate::pipeline::CommitterConfig;
17use crate::pipeline::IngestionConfig;
18use crate::pipeline::Processor;
19use crate::pipeline::processor::processor;
20use crate::pipeline::sequential::collector::BatchedRows;
21use crate::pipeline::sequential::collector::collector;
22use crate::pipeline::sequential::committer::committer;
23use crate::store::SequentialStore;
24use crate::store::Store;
25
26mod collector;
27mod committer;
28
29/// Handlers implement the logic for a given indexing pipeline: How to process checkpoint data (by
30/// implementing [Processor]) into rows for their table, how to combine multiple rows into a single
31/// DB operation, and then how to write those rows atomically to the database.
32///
33/// The handler is also responsible for tuning the various parameters of the pipeline (provided as
34/// associated values).
35///
36/// Sequential handlers can only be used in sequential pipelines, where checkpoint data is
37/// processed out-of-order, but then gathered and written in order. If multiple checkpoints are
38/// available, the pipeline will attempt to combine their writes taking advantage of batching to
39/// avoid emitting redundant writes.
40///
41/// Back-pressure is handled by the bounded subscriber channel from the ingestion service, the
42/// same as concurrent pipelines: the channel blocks broadcaster sends when full, and the adaptive
43/// ingestion controller cuts fetch concurrency as the channel fills up.
44#[async_trait]
45pub trait Handler: Processor {
46    type Store: SequentialStore;
47
48    /// Once this many rows are pending and the next checkpoint to write has arrived, the
49    /// collector stops receiving processed checkpoints until it cuts a batch, which applies
50    /// back-pressure to the processor and ingestion. While the next checkpoint is missing, the
51    /// collector keeps receiving past this limit, because the missing checkpoint may still be in
52    /// its input channel. Under back-pressure, a batch therefore gathers about this many rows,
53    /// which is fewer than `MAX_BATCH_CHECKPOINTS` checkpoints when those hold more rows.
54    const MAX_PENDING_ROWS: usize = 5000;
55
56    /// Maximum number of checkpoints to try and write in a single batch. The larger this number
57    /// is, the more chances the pipeline has to merge redundant writes, but the longer each write
58    /// transaction is likely to be.
59    const MAX_BATCH_CHECKPOINTS: usize = 5 * 60;
60
61    /// A type to combine multiple `Self::Value`-s into. This can be used to avoid redundant writes
62    /// by combining multiple rows into one (e.g. if one row supersedes another, the latter can be
63    /// omitted).
64    type Batch: Default + Send + Sync + 'static;
65
66    /// Add `values` from processing a checkpoint to the current `batch`. Checkpoints are
67    /// guaranteed to be presented to the batch in checkpoint order. The handler takes ownership
68    /// of the iterator and consumes all values.
69    ///
70    /// Returns `BatchStatus::Ready` if the batch is full and should be committed,
71    /// or `BatchStatus::Pending` if the batch can accept more values.
72    ///
73    /// Note: The handler can signal batch readiness via `BatchStatus::Ready`, but the framework
74    /// may also decide to commit a batch based on the trait parameters above.
75    fn batch(&self, batch: &mut Self::Batch, values: std::vec::IntoIter<Self::Value>);
76
77    /// Take a batch of values and commit them to the database, returning the number of rows
78    /// affected.
79    async fn commit<'a>(
80        &self,
81        batch: &Self::Batch,
82        conn: &mut <Self::Store as Store>::Connection<'a>,
83    ) -> anyhow::Result<usize>;
84}
85
86/// Configuration for a sequential pipeline
87#[derive(Serialize, Deserialize, Debug, Clone, Default)]
88pub struct SequentialConfig {
89    /// Configuration for the writer, that makes forward progress.
90    pub committer: CommitterConfig,
91
92    /// Per-pipeline ingestion overrides.
93    pub ingestion: IngestionConfig,
94
95    /// Processor concurrency. Defaults to adaptive scaling up to the number of CPUs.
96    pub fanout: Option<ConcurrencyConfig>,
97
98    /// Override for `Handler::MAX_PENDING_ROWS` (reorder buffer limit).
99    pub max_pending_rows: Option<usize>,
100
101    /// Override for `Handler::MAX_BATCH_CHECKPOINTS` (checkpoints per write batch).
102    pub max_batch_checkpoints: Option<usize>,
103
104    /// Size of the channel between the processor and committer.
105    pub processor_channel_size: Option<usize>,
106
107    /// Depth of the channel between the collector and committer tasks. Allows the collector
108    /// to build the next batch while the previous batch is being flushed to the DB.
109    pub pipeline_depth: Option<usize>,
110}
111
112/// Start a new sequential (in-order) indexing pipeline, served by the handler, `H`. Starting
113/// strictly after the `watermark` (or from the beginning if no watermark was provided).
114///
115/// Each pipeline consists of a processor which takes checkpoint data and breaks it down into rows,
116/// ready for insertion, and a committer which orders the rows and combines them into batches to
117/// write to the database.
118///
119/// Commits are performed in checkpoint order, potentially involving multiple checkpoints at a
120/// time. The call to [Handler::commit] and the associated watermark update are performed in a
121/// transaction to ensure atomicity. Unlike in the case of concurrent pipelines, the data passed to
122/// [Handler::commit] is not chunked up, so the handler must perform this step itself, if
123/// necessary.
124///
125/// Checkpoint data is fed into the pipeline through the `checkpoint_rx` channel, and internal
126/// channels are created to communicate between its various components. The pipeline will shutdown
127/// if any of its input or output channels close, any of its independent tasks fail, or if it is
128/// signalled to shutdown through the returned service handle.
129pub(crate) fn pipeline<H: Handler>(
130    handler: H,
131    next_checkpoint: u64,
132    config: SequentialConfig,
133    store: H::Store,
134    checkpoint_rx: mpsc::Receiver<Arc<CheckpointEnvelope>>,
135    metrics: Arc<IndexerMetrics>,
136) -> Service {
137    info!(
138        pipeline = H::NAME,
139        "Starting pipeline with config: {config:#?}",
140    );
141
142    let concurrency = config
143        .fanout
144        .clone()
145        .unwrap_or(ConcurrencyConfig::Adaptive {
146            initial: 1,
147            min: 1,
148            max: num_cpus::get().max(1),
149            dead_band: None,
150        });
151    let max_pending_rows = config.max_pending_rows.unwrap_or(H::MAX_PENDING_ROWS);
152    let max_batch_checkpoints = config
153        .max_batch_checkpoints
154        .unwrap_or(H::MAX_BATCH_CHECKPOINTS)
155        .max(1);
156
157    let processor_channel_size = config
158        .processor_channel_size
159        .unwrap_or_else(|| num_cpus::get() / 2)
160        .max(1);
161    let (processor_tx, collector_rx) = mpsc::channel(processor_channel_size);
162
163    let pipeline_depth = config
164        .pipeline_depth
165        .unwrap_or_else(|| num_cpus::get() / 2)
166        .max(4);
167    let (collector_tx, committer_rx) = mpsc::channel::<BatchedRows<H>>(pipeline_depth);
168
169    let handler = Arc::new(handler);
170
171    let s_processor = processor(
172        handler.clone(),
173        checkpoint_rx,
174        processor_tx,
175        metrics.clone(),
176        concurrency,
177        store.clone(),
178    );
179
180    let s_collector = collector::<H>(
181        handler.clone(),
182        config,
183        next_checkpoint,
184        collector_rx,
185        metrics.clone(),
186        max_pending_rows,
187        max_batch_checkpoints,
188        collector_tx,
189    );
190
191    let s_committer = committer::<H>(handler, store, metrics.clone(), committer_rx);
192
193    s_processor.merge(s_collector).merge(s_committer)
194}
195
196#[cfg(test)]
197mod tests {
198    use std::time::Duration;
199
200    use prometheus::Registry;
201    use sui_types::full_checkpoint_content::Checkpoint;
202
203    use crate::mocks::store::FallibleMockConnection;
204    use crate::mocks::store::FallibleMockStore;
205    use crate::pipeline::IndexedCheckpoint;
206
207    use super::*;
208
209    // Test implementation of Handler
210    #[derive(Default)]
211    struct TestHandler;
212
213    #[async_trait]
214    impl Processor for TestHandler {
215        const NAME: &'static str = "test";
216        type Value = u64;
217
218        async fn process(&self, _checkpoint: &Arc<Checkpoint>) -> anyhow::Result<Vec<Self::Value>> {
219            Ok(vec![])
220        }
221    }
222
223    #[async_trait]
224    impl Handler for TestHandler {
225        type Store = FallibleMockStore;
226        type Batch = Vec<u64>;
227        const MAX_BATCH_CHECKPOINTS: usize = 3; // Using small max value for testing.
228
229        fn batch(&self, batch: &mut Self::Batch, values: std::vec::IntoIter<Self::Value>) {
230            batch.extend(values);
231        }
232
233        async fn commit<'a>(
234            &self,
235            batch: &Self::Batch,
236            conn: &mut FallibleMockConnection<'a>,
237        ) -> anyhow::Result<usize> {
238            if !batch.is_empty() {
239                let mut sequential_data = conn.0.sequential_checkpoint_data.lock().unwrap();
240                sequential_data.extend(batch.iter().cloned());
241            }
242            Ok(batch.len())
243        }
244    }
245
246    struct TestSetup {
247        store: FallibleMockStore,
248        checkpoint_tx: mpsc::Sender<IndexedCheckpoint<TestHandler>>,
249        #[allow(unused)]
250        service: Service,
251    }
252
253    /// Emulates adding a sequential pipeline to the indexer. Bypasses the processor stage and
254    /// feeds [IndexedCheckpoint]s directly to the collector. `next_checkpoint` is the starting
255    /// checkpoint for the indexer.
256    fn setup_test(
257        next_checkpoint: u64,
258        config: SequentialConfig,
259        store: FallibleMockStore,
260    ) -> TestSetup {
261        let metrics = IndexerMetrics::new(None, &Registry::default());
262
263        let max_pending_rows = config
264            .max_pending_rows
265            .unwrap_or(TestHandler::MAX_PENDING_ROWS);
266        let max_batch_checkpoints = config
267            .max_batch_checkpoints
268            .unwrap_or(TestHandler::MAX_BATCH_CHECKPOINTS);
269        let pipeline_depth = config
270            .pipeline_depth
271            .unwrap_or_else(|| num_cpus::get() / 2)
272            .max(4);
273
274        let (checkpoint_tx, checkpoint_rx) = mpsc::channel(10);
275        let (collector_tx, committer_rx) =
276            mpsc::channel::<BatchedRows<TestHandler>>(pipeline_depth);
277
278        let store_clone = store.clone();
279        let handler = Arc::new(TestHandler);
280
281        let s_collector = collector(
282            handler.clone(),
283            config,
284            next_checkpoint,
285            checkpoint_rx,
286            metrics.clone(),
287            max_pending_rows,
288            max_batch_checkpoints,
289            collector_tx,
290        );
291        let s_committer = committer(handler, store_clone, metrics, committer_rx);
292
293        TestSetup {
294            store,
295            checkpoint_tx,
296            service: s_collector.merge(s_committer),
297        }
298    }
299
300    async fn send_checkpoint(setup: &mut TestSetup, checkpoint: u64) {
301        setup
302            .checkpoint_tx
303            .send(create_checkpoint(checkpoint))
304            .await
305            .unwrap();
306    }
307
308    fn create_checkpoint(checkpoint: u64) -> IndexedCheckpoint<TestHandler> {
309        IndexedCheckpoint::new(
310            checkpoint,        // epoch
311            checkpoint,        // checkpoint number
312            checkpoint,        // tx_hi
313            checkpoint * 1000, // timestamp
314            vec![checkpoint],  // values
315        )
316    }
317
318    /// Starts only the collector, handing batches to a channel with room for `channel_size` of
319    /// them, so tests can inspect each batch.
320    fn spawn_collector(
321        config: SequentialConfig,
322        channel_size: usize,
323    ) -> (
324        mpsc::Sender<IndexedCheckpoint<TestHandler>>,
325        mpsc::Receiver<BatchedRows<TestHandler>>,
326        Service,
327    ) {
328        let metrics = IndexerMetrics::new(None, &Registry::default());
329        let (checkpoint_tx, checkpoint_rx) = mpsc::channel(10);
330        let (collector_tx, collector_rx) = mpsc::channel(channel_size);
331        let max_pending_rows = config
332            .max_pending_rows
333            .unwrap_or(TestHandler::MAX_PENDING_ROWS);
334
335        let service = collector(
336            Arc::new(TestHandler),
337            config,
338            0,
339            checkpoint_rx,
340            metrics,
341            max_pending_rows,
342            TestHandler::MAX_BATCH_CHECKPOINTS,
343            collector_tx,
344        );
345
346        (checkpoint_tx, collector_rx, service)
347    }
348
349    /// Configuration that holds rows until `min_batch_rows` are pending, or for at most
350    /// `max_batch_delay_ms`.
351    fn min_batch_config(min_batch_rows: usize, max_batch_delay_ms: u64) -> SequentialConfig {
352        SequentialConfig {
353            committer: CommitterConfig {
354                min_batch_rows,
355                max_batch_delay_ms,
356                ..Default::default()
357            },
358            ..Default::default()
359        }
360    }
361
362    async fn recv_batch(
363        rx: &mut mpsc::Receiver<BatchedRows<TestHandler>>,
364    ) -> BatchedRows<TestHandler> {
365        tokio::time::timeout(Duration::from_secs(1), rx.recv())
366            .await
367            .expect("timed out waiting for a batch")
368            .expect("collector closed its channel")
369    }
370
371    /// Receives batches until one ends at checkpoint `last`, returning their values in order.
372    async fn recv_until(rx: &mut mpsc::Receiver<BatchedRows<TestHandler>>, last: u64) -> Vec<u64> {
373        let mut written = vec![];
374        while written.last() != Some(&last) {
375            written.extend(recv_batch(rx).await.batch);
376        }
377        written
378    }
379
380    async fn expect_no_batch(rx: &mut mpsc::Receiver<BatchedRows<TestHandler>>) {
381        let result = tokio::time::timeout(Duration::from_secs(1), rx.recv()).await;
382        assert!(result.is_err(), "expected no batch");
383    }
384
385    #[tokio::test]
386    async fn test_committer_processes_sequential_checkpoints() {
387        let config = SequentialConfig::default();
388        let mut setup = setup_test(0, config, FallibleMockStore::default());
389
390        // Send checkpoints in order
391        for i in 0..3 {
392            send_checkpoint(&mut setup, i).await;
393        }
394
395        // Wait for processing
396        tokio::time::sleep(Duration::from_millis(200)).await;
397
398        // Verify data was written in order
399        assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2]);
400
401        // Verify watermark was updated
402        let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
403        assert_eq!(watermark.checkpoint_hi_inclusive, Some(2));
404        assert_eq!(watermark.tx_hi, 2);
405    }
406
407    /// Configure `FallibleMockStore` with no watermark, and emulate `first_checkpoint` by passing
408    /// the `initial_watermark` into the setup.
409    #[tokio::test]
410    async fn test_committer_processes_sequential_checkpoints_with_initial_watermark() {
411        let config = SequentialConfig::default();
412        let mut setup = setup_test(5, config, FallibleMockStore::default());
413
414        // Verify watermark hasn't progressed
415        let watermark = setup.store.watermark(TestHandler::NAME);
416        assert!(watermark.is_none());
417
418        // Send checkpoints in order
419        for i in 0..5 {
420            send_checkpoint(&mut setup, i).await;
421        }
422
423        // Wait for processing
424        tokio::time::sleep(Duration::from_millis(1000)).await;
425
426        // Verify watermark hasn't progressed
427        let watermark = setup.store.watermark(TestHandler::NAME);
428        assert!(watermark.is_none());
429
430        for i in 5..8 {
431            send_checkpoint(&mut setup, i).await;
432        }
433
434        // Wait for processing
435        tokio::time::sleep(Duration::from_millis(1000)).await;
436
437        // Verify data was written in order
438        assert_eq!(setup.store.get_sequential_data(), vec![5, 6, 7]);
439
440        // Verify watermark was updated
441        let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
442        assert_eq!(watermark.checkpoint_hi_inclusive, Some(7));
443        assert_eq!(watermark.tx_hi, 7);
444    }
445
446    #[tokio::test]
447    async fn test_committer_processes_out_of_order_checkpoints() {
448        let config = SequentialConfig::default();
449        let mut setup = setup_test(0, config, FallibleMockStore::default());
450
451        // Send checkpoints out of order
452        for i in [1, 0, 2] {
453            send_checkpoint(&mut setup, i).await;
454        }
455
456        // Wait for processing
457        tokio::time::sleep(Duration::from_millis(200)).await;
458
459        // Verify data was written in order despite receiving out of order
460        assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2]);
461
462        // Verify watermark was updated
463        let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
464        assert_eq!(watermark.checkpoint_hi_inclusive, Some(2));
465        assert_eq!(watermark.tx_hi, 2);
466    }
467
468    #[tokio::test]
469    async fn test_committer_commit_up_to_max_batch_checkpoints() {
470        let config = SequentialConfig::default();
471        let mut setup = setup_test(0, config, FallibleMockStore::default());
472
473        // Send checkpoints up to MAX_BATCH_CHECKPOINTS
474        for i in 0..4 {
475            send_checkpoint(&mut setup, i).await;
476        }
477
478        // Wait for processing
479        tokio::time::sleep(Duration::from_millis(200)).await;
480
481        // Verify data is written in order across batches
482        assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2, 3]);
483    }
484
485    /// The next checkpoint is cut into a batch as soon as the committer has room, and checkpoints
486    /// that arrive while it has none are gathered into the next batch together.
487    #[tokio::test(start_paused = true)]
488    async fn collector_writes_as_soon_as_the_committer_has_room() {
489        let (checkpoint_tx, mut batch_rx, _collector) =
490            spawn_collector(SequentialConfig::default(), 1);
491
492        checkpoint_tx.send(create_checkpoint(0)).await.unwrap();
493        tokio::time::sleep(Duration::from_millis(10)).await;
494
495        // The first batch now fills the committer's channel, so these have to wait.
496        for i in 1..=3 {
497            checkpoint_tx.send(create_checkpoint(i)).await.unwrap();
498        }
499        tokio::time::sleep(Duration::from_millis(10)).await;
500
501        let batch = recv_batch(&mut batch_rx).await;
502        assert_eq!(batch.batch, vec![0]);
503
504        let batch = recv_batch(&mut batch_rx).await;
505        assert_eq!(batch.batch, vec![1, 2, 3]);
506        assert_eq!(batch.watermark.checkpoint_hi_inclusive, 3);
507    }
508
509    /// Checkpoints that arrive ahead of their predecessor are held until it arrives.
510    #[tokio::test(start_paused = true)]
511    async fn collector_waits_for_missing_predecessor() {
512        let (checkpoint_tx, mut batch_rx, _collector) =
513            spawn_collector(SequentialConfig::default(), 10);
514
515        for i in [1, 2] {
516            checkpoint_tx.send(create_checkpoint(i)).await.unwrap();
517        }
518        expect_no_batch(&mut batch_rx).await;
519
520        checkpoint_tx.send(create_checkpoint(0)).await.unwrap();
521
522        let batch = recv_batch(&mut batch_rx).await;
523        assert_eq!(batch.batch, vec![0, 1, 2]);
524    }
525
526    /// Once `max_pending_rows` are pending and the next checkpoint is ready, the collector stops
527    /// receiving, so its input channel fills up, until the committer takes a batch.
528    #[tokio::test(start_paused = true)]
529    async fn collector_stops_receiving_while_pending_rows_are_full() {
530        let config = SequentialConfig {
531            max_pending_rows: Some(3),
532            ..Default::default()
533        };
534        let (checkpoint_tx, mut batch_rx, _collector) = spawn_collector(config, 1);
535
536        // Checkpoint 0 fills the committer's channel.
537        checkpoint_tx.send(create_checkpoint(0)).await.unwrap();
538        tokio::time::sleep(Duration::from_millis(10)).await;
539
540        // Checkpoints 1 to 3 fill the collector's pending rows, and 4 to 13 its input channel.
541        for i in 1..=13 {
542            checkpoint_tx.send(create_checkpoint(i)).await.unwrap();
543        }
544
545        let blocked = tokio::time::timeout(Duration::from_secs(1), checkpoint_tx.reserve()).await;
546        assert!(blocked.is_err(), "collector kept receiving while full");
547
548        // Taking a batch lets the collector cut the next one and receive again.
549        assert_eq!(recv_batch(&mut batch_rx).await.batch, vec![0]);
550        let permit = tokio::time::timeout(Duration::from_secs(1), checkpoint_tx.reserve())
551            .await
552            .expect("collector did not resume receiving")
553            .unwrap();
554        permit.send(create_checkpoint(14));
555
556        let written = recv_until(&mut batch_rx, 14).await;
557        assert_eq!(written, (1..=14).collect::<Vec<_>>());
558    }
559
560    /// While the next checkpoint is missing, the collector keeps receiving past
561    /// `max_pending_rows`, because the missing checkpoint may be behind them in its input channel.
562    #[tokio::test(start_paused = true)]
563    async fn collector_receives_missing_predecessor_past_max_pending_rows() {
564        let config = SequentialConfig {
565            max_pending_rows: Some(2),
566            ..Default::default()
567        };
568        let (checkpoint_tx, mut batch_rx, _collector) = spawn_collector(config, 10);
569
570        // More checkpoints than the collector's pending rows and input channel hold together.
571        for i in 1..=15 {
572            let send = checkpoint_tx.send(create_checkpoint(i));
573            tokio::time::timeout(Duration::from_secs(1), send)
574                .await
575                .expect("collector stopped receiving while checkpoint 0 is missing")
576                .unwrap();
577        }
578        expect_no_batch(&mut batch_rx).await;
579
580        checkpoint_tx.send(create_checkpoint(0)).await.unwrap();
581
582        let written = recv_until(&mut batch_rx, 15).await;
583        assert_eq!(written, (0..=15).collect::<Vec<_>>());
584    }
585
586    /// A full buffer is cut into a batch below `min_batch_rows`, without waiting for
587    /// `max_batch_delay_ms`, because the collector receives nothing more while it is full.
588    #[tokio::test(start_paused = true)]
589    async fn collector_writes_below_min_batch_rows_while_pending_rows_are_full() {
590        let config = SequentialConfig {
591            max_pending_rows: Some(3),
592            ..min_batch_config(100, 60_000)
593        };
594        let (checkpoint_tx, mut batch_rx, _collector) = spawn_collector(config, 10);
595
596        for i in 0..3 {
597            checkpoint_tx.send(create_checkpoint(i)).await.unwrap();
598        }
599
600        let batch = recv_batch(&mut batch_rx).await;
601        assert_eq!(batch.batch, vec![0, 1, 2]);
602    }
603
604    /// A stale duplicate of a written checkpoint does not stop the collector from receiving while
605    /// the next checkpoint is missing.
606    #[tokio::test(start_paused = true)]
607    async fn collector_keeps_receiving_past_stale_checkpoints() {
608        let config = SequentialConfig {
609            max_pending_rows: Some(2),
610            ..Default::default()
611        };
612        let (checkpoint_tx, mut batch_rx, _collector) = spawn_collector(config, 1);
613
614        // Checkpoint 0 fills the committer's channel.
615        checkpoint_tx.send(create_checkpoint(0)).await.unwrap();
616        tokio::time::sleep(Duration::from_millis(10)).await;
617
618        // A duplicate of checkpoint 0 and checkpoint 2 fill the pending rows while 1 is missing,
619        // followed by more checkpoints than the input channel holds.
620        for i in [0, 2].into_iter().chain(3..=13) {
621            let send = checkpoint_tx.send(create_checkpoint(i));
622            tokio::time::timeout(Duration::from_secs(1), send)
623                .await
624                .expect("collector stopped receiving while checkpoint 1 is missing")
625                .unwrap();
626        }
627
628        checkpoint_tx.send(create_checkpoint(1)).await.unwrap();
629
630        let written = recv_until(&mut batch_rx, 13).await;
631        assert_eq!(written, (0..=13).collect::<Vec<_>>());
632    }
633
634    /// A checkpoint received twice before it is written counts its rows once.
635    #[tokio::test(start_paused = true)]
636    async fn collector_counts_duplicate_pending_checkpoint_once() {
637        let (checkpoint_tx, mut batch_rx, _collector) =
638            spawn_collector(min_batch_config(3, 60_000), 10);
639
640        for i in [1, 1, 0] {
641            checkpoint_tx.send(create_checkpoint(i)).await.unwrap();
642        }
643        expect_no_batch(&mut batch_rx).await;
644
645        checkpoint_tx.send(create_checkpoint(2)).await.unwrap();
646
647        let batch = recv_batch(&mut batch_rx).await;
648        assert_eq!(batch.batch, vec![0, 1, 2]);
649    }
650
651    /// Closing the input channel while the collector has stopped receiving still writes out every
652    /// checkpoint, in order, and stops the collector.
653    #[tokio::test(start_paused = true)]
654    async fn collector_writes_everything_when_closed_while_pending_rows_are_full() {
655        let config = SequentialConfig {
656            max_pending_rows: Some(3),
657            ..Default::default()
658        };
659        let (checkpoint_tx, mut batch_rx, mut collector) = spawn_collector(config, 1);
660
661        checkpoint_tx.send(create_checkpoint(0)).await.unwrap();
662        tokio::time::sleep(Duration::from_millis(10)).await;
663        for i in 1..=13 {
664            checkpoint_tx.send(create_checkpoint(i)).await.unwrap();
665        }
666        drop(checkpoint_tx);
667
668        let written = recv_until(&mut batch_rx, 13).await;
669        assert_eq!(written, (0..=13).collect::<Vec<_>>());
670
671        tokio::time::timeout(Duration::from_secs(1), collector.join())
672            .await
673            .expect("collector shutdown timeout")
674            .expect("collector shutdown failed");
675    }
676
677    #[tokio::test(start_paused = true)]
678    async fn collector_holds_checkpoints_until_min_batch_rows() {
679        let (checkpoint_tx, mut batch_rx, _collector) =
680            spawn_collector(min_batch_config(3, 60_000), 10);
681
682        for i in 0..2 {
683            checkpoint_tx.send(create_checkpoint(i)).await.unwrap();
684        }
685        expect_no_batch(&mut batch_rx).await;
686
687        checkpoint_tx.send(create_checkpoint(2)).await.unwrap();
688
689        let batch = recv_batch(&mut batch_rx).await;
690        assert_eq!(batch.batch, vec![0, 1, 2]);
691    }
692
693    #[tokio::test(start_paused = true)]
694    async fn collector_writes_below_min_batch_rows_after_max_batch_delay() {
695        let (checkpoint_tx, mut batch_rx, _collector) =
696            spawn_collector(min_batch_config(3, 3_000), 10);
697
698        for i in 0..2 {
699            checkpoint_tx.send(create_checkpoint(i)).await.unwrap();
700        }
701        expect_no_batch(&mut batch_rx).await;
702
703        tokio::time::sleep(Duration::from_secs(2)).await;
704        let batch = recv_batch(&mut batch_rx).await;
705        assert_eq!(batch.batch, vec![0, 1]);
706    }
707
708    #[tokio::test(start_paused = true)]
709    async fn collector_writes_below_min_batch_rows_on_shutdown() {
710        let (checkpoint_tx, mut batch_rx, mut collector) =
711            spawn_collector(min_batch_config(100, 60_000), 10);
712
713        for i in 0..2 {
714            checkpoint_tx.send(create_checkpoint(i)).await.unwrap();
715        }
716        drop(checkpoint_tx);
717
718        let batch = recv_batch(&mut batch_rx).await;
719        assert_eq!(batch.batch, vec![0, 1]);
720
721        tokio::time::timeout(Duration::from_secs(1), collector.join())
722            .await
723            .expect("collector shutdown timeout")
724            .expect("collector shutdown failed");
725    }
726
727    #[tokio::test]
728    async fn test_committer_retries_on_transaction_failure() {
729        let config = SequentialConfig::default();
730
731        // Create store with transaction failure configuration
732        let store = FallibleMockStore::default().with_transaction_failures(1); // Will fail once before succeeding
733
734        let mut setup = setup_test(10, config, store);
735
736        // Send a checkpoint
737        send_checkpoint(&mut setup, 10).await;
738
739        // Wait long enough for the committer to complete one failed attempt and one successful
740        // retry under exponential backoff (100ms initial).
741        tokio::time::sleep(Duration::from_millis(1_500)).await;
742
743        assert_eq!(setup.store.get_sequential_data(), vec![10]);
744    }
745
746    /// Smoke test for pipelined operation under a slow-commit store: with
747    /// `pipeline_depth = 1` and two full batches to process, both batches must land and
748    /// the watermark must reach the last checkpoint.
749    #[tokio::test]
750    async fn pipelined_commit_runs_under_slow_commit() {
751        let config = SequentialConfig {
752            committer: CommitterConfig::default(),
753            max_batch_checkpoints: Some(3),
754            pipeline_depth: Some(1),
755            ..Default::default()
756        };
757
758        let store = FallibleMockStore::default().with_commit_delay(700);
759        let mut setup = setup_test(0, config, store);
760
761        for i in 0..6 {
762            send_checkpoint(&mut setup, i).await;
763        }
764
765        // Two batches × 700ms commit each + slack.
766        tokio::time::sleep(Duration::from_millis(2_000)).await;
767
768        assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2, 3, 4, 5]);
769        let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
770        assert_eq!(watermark.checkpoint_hi_inclusive, Some(5));
771    }
772
773    /// Watermarks must advance strictly in batch order, even under pipelining.
774    #[tokio::test]
775    async fn pipelined_commit_preserves_watermark_ordering() {
776        let config = SequentialConfig {
777            committer: CommitterConfig::default(),
778            max_batch_checkpoints: Some(2),
779            pipeline_depth: Some(2),
780            ..Default::default()
781        };
782
783        let store = FallibleMockStore::default().with_commit_delay(100);
784        let mut setup = setup_test(0, config, store);
785
786        for i in 0..6 {
787            send_checkpoint(&mut setup, i).await;
788        }
789
790        tokio::time::sleep(Duration::from_millis(1_500)).await;
791
792        assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2, 3, 4, 5]);
793        let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
794        assert_eq!(watermark.checkpoint_hi_inclusive, Some(5));
795        assert_eq!(watermark.tx_hi, 5);
796    }
797}