1use std::sync::Arc;
5use std::time::Duration;
6
7use async_trait::async_trait;
8use serde::Deserialize;
9use serde::Serialize;
10use sui_futures::service::Service;
11use tokio::sync::SetOnce;
12use tokio::sync::mpsc;
13use tracing::info;
14
15use crate::Task;
16use crate::config::ConcurrencyConfig;
17use crate::ingestion::ingestion_client::CheckpointEnvelope;
18use crate::metrics::IndexerMetrics;
19use crate::pipeline::CommitterConfig;
20use crate::pipeline::IngestionConfig;
21use crate::pipeline::Processor;
22use crate::pipeline::WatermarkPart;
23use crate::pipeline::concurrent::collector::collector;
24use crate::pipeline::concurrent::commit_watermark::commit_watermark;
25use crate::pipeline::concurrent::committer::committer;
26use crate::pipeline::concurrent::main_reader_lo::track_main_reader_lo;
27use crate::pipeline::concurrent::pruner::pruner;
28use crate::pipeline::concurrent::reader_watermark::reader_watermark;
29use crate::pipeline::processor::processor;
30use crate::store::ConcurrentStore;
31use crate::store::Store;
32
33mod collector;
34mod commit_watermark;
35mod committer;
36mod main_reader_lo;
37mod pruner;
38mod reader_watermark;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum BatchStatus {
43 Pending,
45 Ready,
47}
48
49#[async_trait]
69pub trait Handler: Processor {
70 type Store: ConcurrentStore;
71 type Batch: Default + Send + Sync + 'static;
72
73 const MAX_PENDING_ROWS: usize = 5000;
75
76 const MAX_WATERMARK_UPDATES: usize = 10_000;
80
81 fn batch(
90 &self,
91 batch: &mut Self::Batch,
92 values: &mut std::vec::IntoIter<Self::Value>,
93 ) -> BatchStatus;
94
95 async fn commit<'a>(
97 &self,
98 batch: &Self::Batch,
99 conn: &mut <Self::Store as Store>::Connection<'a>,
100 ) -> anyhow::Result<usize>;
101
102 async fn prune<'a>(
105 &self,
106 _from: u64,
107 _to_exclusive: u64,
108 _conn: &mut <Self::Store as Store>::Connection<'a>,
109 ) -> anyhow::Result<usize> {
110 Ok(0)
111 }
112}
113
114#[derive(Serialize, Deserialize, Debug, Clone, Default)]
116pub struct ConcurrentConfig {
117 pub committer: CommitterConfig,
119
120 pub ingestion: IngestionConfig,
122
123 pub pruner: Option<PrunerConfig>,
125
126 pub fanout: Option<ConcurrencyConfig>,
128
129 pub max_pending_rows: Option<usize>,
131
132 pub max_watermark_updates: Option<usize>,
134
135 pub processor_channel_size: Option<usize>,
137
138 pub collector_channel_size: Option<usize>,
140
141 pub committer_channel_size: Option<usize>,
143}
144
145#[derive(Serialize, Deserialize, Debug, Clone)]
146pub struct PrunerConfig {
147 pub interval_ms: u64,
149
150 pub delay_ms: u64,
153
154 pub retention: u64,
156
157 pub max_chunk_size: u64,
159
160 pub prune_concurrency: u64,
162}
163
164struct BatchedRows<H: Handler> {
170 batch: H::Batch,
172 batch_len: usize,
174 watermark: Vec<WatermarkPart>,
176}
177
178impl<H, V> BatchedRows<H>
179where
180 H: Handler<Batch = Vec<V>, Value = V>,
181{
182 #[cfg(test)]
183 pub fn from_vec(batch: Vec<V>, watermark: Vec<WatermarkPart>) -> Self {
184 let batch_len = batch.len();
185 Self {
186 batch,
187 batch_len,
188 watermark,
189 }
190 }
191}
192
193impl PrunerConfig {
194 pub fn interval(&self) -> Duration {
195 Duration::from_millis(self.interval_ms)
196 }
197
198 pub fn delay(&self) -> Duration {
199 Duration::from_millis(self.delay_ms)
200 }
201}
202
203impl Default for PrunerConfig {
204 fn default() -> Self {
205 Self {
206 interval_ms: 300_000,
207 delay_ms: 120_000,
208 retention: 4_000_000,
209 max_chunk_size: 2_000,
210 prune_concurrency: 1,
211 }
212 }
213}
214
215pub(crate) fn pipeline<H: Handler>(
235 handler: H,
236 next_checkpoint: u64,
237 config: ConcurrentConfig,
238 store: H::Store,
239 task: Option<Task>,
240 checkpoint_rx: mpsc::Receiver<Arc<CheckpointEnvelope>>,
241 metrics: Arc<IndexerMetrics>,
242) -> Service {
243 info!(
244 pipeline = H::NAME,
245 "Starting pipeline with config: {config:#?}",
246 );
247
248 let ConcurrentConfig {
249 committer: committer_config,
250 ingestion: _,
251 pruner: pruner_config,
252 fanout,
253 max_pending_rows,
254 max_watermark_updates,
255 processor_channel_size,
256 collector_channel_size,
257 committer_channel_size,
258 } = config;
259
260 let concurrency = fanout.unwrap_or(ConcurrencyConfig::Adaptive {
261 initial: 1,
262 min: 1,
263 max: num_cpus::get().max(1),
264 dead_band: None,
265 });
266 let max_pending_rows = max_pending_rows.unwrap_or(H::MAX_PENDING_ROWS);
267 let max_watermark_updates = max_watermark_updates
268 .unwrap_or(H::MAX_WATERMARK_UPDATES)
269 .max(1);
270
271 let processor_channel_size = processor_channel_size
272 .unwrap_or_else(|| num_cpus::get() / 2)
273 .max(1);
274 let (processor_tx, collector_rx) = mpsc::channel(processor_channel_size);
275
276 let collector_channel_size = collector_channel_size
277 .unwrap_or_else(|| num_cpus::get() / 2)
278 .max(1);
279 let (collector_tx, committer_rx) = mpsc::channel(collector_channel_size);
281 let committer_channel_size = committer_channel_size.unwrap_or_else(num_cpus::get).max(1);
283 let (committer_tx, watermark_rx) = mpsc::channel(committer_channel_size);
284 let main_reader_lo = Arc::new(SetOnce::new());
285
286 let handler = Arc::new(handler);
287
288 let s_processor = processor(
289 handler.clone(),
290 checkpoint_rx,
291 processor_tx,
292 metrics.clone(),
293 concurrency,
294 store.clone(),
295 );
296
297 let s_collector = collector::<H>(
298 handler.clone(),
299 committer_config.clone(),
300 collector_rx,
301 collector_tx,
302 main_reader_lo.clone(),
303 metrics.clone(),
304 max_pending_rows,
305 max_watermark_updates,
306 );
307
308 let s_committer = committer::<H>(
309 handler.clone(),
310 committer_config.clone(),
311 committer_rx,
312 committer_tx,
313 store.clone(),
314 metrics.clone(),
315 );
316
317 let s_commit_watermark = commit_watermark::<H>(
318 next_checkpoint,
319 committer_config,
320 watermark_rx,
321 store.clone(),
322 task.as_ref().map(|t| t.task.clone()),
323 metrics.clone(),
324 );
325
326 let s_track_reader_lo = track_main_reader_lo::<H>(
327 main_reader_lo.clone(),
328 task.as_ref().map(|t| t.reader_interval),
329 store.clone(),
330 );
331
332 let s_reader_watermark =
333 reader_watermark::<H>(pruner_config.clone(), store.clone(), metrics.clone());
334
335 let s_pruner = pruner(handler, pruner_config, store, metrics);
336
337 s_processor
338 .merge(s_collector)
339 .merge(s_committer)
340 .merge(s_commit_watermark)
341 .attach(s_track_reader_lo)
342 .attach(s_reader_watermark)
343 .attach(s_pruner)
344}
345
346#[cfg(test)]
347mod tests {
348 use std::sync::Arc;
349 use std::time::Duration;
350
351 use prometheus::Registry;
352 use sui_types::digests::CheckpointDigest;
353 use tokio::sync::mpsc;
354 use tokio::time::timeout;
355
356 use crate::FieldCount;
357 use crate::metrics::IndexerMetrics;
358 use crate::mocks::store::FallibleMockConnection;
359 use crate::mocks::store::FallibleMockStore;
360 use crate::pipeline::Processor;
361 use crate::types::full_checkpoint_content::Checkpoint;
362 use crate::types::test_checkpoint_data_builder::TestCheckpointBuilder;
363
364 use super::*;
365
366 const TEST_TIMEOUT: Duration = Duration::from_secs(60);
367 const TEST_SUBSCRIBER_CHANNEL_SIZE: usize = 3; #[derive(Clone, Debug, FieldCount)]
370 struct TestValue {
371 checkpoint: u64,
372 data: u64,
373 }
374
375 struct DataPipeline;
376
377 #[async_trait]
378 impl Processor for DataPipeline {
379 const NAME: &'static str = "test_handler";
380 type Value = TestValue;
381
382 async fn process(&self, checkpoint: &Arc<Checkpoint>) -> anyhow::Result<Vec<Self::Value>> {
383 let cp_num = checkpoint.summary.sequence_number;
384
385 Ok(vec![
387 TestValue {
388 checkpoint: cp_num,
389 data: cp_num * 10 + 1,
390 },
391 TestValue {
392 checkpoint: cp_num,
393 data: cp_num * 10 + 2,
394 },
395 ])
396 }
397 }
398
399 #[async_trait]
400 impl Handler for DataPipeline {
401 type Store = FallibleMockStore;
402 type Batch = Vec<TestValue>;
403
404 const MAX_PENDING_ROWS: usize = 4; const MAX_WATERMARK_UPDATES: usize = 1; fn batch(
408 &self,
409 batch: &mut Self::Batch,
410 values: &mut std::vec::IntoIter<Self::Value>,
411 ) -> BatchStatus {
412 batch.extend(values);
414 BatchStatus::Pending
415 }
416
417 async fn commit<'a>(
418 &self,
419 batch: &Self::Batch,
420 conn: &mut FallibleMockConnection<'a>,
421 ) -> anyhow::Result<usize> {
422 let mut grouped: std::collections::HashMap<u64, Vec<u64>> =
424 std::collections::HashMap::new();
425 for value in batch {
426 grouped
427 .entry(value.checkpoint)
428 .or_default()
429 .push(value.data);
430 }
431
432 conn.0.commit_bulk_data(DataPipeline::NAME, grouped).await
434 }
435
436 async fn prune<'a>(
437 &self,
438 from: u64,
439 to_exclusive: u64,
440 conn: &mut FallibleMockConnection<'a>,
441 ) -> anyhow::Result<usize> {
442 conn.0.prune_data(DataPipeline::NAME, from, to_exclusive)
443 }
444 }
445
446 struct TestSetup {
447 store: FallibleMockStore,
448 checkpoint_tx: mpsc::Sender<Arc<CheckpointEnvelope>>,
449 #[allow(unused)]
450 pipeline: Service,
451 }
452
453 impl TestSetup {
454 async fn new(
455 config: ConcurrentConfig,
456 store: FallibleMockStore,
457 next_checkpoint: u64,
458 ) -> Self {
459 let (checkpoint_tx, checkpoint_rx) = mpsc::channel(TEST_SUBSCRIBER_CHANNEL_SIZE);
460 let metrics = IndexerMetrics::new(None, &Registry::default());
461
462 let pipeline = pipeline(
463 DataPipeline,
464 next_checkpoint,
465 config,
466 store.clone(),
467 None,
468 checkpoint_rx,
469 metrics,
470 );
471
472 Self {
473 store,
474 checkpoint_tx,
475 pipeline,
476 }
477 }
478
479 async fn send_checkpoint(&self, checkpoint: u64) -> anyhow::Result<()> {
480 let checkpoint_envelope = Arc::new(CheckpointEnvelope {
481 checkpoint: Arc::new(
482 TestCheckpointBuilder::new(checkpoint)
483 .with_epoch(1)
484 .with_network_total_transactions(checkpoint * 2)
485 .with_timestamp_ms(1000000000 + checkpoint * 1000)
486 .build_checkpoint(),
487 ),
488 chain_id: CheckpointDigest::new([1; 32]).into(),
489 });
490 self.checkpoint_tx.send(checkpoint_envelope).await?;
491 Ok(())
492 }
493
494 async fn send_checkpoint_with_timeout(
495 &self,
496 checkpoint: u64,
497 timeout_duration: Duration,
498 ) -> anyhow::Result<()> {
499 timeout(timeout_duration, self.send_checkpoint(checkpoint)).await?
500 }
501
502 async fn send_checkpoint_expect_timeout(
503 &self,
504 checkpoint: u64,
505 timeout_duration: Duration,
506 ) {
507 timeout(timeout_duration, self.send_checkpoint(checkpoint))
508 .await
509 .unwrap_err(); }
511 }
512
513 #[tokio::test]
514 async fn test_e2e_pipeline() {
515 let config = ConcurrentConfig {
516 pruner: Some(PrunerConfig {
517 interval_ms: 5_000, delay_ms: 100, retention: 3, ..Default::default()
521 }),
522 ..Default::default()
523 };
524 let store = FallibleMockStore::default();
525 let setup = TestSetup::new(config, store, 0).await;
526
527 for i in 0..3 {
529 setup
530 .send_checkpoint_with_timeout(i, Duration::from_millis(200))
531 .await
532 .unwrap();
533 }
534
535 for i in 0..3 {
537 let data = setup
538 .store
539 .wait_for_data(DataPipeline::NAME, i, TEST_TIMEOUT)
540 .await;
541 assert_eq!(data, vec![i * 10 + 1, i * 10 + 2]);
542 }
543
544 for i in 3..6 {
546 setup
547 .send_checkpoint_with_timeout(i, Duration::from_millis(200))
548 .await
549 .unwrap();
550 }
551
552 for i in 0..6 {
555 let data = setup
556 .store
557 .wait_for_data(DataPipeline::NAME, i, Duration::from_secs(1))
558 .await;
559 assert_eq!(data, vec![i * 10 + 1, i * 10 + 2]);
560 }
561
562 let pruning_deadline = Duration::from_secs(15);
566 let start = tokio::time::Instant::now();
567 loop {
568 let pruned = {
569 let data = setup.store.data.get(DataPipeline::NAME).unwrap();
570 !data.contains_key(&0) && !data.contains_key(&1) && !data.contains_key(&2)
571 };
572 if pruned {
573 break;
574 }
575 assert!(
576 start.elapsed() < pruning_deadline,
577 "Timed out waiting for pruning to occur"
578 );
579 tokio::time::sleep(Duration::from_millis(100)).await;
580 }
581
582 {
584 let data = setup.store.data.get(DataPipeline::NAME).unwrap();
585 assert!(data.contains_key(&3));
586 assert!(data.contains_key(&4));
587 assert!(data.contains_key(&5));
588 };
589 }
590
591 #[tokio::test]
592 async fn test_e2e_pipeline_without_pruning() {
593 let config = ConcurrentConfig {
594 pruner: None,
595 ..Default::default()
596 };
597 let store = FallibleMockStore::default();
598 let setup = TestSetup::new(config, store, 0).await;
599
600 for i in 0..10 {
602 setup
603 .send_checkpoint_with_timeout(i, Duration::from_millis(200))
604 .await
605 .unwrap();
606 }
607
608 let watermark = setup
610 .store
611 .wait_for_watermark(DataPipeline::NAME, 9, TEST_TIMEOUT)
612 .await;
613
614 for i in 0..10 {
616 let data = setup
617 .store
618 .wait_for_data(DataPipeline::NAME, i, Duration::from_secs(1))
619 .await;
620 assert_eq!(data, vec![i * 10 + 1, i * 10 + 2]);
621 }
622
623 assert_eq!(watermark.checkpoint_hi_inclusive, Some(9));
625 assert_eq!(watermark.tx_hi, 18); assert_eq!(watermark.timestamp_ms_hi_inclusive, 1000009000); let total_checkpoints = {
630 let data = setup.store.data.get(DataPipeline::NAME).unwrap();
631 data.len()
632 };
633 assert_eq!(total_checkpoints, 10);
634 }
635
636 #[tokio::test]
637 async fn test_out_of_order_processing() {
638 let config = ConcurrentConfig::default();
639 let store = FallibleMockStore::default();
640 let setup = TestSetup::new(config, store, 0).await;
641
642 let checkpoints = vec![2, 0, 4, 1, 3];
644 for cp in checkpoints {
645 setup
646 .send_checkpoint_with_timeout(cp, Duration::from_millis(200))
647 .await
648 .unwrap();
649 }
650
651 setup
653 .store
654 .wait_for_watermark(DataPipeline::NAME, 4, Duration::from_secs(5))
655 .await;
656
657 for i in 0..5 {
659 let data = setup
660 .store
661 .wait_for_data(DataPipeline::NAME, i, Duration::from_secs(1))
662 .await;
663 assert_eq!(data, vec![i * 10 + 1, i * 10 + 2]);
664 }
665 }
666
667 #[tokio::test]
668 async fn test_watermark_progression_with_gaps() {
669 let config = ConcurrentConfig::default();
670 let store = FallibleMockStore::default();
671 let setup = TestSetup::new(config, store, 0).await;
672
673 for cp in [0, 1, 3, 4] {
675 setup
676 .send_checkpoint_with_timeout(cp, Duration::from_millis(200))
677 .await
678 .unwrap();
679 }
680
681 tokio::time::sleep(Duration::from_secs(1)).await;
683
684 let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
686 assert_eq!(watermark.checkpoint_hi_inclusive, Some(1));
687
688 setup
690 .send_checkpoint_with_timeout(2, Duration::from_millis(200))
691 .await
692 .unwrap();
693
694 let watermark = setup
696 .store
697 .wait_for_watermark(DataPipeline::NAME, 4, TEST_TIMEOUT)
698 .await;
699 assert_eq!(watermark.checkpoint_hi_inclusive, Some(4));
700 }
701
702 #[tokio::test]
705 async fn test_back_pressure_collector_max_pending_rows() {
706 let config = ConcurrentConfig {
719 committer: CommitterConfig {
720 min_batch_rows: 1000,
723 max_batch_delay_ms: 5_000,
724 write_concurrency: 1,
725 ..Default::default()
726 },
727 fanout: Some(ConcurrencyConfig::Fixed { value: 2 }),
728 processor_channel_size: Some(7),
729 collector_channel_size: Some(6),
730 ..Default::default()
731 };
732 let store = FallibleMockStore::default();
733 let setup = TestSetup::new(config, store, 0).await;
734
735 tokio::time::sleep(Duration::from_millis(200)).await;
737
738 for i in 0..14 {
751 setup
752 .send_checkpoint_with_timeout(i, Duration::from_millis(200))
753 .await
754 .unwrap();
755 }
756
757 setup
759 .send_checkpoint_expect_timeout(14, Duration::from_millis(200))
760 .await;
761
762 setup
764 .send_checkpoint_with_timeout(14, TEST_TIMEOUT)
765 .await
766 .unwrap();
767
768 let data = setup
770 .store
771 .wait_for_data(DataPipeline::NAME, 0, TEST_TIMEOUT)
772 .await;
773 assert_eq!(data, vec![1, 2]);
774 }
775
776 #[tokio::test]
777 async fn test_back_pressure_committer_slow_commits() {
778 let config = ConcurrentConfig {
791 committer: CommitterConfig {
792 write_concurrency: 1, ..Default::default()
794 },
795 fanout: Some(ConcurrencyConfig::Fixed { value: 2 }),
796 processor_channel_size: Some(7),
797 collector_channel_size: Some(6),
798 ..Default::default()
799 };
800 let store = FallibleMockStore::default().with_commit_delay(10_000); let setup = TestSetup::new(config, store, 0).await;
802
803 for i in 0..19 {
817 setup
818 .send_checkpoint_with_timeout(i, Duration::from_millis(100))
819 .await
820 .unwrap();
821 }
822
823 let mut back_pressure_checkpoint = None;
828 for checkpoint in 19..22 {
829 if setup
830 .send_checkpoint_with_timeout(checkpoint, Duration::from_millis(100))
831 .await
832 .is_err()
833 {
834 back_pressure_checkpoint = Some(checkpoint);
835 break;
836 }
837 }
838 assert!(
839 back_pressure_checkpoint.is_some(),
840 "Back pressure should occur between checkpoints 19-21"
841 );
842
843 setup
845 .store
846 .wait_for_any_data(DataPipeline::NAME, TEST_TIMEOUT)
847 .await;
848
849 setup
851 .send_checkpoint_with_timeout(back_pressure_checkpoint.unwrap(), TEST_TIMEOUT)
852 .await
853 .unwrap();
854 }
855
856 #[tokio::test]
859 async fn test_commit_failure_retry() {
860 let config = ConcurrentConfig::default();
861 let store = FallibleMockStore::default().with_commit_failures(2); let setup = TestSetup::new(config, store, 0).await;
863
864 setup
866 .send_checkpoint_with_timeout(0, Duration::from_millis(200))
867 .await
868 .unwrap();
869
870 setup
872 .store
873 .wait_for_watermark(DataPipeline::NAME, 0, TEST_TIMEOUT)
874 .await;
875
876 let data = setup
878 .store
879 .wait_for_data(DataPipeline::NAME, 0, Duration::from_secs(1))
880 .await;
881 assert_eq!(data, vec![1, 2]);
882 }
883
884 #[tokio::test]
885 async fn test_prune_failure_retry() {
886 let config = ConcurrentConfig {
887 pruner: Some(PrunerConfig {
888 interval_ms: 2000, delay_ms: 100, retention: 2, ..Default::default()
892 }),
893 ..Default::default()
894 };
895
896 let store = FallibleMockStore::default().with_prune_failures(0, 2, 1);
898 let setup = TestSetup::new(config, store, 0).await;
899
900 for i in 0..4 {
902 setup
903 .send_checkpoint_with_timeout(i, Duration::from_millis(200))
904 .await
905 .unwrap();
906 }
907
908 for i in 0..4 {
911 let data = setup
912 .store
913 .wait_for_data(DataPipeline::NAME, i, Duration::from_secs(1))
914 .await;
915 assert_eq!(data, vec![i * 10 + 1, i * 10 + 2]);
916 }
917
918 setup
920 .store
921 .wait_for_prune_attempts(0, 2, 1, TEST_TIMEOUT)
922 .await;
923 {
924 let data = setup.store.data.get(DataPipeline::NAME).unwrap();
925 for i in 0..4 {
926 assert!(data.contains_key(&i));
927 }
928 };
929
930 setup
932 .store
933 .wait_for_prune_attempts(0, 2, 2, TEST_TIMEOUT)
934 .await;
935 {
936 let data = setup.store.data.get(DataPipeline::NAME).unwrap();
937 assert!(data.contains_key(&2));
939 assert!(data.contains_key(&3));
940
941 assert!(!data.contains_key(&0));
943 assert!(!data.contains_key(&1));
944 };
945 }
946
947 #[tokio::test]
948 async fn test_reader_watermark_failure_retry() {
949 let config = ConcurrentConfig {
950 pruner: Some(PrunerConfig {
951 interval_ms: 100, delay_ms: 100, retention: 3, ..Default::default()
955 }),
956 ..Default::default()
957 };
958
959 let store = FallibleMockStore::default().with_reader_watermark_failures(2);
961 let setup = TestSetup::new(config, store, 0).await;
962
963 for i in 0..6 {
965 setup
966 .send_checkpoint_with_timeout(i, Duration::from_millis(200))
967 .await
968 .unwrap();
969 }
970
971 setup
973 .store
974 .wait_for_watermark(DataPipeline::NAME, 5, TEST_TIMEOUT)
975 .await;
976
977 tokio::time::sleep(Duration::from_secs(2)).await;
979
980 let watermark = setup.store.watermark(DataPipeline::NAME).unwrap();
982 assert_eq!(watermark.reader_lo, 3);
983 }
984
985 #[tokio::test]
986 async fn test_database_connection_failure_retry() {
987 let config = ConcurrentConfig::default();
988 let store = FallibleMockStore::default().with_connection_failures(2); let setup = TestSetup::new(config, store, 0).await;
990
991 setup
993 .send_checkpoint_with_timeout(0, Duration::from_millis(200))
994 .await
995 .unwrap();
996
997 setup
999 .store
1000 .wait_for_watermark(DataPipeline::NAME, 0, TEST_TIMEOUT)
1001 .await;
1002
1003 let data = setup
1005 .store
1006 .wait_for_data(DataPipeline::NAME, 0, TEST_TIMEOUT)
1007 .await;
1008 assert_eq!(data, vec![1, 2]);
1009 }
1010}