1use std::collections::BTreeSet;
5use std::ops::Bound;
6use std::sync::Arc;
7use std::time::Duration;
8
9use anyhow::Context;
10use anyhow::bail;
11use anyhow::ensure;
12use cohort::cohorts;
13use ingestion::ArcStreamingClient;
14use ingestion::ClientArgs;
15use ingestion::IngestionConfig;
16use ingestion::IngestionFactory;
17use ingestion::IngestionService;
18use ingestion::ingestion_client::IngestionClient;
19use metrics::IndexerMetrics;
20use prometheus::Registry;
21use sui_indexer_alt_framework_store_traits::ConcurrentStore;
22use sui_indexer_alt_framework_store_traits::Connection;
23use sui_indexer_alt_framework_store_traits::SequentialStore;
24use sui_indexer_alt_framework_store_traits::Store;
25use sui_indexer_alt_framework_store_traits::pipeline_task;
26use tracing::info;
27
28use crate::metrics::IngestionMetrics;
29use crate::pipeline::Processor;
30use crate::pipeline::concurrent::ConcurrentConfig;
31use crate::pipeline::concurrent::{self};
32use crate::pipeline::sequential::SequentialConfig;
33use crate::pipeline::sequential::{self};
34use crate::service::Service;
35
36pub use sui_field_count::FieldCount;
37pub use sui_futures::service;
38pub use sui_indexer_alt_framework_store_traits as store;
40pub use sui_types as types;
41
42#[cfg(feature = "cluster")]
43pub mod cluster;
44mod cohort;
45pub mod config;
46pub mod ingestion;
47pub mod metrics;
48pub mod pipeline;
49#[cfg(feature = "postgres")]
50pub mod postgres;
51
52#[cfg(test)]
53pub mod mocks;
54
55#[derive(clap::Args, Default, Debug, Clone)]
57pub struct IndexerArgs {
58 #[arg(long)]
65 pub first_checkpoint: Option<u64>,
66
67 #[arg(long)]
70 pub last_checkpoint: Option<u64>,
71
72 #[arg(long, action = clap::ArgAction::Append)]
75 pub pipeline: Vec<String>,
76
77 #[clap(flatten)]
79 pub task: TaskArgs,
80}
81
82#[derive(clap::Parser, Default, Debug, Clone)]
84pub struct TaskArgs {
85 #[arg(long, requires = "reader_interval_ms")]
97 task: Option<String>,
98
99 #[arg(long, requires = "task")]
110 reader_interval_ms: Option<u64>,
111}
112
113pub struct Indexer<S: Store> {
114 store: S,
118
119 metrics: Arc<IndexerMetrics>,
121
122 ingestion_factory: IngestionFactory,
125
126 first_checkpoint: Option<u64>,
129
130 last_checkpoint: Option<u64>,
133
134 latest_checkpoint: u64,
136
137 next_checkpoint: u64,
140
141 next_sequential_checkpoint: Option<u64>,
144
145 task: Option<Task>,
157
158 enabled_pipelines: Option<BTreeSet<String>>,
162
163 added_pipelines: BTreeSet<&'static str>,
166
167 pipelines: Vec<PendingPipeline>,
170}
171
172#[derive(Clone)]
174pub(crate) struct Task {
175 task: String,
178 reader_interval: Duration,
181}
182
183struct PendingPipeline {
187 name: &'static str,
189
190 next_checkpoint: u64,
192
193 build: PipelineBuilder,
196}
197
198type PipelineBuilder = Box<dyn FnOnce(&mut IngestionService) -> Service + Send + Sync>;
206
207impl TaskArgs {
208 pub fn tasked(task: String, reader_interval_ms: u64) -> Self {
209 Self {
210 task: Some(task),
211 reader_interval_ms: Some(reader_interval_ms),
212 }
213 }
214
215 fn into_task(self) -> Option<Task> {
216 Some(Task {
217 task: self.task?,
218 reader_interval: Duration::from_millis(self.reader_interval_ms?),
219 })
220 }
221}
222
223impl<S: Store> Indexer<S> {
224 pub async fn new(
236 store: S,
237 indexer_args: IndexerArgs,
238 client_args: ClientArgs,
239 ingestion_config: IngestionConfig,
240 metrics_prefix: Option<&str>,
241 registry: &Registry,
242 ) -> anyhow::Result<Self> {
243 let ingestion_factory =
244 IngestionFactory::new(client_args, ingestion_config, metrics_prefix, registry)?;
245 Self::with_ingestion_factory(
246 store,
247 indexer_args,
248 ingestion_factory,
249 metrics_prefix,
250 registry,
251 )
252 .await
253 }
254
255 pub async fn with_ingestion_clients(
267 store: S,
268 indexer_args: IndexerArgs,
269 ingestion_client: IngestionClient,
270 streaming_client: Option<ArcStreamingClient>,
271 ingestion_config: IngestionConfig,
272 metrics_prefix: Option<&str>,
273 registry: &Registry,
274 ) -> anyhow::Result<Self> {
275 let ingestion_factory =
276 IngestionFactory::with_clients(ingestion_client, streaming_client, ingestion_config);
277 Self::with_ingestion_factory(
278 store,
279 indexer_args,
280 ingestion_factory,
281 metrics_prefix,
282 registry,
283 )
284 .await
285 }
286
287 async fn with_ingestion_factory(
290 store: S,
291 indexer_args: IndexerArgs,
292 ingestion_factory: IngestionFactory,
293 metrics_prefix: Option<&str>,
294 registry: &Registry,
295 ) -> anyhow::Result<Self> {
296 let IndexerArgs {
297 first_checkpoint,
298 last_checkpoint,
299 pipeline,
300 task,
301 } = indexer_args;
302
303 let metrics = IndexerMetrics::new(metrics_prefix, registry);
304
305 let latest_checkpoint = ingestion_factory.latest_checkpoint_number().await?;
306
307 info!(latest_checkpoint);
308
309 Ok(Self {
310 store,
311 metrics,
312 ingestion_factory,
313 first_checkpoint,
314 last_checkpoint,
315 latest_checkpoint,
316 next_checkpoint: u64::MAX,
317 next_sequential_checkpoint: None,
318 task: task.into_task(),
319 enabled_pipelines: if pipeline.is_empty() {
320 None
321 } else {
322 Some(pipeline.into_iter().collect())
323 },
324 added_pipelines: BTreeSet::new(),
325 pipelines: vec![],
326 })
327 }
328
329 pub fn store(&self) -> &S {
331 &self.store
332 }
333
334 pub fn ingestion_client(&self) -> &IngestionClient {
336 self.ingestion_factory.ingestion_client()
337 }
338
339 pub fn indexer_metrics(&self) -> &Arc<IndexerMetrics> {
341 &self.metrics
342 }
343
344 pub fn ingestion_metrics(&self) -> &Arc<IngestionMetrics> {
346 self.ingestion_factory.metrics()
347 }
348
349 pub fn pipelines(&self) -> impl Iterator<Item = &'static str> + '_ {
351 self.added_pipelines.iter().copied().filter(|p| {
352 self.enabled_pipelines
353 .as_ref()
354 .is_none_or(|e| e.contains(*p))
355 })
356 }
357
358 pub fn next_sequential_checkpoint(&self) -> Option<u64> {
362 self.next_sequential_checkpoint
363 }
364
365 pub async fn run(self) -> anyhow::Result<Service> {
375 let Self {
376 ingestion_factory,
377 last_checkpoint,
378 latest_checkpoint,
379 enabled_pipelines,
380 pipelines,
381 ..
382 } = self;
383
384 if let Some(enabled_pipelines) = enabled_pipelines {
385 ensure!(
386 enabled_pipelines.is_empty(),
387 "Tried to enable pipelines that this indexer does not know about: \
388 {enabled_pipelines:#?}",
389 );
390 }
391
392 ensure!(!pipelines.is_empty(), "No pipelines registered to run");
393
394 let groups = cohorts(
395 pipelines,
396 latest_checkpoint,
397 ingestion_factory.config().min_cohort_boundary,
398 );
399
400 info!(
401 cohorts = groups.len(),
402 latest_checkpoint, "Grouped pipelines into ingestion cohorts"
403 );
404
405 let end = last_checkpoint.map_or(Bound::Unbounded, Bound::Included);
406
407 let mut service = Service::new();
408 for (cohort, group) in groups.into_iter().enumerate() {
409 let mut ingestion = ingestion_factory.create(cohort);
410
411 let mut start = u64::MAX;
412 let mut names = Vec::with_capacity(group.len());
413 let mut members = Vec::with_capacity(group.len());
414 for pending in group {
415 start = start.min(pending.next_checkpoint);
416 names.push(pending.name);
417 members.push((pending.build)(&mut ingestion));
418 }
419
420 info!(cohort, start, end = last_checkpoint, pipelines = ?names, "Ingestion range");
421
422 let mut cohort_service = ingestion
423 .run((Bound::Included(start), end))
424 .await
425 .context("Failed to start ingestion service")?;
426
427 for member in members {
428 cohort_service = cohort_service.merge(member);
429 }
430
431 service = service.merge(cohort_service);
432 }
433
434 Ok(service)
435 }
436
437 async fn add_pipeline<P: Processor + 'static>(
446 &mut self,
447 pipeline_task: String,
448 retention: Option<u64>,
449 ) -> anyhow::Result<Option<u64>> {
450 ensure!(
451 self.added_pipelines.insert(P::NAME),
452 "Pipeline {:?} already added",
453 P::NAME,
454 );
455
456 if let Some(enabled_pipelines) = &mut self.enabled_pipelines
457 && !enabled_pipelines.remove(P::NAME)
458 {
459 info!(pipeline = P::NAME, "Skipping");
460 return Ok(None);
461 }
462
463 let proposed_next_checkpoint = if let Some(first_checkpoint) = self.first_checkpoint {
466 first_checkpoint
467 } else if let Some(retention) = retention {
468 self.latest_checkpoint.saturating_sub(retention)
469 } else {
470 0
471 };
472 let mut conn = self.store.connect().await?;
473 let init_watermark = conn
474 .init_watermark(&pipeline_task, proposed_next_checkpoint.checked_sub(1))
475 .await
476 .with_context(|| format!("Failed to init watermark for {pipeline_task}"))?;
477
478 let next_checkpoint = if let Some(init_watermark) = init_watermark {
479 if let Some(checkpoint_hi_inclusive) = init_watermark.checkpoint_hi_inclusive {
480 checkpoint_hi_inclusive + 1
481 } else {
482 0
483 }
484 } else {
485 proposed_next_checkpoint
486 };
487
488 self.next_checkpoint = next_checkpoint.min(self.next_checkpoint);
489
490 Ok(Some(next_checkpoint))
491 }
492}
493
494impl<S: ConcurrentStore> Indexer<S> {
495 pub async fn concurrent_pipeline<H: concurrent::Handler<Store = S>>(
502 &mut self,
503 handler: H,
504 config: ConcurrentConfig,
505 ) -> anyhow::Result<()> {
506 let pipeline_task =
507 pipeline_task::<S>(H::NAME, self.task.as_ref().map(|t| t.task.as_str()))?;
508 let retention = config.pruner.as_ref().map(|p| p.retention);
509 let Some(next_checkpoint) = self.add_pipeline::<H>(pipeline_task, retention).await? else {
510 return Ok(());
511 };
512
513 let store = self.store.clone();
514 let task = self.task.clone();
515 let metrics = self.metrics.clone();
516 self.pipelines.push(PendingPipeline {
517 name: H::NAME,
518 next_checkpoint,
519 build: Box::new(move |ingestion| {
520 let checkpoint_rx =
521 ingestion.subscribe_bounded(config.ingestion.subscriber_channel_size());
522 concurrent::pipeline::<H>(
523 handler,
524 next_checkpoint,
525 config,
526 store,
527 task,
528 checkpoint_rx,
529 metrics,
530 )
531 }),
532 });
533
534 Ok(())
535 }
536}
537
538impl<T: SequentialStore> Indexer<T> {
539 pub async fn sequential_pipeline<H: sequential::Handler<Store = T>>(
547 &mut self,
548 handler: H,
549 config: SequentialConfig,
550 ) -> anyhow::Result<()> {
551 if self.task.is_some() {
552 bail!(
553 "Sequential pipelines do not support pipeline tasks. \
554 These pipelines guarantee that each checkpoint is committed exactly once and in order. \
555 Running the same pipeline under a different task would violate these guarantees."
556 );
557 }
558
559 let Some(next_checkpoint) = self.add_pipeline::<H>(H::NAME.to_owned(), None).await? else {
560 return Ok(());
561 };
562
563 self.next_sequential_checkpoint = Some(
565 self.next_sequential_checkpoint
566 .map_or(next_checkpoint, |n| n.min(next_checkpoint)),
567 );
568
569 let store = self.store.clone();
570 let metrics = self.metrics.clone();
571 self.pipelines.push(PendingPipeline {
572 name: H::NAME,
573 next_checkpoint,
574 build: Box::new(move |ingestion| {
575 let checkpoint_rx =
576 ingestion.subscribe_bounded(config.ingestion.subscriber_channel_size());
577 sequential::pipeline::<H>(
578 handler,
579 next_checkpoint,
580 config,
581 store,
582 checkpoint_rx,
583 metrics,
584 )
585 }),
586 });
587
588 Ok(())
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use std::sync::Arc;
595
596 use async_trait::async_trait;
597 use clap::Parser;
598 use sui_indexer_alt_framework_store_traits::PrunerWatermark;
599 use sui_synthetic_ingestion::synthetic_ingestion;
600 use tokio::sync::watch;
601
602 use crate::FieldCount;
603 use crate::config::ConcurrencyConfig;
604 use crate::ingestion::ingestion_client::IngestionClientArgs;
605 use crate::ingestion::store_client::ObjectStoreWatermark;
606 use crate::ingestion::store_client::WATERMARK_PATH;
607 use crate::mocks::store::FallibleMockStore;
608 use crate::pipeline::CommitterConfig;
609 use crate::pipeline::Processor;
610 use crate::pipeline::concurrent::ConcurrentConfig;
611 use crate::store::CommitterWatermark;
612 use crate::store::ConcurrentConnection as _;
613 use crate::store::Connection as _;
614
615 use super::*;
616
617 #[allow(dead_code)]
618 #[derive(Clone, FieldCount)]
619 struct MockValue(u64);
620
621 struct ControllableHandler {
623 process_below: watch::Receiver<u64>,
625 }
626
627 impl ControllableHandler {
628 fn with_limit(limit: u64) -> (Self, watch::Sender<u64>) {
629 let (tx, rx) = watch::channel(limit);
630 (Self { process_below: rx }, tx)
631 }
632 }
633
634 #[async_trait]
635 impl Processor for ControllableHandler {
636 const NAME: &'static str = "controllable";
637 type Value = MockValue;
638
639 async fn process(
640 &self,
641 checkpoint: &Arc<sui_types::full_checkpoint_content::Checkpoint>,
642 ) -> anyhow::Result<Vec<Self::Value>> {
643 let cp_num = checkpoint.summary.sequence_number;
644
645 self.process_below
647 .clone()
648 .wait_for(|&limit| cp_num <= limit)
649 .await
650 .ok();
651
652 Ok(vec![MockValue(cp_num)])
653 }
654 }
655
656 #[async_trait]
657 impl concurrent::Handler for ControllableHandler {
658 type Store = FallibleMockStore;
659 type Batch = Vec<MockValue>;
660
661 fn batch(
662 &self,
663 batch: &mut Self::Batch,
664 values: &mut std::vec::IntoIter<Self::Value>,
665 ) -> concurrent::BatchStatus {
666 batch.extend(values);
667 concurrent::BatchStatus::Ready
668 }
669
670 async fn commit<'a>(
671 &self,
672 batch: &Self::Batch,
673 conn: &mut <Self::Store as Store>::Connection<'a>,
674 ) -> anyhow::Result<usize> {
675 for value in batch {
676 conn.0
677 .commit_data(Self::NAME, value.0, vec![value.0])
678 .await?;
679 }
680 Ok(batch.len())
681 }
682 }
683
684 macro_rules! test_pipeline {
685 ($handler:ident, $name:literal) => {
686 struct $handler;
687
688 #[async_trait]
689 impl Processor for $handler {
690 const NAME: &'static str = $name;
691 type Value = MockValue;
692 async fn process(
693 &self,
694 checkpoint: &Arc<sui_types::full_checkpoint_content::Checkpoint>,
695 ) -> anyhow::Result<Vec<Self::Value>> {
696 Ok(vec![MockValue(checkpoint.summary.sequence_number)])
697 }
698 }
699
700 #[async_trait]
701 impl crate::pipeline::concurrent::Handler for $handler {
702 type Store = FallibleMockStore;
703 type Batch = Vec<Self::Value>;
704
705 fn batch(
706 &self,
707 batch: &mut Self::Batch,
708 values: &mut std::vec::IntoIter<Self::Value>,
709 ) -> crate::pipeline::concurrent::BatchStatus {
710 batch.extend(values);
711 crate::pipeline::concurrent::BatchStatus::Pending
712 }
713
714 async fn commit<'a>(
715 &self,
716 batch: &Self::Batch,
717 conn: &mut <Self::Store as Store>::Connection<'a>,
718 ) -> anyhow::Result<usize> {
719 for value in batch {
720 conn.0
721 .commit_data(Self::NAME, value.0, vec![value.0])
722 .await?;
723 }
724 Ok(batch.len())
725 }
726 }
727
728 #[async_trait]
729 impl crate::pipeline::sequential::Handler for $handler {
730 type Store = FallibleMockStore;
731 type Batch = Vec<Self::Value>;
732
733 fn batch(&self, batch: &mut Self::Batch, values: std::vec::IntoIter<Self::Value>) {
734 batch.extend(values);
735 }
736
737 async fn commit<'a>(
738 &self,
739 _batch: &Self::Batch,
740 _conn: &mut <Self::Store as Store>::Connection<'a>,
741 ) -> anyhow::Result<usize> {
742 Ok(1)
743 }
744 }
745 };
746 }
747
748 test_pipeline!(MockHandler, "test_processor");
749 test_pipeline!(SequentialHandler, "sequential_handler");
750 test_pipeline!(MockCheckpointSequenceNumberHandler, "test");
751
752 fn init_ingestion_dir(latest_checkpoint: Option<u64>) -> tempfile::TempDir {
753 let dir = tempfile::tempdir().unwrap();
754 if let Some(cp) = latest_checkpoint {
755 let watermark_path = dir.path().join(WATERMARK_PATH);
756 std::fs::create_dir_all(watermark_path.parent().unwrap()).unwrap();
757 let watermark = ObjectStoreWatermark {
758 checkpoint_hi_inclusive: cp,
759 };
760 std::fs::write(watermark_path, serde_json::to_string(&watermark).unwrap()).unwrap();
761 }
762 dir
763 }
764
765 async fn create_test_indexer(
768 store: FallibleMockStore,
769 indexer_args: IndexerArgs,
770 registry: &Registry,
771 ingestion_data: Option<(u64, u64)>,
772 ) -> (Indexer<FallibleMockStore>, tempfile::TempDir) {
773 let temp_dir = init_ingestion_dir(None);
774 if let Some((num_checkpoints, checkpoint_size)) = ingestion_data {
775 synthetic_ingestion::generate_ingestion(synthetic_ingestion::Config {
776 ingestion_dir: temp_dir.path().to_owned(),
777 starting_checkpoint: 0,
778 num_checkpoints,
779 checkpoint_size,
780 })
781 .await;
782 }
783 let client_args = ClientArgs {
784 ingestion: IngestionClientArgs {
785 local_ingestion_path: Some(temp_dir.path().to_owned()),
786 ..Default::default()
787 },
788 ..Default::default()
789 };
790 let indexer = Indexer::new(
791 store,
792 indexer_args,
793 client_args,
794 IngestionConfig::default(),
795 None,
796 registry,
797 )
798 .await
799 .unwrap();
800 (indexer, temp_dir)
801 }
802
803 async fn set_committer_watermark(
804 conn: &mut <FallibleMockStore as Store>::Connection<'_>,
805 name: &str,
806 hi: u64,
807 ) {
808 conn.set_committer_watermark(
809 name,
810 CommitterWatermark {
811 checkpoint_hi_inclusive: hi,
812 ..Default::default()
813 },
814 )
815 .await
816 .unwrap();
817 }
818
819 async fn add_concurrent<H: concurrent::Handler<Store = FallibleMockStore>>(
820 indexer: &mut Indexer<FallibleMockStore>,
821 handler: H,
822 ) {
823 indexer
824 .concurrent_pipeline(handler, ConcurrentConfig::default())
825 .await
826 .unwrap();
827 }
828
829 async fn add_sequential<H: sequential::Handler<Store = FallibleMockStore>>(
830 indexer: &mut Indexer<FallibleMockStore>,
831 handler: H,
832 ) {
833 indexer
834 .sequential_pipeline(handler, SequentialConfig::default())
835 .await
836 .unwrap();
837 }
838
839 async fn multi_cohort_setup(
846 store: &FallibleMockStore,
847 near: &str,
848 far: &str,
849 ) -> tempfile::TempDir {
850 let mut conn = store.connect().await.unwrap();
851 set_committer_watermark(&mut conn, near, 100_100).await;
852 set_committer_watermark(&mut conn, far, 9).await;
853
854 let temp_dir = init_ingestion_dir(Some(100_000));
855 synthetic_ingestion::generate_ingestion(synthetic_ingestion::Config {
856 ingestion_dir: temp_dir.path().to_owned(),
857 starting_checkpoint: 0,
858 num_checkpoints: 60,
859 checkpoint_size: 1,
860 })
861 .await;
862
863 temp_dir
864 }
865
866 macro_rules! assert_out_of_order {
867 ($metrics:expr, $pipeline:expr, $expected:expr) => {
868 assert_eq!(
869 $metrics
870 .total_watermarks_out_of_order
871 .get_metric_with_label_values(&[$pipeline])
872 .unwrap()
873 .get(),
874 $expected,
875 );
876 };
877 }
878
879 async fn test_init_watermark(
880 first_checkpoint: Option<u64>,
881 is_concurrent: bool,
882 ) -> (Option<CommitterWatermark>, Option<PrunerWatermark>) {
883 let registry = Registry::new();
884 let store = FallibleMockStore::default();
885
886 test_pipeline!(A, "pipeline_name");
887
888 let mut conn = store.connect().await.unwrap();
889
890 let indexer_args = IndexerArgs {
891 first_checkpoint,
892 ..IndexerArgs::default()
893 };
894 let (mut indexer, _temp_dir) =
895 create_test_indexer(store.clone(), indexer_args, ®istry, None).await;
896
897 if is_concurrent {
898 add_concurrent(&mut indexer, A).await;
899 } else {
900 add_sequential(&mut indexer, A).await;
901 }
902
903 (
904 conn.committer_watermark(A::NAME).await.unwrap(),
905 conn.pruner_watermark(A::NAME, Duration::ZERO)
906 .await
907 .unwrap(),
908 )
909 }
910
911 const LATEST_CHECKPOINT: u64 = 10;
912
913 async fn test_next_checkpoint(
918 watermark: Option<u64>,
919 first_checkpoint: Option<u64>,
920 concurrent_config: ConcurrentConfig,
921 ) -> Indexer<FallibleMockStore> {
922 let registry = Registry::new();
923 let store = FallibleMockStore::default();
924
925 test_pipeline!(A, "concurrent_a");
926
927 if let Some(checkpoint_hi_inclusive) = watermark {
928 let mut conn = store.connect().await.unwrap();
929 conn.set_committer_watermark(
930 A::NAME,
931 CommitterWatermark {
932 checkpoint_hi_inclusive,
933 ..Default::default()
934 },
935 )
936 .await
937 .unwrap();
938 }
939
940 let temp_dir = init_ingestion_dir(Some(LATEST_CHECKPOINT));
941 let mut indexer = Indexer::new(
942 store,
943 IndexerArgs {
944 first_checkpoint,
945 ..Default::default()
946 },
947 ClientArgs {
948 ingestion: IngestionClientArgs {
949 local_ingestion_path: Some(temp_dir.path().to_owned()),
950 ..Default::default()
951 },
952 ..Default::default()
953 },
954 IngestionConfig::default(),
955 None,
956 ®istry,
957 )
958 .await
959 .unwrap();
960
961 assert_eq!(indexer.latest_checkpoint, LATEST_CHECKPOINT);
962
963 indexer
964 .concurrent_pipeline::<A>(A, concurrent_config)
965 .await
966 .unwrap();
967
968 indexer
969 }
970
971 fn pruner_config(retention: u64) -> ConcurrentConfig {
972 ConcurrentConfig {
973 pruner: Some(concurrent::PrunerConfig {
974 retention,
975 ..Default::default()
976 }),
977 ..Default::default()
978 }
979 }
980
981 #[test]
982 fn test_arg_parsing() {
983 #[derive(Parser)]
984 struct Args {
985 #[clap(flatten)]
986 indexer: IndexerArgs,
987 }
988
989 let args = Args::try_parse_from([
990 "cmd",
991 "--first-checkpoint",
992 "10",
993 "--last-checkpoint",
994 "100",
995 "--pipeline",
996 "a",
997 "--pipeline",
998 "b",
999 "--task",
1000 "t",
1001 "--reader-interval-ms",
1002 "5000",
1003 ])
1004 .unwrap();
1005
1006 assert_eq!(args.indexer.first_checkpoint, Some(10));
1007 assert_eq!(args.indexer.last_checkpoint, Some(100));
1008 assert_eq!(args.indexer.pipeline, vec!["a", "b"]);
1009 assert_eq!(args.indexer.task.task, Some("t".to_owned()));
1010 assert_eq!(args.indexer.task.reader_interval_ms, Some(5000));
1011 }
1012
1013 #[tokio::test]
1015 async fn test_next_checkpoint_all_pipelines_have_watermarks() {
1016 let registry = Registry::new();
1017 let store = FallibleMockStore::default();
1018
1019 test_pipeline!(A, "concurrent_a");
1020 test_pipeline!(B, "concurrent_b");
1021 test_pipeline!(C, "sequential_c");
1022 test_pipeline!(D, "sequential_d");
1023
1024 let mut conn = store.connect().await.unwrap();
1025
1026 conn.init_watermark(A::NAME, Some(0)).await.unwrap();
1027 set_committer_watermark(&mut conn, A::NAME, 100).await;
1028
1029 conn.init_watermark(B::NAME, Some(0)).await.unwrap();
1030 set_committer_watermark(&mut conn, B::NAME, 10).await;
1031
1032 conn.init_watermark(C::NAME, Some(0)).await.unwrap();
1033 set_committer_watermark(&mut conn, C::NAME, 1).await;
1034
1035 conn.init_watermark(D::NAME, Some(0)).await.unwrap();
1036 set_committer_watermark(&mut conn, D::NAME, 50).await;
1037
1038 let (mut indexer, _temp_dir) =
1039 create_test_indexer(store, IndexerArgs::default(), ®istry, None).await;
1040
1041 add_concurrent(&mut indexer, A).await;
1042 add_concurrent(&mut indexer, B).await;
1043 add_sequential(&mut indexer, C).await;
1044 add_sequential(&mut indexer, D).await;
1045
1046 assert_eq!(indexer.first_checkpoint, None);
1047 assert_eq!(indexer.last_checkpoint, None);
1048 assert_eq!(indexer.latest_checkpoint, 0);
1049 assert_eq!(indexer.next_checkpoint, 2);
1050 assert_eq!(indexer.next_sequential_checkpoint, Some(2));
1051 }
1052
1053 #[tokio::test]
1055 async fn test_next_checkpoint_not_all_pipelines_have_watermarks() {
1056 let registry = Registry::new();
1057 let store = FallibleMockStore::default();
1058
1059 test_pipeline!(A, "concurrent_a");
1060 test_pipeline!(B, "concurrent_b");
1061 test_pipeline!(C, "sequential_c");
1062 test_pipeline!(D, "sequential_d");
1063
1064 let mut conn = store.connect().await.unwrap();
1065 set_committer_watermark(&mut conn, B::NAME, 10).await;
1066 set_committer_watermark(&mut conn, C::NAME, 1).await;
1067
1068 let (mut indexer, _temp_dir) =
1069 create_test_indexer(store, IndexerArgs::default(), ®istry, None).await;
1070
1071 add_concurrent(&mut indexer, A).await;
1072 add_concurrent(&mut indexer, B).await;
1073 add_sequential(&mut indexer, C).await;
1074 add_sequential(&mut indexer, D).await;
1075
1076 assert_eq!(indexer.first_checkpoint, None);
1077 assert_eq!(indexer.last_checkpoint, None);
1078 assert_eq!(indexer.latest_checkpoint, 0);
1079 assert_eq!(indexer.next_checkpoint, 0);
1080 assert_eq!(indexer.next_sequential_checkpoint, Some(0));
1081 }
1082
1083 #[tokio::test]
1085 async fn test_next_checkpoint_smallest_is_0() {
1086 let registry = Registry::new();
1087 let store = FallibleMockStore::default();
1088
1089 test_pipeline!(A, "concurrent_a");
1090 test_pipeline!(B, "concurrent_b");
1091 test_pipeline!(C, "sequential_c");
1092 test_pipeline!(D, "sequential_d");
1093
1094 let mut conn = store.connect().await.unwrap();
1095 set_committer_watermark(&mut conn, A::NAME, 100).await;
1096 set_committer_watermark(&mut conn, B::NAME, 10).await;
1097 set_committer_watermark(&mut conn, C::NAME, 1).await;
1098 set_committer_watermark(&mut conn, D::NAME, 0).await;
1099
1100 let (mut indexer, _temp_dir) =
1101 create_test_indexer(store, IndexerArgs::default(), ®istry, None).await;
1102
1103 add_concurrent(&mut indexer, A).await;
1104 add_concurrent(&mut indexer, B).await;
1105 add_sequential(&mut indexer, C).await;
1106 add_sequential(&mut indexer, D).await;
1107
1108 assert_eq!(indexer.next_checkpoint, 1);
1109 }
1110
1111 #[tokio::test]
1114 async fn test_next_checkpoint_first_checkpoint_and_no_watermark() {
1115 let registry = Registry::new();
1116 let store = FallibleMockStore::default();
1117
1118 test_pipeline!(A, "concurrent_a");
1119 test_pipeline!(B, "concurrent_b");
1120 test_pipeline!(C, "sequential_c");
1121 test_pipeline!(D, "sequential_d");
1122
1123 let mut conn = store.connect().await.unwrap();
1124 set_committer_watermark(&mut conn, B::NAME, 50).await;
1125 set_committer_watermark(&mut conn, C::NAME, 10).await;
1126
1127 let indexer_args = IndexerArgs {
1128 first_checkpoint: Some(5),
1129 ..Default::default()
1130 };
1131 let (mut indexer, _temp_dir) =
1132 create_test_indexer(store, indexer_args, ®istry, None).await;
1133
1134 add_concurrent(&mut indexer, A).await;
1135 add_concurrent(&mut indexer, B).await;
1136 add_sequential(&mut indexer, C).await;
1137 add_sequential(&mut indexer, D).await;
1138
1139 assert_eq!(indexer.first_checkpoint, Some(5));
1140 assert_eq!(indexer.last_checkpoint, None);
1141 assert_eq!(indexer.latest_checkpoint, 0);
1142 assert_eq!(indexer.next_checkpoint, 5);
1143 assert_eq!(indexer.next_sequential_checkpoint, Some(5));
1144 }
1145
1146 #[tokio::test]
1149 async fn test_next_checkpoint_ignore_first_checkpoint() {
1150 let registry = Registry::new();
1151 let store = FallibleMockStore::default();
1152
1153 test_pipeline!(B, "concurrent_b");
1154 test_pipeline!(C, "sequential_c");
1155
1156 let mut conn = store.connect().await.unwrap();
1157 set_committer_watermark(&mut conn, B::NAME, 50).await;
1158 set_committer_watermark(&mut conn, C::NAME, 10).await;
1159
1160 let indexer_args = IndexerArgs {
1161 first_checkpoint: Some(5),
1162 ..Default::default()
1163 };
1164 let (mut indexer, _temp_dir) =
1165 create_test_indexer(store, indexer_args, ®istry, None).await;
1166
1167 add_concurrent(&mut indexer, B).await;
1168 add_sequential(&mut indexer, C).await;
1169
1170 assert_eq!(indexer.first_checkpoint, Some(5));
1171 assert_eq!(indexer.last_checkpoint, None);
1172 assert_eq!(indexer.latest_checkpoint, 0);
1173 assert_eq!(indexer.next_checkpoint, 11);
1174 assert_eq!(indexer.next_sequential_checkpoint, Some(11));
1175 }
1176
1177 #[tokio::test]
1181 async fn test_next_checkpoint_large_first_checkpoint() {
1182 let registry = Registry::new();
1183 let store = FallibleMockStore::default();
1184
1185 test_pipeline!(A, "concurrent_a");
1186 test_pipeline!(B, "concurrent_b");
1187 test_pipeline!(C, "sequential_c");
1188
1189 let mut conn = store.connect().await.unwrap();
1190 set_committer_watermark(&mut conn, B::NAME, 50).await;
1191 set_committer_watermark(&mut conn, C::NAME, 10).await;
1192
1193 let indexer_args = IndexerArgs {
1194 first_checkpoint: Some(24),
1195 ..Default::default()
1196 };
1197 let (mut indexer, _temp_dir) =
1198 create_test_indexer(store, indexer_args, ®istry, None).await;
1199
1200 add_concurrent(&mut indexer, A).await;
1201 add_concurrent(&mut indexer, B).await;
1202 add_sequential(&mut indexer, C).await;
1203
1204 assert_eq!(indexer.first_checkpoint, Some(24));
1205 assert_eq!(indexer.last_checkpoint, None);
1206 assert_eq!(indexer.latest_checkpoint, 0);
1207 assert_eq!(indexer.next_checkpoint, 11);
1208 assert_eq!(indexer.next_sequential_checkpoint, Some(11));
1209 }
1210
1211 #[tokio::test]
1213 async fn test_latest_checkpoint_from_watermark() {
1214 let registry = Registry::new();
1215 let store = FallibleMockStore::default();
1216 let temp_dir = init_ingestion_dir(Some(30));
1217 let indexer = Indexer::new(
1218 store,
1219 IndexerArgs::default(),
1220 ClientArgs {
1221 ingestion: IngestionClientArgs {
1222 local_ingestion_path: Some(temp_dir.path().to_owned()),
1223 ..Default::default()
1224 },
1225 ..Default::default()
1226 },
1227 IngestionConfig::default(),
1228 None,
1229 ®istry,
1230 )
1231 .await
1232 .unwrap();
1233
1234 assert_eq!(indexer.latest_checkpoint, 30);
1235 }
1236
1237 #[tokio::test]
1240 async fn test_next_checkpoint_with_pruner_uses_retention() {
1241 let retention = LATEST_CHECKPOINT - 1;
1242 let indexer = test_next_checkpoint(None, None, pruner_config(retention)).await;
1243 assert_eq!(indexer.next_checkpoint, LATEST_CHECKPOINT - retention);
1244 }
1245
1246 #[tokio::test]
1248 async fn test_next_checkpoint_without_pruner_falls_back_to_genesis() {
1249 let indexer = test_next_checkpoint(None, None, ConcurrentConfig::default()).await;
1250 assert_eq!(indexer.next_checkpoint, 0);
1251 }
1252
1253 #[tokio::test]
1255 async fn test_next_checkpoint_watermark_takes_priority_over_pruner() {
1256 let retention = LATEST_CHECKPOINT - 1;
1257 let indexer = test_next_checkpoint(Some(5), None, pruner_config(retention)).await;
1258 assert_eq!(indexer.next_checkpoint, 6);
1259 }
1260
1261 #[tokio::test]
1264 async fn test_next_checkpoint_first_checkpoint_takes_priority_over_pruner() {
1265 let retention = LATEST_CHECKPOINT - 1;
1266 let indexer = test_next_checkpoint(None, Some(2), pruner_config(retention)).await;
1267 assert_eq!(indexer.next_checkpoint, 2);
1268 }
1269
1270 #[tokio::test]
1272 async fn test_next_checkpoint_retention_exceeds_latest_checkpoint() {
1273 let retention = LATEST_CHECKPOINT + 1;
1274 let indexer = test_next_checkpoint(None, None, pruner_config(retention)).await;
1275 assert_eq!(indexer.next_checkpoint, 0);
1276 }
1277
1278 #[tokio::test]
1280 async fn test_indexer_ingestion_existing_watermarks_no_first_checkpoint() {
1281 let registry = Registry::new();
1282 let store = FallibleMockStore::default();
1283
1284 test_pipeline!(A, "concurrent_a");
1285 test_pipeline!(B, "concurrent_b");
1286 test_pipeline!(C, "sequential_c");
1287 test_pipeline!(D, "sequential_d");
1288
1289 let mut conn = store.connect().await.unwrap();
1290 set_committer_watermark(&mut conn, A::NAME, 5).await;
1291 set_committer_watermark(&mut conn, B::NAME, 10).await;
1292 set_committer_watermark(&mut conn, C::NAME, 15).await;
1293 set_committer_watermark(&mut conn, D::NAME, 20).await;
1294
1295 let indexer_args = IndexerArgs {
1296 last_checkpoint: Some(29),
1297 ..Default::default()
1298 };
1299 let (mut indexer, _temp_dir) =
1300 create_test_indexer(store.clone(), indexer_args, ®istry, Some((30, 1))).await;
1301
1302 add_concurrent(&mut indexer, A).await;
1303 add_concurrent(&mut indexer, B).await;
1304 add_sequential(&mut indexer, C).await;
1305 add_sequential(&mut indexer, D).await;
1306
1307 let ingestion_metrics = indexer.ingestion_metrics().clone();
1308 let indexer_metrics = indexer.indexer_metrics().clone();
1309
1310 indexer.run().await.unwrap().join().await.unwrap();
1311
1312 assert_eq!(
1313 ingestion_metrics
1314 .total_ingested_checkpoints
1315 .with_label_values(&["0"])
1316 .get(),
1317 24
1318 );
1319 assert_out_of_order!(indexer_metrics, A::NAME, 0);
1320 assert_out_of_order!(indexer_metrics, B::NAME, 5);
1321 assert_out_of_order!(indexer_metrics, C::NAME, 10);
1322 assert_out_of_order!(indexer_metrics, D::NAME, 15);
1323 }
1324
1325 #[tokio::test]
1327 async fn test_indexer_ingestion_existing_watermarks_ignore_first_checkpoint() {
1328 let registry = Registry::new();
1329 let store = FallibleMockStore::default();
1330
1331 test_pipeline!(A, "concurrent_a");
1332 test_pipeline!(B, "concurrent_b");
1333 test_pipeline!(C, "sequential_c");
1334 test_pipeline!(D, "sequential_d");
1335
1336 let mut conn = store.connect().await.unwrap();
1337 set_committer_watermark(&mut conn, A::NAME, 5).await;
1338 set_committer_watermark(&mut conn, B::NAME, 10).await;
1339 set_committer_watermark(&mut conn, C::NAME, 15).await;
1340 set_committer_watermark(&mut conn, D::NAME, 20).await;
1341
1342 let indexer_args = IndexerArgs {
1343 first_checkpoint: Some(3),
1344 last_checkpoint: Some(29),
1345 ..Default::default()
1346 };
1347 let (mut indexer, _temp_dir) =
1348 create_test_indexer(store.clone(), indexer_args, ®istry, Some((30, 1))).await;
1349
1350 add_concurrent(&mut indexer, A).await;
1351 add_concurrent(&mut indexer, B).await;
1352 add_sequential(&mut indexer, C).await;
1353 add_sequential(&mut indexer, D).await;
1354
1355 let ingestion_metrics = indexer.ingestion_metrics().clone();
1356 let metrics = indexer.indexer_metrics().clone();
1357 indexer.run().await.unwrap().join().await.unwrap();
1358
1359 assert_eq!(
1360 ingestion_metrics
1361 .total_ingested_checkpoints
1362 .with_label_values(&["0"])
1363 .get(),
1364 24
1365 );
1366 assert_out_of_order!(metrics, A::NAME, 0);
1367 assert_out_of_order!(metrics, B::NAME, 5);
1368 assert_out_of_order!(metrics, C::NAME, 10);
1369 assert_out_of_order!(metrics, D::NAME, 15);
1370 }
1371
1372 #[tokio::test]
1374 async fn test_indexer_ingestion_missing_watermarks_no_first_checkpoint() {
1375 let registry = Registry::new();
1376 let store = FallibleMockStore::default();
1377
1378 test_pipeline!(A, "concurrent_a");
1379 test_pipeline!(B, "concurrent_b");
1380 test_pipeline!(C, "sequential_c");
1381 test_pipeline!(D, "sequential_d");
1382
1383 let mut conn = store.connect().await.unwrap();
1384 set_committer_watermark(&mut conn, B::NAME, 10).await;
1385 set_committer_watermark(&mut conn, C::NAME, 15).await;
1386 set_committer_watermark(&mut conn, D::NAME, 20).await;
1387
1388 let indexer_args = IndexerArgs {
1389 last_checkpoint: Some(29),
1390 ..Default::default()
1391 };
1392 let (mut indexer, _temp_dir) =
1393 create_test_indexer(store.clone(), indexer_args, ®istry, Some((30, 1))).await;
1394
1395 add_concurrent(&mut indexer, A).await;
1396 add_concurrent(&mut indexer, B).await;
1397 add_sequential(&mut indexer, C).await;
1398 add_sequential(&mut indexer, D).await;
1399
1400 let ingestion_metrics = indexer.ingestion_metrics().clone();
1401 let metrics = indexer.indexer_metrics().clone();
1402 indexer.run().await.unwrap().join().await.unwrap();
1403
1404 assert_eq!(
1405 ingestion_metrics
1406 .total_ingested_checkpoints
1407 .with_label_values(&["0"])
1408 .get(),
1409 30
1410 );
1411 assert_out_of_order!(metrics, A::NAME, 0);
1412 assert_out_of_order!(metrics, B::NAME, 11);
1413 assert_out_of_order!(metrics, C::NAME, 16);
1414 assert_out_of_order!(metrics, D::NAME, 21);
1415 }
1416
1417 #[tokio::test]
1419 async fn test_indexer_ingestion_use_first_checkpoint() {
1420 let registry = Registry::new();
1421 let store = FallibleMockStore::default();
1422
1423 test_pipeline!(A, "concurrent_a");
1424 test_pipeline!(B, "concurrent_b");
1425 test_pipeline!(C, "sequential_c");
1426 test_pipeline!(D, "sequential_d");
1427
1428 let mut conn = store.connect().await.unwrap();
1429 set_committer_watermark(&mut conn, B::NAME, 10).await;
1430 set_committer_watermark(&mut conn, C::NAME, 15).await;
1431 set_committer_watermark(&mut conn, D::NAME, 20).await;
1432
1433 let indexer_args = IndexerArgs {
1434 first_checkpoint: Some(10),
1435 last_checkpoint: Some(29),
1436 ..Default::default()
1437 };
1438 let (mut indexer, _temp_dir) =
1439 create_test_indexer(store.clone(), indexer_args, ®istry, Some((30, 1))).await;
1440
1441 add_concurrent(&mut indexer, A).await;
1442 add_concurrent(&mut indexer, B).await;
1443 add_sequential(&mut indexer, C).await;
1444 add_sequential(&mut indexer, D).await;
1445
1446 let ingestion_metrics = indexer.ingestion_metrics().clone();
1447 let metrics = indexer.indexer_metrics().clone();
1448 indexer.run().await.unwrap().join().await.unwrap();
1449
1450 assert_eq!(
1451 ingestion_metrics
1452 .total_ingested_checkpoints
1453 .with_label_values(&["0"])
1454 .get(),
1455 20
1456 );
1457 assert_out_of_order!(metrics, A::NAME, 0);
1458 assert_out_of_order!(metrics, B::NAME, 1);
1459 assert_out_of_order!(metrics, C::NAME, 6);
1460 assert_out_of_order!(metrics, D::NAME, 11);
1461 }
1462
1463 #[tokio::test]
1464 async fn test_init_watermark_concurrent_no_first_checkpoint() {
1465 let (committer_watermark, pruner_watermark) = test_init_watermark(None, true).await;
1466 assert_eq!(committer_watermark, None);
1467 assert_eq!(pruner_watermark, None);
1468 }
1469
1470 #[tokio::test]
1471 async fn test_init_watermark_concurrent_first_checkpoint_0() {
1472 let (committer_watermark, pruner_watermark) = test_init_watermark(Some(0), true).await;
1473 assert_eq!(committer_watermark, None);
1474 assert_eq!(pruner_watermark, None);
1475 }
1476
1477 #[tokio::test]
1478 async fn test_init_watermark_concurrent_first_checkpoint_1() {
1479 let (committer_watermark, pruner_watermark) = test_init_watermark(Some(1), true).await;
1480
1481 let committer_watermark = committer_watermark.unwrap();
1482 assert_eq!(committer_watermark.checkpoint_hi_inclusive, 0);
1483
1484 let pruner_watermark = pruner_watermark.unwrap();
1485 assert_eq!(pruner_watermark.reader_lo, 1);
1486 assert_eq!(pruner_watermark.pruner_hi, 1);
1487 }
1488
1489 #[tokio::test]
1490 async fn test_init_watermark_sequential() {
1491 let (committer_watermark, pruner_watermark) = test_init_watermark(Some(1), false).await;
1492
1493 let committer_watermark = committer_watermark.unwrap();
1494 assert_eq!(committer_watermark.checkpoint_hi_inclusive, 0);
1495
1496 let pruner_watermark = pruner_watermark.unwrap();
1497 assert_eq!(pruner_watermark.reader_lo, 1);
1498 assert_eq!(pruner_watermark.pruner_hi, 1);
1499 }
1500
1501 #[tokio::test]
1502 async fn test_multiple_sequential_pipelines_next_checkpoint() {
1503 let registry = Registry::new();
1504 let store = FallibleMockStore::default();
1505
1506 let mut conn = store.connect().await.unwrap();
1507 set_committer_watermark(&mut conn, MockHandler::NAME, 10).await;
1508 set_committer_watermark(&mut conn, SequentialHandler::NAME, 5).await;
1509
1510 let indexer_args = IndexerArgs {
1511 first_checkpoint: None,
1512 last_checkpoint: Some(19),
1513 pipeline: vec![],
1514 ..Default::default()
1515 };
1516 let (mut indexer, _temp_dir) =
1517 create_test_indexer(store.clone(), indexer_args, ®istry, Some((20, 2))).await;
1518
1519 add_sequential(&mut indexer, MockHandler).await;
1521
1522 assert_eq!(
1524 indexer.next_sequential_checkpoint(),
1525 Some(11),
1526 "next_sequential_checkpoint should be 11"
1527 );
1528
1529 add_sequential(&mut indexer, SequentialHandler).await;
1531
1532 assert_eq!(
1534 indexer.next_sequential_checkpoint(),
1535 Some(6),
1536 "next_sequential_checkpoint should still be 6"
1537 );
1538
1539 indexer.run().await.unwrap().join().await.unwrap();
1541
1542 let watermark1 = conn.committer_watermark(MockHandler::NAME).await.unwrap();
1544 let watermark2 = conn
1545 .committer_watermark(SequentialHandler::NAME)
1546 .await
1547 .unwrap();
1548
1549 assert_eq!(watermark1.unwrap().checkpoint_hi_inclusive, 19);
1550 assert_eq!(watermark2.unwrap().checkpoint_hi_inclusive, 19);
1551 }
1552
1553 #[tokio::test]
1557 async fn test_tasked_pipelines_ignore_below_main_reader_lo() {
1558 let registry = Registry::new();
1559 let store = FallibleMockStore::default();
1560
1561 let mut conn = store.connect().await.unwrap();
1564 set_committer_watermark(&mut conn, MockCheckpointSequenceNumberHandler::NAME, 10).await;
1565 conn.set_reader_watermark(MockCheckpointSequenceNumberHandler::NAME, 7)
1566 .await
1567 .unwrap();
1568
1569 let indexer_args = IndexerArgs {
1572 first_checkpoint: Some(0),
1573 last_checkpoint: Some(15),
1574 task: TaskArgs::tasked("task".to_string(), 10),
1575 ..Default::default()
1576 };
1577 let (mut tasked_indexer, _temp_dir) =
1578 create_test_indexer(store.clone(), indexer_args, ®istry, Some((16, 2))).await;
1579
1580 add_concurrent(&mut tasked_indexer, MockCheckpointSequenceNumberHandler).await;
1581
1582 let ingestion_metrics = tasked_indexer.ingestion_metrics().clone();
1583 let metrics = tasked_indexer.indexer_metrics().clone();
1584
1585 tasked_indexer.run().await.unwrap().join().await.unwrap();
1586
1587 assert_eq!(
1588 ingestion_metrics
1589 .total_ingested_checkpoints
1590 .with_label_values(&["0"])
1591 .get(),
1592 16
1593 );
1594 assert_eq!(
1595 metrics
1596 .total_collector_skipped_checkpoints
1597 .get_metric_with_label_values(&[MockCheckpointSequenceNumberHandler::NAME])
1598 .unwrap()
1599 .get(),
1600 7
1601 );
1602 let data = store
1603 .data
1604 .get(MockCheckpointSequenceNumberHandler::NAME)
1605 .unwrap();
1606 assert_eq!(data.len(), 9);
1607 for i in 0..7 {
1608 assert!(data.get(&i).is_none());
1609 }
1610 for i in 7..16 {
1611 assert!(data.get(&i).is_some());
1612 }
1613 }
1614
1615 #[tokio::test]
1617 async fn test_tasked_pipelines_surpass_main_pipeline_committer_hi() {
1618 let registry = Registry::new();
1619 let store = FallibleMockStore::default();
1620
1621 let mut conn = store.connect().await.unwrap();
1622 set_committer_watermark(&mut conn, "test", 10).await;
1623 conn.set_reader_watermark("test", 5).await.unwrap();
1624
1625 let indexer_args = IndexerArgs {
1628 first_checkpoint: Some(9),
1629 last_checkpoint: Some(25),
1630 task: TaskArgs::tasked("task".to_string(), 10),
1631 ..Default::default()
1632 };
1633 let (mut tasked_indexer, _temp_dir) =
1634 create_test_indexer(store.clone(), indexer_args, ®istry, Some((26, 2))).await;
1635
1636 add_concurrent(&mut tasked_indexer, MockCheckpointSequenceNumberHandler).await;
1637
1638 let ingestion_metrics = tasked_indexer.ingestion_metrics().clone();
1639 let metrics = tasked_indexer.indexer_metrics().clone();
1640
1641 tasked_indexer.run().await.unwrap().join().await.unwrap();
1642
1643 assert_eq!(
1644 ingestion_metrics
1645 .total_ingested_checkpoints
1646 .with_label_values(&["0"])
1647 .get(),
1648 17
1649 );
1650 assert_out_of_order!(metrics, "test", 0);
1651 assert_eq!(
1652 metrics
1653 .total_collector_skipped_checkpoints
1654 .get_metric_with_label_values(&[MockCheckpointSequenceNumberHandler::NAME])
1655 .unwrap()
1656 .get(),
1657 0
1658 );
1659
1660 let data = store.data.get("test").unwrap();
1661 assert_eq!(data.len(), 17);
1662 for i in 0..9 {
1663 assert!(data.get(&i).is_none());
1664 }
1665 for i in 9..26 {
1666 assert!(data.get(&i).is_some());
1667 }
1668 let main_pipeline_watermark = store.watermark("test").unwrap();
1669 assert_eq!(main_pipeline_watermark.checkpoint_hi_inclusive, Some(10));
1671 assert_eq!(main_pipeline_watermark.reader_lo, 5);
1672 let tasked_pipeline_watermark = store.watermark("test@task").unwrap();
1673 assert_eq!(tasked_pipeline_watermark.checkpoint_hi_inclusive, Some(25));
1674 assert_eq!(tasked_pipeline_watermark.reader_lo, 9);
1675 }
1676
1677 #[tokio::test]
1680 async fn test_tasked_pipelines_skip_checkpoints_trailing_main_reader_lo() {
1681 let registry = Registry::new();
1682 let store = FallibleMockStore::default();
1683 let mut conn = store.connect().await.unwrap();
1684 set_committer_watermark(&mut conn, ControllableHandler::NAME, 11).await;
1686
1687 let indexer_args = IndexerArgs {
1689 first_checkpoint: Some(0),
1690 last_checkpoint: Some(500),
1691 task: TaskArgs::tasked("task".to_string(), 10 ),
1692 ..Default::default()
1693 };
1694 let (mut tasked_indexer, _temp_dir) =
1695 create_test_indexer(store.clone(), indexer_args, ®istry, Some((501, 2))).await;
1696 let mut allow_process = 10;
1697 let (controllable_handler, process_below) = ControllableHandler::with_limit(allow_process);
1699 let _ = tasked_indexer
1700 .concurrent_pipeline(
1701 controllable_handler,
1702 ConcurrentConfig {
1703 committer: CommitterConfig {
1704 collect_interval_ms: 10,
1705 watermark_interval_ms: 10,
1706 ..Default::default()
1707 },
1708 fanout: Some(ConcurrencyConfig::Fixed { value: 501 }),
1711 ..Default::default()
1712 },
1713 )
1714 .await;
1715 let metrics = tasked_indexer.indexer_metrics().clone();
1716
1717 let mut s_indexer = tasked_indexer.run().await.unwrap();
1718
1719 store
1723 .wait_for_watermark(
1724 &pipeline_task::<FallibleMockStore>(ControllableHandler::NAME, Some("task"))
1725 .unwrap(),
1726 10,
1727 Duration::from_secs(10),
1728 )
1729 .await;
1730
1731 conn.set_reader_watermark(ControllableHandler::NAME, 250)
1736 .await
1737 .unwrap();
1738
1739 let reader_lo = metrics
1740 .collector_reader_lo
1741 .with_label_values(&[ControllableHandler::NAME]);
1742
1743 let mut interval = tokio::time::interval(Duration::from_millis(10));
1747 while reader_lo.get() != 250 {
1748 interval.tick().await;
1749 allow_process += 1;
1751 assert!(
1752 allow_process <= 500,
1753 "Released all checkpoints but collector never observed new reader_lo"
1754 );
1755 process_below.send(allow_process).ok();
1756 }
1757
1758 process_below.send(500).ok();
1765
1766 s_indexer.join().await.unwrap();
1767
1768 let data = store.data.get(ControllableHandler::NAME).unwrap();
1769
1770 for chkpt in (allow_process + 1)..250 {
1772 assert!(
1773 data.get(&chkpt).is_none(),
1774 "Checkpoint {chkpt} should have been skipped"
1775 );
1776 }
1777
1778 for chkpt in 250..=500 {
1780 assert!(
1781 data.get(&chkpt).is_some(),
1782 "Checkpoint {chkpt} should have been committed (>= reader_lo)"
1783 );
1784 }
1785
1786 for chkpt in 0..=10 {
1788 assert!(
1789 data.get(&chkpt).is_some(),
1790 "Checkpoint {chkpt} should have been committed (baseline)"
1791 );
1792 }
1793 }
1794
1795 #[tokio::test]
1799 async fn test_multi_cohort_ingestion() {
1800 let registry = Registry::new();
1801 let store = FallibleMockStore::default();
1802
1803 test_pipeline!(A, "near_tip");
1804 test_pipeline!(B, "far_behind");
1805
1806 let temp_dir = multi_cohort_setup(&store, A::NAME, B::NAME).await;
1807
1808 let mut indexer = Indexer::new(
1809 store.clone(),
1810 IndexerArgs {
1811 last_checkpoint: Some(59),
1812 ..Default::default()
1813 },
1814 ClientArgs {
1815 ingestion: IngestionClientArgs {
1816 local_ingestion_path: Some(temp_dir.path().to_owned()),
1817 ..Default::default()
1818 },
1819 ..Default::default()
1820 },
1821 IngestionConfig::default(),
1822 None,
1823 ®istry,
1824 )
1825 .await
1826 .unwrap();
1827
1828 add_concurrent(&mut indexer, A).await;
1829 add_concurrent(&mut indexer, B).await;
1830
1831 let ingestion_metrics = indexer.ingestion_metrics().clone();
1832 let indexer_metrics = indexer.indexer_metrics().clone();
1833 indexer.run().await.unwrap().join().await.unwrap();
1834
1835 assert_eq!(
1839 ingestion_metrics
1840 .total_ingested_checkpoints
1841 .with_label_values(&["0"])
1842 .get(),
1843 0
1844 );
1845 assert_eq!(
1846 ingestion_metrics
1847 .total_ingested_checkpoints
1848 .with_label_values(&["1"])
1849 .get(),
1850 50
1851 );
1852
1853 let received = |name| {
1856 indexer_metrics
1857 .total_handler_checkpoints_received
1858 .get_metric_with_label_values(&[name])
1859 .unwrap()
1860 .get()
1861 };
1862 assert_eq!(received(A::NAME), 0);
1863 assert_eq!(received(B::NAME), 50);
1864
1865 let watermark = store.watermark(B::NAME).unwrap();
1868 assert_eq!(watermark.checkpoint_hi_inclusive, Some(59));
1869 let watermark = store.watermark(A::NAME).unwrap();
1870 assert_eq!(watermark.checkpoint_hi_inclusive, Some(100_100));
1871 }
1872
1873 #[tokio::test]
1876 async fn test_multi_cohort_ingestion_with_clients() {
1877 let registry = Registry::new();
1878 let store = FallibleMockStore::default();
1879
1880 test_pipeline!(A, "near_tip");
1881 test_pipeline!(B, "far_behind");
1882
1883 let temp_dir = multi_cohort_setup(&store, A::NAME, B::NAME).await;
1884
1885 let ingestion_metrics = IngestionMetrics::new(None, ®istry);
1886 let ingestion_client = IngestionClient::new(
1887 IngestionClientArgs {
1888 local_ingestion_path: Some(temp_dir.path().to_owned()),
1889 ..Default::default()
1890 },
1891 ingestion_metrics.clone(),
1892 )
1893 .unwrap();
1894
1895 let mut indexer = Indexer::with_ingestion_clients(
1896 store.clone(),
1897 IndexerArgs {
1898 last_checkpoint: Some(59),
1899 ..Default::default()
1900 },
1901 ingestion_client,
1902 None,
1903 IngestionConfig::default(),
1904 None,
1905 ®istry,
1906 )
1907 .await
1908 .unwrap();
1909
1910 add_concurrent(&mut indexer, A).await;
1911 add_concurrent(&mut indexer, B).await;
1912
1913 let indexer_metrics = indexer.indexer_metrics().clone();
1914 indexer.run().await.unwrap().join().await.unwrap();
1915
1916 assert_eq!(
1919 ingestion_metrics
1920 .total_ingested_checkpoints
1921 .with_label_values(&["0"])
1922 .get(),
1923 0
1924 );
1925 assert_eq!(
1926 ingestion_metrics
1927 .total_ingested_checkpoints
1928 .with_label_values(&["1"])
1929 .get(),
1930 50
1931 );
1932 assert_eq!(
1933 indexer_metrics
1934 .total_handler_checkpoints_received
1935 .get_metric_with_label_values(&[A::NAME])
1936 .unwrap()
1937 .get(),
1938 0
1939 );
1940
1941 let watermark = store.watermark(B::NAME).unwrap();
1942 assert_eq!(watermark.checkpoint_hi_inclusive, Some(59));
1943 }
1944
1945 #[tokio::test]
1950 async fn test_near_tip_cohort_progresses_while_far_behind_stalls() {
1951 const TIP: u64 = 100;
1952
1953 let registry = Registry::new();
1954 let store = FallibleMockStore::default();
1955
1956 test_pipeline!(A, "near_tip");
1957
1958 let (far_behind, _release) = ControllableHandler::with_limit(9);
1962
1963 let temp_dir = init_ingestion_dir(Some(TIP));
1964 let mut conn = store.connect().await.unwrap();
1965 set_committer_watermark(&mut conn, A::NAME, 94).await; set_committer_watermark(&mut conn, ControllableHandler::NAME, 9).await; synthetic_ingestion::generate_ingestion(synthetic_ingestion::Config {
1968 ingestion_dir: temp_dir.path().to_owned(),
1969 starting_checkpoint: 0,
1970 num_checkpoints: TIP + 1, checkpoint_size: 1,
1972 })
1973 .await;
1974
1975 let mut indexer = Indexer::new(
1976 store.clone(),
1977 IndexerArgs {
1978 last_checkpoint: Some(TIP),
1979 ..Default::default()
1980 },
1981 ClientArgs {
1982 ingestion: IngestionClientArgs {
1983 local_ingestion_path: Some(temp_dir.path().to_owned()),
1984 ..Default::default()
1985 },
1986 ..Default::default()
1987 },
1988 IngestionConfig {
1991 min_cohort_boundary: 10,
1992 ..Default::default()
1993 },
1994 None,
1995 ®istry,
1996 )
1997 .await
1998 .unwrap();
1999
2000 indexer
2003 .concurrent_pipeline(
2004 A,
2005 ConcurrentConfig {
2006 committer: CommitterConfig {
2007 collect_interval_ms: 10,
2008 watermark_interval_ms: 10,
2009 ..Default::default()
2010 },
2011 ..Default::default()
2012 },
2013 )
2014 .await
2015 .unwrap();
2016 add_concurrent(&mut indexer, far_behind).await;
2018
2019 let service = indexer.run().await.unwrap();
2020
2021 store
2025 .wait_for_watermark(A::NAME, TIP, Duration::from_secs(10))
2026 .await;
2027
2028 assert_eq!(
2031 store
2032 .watermark(ControllableHandler::NAME)
2033 .unwrap()
2034 .checkpoint_hi_inclusive,
2035 Some(9),
2036 );
2037 assert!(store.data.get(ControllableHandler::NAME).is_none());
2038
2039 drop(service);
2041 }
2042}