1use 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#[async_trait]
45pub trait Handler: Processor {
46 type Store: SequentialStore;
47
48 const MAX_PENDING_ROWS: usize = 5000;
55
56 const MAX_BATCH_CHECKPOINTS: usize = 5 * 60;
60
61 type Batch: Default + Send + Sync + 'static;
65
66 fn batch(&self, batch: &mut Self::Batch, values: std::vec::IntoIter<Self::Value>);
76
77 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#[derive(Serialize, Deserialize, Debug, Clone, Default)]
88pub struct SequentialConfig {
89 pub committer: CommitterConfig,
91
92 pub ingestion: IngestionConfig,
94
95 pub fanout: Option<ConcurrencyConfig>,
97
98 pub max_pending_rows: Option<usize>,
100
101 pub max_batch_checkpoints: Option<usize>,
103
104 pub processor_channel_size: Option<usize>,
106
107 pub pipeline_depth: Option<usize>,
110}
111
112pub(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 #[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; 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 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, checkpoint, checkpoint, checkpoint * 1000, vec![checkpoint], )
316 }
317
318 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 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 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 for i in 0..3 {
392 send_checkpoint(&mut setup, i).await;
393 }
394
395 tokio::time::sleep(Duration::from_millis(200)).await;
397
398 assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2]);
400
401 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 #[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 let watermark = setup.store.watermark(TestHandler::NAME);
416 assert!(watermark.is_none());
417
418 for i in 0..5 {
420 send_checkpoint(&mut setup, i).await;
421 }
422
423 tokio::time::sleep(Duration::from_millis(1000)).await;
425
426 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 tokio::time::sleep(Duration::from_millis(1000)).await;
436
437 assert_eq!(setup.store.get_sequential_data(), vec![5, 6, 7]);
439
440 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 for i in [1, 0, 2] {
453 send_checkpoint(&mut setup, i).await;
454 }
455
456 tokio::time::sleep(Duration::from_millis(200)).await;
458
459 assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2]);
461
462 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 for i in 0..4 {
475 send_checkpoint(&mut setup, i).await;
476 }
477
478 tokio::time::sleep(Duration::from_millis(200)).await;
480
481 assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2, 3]);
483 }
484
485 #[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 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 #[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 #[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_tx.send(create_checkpoint(0)).await.unwrap();
538 tokio::time::sleep(Duration::from_millis(10)).await;
539
540 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 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 #[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 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 #[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 #[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_tx.send(create_checkpoint(0)).await.unwrap();
616 tokio::time::sleep(Duration::from_millis(10)).await;
617
618 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 #[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 #[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 let store = FallibleMockStore::default().with_transaction_failures(1); let mut setup = setup_test(10, config, store);
735
736 send_checkpoint(&mut setup, 10).await;
738
739 tokio::time::sleep(Duration::from_millis(1_500)).await;
742
743 assert_eq!(setup.store.get_sequential_data(), vec![10]);
744 }
745
746 #[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 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 #[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}