Skip to main content

sui_indexer_alt_framework/pipeline/concurrent/
commit_watermark.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::cmp::Ordering;
5use std::collections::BTreeMap;
6use std::collections::btree_map::Entry;
7use std::sync::Arc;
8
9use sui_futures::service::Service;
10use tokio::sync::mpsc;
11use tracing::debug;
12use tracing::error;
13use tracing::info;
14use tracing::warn;
15
16use crate::metrics::CheckpointLagMetricReporter;
17use crate::metrics::IndexerMetrics;
18use crate::pipeline::CommitterConfig;
19use crate::pipeline::WARN_PENDING_WATERMARKS;
20use crate::pipeline::WatermarkPart;
21use crate::pipeline::concurrent::Handler;
22use crate::pipeline::logging::WatermarkLogger;
23use crate::store::CommitterWatermark;
24use crate::store::Connection;
25use crate::store::Store;
26use crate::store::pipeline_task;
27
28/// The watermark task is responsible for keeping track of a pipeline's out-of-order commits and
29/// updating its row in the `watermarks` table when a continuous run of checkpoints have landed
30/// since the last watermark update.
31///
32/// It receives watermark "parts" that detail the proportion of each checkpoint's data that has been
33/// written out by the committer and, after every message, checks if the watermark for the pipeline
34/// can be pushed forward. The watermark can be pushed forward if there is one or more complete (all
35/// data for that checkpoint written out) watermarks spanning contiguously from the current high
36/// watermark into the future.
37///
38/// Writes of the watermark to the DB are spaced at least `watermark_interval_ms` (plus jitter)
39/// apart: when the watermark advances and that much time has passed since the previous write, it
40/// is written immediately; otherwise the task waits out the remainder of the gap, then writes the
41/// latest watermark it has gathered.
42///
43/// If it detects that more than [WARN_PENDING_WATERMARKS] watermarks have built up, it will issue a
44/// warning, as this could be the indication of a memory leak, and the caller probably intended to
45/// run the indexer with watermarking disabled (e.g. if they are running a backfill).
46///
47/// The task regularly traces its progress, outputting at a higher log level every
48/// [LOUD_WATERMARK_UPDATE_INTERVAL]-many checkpoints.
49///
50/// The task will shutdown if the `rx` channel closes and the watermark cannot be progressed.
51pub(super) fn commit_watermark<H: Handler>(
52    mut next_checkpoint: u64,
53    config: CommitterConfig,
54    mut rx: mpsc::Receiver<Vec<WatermarkPart>>,
55    store: H::Store,
56    task: Option<String>,
57    metrics: Arc<IndexerMetrics>,
58) -> Service {
59    // SAFETY: on indexer instantiation, we've checked that the pipeline name is valid.
60    let pipeline_task = pipeline_task::<H::Store>(H::NAME, task.as_deref()).unwrap();
61    Service::new().spawn_aborting(async move {
62        // To correctly update the watermark, the task tracks the watermark it last tried to write
63        // and the watermark parts for any checkpoints that have been written since then
64        // ("pre-committed"). After each batch is written, the task will try to progress the
65        // watermark as much as possible without going over any holes in the sequence of
66        // checkpoints (entirely missing watermarks, or incomplete watermarks).
67        let mut precommitted: BTreeMap<u64, WatermarkPart> = BTreeMap::new();
68
69        // The watermark task will periodically output a log message at a higher log level to
70        // demonstrate that the pipeline is making progress.
71        let mut logger = WatermarkLogger::new("concurrent_committer");
72
73        let checkpoint_lag_reporter = CheckpointLagMetricReporter::new_for_pipeline::<H>(
74            &metrics.watermarked_checkpoint_timestamp_lag,
75            &metrics.latest_watermarked_checkpoint_timestamp_lag_ms,
76            &metrics.watermark_checkpoint_in_db,
77        );
78
79        info!(
80            pipeline = H::NAME,
81            next_checkpoint, "Starting commit watermark task"
82        );
83
84        // The earliest time at which the next DB write may start. Starts at "now" so the first
85        // advance is written immediately.
86        let mut next_write_at = tokio::time::Instant::now();
87        let mut pending_watermark = None;
88
89        loop {
90            // The timer is only armed when there is a gathered watermark waiting to be written.
91            let write_pending = pending_watermark.is_some();
92
93            tokio::select! {
94                biased;
95
96                parts = rx.recv() => {
97                    let Some(parts) = parts else {
98                        info!(pipeline = H::NAME, "Committer closed channel");
99                        break;
100                    };
101
102                    // Take every message that is already queued, so that a single gather (and at
103                    // most one write) covers all of them.
104                    add_parts(&mut precommitted, parts);
105                    while let Ok(parts) = rx.try_recv() {
106                        add_parts(&mut precommitted, parts);
107                    }
108                }
109
110                () = tokio::time::sleep_until(next_write_at), if write_pending => {}
111            }
112
113            // Advance the watermark through contiguous precommitted entries on every
114            // iteration, whether or not a DB write follows, so that the next write carries the
115            // latest watermark and the `watermark_*` gauges stay current.
116            let guard = metrics
117                .watermark_gather_latency
118                .with_label_values(&[H::NAME])
119                .start_timer();
120
121            while let Some(pending) = precommitted.first_entry() {
122                let part = pending.get();
123
124                // Some rows from the next watermark have not landed yet.
125                if !part.is_complete() {
126                    break;
127                }
128
129                match next_checkpoint.cmp(&part.watermark.checkpoint_hi_inclusive) {
130                    // Next pending checkpoint is from the future.
131                    Ordering::Less => break,
132
133                    // This is the next checkpoint -- include it.
134                    Ordering::Equal => {
135                        pending_watermark = Some(pending.remove().watermark);
136                        next_checkpoint += 1;
137                    }
138
139                    // Next pending checkpoint is in the past. Out of order watermarks can
140                    // be encountered when a pipeline is starting up, because ingestion
141                    // must start at the lowest checkpoint across all pipelines, or because
142                    // of a backfill, where the initial checkpoint has been overridden.
143                    Ordering::Greater => {
144                        // Track how many we see to make sure it doesn't grow without bound.
145                        metrics
146                            .total_watermarks_out_of_order
147                            .with_label_values(&[H::NAME])
148                            .inc();
149
150                        pending.remove();
151                    }
152                }
153            }
154
155            let elapsed = guard.stop_and_record();
156
157            if let Some(ref watermark) = pending_watermark {
158                metrics
159                    .watermark_epoch
160                    .with_label_values(&[H::NAME])
161                    .set(watermark.epoch_hi_inclusive as i64);
162
163                metrics
164                    .watermark_checkpoint
165                    .with_label_values(&[H::NAME])
166                    .set(watermark.checkpoint_hi_inclusive as i64);
167
168                metrics
169                    .watermark_transaction
170                    .with_label_values(&[H::NAME])
171                    .set(watermark.tx_hi as i64);
172
173                metrics
174                    .watermark_timestamp_ms
175                    .with_label_values(&[H::NAME])
176                    .set(watermark.timestamp_ms_hi_inclusive as i64);
177
178                debug!(
179                    pipeline = H::NAME,
180                    elapsed_ms = elapsed * 1000.0,
181                    watermark = watermark.checkpoint_hi_inclusive,
182                    timestamp = %watermark.timestamp(),
183                    pending = precommitted.len(),
184                    "Gathered watermarks",
185                );
186            }
187
188            if precommitted.len() > WARN_PENDING_WATERMARKS {
189                warn!(
190                    pipeline = H::NAME,
191                    pending = precommitted.len(),
192                    "Pipeline has a large number of pending commit watermarks",
193                );
194            }
195
196            if tokio::time::Instant::now() >= next_write_at
197                && let Some(watermark) = pending_watermark.take()
198            {
199                next_write_at = config.watermark_interval_with_jitter();
200                if write_watermark::<H>(
201                    &store,
202                    &pipeline_task,
203                    &watermark,
204                    &mut logger,
205                    &checkpoint_lag_reporter,
206                    &metrics,
207                )
208                .await
209                .is_err()
210                {
211                    pending_watermark = Some(watermark);
212                }
213            }
214        }
215
216        if let Some(watermark) = pending_watermark
217            && write_watermark::<H>(
218                &store,
219                &pipeline_task,
220                &watermark,
221                &mut logger,
222                &checkpoint_lag_reporter,
223                &metrics,
224            )
225            .await
226            .is_err()
227        {
228            warn!(
229                pipeline = H::NAME,
230                ?watermark,
231                "Failed to write final watermark on shutdown, will not retry",
232            );
233        }
234
235        info!(pipeline = H::NAME, "Stopping committer watermark task");
236        Ok(())
237    })
238}
239
240/// Merge `parts` into `precommitted`, combining parts that belong to the same checkpoint.
241fn add_parts(precommitted: &mut BTreeMap<u64, WatermarkPart>, parts: Vec<WatermarkPart>) {
242    for part in parts {
243        match precommitted.entry(part.checkpoint()) {
244            Entry::Vacant(entry) => {
245                entry.insert(part);
246            }
247
248            Entry::Occupied(mut entry) => {
249                entry.get_mut().add(part);
250            }
251        }
252    }
253}
254
255/// Write the watermark to DB and update metrics. Returns `Err` on failure so the caller can
256/// preserve the watermark and retry it once the write interval has passed.
257async fn write_watermark<H: Handler>(
258    store: &H::Store,
259    pipeline_task: &str,
260    watermark: &CommitterWatermark,
261    logger: &mut WatermarkLogger,
262    checkpoint_lag_reporter: &CheckpointLagMetricReporter,
263    metrics: &IndexerMetrics,
264) -> Result<(), ()> {
265    let Ok(mut conn) = store.connect().await else {
266        warn!(
267            pipeline = H::NAME,
268            "Commit watermark task failed to get connection for DB"
269        );
270        return Err(());
271    };
272
273    let guard = metrics
274        .watermark_commit_latency
275        .with_label_values(&[H::NAME])
276        .start_timer();
277
278    // TODO: If initial_watermark is empty, when we update watermark
279    // for the first time, we should also update the low watermark.
280    match conn
281        .set_committer_watermark(pipeline_task, *watermark)
282        .await
283    {
284        Err(e) => {
285            let elapsed = guard.stop_and_record();
286            error!(
287                pipeline = H::NAME,
288                elapsed_ms = elapsed * 1000.0,
289                ?watermark,
290                "Error updating commit watermark: {e}",
291            );
292            Err(())
293        }
294
295        Ok(true) => {
296            let elapsed = guard.stop_and_record();
297
298            logger.log::<H>(watermark, elapsed);
299
300            checkpoint_lag_reporter.report_lag(
301                watermark.checkpoint_hi_inclusive,
302                watermark.timestamp_ms_hi_inclusive,
303            );
304
305            metrics
306                .watermark_epoch_in_db
307                .with_label_values(&[H::NAME])
308                .set(watermark.epoch_hi_inclusive as i64);
309
310            metrics
311                .watermark_transaction_in_db
312                .with_label_values(&[H::NAME])
313                .set(watermark.tx_hi as i64);
314
315            metrics
316                .watermark_timestamp_in_db_ms
317                .with_label_values(&[H::NAME])
318                .set(watermark.timestamp_ms_hi_inclusive as i64);
319
320            Ok(())
321        }
322        Ok(false) => Ok(()),
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use std::sync::Arc;
329    use std::time::Duration;
330
331    use async_trait::async_trait;
332    use sui_types::full_checkpoint_content::Checkpoint;
333    use tokio::sync::mpsc;
334
335    use crate::FieldCount;
336    use crate::metrics::IndexerMetrics;
337    use crate::mocks::store::FallibleMockConnection;
338    use crate::mocks::store::FallibleMockStore;
339    use crate::pipeline::CommitterConfig;
340    use crate::pipeline::Processor;
341    use crate::pipeline::WatermarkPart;
342    use crate::pipeline::concurrent::BatchStatus;
343    use crate::store::CommitterWatermark;
344
345    use super::*;
346
347    #[derive(Clone, FieldCount)]
348    pub struct StoredData;
349
350    pub struct DataPipeline;
351
352    #[async_trait]
353    impl Processor for DataPipeline {
354        const NAME: &'static str = "data";
355        type Value = StoredData;
356
357        async fn process(&self, _checkpoint: &Arc<Checkpoint>) -> anyhow::Result<Vec<Self::Value>> {
358            Ok(vec![])
359        }
360    }
361
362    #[async_trait]
363    impl Handler for DataPipeline {
364        type Store = FallibleMockStore;
365        type Batch = Vec<Self::Value>;
366
367        fn batch(
368            &self,
369            batch: &mut Self::Batch,
370            values: &mut std::vec::IntoIter<Self::Value>,
371        ) -> BatchStatus {
372            batch.extend(values);
373            BatchStatus::Pending
374        }
375
376        async fn commit<'a>(
377            &self,
378            _batch: &Self::Batch,
379            _conn: &mut FallibleMockConnection<'a>,
380        ) -> anyhow::Result<usize> {
381            Ok(0)
382        }
383    }
384
385    struct TestSetup {
386        store: FallibleMockStore,
387        watermark_tx: mpsc::Sender<Vec<WatermarkPart>>,
388        #[allow(unused)]
389        commit_watermark: Service,
390    }
391
392    fn setup_test<H: Handler<Store = FallibleMockStore>>(
393        config: CommitterConfig,
394        next_checkpoint: u64,
395        store: FallibleMockStore,
396    ) -> TestSetup {
397        let (watermark_tx, watermark_rx) = mpsc::channel(100);
398        let metrics = IndexerMetrics::new(None, &Default::default());
399
400        let store_clone = store.clone();
401
402        let commit_watermark = commit_watermark::<H>(
403            next_checkpoint,
404            config,
405            watermark_rx,
406            store_clone,
407            None,
408            metrics,
409        );
410
411        TestSetup {
412            store,
413            watermark_tx,
414            commit_watermark,
415        }
416    }
417
418    fn create_watermark_part_for_checkpoint(checkpoint: u64) -> WatermarkPart {
419        WatermarkPart {
420            watermark: CommitterWatermark {
421                checkpoint_hi_inclusive: checkpoint,
422                ..Default::default()
423            },
424            batch_rows: 1,
425            total_rows: 1,
426        }
427    }
428
429    fn written_checkpoint_hi(store: &FallibleMockStore) -> Option<u64> {
430        store
431            .watermark(DataPipeline::NAME)
432            .and_then(|watermark| watermark.checkpoint_hi_inclusive)
433    }
434
435    #[tokio::test]
436    async fn test_basic_watermark_progression() {
437        let config = CommitterConfig::default();
438        let setup = setup_test::<DataPipeline>(config, 1, FallibleMockStore::default());
439
440        // Send watermark parts in order
441        for cp in 1..4 {
442            let part = create_watermark_part_for_checkpoint(cp);
443            setup.watermark_tx.send(vec![part]).await.unwrap();
444        }
445
446        // Wait for processing
447        tokio::time::sleep(Duration::from_millis(100)).await;
448
449        // Verify watermark progression
450        let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
451        assert_eq!(watermark.checkpoint_hi_inclusive, Some(3));
452    }
453
454    #[tokio::test]
455    async fn test_out_of_order_watermarks() {
456        let config = CommitterConfig::default();
457        let setup = setup_test::<DataPipeline>(config, 1, FallibleMockStore::default());
458
459        // Send watermark parts out of order
460        let parts = vec![
461            create_watermark_part_for_checkpoint(4),
462            create_watermark_part_for_checkpoint(2),
463            create_watermark_part_for_checkpoint(1),
464        ];
465        setup.watermark_tx.send(parts).await.unwrap();
466
467        // Wait for processing
468        tokio::time::sleep(Duration::from_millis(100)).await;
469
470        // Verify watermark hasn't progressed past 2
471        let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
472        assert_eq!(watermark.checkpoint_hi_inclusive, Some(2));
473
474        // Send checkpoint 3 to fill the gap
475        setup
476            .watermark_tx
477            .send(vec![create_watermark_part_for_checkpoint(3)])
478            .await
479            .unwrap();
480
481        // Wait for the next polling and processing
482        tokio::time::sleep(Duration::from_secs(1)).await;
483
484        // Verify watermark has progressed to 4
485        let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
486        assert_eq!(watermark.checkpoint_hi_inclusive, Some(4));
487    }
488
489    #[tokio::test]
490    async fn test_watermark_with_connection_failure() {
491        let config = CommitterConfig {
492            watermark_interval_ms: 1_000, // Long polling interval to test connection retry
493            ..Default::default()
494        };
495        let store = FallibleMockStore::default().with_connection_failures(1);
496        let setup = setup_test::<DataPipeline>(config, 1, store);
497
498        // Send watermark part
499        let part = create_watermark_part_for_checkpoint(1);
500        setup.watermark_tx.send(vec![part]).await.unwrap();
501
502        // Wait for processing
503        tokio::time::sleep(Duration::from_millis(200)).await;
504
505        // Verify watermark hasn't progressed
506        let watermark = setup.store.watermark(DataPipeline::NAME);
507        assert!(watermark.is_none());
508
509        // Wait for next polling and processing
510        tokio::time::sleep(Duration::from_millis(1_200)).await;
511
512        // Verify watermark has progressed
513        let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
514        assert_eq!(watermark.checkpoint_hi_inclusive, Some(1));
515    }
516
517    #[tokio::test]
518    async fn test_committer_retries_on_commit_watermark_failure() {
519        let config = CommitterConfig {
520            watermark_interval_ms: 1_000, // Long polling interval to test connection retry
521            ..Default::default()
522        };
523        // Create store with transaction failure configuration
524        let store = FallibleMockStore::default().with_commit_watermark_failures(1); // Will fail once before succeeding
525        let setup = setup_test::<DataPipeline>(config, 10, store);
526
527        let part = WatermarkPart {
528            watermark: CommitterWatermark {
529                checkpoint_hi_inclusive: 10,
530                ..Default::default()
531            },
532            batch_rows: 1,
533            total_rows: 1,
534        };
535        setup.watermark_tx.send(vec![part]).await.unwrap();
536
537        // Wait for initial poll to be over
538        tokio::time::sleep(Duration::from_millis(200)).await;
539        let watermark = setup.store.watermark(DataPipeline::NAME);
540        assert!(watermark.is_none());
541
542        // Wait for retries to complete
543        tokio::time::sleep(Duration::from_millis(1_200)).await;
544
545        // Verify watermark has progressed after retry
546        let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
547        assert_eq!(watermark.checkpoint_hi_inclusive, Some(10));
548    }
549
550    #[tokio::test]
551    async fn test_committer_retries_on_commit_watermark_failure_advances() {
552        let config = CommitterConfig {
553            watermark_interval_ms: 1_000, // Long polling interval to test connection retry
554            ..Default::default()          // Create store with transaction failure configuration
555        };
556        let store = FallibleMockStore::default().with_commit_watermark_failures(1); // Will fail once before succeeding
557        let setup = setup_test::<DataPipeline>(config, 10, store);
558
559        let part = WatermarkPart {
560            watermark: CommitterWatermark {
561                checkpoint_hi_inclusive: 10,
562                ..Default::default()
563            },
564            batch_rows: 1,
565            total_rows: 1,
566        };
567        setup.watermark_tx.send(vec![part]).await.unwrap();
568
569        // Wait for initial poll to be over
570        tokio::time::sleep(Duration::from_millis(200)).await;
571        let watermark = setup.store.watermark(DataPipeline::NAME);
572        assert!(watermark.is_none());
573
574        let part = WatermarkPart {
575            watermark: CommitterWatermark {
576                checkpoint_hi_inclusive: 11,
577                ..Default::default()
578            },
579            batch_rows: 1,
580            total_rows: 1,
581        };
582        setup.watermark_tx.send(vec![part]).await.unwrap();
583
584        // Wait for retries to complete
585        tokio::time::sleep(Duration::from_millis(1_200)).await;
586
587        let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
588        assert_eq!(watermark.checkpoint_hi_inclusive, Some(11));
589    }
590
591    #[tokio::test]
592    async fn test_incomplete_watermark() {
593        let config = CommitterConfig {
594            watermark_interval_ms: 1_000, // Long polling interval to test adding complete part
595            ..Default::default()
596        };
597        let setup = setup_test::<DataPipeline>(config, 1, FallibleMockStore::default());
598
599        // Send the first incomplete watermark part
600        let part = WatermarkPart {
601            watermark: CommitterWatermark {
602                checkpoint_hi_inclusive: 1,
603                ..Default::default()
604            },
605            batch_rows: 1,
606            total_rows: 3,
607        };
608        setup.watermark_tx.send(vec![part.clone()]).await.unwrap();
609
610        // Wait for processing
611        tokio::time::sleep(Duration::from_millis(200)).await;
612
613        // Verify watermark hasn't progressed
614        let watermark = setup.store.watermark(DataPipeline::NAME);
615        assert!(watermark.is_none());
616
617        // Send the other two parts to complete the watermark
618        setup
619            .watermark_tx
620            .send(vec![part.clone(), part.clone()])
621            .await
622            .unwrap();
623
624        // Wait for next polling and processing
625        tokio::time::sleep(Duration::from_millis(1_200)).await;
626
627        // Verify watermark has progressed
628        let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
629        assert_eq!(watermark.checkpoint_hi_inclusive, Some(1));
630    }
631
632    #[tokio::test]
633    async fn test_no_initial_watermark() {
634        let config = CommitterConfig::default();
635        let setup = setup_test::<DataPipeline>(config, 0, FallibleMockStore::default());
636
637        // Send the checkpoint 1 watermark
638        setup
639            .watermark_tx
640            .send(vec![create_watermark_part_for_checkpoint(1)])
641            .await
642            .unwrap();
643
644        // Wait for processing
645        tokio::time::sleep(Duration::from_millis(200)).await;
646
647        // Verify watermark hasn't progressed
648        let watermark = setup.store.watermark(DataPipeline::NAME);
649        assert!(watermark.is_none());
650
651        // Send the checkpoint 0 watermark to fill the gap.
652        setup
653            .watermark_tx
654            .send(vec![create_watermark_part_for_checkpoint(0)])
655            .await
656            .unwrap();
657
658        // Wait for processing
659        tokio::time::sleep(Duration::from_millis(1200)).await;
660
661        // Verify watermark has progressed
662        let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
663        assert_eq!(watermark.checkpoint_hi_inclusive, Some(1));
664    }
665
666    /// Watermark writes are spaced at least `watermark_interval_ms` apart, measured from the
667    /// previous write, but an advance that lands after the gap has passed is written straight
668    /// away, not on the next fixed tick.
669    #[tokio::test(start_paused = true)]
670    async fn test_watermark_writes_respect_minimum_gap() {
671        let config = CommitterConfig {
672            watermark_interval_ms: 1_000,
673            ..Default::default()
674        };
675        let setup = setup_test::<DataPipeline>(config, 1, FallibleMockStore::default());
676
677        // The first advance is written immediately.
678        setup
679            .watermark_tx
680            .send(vec![create_watermark_part_for_checkpoint(1)])
681            .await
682            .unwrap();
683        tokio::time::sleep(Duration::from_millis(10)).await;
684        assert_eq!(written_checkpoint_hi(&setup.store), Some(1));
685
686        // An advance inside the gap is written when the gap since the previous write (at t=0)
687        // ends, at t=1000, not a full interval after the advance itself (t=1500).
688        tokio::time::sleep(Duration::from_millis(490)).await;
689        setup
690            .watermark_tx
691            .send(vec![create_watermark_part_for_checkpoint(2)])
692            .await
693            .unwrap();
694        tokio::time::sleep(Duration::from_millis(10)).await;
695        assert_eq!(written_checkpoint_hi(&setup.store), Some(1));
696
697        tokio::time::sleep(Duration::from_millis(480)).await;
698        assert_eq!(written_checkpoint_hi(&setup.store), Some(1));
699
700        tokio::time::sleep(Duration::from_millis(20)).await;
701        assert_eq!(written_checkpoint_hi(&setup.store), Some(2));
702
703        // Once the gap since the last write has passed, the next advance is written immediately.
704        tokio::time::sleep(Duration::from_millis(1_500)).await;
705        setup
706            .watermark_tx
707            .send(vec![create_watermark_part_for_checkpoint(3)])
708            .await
709            .unwrap();
710        tokio::time::sleep(Duration::from_millis(10)).await;
711        assert_eq!(written_checkpoint_hi(&setup.store), Some(3));
712    }
713
714    /// Parts that queue up while the task is busy are all gathered before the next write, so a
715    /// backlog of committer messages is covered by one write instead of one write per interval.
716    #[tokio::test(start_paused = true)]
717    async fn test_queued_parts_are_written_together() {
718        let config = CommitterConfig {
719            watermark_interval_ms: 1_000,
720            ..Default::default()
721        };
722        let setup = setup_test::<DataPipeline>(config, 1, FallibleMockStore::default());
723
724        for cp in 1..=5 {
725            setup
726                .watermark_tx
727                .send(vec![create_watermark_part_for_checkpoint(cp)])
728                .await
729                .unwrap();
730        }
731
732        tokio::time::sleep(Duration::from_millis(10)).await;
733        assert_eq!(written_checkpoint_hi(&setup.store), Some(5));
734    }
735}