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 MIN_EAGER_ROWS: usize = 50;
50
51 const MAX_PENDING_ROWS: usize = 5000;
57
58 const MAX_BATCH_CHECKPOINTS: usize = 5 * 60;
62
63 type Batch: Default + Send + Sync + 'static;
67
68 fn batch(&self, batch: &mut Self::Batch, values: std::vec::IntoIter<Self::Value>);
78
79 async fn commit<'a>(
82 &self,
83 batch: &Self::Batch,
84 conn: &mut <Self::Store as Store>::Connection<'a>,
85 ) -> anyhow::Result<usize>;
86}
87
88#[derive(Serialize, Deserialize, Debug, Clone, Default)]
90pub struct SequentialConfig {
91 pub committer: CommitterConfig,
93
94 pub ingestion: IngestionConfig,
96
97 pub fanout: Option<ConcurrencyConfig>,
99
100 pub min_eager_rows: Option<usize>,
102
103 pub max_pending_rows: Option<usize>,
105
106 pub max_batch_checkpoints: Option<usize>,
108
109 pub processor_channel_size: Option<usize>,
111
112 pub pipeline_depth: Option<usize>,
115}
116
117pub(crate) fn pipeline<H: Handler>(
135 handler: H,
136 next_checkpoint: u64,
137 config: SequentialConfig,
138 store: H::Store,
139 checkpoint_rx: mpsc::Receiver<Arc<CheckpointEnvelope>>,
140 metrics: Arc<IndexerMetrics>,
141) -> Service {
142 info!(
143 pipeline = H::NAME,
144 "Starting pipeline with config: {config:#?}",
145 );
146
147 let concurrency = config
148 .fanout
149 .clone()
150 .unwrap_or(ConcurrencyConfig::Adaptive {
151 initial: 1,
152 min: 1,
153 max: num_cpus::get().max(1),
154 dead_band: None,
155 });
156 let min_eager_rows = config.min_eager_rows.unwrap_or(H::MIN_EAGER_ROWS);
157 let max_pending_rows = config.max_pending_rows.unwrap_or(H::MAX_PENDING_ROWS);
158 let max_batch_checkpoints = config
159 .max_batch_checkpoints
160 .unwrap_or(H::MAX_BATCH_CHECKPOINTS);
161
162 let processor_channel_size = config
163 .processor_channel_size
164 .unwrap_or_else(|| num_cpus::get() / 2)
165 .max(1);
166 let (processor_tx, collector_rx) = mpsc::channel(processor_channel_size);
167
168 let pipeline_depth = config
169 .pipeline_depth
170 .unwrap_or_else(|| num_cpus::get() / 2)
171 .max(4);
172 let (collector_tx, committer_rx) = mpsc::channel::<BatchedRows<H>>(pipeline_depth);
173
174 let handler = Arc::new(handler);
175
176 let s_processor = processor(
177 handler.clone(),
178 checkpoint_rx,
179 processor_tx,
180 metrics.clone(),
181 concurrency,
182 store.clone(),
183 );
184
185 let s_collector = collector::<H>(
186 handler.clone(),
187 config,
188 next_checkpoint,
189 collector_rx,
190 metrics.clone(),
191 min_eager_rows,
192 max_pending_rows,
193 max_batch_checkpoints,
194 collector_tx,
195 );
196
197 let s_committer = committer::<H>(handler, store, metrics.clone(), committer_rx);
198
199 s_processor.merge(s_collector).merge(s_committer)
200}
201
202#[cfg(test)]
203mod tests {
204 use std::time::Duration;
205
206 use prometheus::Registry;
207 use sui_types::full_checkpoint_content::Checkpoint;
208
209 use crate::mocks::store::FallibleMockConnection;
210 use crate::mocks::store::FallibleMockStore;
211 use crate::pipeline::IndexedCheckpoint;
212
213 use super::*;
214
215 #[derive(Default)]
217 struct TestHandler;
218
219 #[async_trait]
220 impl Processor for TestHandler {
221 const NAME: &'static str = "test";
222 type Value = u64;
223
224 async fn process(&self, _checkpoint: &Arc<Checkpoint>) -> anyhow::Result<Vec<Self::Value>> {
225 Ok(vec![])
226 }
227 }
228
229 #[async_trait]
230 impl Handler for TestHandler {
231 type Store = FallibleMockStore;
232 type Batch = Vec<u64>;
233 const MAX_BATCH_CHECKPOINTS: usize = 3; const MIN_EAGER_ROWS: usize = 4; fn batch(&self, batch: &mut Self::Batch, values: std::vec::IntoIter<Self::Value>) {
237 batch.extend(values);
238 }
239
240 async fn commit<'a>(
241 &self,
242 batch: &Self::Batch,
243 conn: &mut FallibleMockConnection<'a>,
244 ) -> anyhow::Result<usize> {
245 if !batch.is_empty() {
246 let mut sequential_data = conn.0.sequential_checkpoint_data.lock().unwrap();
247 sequential_data.extend(batch.iter().cloned());
248 }
249 Ok(batch.len())
250 }
251 }
252
253 struct TestSetup {
254 store: FallibleMockStore,
255 checkpoint_tx: mpsc::Sender<IndexedCheckpoint<TestHandler>>,
256 #[allow(unused)]
257 service: Service,
258 }
259
260 fn setup_test(
264 next_checkpoint: u64,
265 config: SequentialConfig,
266 store: FallibleMockStore,
267 ) -> TestSetup {
268 let metrics = IndexerMetrics::new(None, &Registry::default());
269
270 let min_eager_rows = config.min_eager_rows.unwrap_or(TestHandler::MIN_EAGER_ROWS);
271 let max_pending_rows = config
272 .max_pending_rows
273 .unwrap_or(TestHandler::MAX_PENDING_ROWS);
274 let max_batch_checkpoints = config
275 .max_batch_checkpoints
276 .unwrap_or(TestHandler::MAX_BATCH_CHECKPOINTS);
277 let pipeline_depth = config
278 .pipeline_depth
279 .unwrap_or_else(|| num_cpus::get() / 2)
280 .max(4);
281
282 let (checkpoint_tx, checkpoint_rx) = mpsc::channel(10);
283 let (collector_tx, committer_rx) =
284 mpsc::channel::<BatchedRows<TestHandler>>(pipeline_depth);
285
286 let store_clone = store.clone();
287 let handler = Arc::new(TestHandler);
288
289 let s_collector = collector(
290 handler.clone(),
291 config,
292 next_checkpoint,
293 checkpoint_rx,
294 metrics.clone(),
295 min_eager_rows,
296 max_pending_rows,
297 max_batch_checkpoints,
298 collector_tx,
299 );
300 let s_committer = committer(handler, store_clone, metrics, committer_rx);
301
302 TestSetup {
303 store,
304 checkpoint_tx,
305 service: s_collector.merge(s_committer),
306 }
307 }
308
309 async fn send_checkpoint(setup: &mut TestSetup, checkpoint: u64) {
310 setup
311 .checkpoint_tx
312 .send(create_checkpoint(checkpoint))
313 .await
314 .unwrap();
315 }
316
317 fn create_checkpoint(checkpoint: u64) -> IndexedCheckpoint<TestHandler> {
318 IndexedCheckpoint::new(
319 checkpoint, checkpoint, checkpoint, checkpoint * 1000, vec![checkpoint], )
325 }
326
327 #[tokio::test]
328 async fn test_committer_processes_sequential_checkpoints() {
329 let config = SequentialConfig::default();
330 let mut setup = setup_test(0, config, FallibleMockStore::default());
331
332 for i in 0..3 {
334 send_checkpoint(&mut setup, i).await;
335 }
336
337 tokio::time::sleep(Duration::from_millis(200)).await;
339
340 assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2]);
342
343 let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
345 assert_eq!(watermark.checkpoint_hi_inclusive, Some(2));
346 assert_eq!(watermark.tx_hi, 2);
347 }
348
349 #[tokio::test]
352 async fn test_committer_processes_sequential_checkpoints_with_initial_watermark() {
353 let config = SequentialConfig::default();
354 let mut setup = setup_test(5, config, FallibleMockStore::default());
355
356 let watermark = setup.store.watermark(TestHandler::NAME);
358 assert!(watermark.is_none());
359
360 for i in 0..5 {
362 send_checkpoint(&mut setup, i).await;
363 }
364
365 tokio::time::sleep(Duration::from_millis(1000)).await;
367
368 let watermark = setup.store.watermark(TestHandler::NAME);
370 assert!(watermark.is_none());
371
372 for i in 5..8 {
373 send_checkpoint(&mut setup, i).await;
374 }
375
376 tokio::time::sleep(Duration::from_millis(1000)).await;
378
379 assert_eq!(setup.store.get_sequential_data(), vec![5, 6, 7]);
381
382 let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
384 assert_eq!(watermark.checkpoint_hi_inclusive, Some(7));
385 assert_eq!(watermark.tx_hi, 7);
386 }
387
388 #[tokio::test]
389 async fn test_committer_processes_out_of_order_checkpoints() {
390 let config = SequentialConfig::default();
391 let mut setup = setup_test(0, config, FallibleMockStore::default());
392
393 for i in [1, 0, 2] {
395 send_checkpoint(&mut setup, i).await;
396 }
397
398 tokio::time::sleep(Duration::from_millis(200)).await;
400
401 assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2]);
403
404 let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
406 assert_eq!(watermark.checkpoint_hi_inclusive, Some(2));
407 assert_eq!(watermark.tx_hi, 2);
408 }
409
410 #[tokio::test]
411 async fn test_committer_commit_up_to_max_batch_checkpoints() {
412 let config = SequentialConfig::default();
413 let mut setup = setup_test(0, config, FallibleMockStore::default());
414
415 for i in 0..4 {
417 send_checkpoint(&mut setup, i).await;
418 }
419
420 tokio::time::sleep(Duration::from_millis(200)).await;
422
423 assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2, 3]);
425 }
426
427 #[tokio::test]
428 async fn test_committer_commits_eagerly() {
429 let config = SequentialConfig {
430 committer: CommitterConfig {
431 collect_interval_ms: 4_000, ..Default::default()
433 },
434 ..Default::default()
435 };
436 let mut setup = setup_test(0, config, FallibleMockStore::default());
437
438 tokio::time::sleep(Duration::from_millis(200)).await;
440
441 for i in 0..3 {
443 send_checkpoint(&mut setup, i).await;
444 }
445
446 assert_eq!(setup.store.get_sequential_data(), Vec::<u64>::new());
448
449 send_checkpoint(&mut setup, 3).await;
451
452 tokio::time::sleep(Duration::from_millis(200)).await;
454
455 assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2, 3]);
457 }
458
459 #[tokio::test]
460 async fn test_committer_retries_on_transaction_failure() {
461 let config = SequentialConfig {
462 committer: CommitterConfig {
463 collect_interval_ms: 1_000, ..Default::default()
465 },
466 ..Default::default()
467 };
468
469 let store = FallibleMockStore::default().with_transaction_failures(1); let mut setup = setup_test(10, config, store);
473
474 send_checkpoint(&mut setup, 10).await;
476
477 tokio::time::sleep(Duration::from_millis(1_500)).await;
481
482 assert_eq!(setup.store.get_sequential_data(), vec![10]);
483 }
484
485 #[tokio::test]
489 async fn pipelined_commit_runs_under_slow_commit() {
490 let config = SequentialConfig {
491 committer: CommitterConfig::default(),
492 max_batch_checkpoints: Some(3),
493 min_eager_rows: Some(1),
494 pipeline_depth: Some(1),
495 ..Default::default()
496 };
497
498 let store = FallibleMockStore::default().with_commit_delay(700);
499 let mut setup = setup_test(0, config, store);
500
501 for i in 0..6 {
502 send_checkpoint(&mut setup, i).await;
503 }
504
505 tokio::time::sleep(Duration::from_millis(2_000)).await;
507
508 assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2, 3, 4, 5]);
509 let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
510 assert_eq!(watermark.checkpoint_hi_inclusive, Some(5));
511 }
512
513 #[tokio::test]
515 async fn pipelined_commit_preserves_watermark_ordering() {
516 let config = SequentialConfig {
517 committer: CommitterConfig::default(),
518 max_batch_checkpoints: Some(2),
519 min_eager_rows: Some(1),
520 pipeline_depth: Some(2),
521 ..Default::default()
522 };
523
524 let store = FallibleMockStore::default().with_commit_delay(100);
525 let mut setup = setup_test(0, config, store);
526
527 for i in 0..6 {
528 send_checkpoint(&mut setup, i).await;
529 }
530
531 tokio::time::sleep(Duration::from_millis(1_500)).await;
532
533 assert_eq!(setup.store.get_sequential_data(), vec![0, 1, 2, 3, 4, 5]);
534 let watermark = setup.store.watermark(TestHandler::NAME).unwrap();
535 assert_eq!(watermark.checkpoint_hi_inclusive, Some(5));
536 assert_eq!(watermark.tx_hi, 5);
537 }
538}