sui_indexer_alt_framework/pipeline/concurrent/
commit_watermark.rs1use 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
28pub(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 let pipeline_task = pipeline_task::<H::Store>(H::NAME, task.as_deref()).unwrap();
61 Service::new().spawn_aborting(async move {
62 let mut precommitted: BTreeMap<u64, WatermarkPart> = BTreeMap::new();
68
69 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 let mut next_write_at = tokio::time::Instant::now();
87 let mut pending_watermark = None;
88
89 loop {
90 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 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 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 if !part.is_complete() {
126 break;
127 }
128
129 match next_checkpoint.cmp(&part.watermark.checkpoint_hi_inclusive) {
130 Ordering::Less => break,
132
133 Ordering::Equal => {
135 pending_watermark = Some(pending.remove().watermark);
136 next_checkpoint += 1;
137 }
138
139 Ordering::Greater => {
144 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
240fn 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
255async 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 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 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 tokio::time::sleep(Duration::from_millis(100)).await;
448
449 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 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 tokio::time::sleep(Duration::from_millis(100)).await;
469
470 let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
472 assert_eq!(watermark.checkpoint_hi_inclusive, Some(2));
473
474 setup
476 .watermark_tx
477 .send(vec![create_watermark_part_for_checkpoint(3)])
478 .await
479 .unwrap();
480
481 tokio::time::sleep(Duration::from_secs(1)).await;
483
484 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, ..Default::default()
494 };
495 let store = FallibleMockStore::default().with_connection_failures(1);
496 let setup = setup_test::<DataPipeline>(config, 1, store);
497
498 let part = create_watermark_part_for_checkpoint(1);
500 setup.watermark_tx.send(vec![part]).await.unwrap();
501
502 tokio::time::sleep(Duration::from_millis(200)).await;
504
505 let watermark = setup.store.watermark(DataPipeline::NAME);
507 assert!(watermark.is_none());
508
509 tokio::time::sleep(Duration::from_millis(1_200)).await;
511
512 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, ..Default::default()
522 };
523 let store = FallibleMockStore::default().with_commit_watermark_failures(1); 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 tokio::time::sleep(Duration::from_millis(200)).await;
539 let watermark = setup.store.watermark(DataPipeline::NAME);
540 assert!(watermark.is_none());
541
542 tokio::time::sleep(Duration::from_millis(1_200)).await;
544
545 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, ..Default::default() };
556 let store = FallibleMockStore::default().with_commit_watermark_failures(1); 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 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 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, ..Default::default()
596 };
597 let setup = setup_test::<DataPipeline>(config, 1, FallibleMockStore::default());
598
599 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 tokio::time::sleep(Duration::from_millis(200)).await;
612
613 let watermark = setup.store.watermark(DataPipeline::NAME);
615 assert!(watermark.is_none());
616
617 setup
619 .watermark_tx
620 .send(vec![part.clone(), part.clone()])
621 .await
622 .unwrap();
623
624 tokio::time::sleep(Duration::from_millis(1_200)).await;
626
627 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 setup
639 .watermark_tx
640 .send(vec![create_watermark_part_for_checkpoint(1)])
641 .await
642 .unwrap();
643
644 tokio::time::sleep(Duration::from_millis(200)).await;
646
647 let watermark = setup.store.watermark(DataPipeline::NAME);
649 assert!(watermark.is_none());
650
651 setup
653 .watermark_tx
654 .send(vec![create_watermark_part_for_checkpoint(0)])
655 .await
656 .unwrap();
657
658 tokio::time::sleep(Duration::from_millis(1200)).await;
660
661 let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
663 assert_eq!(watermark.checkpoint_hi_inclusive, Some(1));
664 }
665
666 #[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 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 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 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 #[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}