1use std::num::NonZeroUsize;
5use std::sync::Arc;
6use std::time::Duration;
7
8use prometheus::Registry;
9use serde::Deserialize;
10use serde::Serialize;
11use sui_futures::service::Service;
12use tokio::sync::mpsc;
13use tracing::warn;
14
15use crate::cohort::DEFAULT_MIN_COHORT_BOUNDARY;
16pub use crate::config::ConcurrencyConfig as IngestConcurrencyConfig;
17use crate::ingestion::broadcaster::broadcaster;
18use crate::ingestion::error::Error;
19use crate::ingestion::error::Result;
20use crate::ingestion::ingestion_client::CheckpointEnvelope;
21use crate::ingestion::ingestion_client::IngestionClient;
22use crate::ingestion::ingestion_client::IngestionClientArgs;
23use crate::ingestion::ingestion_client::retry_transient_with_slow_monitor;
24use crate::ingestion::streaming_client::CheckpointStreamingClient;
25use crate::ingestion::streaming_client::GrpcStreamingClient;
26use crate::ingestion::streaming_client::StreamingClientArgs;
27use crate::metrics::IngestionMetrics;
28
29pub type ArcStreamingClient = Arc<dyn CheckpointStreamingClient + Send + Sync>;
38
39mod broadcaster;
40mod byte_count;
41pub(crate) mod decode;
42pub mod error;
43pub mod ingestion_client;
44mod rpc_client;
45pub mod store_client;
46pub mod streaming_client;
47#[cfg(test)]
48mod test_utils;
49
50pub(crate) const MAX_GRPC_MESSAGE_SIZE_BYTES: usize = 128 * 1024 * 1024;
51
52#[derive(clap::Args, Clone, Debug, Default)]
55pub struct ClientArgs {
56 #[clap(flatten)]
57 pub ingestion: IngestionClientArgs,
58
59 #[clap(flatten)]
60 pub streaming: StreamingClientArgs,
61}
62
63#[derive(Serialize, Deserialize, Debug, Clone)]
64#[serde(default)]
65pub struct IngestionConfig {
66 pub ingest_concurrency: IngestConcurrencyConfig,
70
71 pub retry_interval_ms: u64,
73
74 pub streaming_backoff_initial_batch_size: NonZeroUsize,
76
77 pub streaming_backoff_max_batch_size: usize,
79
80 pub streaming_connection_timeout_ms: u64,
82
83 pub streaming_statement_timeout_ms: u64,
85
86 pub min_cohort_boundary: u64,
91}
92
93pub struct IngestionService {
94 config: IngestionConfig,
95 ingestion_client: IngestionClient,
96 streaming_client: Option<ArcStreamingClient>,
97 gap_fill_client: Option<IngestionClient>,
98 subscribers: Vec<mpsc::Sender<Arc<CheckpointEnvelope>>>,
99}
100
101pub(crate) struct IngestionFactory {
109 config: IngestionConfig,
111
112 ingestion_client: IngestionClient,
115
116 streaming_client: Option<ArcStreamingClient>,
118
119 gap_fill_client: Option<IngestionClient>,
121}
122
123impl IngestionConfig {
124 pub fn retry_interval(&self) -> Duration {
125 Duration::from_millis(self.retry_interval_ms)
126 }
127
128 pub fn streaming_connection_timeout(&self) -> Duration {
129 Duration::from_millis(self.streaming_connection_timeout_ms)
130 }
131
132 pub fn streaming_statement_timeout(&self) -> Duration {
133 Duration::from_millis(self.streaming_statement_timeout_ms)
134 }
135}
136
137impl IngestionService {
138 pub async fn new(
149 args: ClientArgs,
150 config: IngestionConfig,
151 metrics_prefix: Option<&str>,
152 registry: &Registry,
153 ) -> Result<Self> {
154 let metrics = IngestionMetrics::new(metrics_prefix, registry);
155 let (ingestion_client, streaming_client, gap_fill_client) =
156 clients_from_args(args, &config, metrics).await?;
157 Ok(Self::with_clients(
158 ingestion_client,
159 streaming_client,
160 gap_fill_client,
161 config,
162 ))
163 }
164
165 pub fn with_clients(
177 ingestion_client: IngestionClient,
178 streaming_client: Option<ArcStreamingClient>,
179 gap_fill_client: Option<IngestionClient>,
180 config: IngestionConfig,
181 ) -> Self {
182 Self {
183 config,
184 ingestion_client,
185 streaming_client,
186 gap_fill_client,
187 subscribers: Vec::new(),
188 }
189 }
190
191 pub fn config(&self) -> &IngestionConfig {
193 &self.config
194 }
195
196 pub fn subscribe_bounded(&mut self, size: usize) -> mpsc::Receiver<Arc<CheckpointEnvelope>> {
203 let (tx, rx) = mpsc::channel(size);
204 self.subscribers.push(tx);
205 rx
206 }
207
208 pub async fn run<R>(self, checkpoints: R) -> Result<Service>
221 where
222 R: std::ops::RangeBounds<u64> + Send + 'static,
223 {
224 let IngestionService {
225 config,
226 ingestion_client,
227 streaming_client,
228 gap_fill_client,
229 subscribers,
230 } = self;
231
232 if subscribers.is_empty() {
233 return Err(Error::NoSubscribers);
234 }
235
236 Ok(broadcaster(
237 checkpoints,
238 streaming_client,
239 config,
240 ingestion_client,
241 gap_fill_client,
242 subscribers,
243 ))
244 }
245}
246
247impl IngestionFactory {
248 pub(crate) async fn new(
251 args: ClientArgs,
252 config: IngestionConfig,
253 metrics_prefix: Option<&str>,
254 registry: &Registry,
255 ) -> Result<Self> {
256 let metrics = IngestionMetrics::new(metrics_prefix, registry);
257 let (ingestion_client, streaming_client, gap_fill_client) =
258 clients_from_args(args, &config, metrics).await?;
259 Ok(Self::with_clients(
260 ingestion_client,
261 streaming_client,
262 gap_fill_client,
263 config,
264 ))
265 }
266
267 pub(crate) fn with_clients(
274 ingestion_client: IngestionClient,
275 streaming_client: Option<ArcStreamingClient>,
276 gap_fill_client: Option<IngestionClient>,
277 config: IngestionConfig,
278 ) -> Self {
279 Self {
280 config,
281 ingestion_client,
282 streaming_client,
283 gap_fill_client,
284 }
285 }
286
287 pub(crate) async fn latest_checkpoint_number(&self) -> anyhow::Result<u64> {
290 latest_checkpoint_number(self.streaming_client.as_deref(), &self.ingestion_client).await
291 }
292
293 pub(crate) fn ingestion_client(&self) -> &IngestionClient {
295 &self.ingestion_client
296 }
297
298 pub(crate) fn metrics(&self) -> &Arc<IngestionMetrics> {
300 self.ingestion_client.metrics()
301 }
302
303 pub(crate) fn config(&self) -> &IngestionConfig {
305 &self.config
306 }
307
308 pub(crate) fn create(&self, cohort: usize) -> IngestionService {
311 IngestionService::with_clients(
312 self.ingestion_client.for_cohort(cohort),
313 self.streaming_client.clone(),
314 self.gap_fill_client
315 .as_ref()
316 .map(|client| client.for_cohort(cohort)),
317 self.config.clone(),
318 )
319 }
320}
321
322impl Default for IngestionConfig {
323 fn default() -> Self {
324 Self {
325 ingest_concurrency: IngestConcurrencyConfig::Adaptive {
326 initial: 1,
327 min: 1,
328 max: 500,
329 dead_band: None,
330 },
331 retry_interval_ms: 200,
332 streaming_backoff_initial_batch_size: NonZeroUsize::new(10)
333 .expect("default initial streaming backoff is non-zero"), streaming_backoff_max_batch_size: 10000, streaming_connection_timeout_ms: 5000, streaming_statement_timeout_ms: 5000, min_cohort_boundary: DEFAULT_MIN_COHORT_BOUNDARY,
338 }
339 }
340}
341
342async fn clients_from_args(
345 args: ClientArgs,
346 config: &IngestionConfig,
347 metrics: Arc<IngestionMetrics>,
348) -> Result<(
349 IngestionClient,
350 Option<ArcStreamingClient>,
351 Option<IngestionClient>,
352)> {
353 let gap_fill_client = args
354 .streaming
355 .gap_fill_client(config.streaming_connection_timeout(), metrics.clone())
356 .await?;
357 let ingestion_client = IngestionClient::new(args.ingestion, metrics)?;
358 let streaming_client = args.streaming.streaming_url.map(|uri| {
359 Arc::new(GrpcStreamingClient::new(
360 uri,
361 config.streaming_connection_timeout(),
362 config.streaming_statement_timeout(),
363 )) as ArcStreamingClient
364 });
365 Ok((ingestion_client, streaming_client, gap_fill_client))
366}
367
368async fn latest_checkpoint_number<S>(
371 streaming_client: Option<&S>,
372 ingestion_client: &IngestionClient,
373) -> anyhow::Result<u64>
374where
375 S: CheckpointStreamingClient + Send + Sync + ?Sized,
376{
377 if let Some(streaming_client) = streaming_client {
378 match streaming_client.latest_checkpoint_number().await {
379 Ok(checkpoint_number) => return Ok(checkpoint_number),
380 Err(e) => {
381 warn!(
382 operation = "latest_checkpoint_number",
383 "Failed to get latest checkpoint number from streaming client: {e}"
384 );
385 }
386 }
387 }
388
389 let client = ingestion_client.clone();
390 let future = move || {
391 let client = client.clone();
392 async move {
393 client
394 .latest_checkpoint_number()
395 .await
396 .map_err(|e| backoff::Error::transient(Error::LatestCheckpointError(e)))
397 }
398 };
399
400 Ok(retry_transient_with_slow_monitor(
401 "latest_checkpoint_number",
402 future,
403 &ingestion_client
404 .metrics()
405 .ingested_latest_checkpoint_latency,
406 )
407 .await?)
408}
409
410#[cfg(test)]
411mod tests {
412 use std::sync::Mutex;
413
414 use axum::http::StatusCode;
415 use sui_futures::task::TaskGuard;
416 use url::Url;
417 use wiremock::MockServer;
418 use wiremock::Request;
419
420 use crate::ingestion::ingestion_client::tests::MockIngestionClient;
421 use crate::ingestion::store_client::tests::respond_with;
422 use crate::ingestion::store_client::tests::respond_with_chain_id;
423 use crate::ingestion::store_client::tests::status;
424 use crate::ingestion::streaming_client::test_utils::MockStreamingClient;
425 use crate::ingestion::test_utils::test_checkpoint_data;
426 use crate::metrics::IngestionMetrics;
427
428 use super::*;
429
430 const FALLBACK: u64 = 99;
431
432 fn mock_ingestion_client(latest_checkpoint: u64) -> IngestionClient {
433 let metrics = IngestionMetrics::new(None, &Registry::new());
434 let mock = MockIngestionClient {
435 latest_checkpoint,
436 ..Default::default()
437 };
438 IngestionClient::from_trait(Arc::new(mock), metrics)
439 }
440
441 async fn test_ingestion(uri: String, ingest_concurrency: usize) -> IngestionService {
442 let registry = Registry::new();
443 IngestionService::new(
444 ClientArgs {
445 ingestion: IngestionClientArgs {
446 remote_store_url: Some(Url::parse(&uri).unwrap()),
447 ..Default::default()
448 },
449 ..Default::default()
450 },
451 IngestionConfig {
452 ingest_concurrency: IngestConcurrencyConfig::Fixed {
453 value: ingest_concurrency,
454 },
455 ..Default::default()
456 },
457 None,
458 ®istry,
459 )
460 .await
461 .unwrap()
462 }
463
464 async fn test_subscriber(
465 stop_after: usize,
466 mut rx: mpsc::Receiver<Arc<CheckpointEnvelope>>,
467 ) -> TaskGuard<Vec<u64>> {
468 TaskGuard::new(tokio::spawn(async move {
469 let mut seqs = vec![];
470 for _ in 0..stop_after {
471 let Some(checkpoint_envelope) = rx.recv().await else {
472 break;
473 };
474
475 seqs.push(checkpoint_envelope.checkpoint.summary.sequence_number);
476 }
477
478 seqs
479 }))
480 }
481
482 #[tokio::test]
485 async fn fail_on_no_subscribers() {
486 let server = MockServer::start().await;
489 respond_with(&server, status(StatusCode::NOT_FOUND)).await;
490
491 let ingestion_service = test_ingestion(server.uri(), 1).await;
492
493 let res = ingestion_service.run(0..).await;
494 assert!(matches!(res, Err(Error::NoSubscribers)));
495 }
496
497 #[tokio::test]
500 async fn shutdown() {
501 let server = MockServer::start().await;
502 respond_with(
503 &server,
504 status(StatusCode::OK).set_body_bytes(test_checkpoint_data(42)),
505 )
506 .await;
507 respond_with_chain_id(&server).await;
508
509 let mut ingestion_service = test_ingestion(server.uri(), 1).await;
510
511 let rx = ingestion_service.subscribe_bounded(1);
512 let subscriber = test_subscriber(usize::MAX, rx).await;
513 let svc = ingestion_service.run(0..).await.unwrap();
514
515 svc.shutdown().await.unwrap();
516 subscriber.await.unwrap();
517 }
518
519 #[tokio::test]
522 async fn shutdown_on_subscriber_drop() {
523 let server = MockServer::start().await;
524 respond_with(
525 &server,
526 status(StatusCode::OK).set_body_bytes(test_checkpoint_data(42)),
527 )
528 .await;
529 respond_with_chain_id(&server).await;
530
531 let mut ingestion_service = test_ingestion(server.uri(), 1).await;
532
533 let rx = ingestion_service.subscribe_bounded(1);
534 let subscriber = test_subscriber(1, rx).await;
535 let mut svc = ingestion_service.run(0..).await.unwrap();
536
537 drop(subscriber);
538 svc.join().await.unwrap();
539 }
540
541 #[tokio::test]
544 async fn retry_on_not_found() {
545 let server = MockServer::start().await;
546 let times: Mutex<u64> = Mutex::new(0);
547 respond_with(&server, move |_: &Request| {
548 let mut times = times.lock().unwrap();
549 *times += 1;
550 match *times {
551 1..4 => status(StatusCode::OK).set_body_bytes(test_checkpoint_data(*times)),
552 4..6 => status(StatusCode::NOT_FOUND),
553 _ => status(StatusCode::OK).set_body_bytes(test_checkpoint_data(*times)),
554 }
555 })
556 .await;
557 respond_with_chain_id(&server).await;
558
559 let mut ingestion_service = test_ingestion(server.uri(), 1).await;
560
561 let rx = ingestion_service.subscribe_bounded(1);
562 let subscriber = test_subscriber(6, rx).await;
563 let _svc = ingestion_service.run(0..).await.unwrap();
564
565 let seqs = subscriber.await.unwrap();
566 assert_eq!(seqs, vec![0, 1, 2, 3, 6, 7]);
567 }
568
569 #[tokio::test]
571 async fn retry_on_transient_error() {
572 let server = MockServer::start().await;
573 let times: Mutex<u64> = Mutex::new(0);
574 respond_with(&server, move |_: &Request| {
575 let mut times = times.lock().unwrap();
576 *times += 1;
577 match *times {
578 1..4 => status(StatusCode::OK).set_body_bytes(test_checkpoint_data(*times)),
579 4..6 => status(StatusCode::REQUEST_TIMEOUT),
580 _ => status(StatusCode::OK).set_body_bytes(test_checkpoint_data(*times)),
581 }
582 })
583 .await;
584 respond_with_chain_id(&server).await;
585
586 let mut ingestion_service = test_ingestion(server.uri(), 1).await;
587
588 let rx = ingestion_service.subscribe_bounded(1);
589 let subscriber = test_subscriber(6, rx).await;
590 let _svc = ingestion_service.run(0..).await.unwrap();
591
592 let seqs = subscriber.await.unwrap();
593 assert_eq!(seqs, vec![0, 1, 2, 3, 6, 7]);
594 }
595
596 #[tokio::test]
600 async fn back_pressure_and_buffering() {
601 let server = MockServer::start().await;
602 let times: Mutex<u64> = Mutex::new(0);
603 respond_with(&server, move |_: &Request| {
604 let mut times = times.lock().unwrap();
605 *times += 1;
606 status(StatusCode::OK).set_body_bytes(test_checkpoint_data(*times))
607 })
608 .await;
609 respond_with_chain_id(&server).await;
610
611 let mut ingestion_service = test_ingestion(server.uri(), 1).await;
612 let size = 3;
613
614 let mut laggard = ingestion_service.subscribe_bounded(size);
616 async fn unblock(laggard: &mut mpsc::Receiver<Arc<CheckpointEnvelope>>) -> u64 {
617 let checkpoint_envelope = laggard.recv().await.unwrap();
618 checkpoint_envelope.checkpoint.summary.sequence_number
619 }
620
621 let rx = ingestion_service.subscribe_bounded(size);
622 let subscriber = test_subscriber(6, rx).await;
623 let _svc = ingestion_service.run(0..).await.unwrap();
624
625 assert_eq!(unblock(&mut laggard).await, 0);
630 assert_eq!(unblock(&mut laggard).await, 1);
631 assert_eq!(unblock(&mut laggard).await, 2);
632
633 let seqs = subscriber.await.unwrap();
634 assert_eq!(seqs, vec![0, 1, 2, 3, 4, 5]);
635 }
636
637 #[tokio::test]
638 async fn latest_checkpoint_number_no_streaming_client() {
639 let client = mock_ingestion_client(FALLBACK);
640 let result = latest_checkpoint_number(None::<&MockStreamingClient>, &client).await;
641 assert_eq!(result.unwrap(), FALLBACK);
642 }
643
644 #[tokio::test]
645 async fn latest_checkpoint_number_from_stream() {
646 let client = mock_ingestion_client(FALLBACK);
647 let streaming = Some(MockStreamingClient::new([42], None));
648 let result = latest_checkpoint_number(streaming.as_ref(), &client).await;
649 assert_eq!(result.unwrap(), 42);
650 }
651
652 #[tokio::test]
653 async fn latest_checkpoint_number_stream_error_falls_back() {
654 let client = mock_ingestion_client(FALLBACK);
655 let mut mock = MockStreamingClient::new(std::iter::empty::<u64>(), None);
656 mock.insert_error();
657 let result = latest_checkpoint_number(Some(&mock), &client).await;
658 assert_eq!(result.unwrap(), FALLBACK);
659 }
660
661 #[tokio::test]
662 async fn latest_checkpoint_number_empty_stream_falls_back() {
663 let client = mock_ingestion_client(FALLBACK);
664 let streaming = Some(MockStreamingClient::new(std::iter::empty::<u64>(), None));
665 let result = latest_checkpoint_number(streaming.as_ref(), &client).await;
666 assert_eq!(result.unwrap(), FALLBACK);
667 }
668
669 #[tokio::test]
670 async fn latest_checkpoint_number_connection_failure_falls_back() {
671 let client = mock_ingestion_client(FALLBACK);
672 let streaming = Some(
673 MockStreamingClient::new(std::iter::empty::<u64>(), None).fail_connection_times(1),
674 );
675 let result = latest_checkpoint_number(streaming.as_ref(), &client).await;
676 assert_eq!(result.unwrap(), FALLBACK);
677 }
678
679 #[test]
680 fn reject_zero_initial_streaming_backoff_batch_size() {
681 let mut config = serde_json::to_value(IngestionConfig::default()).unwrap();
682 config["streaming_backoff_initial_batch_size"] = serde_json::json!(0);
683
684 let error = serde_json::from_value::<IngestionConfig>(config).unwrap_err();
685 assert!(error.to_string().contains("nonzero"));
686 }
687
688 #[tokio::test]
691 async fn factory_from_args_shares_metrics() {
692 let registry = Registry::new();
693 let dir = tempfile::tempdir().unwrap();
694 let args = ClientArgs {
695 ingestion: IngestionClientArgs {
696 local_ingestion_path: Some(dir.path().to_owned()),
697 ..Default::default()
698 },
699 ..Default::default()
700 };
701
702 let factory = IngestionFactory::new(args, IngestionConfig::default(), None, ®istry)
703 .await
704 .unwrap();
705
706 let first = factory.create(0);
707 let second = factory.create(1);
708 assert!(Arc::ptr_eq(
709 first.ingestion_client.metrics(),
710 factory.metrics()
711 ));
712 assert!(Arc::ptr_eq(
713 second.ingestion_client.metrics(),
714 factory.metrics()
715 ));
716 assert!(
717 registry
718 .gather()
719 .iter()
720 .all(|family| !family.name().contains("cohort"))
721 );
722 }
723
724 #[tokio::test]
729 async fn factory_from_clients_shares_metrics() {
730 let registry = Registry::new();
731 let metrics = IngestionMetrics::new(None, ®istry);
732 let client = IngestionClient::from_trait(
733 Arc::new(MockIngestionClient {
734 latest_checkpoint: FALLBACK,
735 ..Default::default()
736 }),
737 metrics.clone(),
738 );
739
740 let factory = IngestionFactory::with_clients(
741 client,
742 Some(Arc::new(MockStreamingClient::new([42], None))),
743 None,
744 IngestionConfig::default(),
745 );
746
747 assert_eq!(factory.latest_checkpoint_number().await.unwrap(), 42);
748
749 let first = factory.create(0);
750 let second = factory.create(1);
751 assert!(Arc::ptr_eq(first.ingestion_client.metrics(), &metrics));
752 assert!(Arc::ptr_eq(second.ingestion_client.metrics(), &metrics));
753 assert!(first.streaming_client.is_some());
754 assert!(second.streaming_client.is_some());
755
756 assert_eq!(factory.latest_checkpoint_number().await.unwrap(), FALLBACK);
757 }
758}