1use std::time::Duration;
2
3use futures::StreamExt;
4use tonic::Request;
5use tonic::Status;
6use tonic::codegen::BoxStream;
7
8use super::super::Client;
9use super::super::Result;
10use super::adapter::CursorDomain;
11use super::adapter::ListResponseParts;
12use super::adapter::ListScanDirection;
13use super::adapter::LiveFrame;
14use super::adapter::Progress;
15use super::adapter::ProgressAdvance;
16use super::adapter::Recovery;
17use super::adapter::RecoveryGap;
18use super::adapter::SubscriptionAdapter;
19use super::adapter::validate_caller_read_mask;
20use super::list::LedgerTipPolicy;
21use super::list::ListAction;
22use super::list::ListMachine;
23use super::list::build_initial_list_request;
24use super::list::build_live_tip_baseline_list_request;
25use super::list::build_polling_list_request;
26use super::list::build_recovery_list_request;
27use super::observability::LedgerStreamEvent;
28use super::observability::LedgerStreamObservability;
29use super::observability::LedgerStreamOperation;
30use super::observability::LedgerStreamStage;
31use super::retry::FailurePhase;
32use super::retry::RetryState;
33use super::subscription::BufferedSubscriptionDrain;
34use super::subscription::BufferedSubscriptionState;
35use super::subscription::GapReplay;
36use super::subscription::LiveSubscription;
37use super::types::Delivery;
38use super::types::LedgerStreamConfig;
39use crate::proto::sui::rpc::v2::GetServiceInfoRequest;
40use crate::proto::sui::rpc::v2::Watermark;
41use prost::bytes::Bytes;
42
43enum Phase<A: SubscriptionAdapter> {
44 Prologue(ProloguePhase<A>),
45 Subscribing(SubscribingPhase<A>),
46 Polling(PollingPhase<A>),
47 Terminal(Status),
48 Done,
49}
50
51pub(super) enum Start {
52 Tip,
53 Checkpoint(u64),
54 After(Bytes),
55}
56
57enum ProloguePhase<A: SubscriptionAdapter> {
58 ReadInitialTip,
59 Replay {
60 list_machine: Box<ListMachine<A>>,
62 target_checkpoint: u64,
63 },
64}
65
66enum SubscribingPhase<A: SubscriptionAdapter> {
67 Connect(ConnectMachine<A>),
68 MuteUntilCommitted(LiveSubscription<A>),
69 GapReplay(Box<GapReplay<A>>),
70 DrainGapBuffer(BufferedSubscriptionDrain<A>),
71 Live(LiveSubscription<A>),
72}
73
74struct ConnectMachine<A: SubscriptionAdapter> {
75 context: ConnectContext,
76 phase: ConnectPhase<A>,
77}
78
79#[derive(Clone, Copy)]
80enum ConnectContext {
81 LiveTipStartup,
82 Resume,
83}
84
85enum ConnectPhase<A: SubscriptionAdapter> {
86 Attempt,
87 RetryDelay {
88 delay: Duration,
89 },
90 ReadRecoveryTip,
91 ReplayToRecoveryTip {
92 list_machine: Box<ListMachine<A>>,
93 target_checkpoint: u64,
94 output: ReplayOutput,
95 },
96}
97
98enum PollingPhase<A: SubscriptionAdapter> {
99 ReadBaselineTip,
100 Replay {
101 list_machine: Box<ListMachine<A>>,
102 target_checkpoint: u64,
103 output: ReplayOutput,
104 },
105 Sleep,
106 ReadTip,
107}
108
109#[derive(Clone, Copy)]
110enum ReplayOutput {
111 Suppress,
112 Yield,
113}
114
115struct ReplayFrame<I, P> {
116 item: Option<I>,
117 watermark: Watermark,
118 progress: Option<P>,
119}
120
121fn parse_replay_frame<A: SubscriptionAdapter>(
122 response: A::ListResponse,
123 progress: Option<Progress<A::Cursor>>,
124) -> Result<ReplayFrame<A::Item, Progress<A::Cursor>>> {
125 let ListResponseParts {
126 item, watermark, ..
127 } = A::split_list(response)?;
128 let watermark =
129 watermark.ok_or_else(|| Status::data_loss("List frame is missing its watermark"))?;
130 Ok(ReplayFrame {
131 item,
132 watermark,
133 progress,
134 })
135}
136
137enum ReplayStep<A: SubscriptionAdapter> {
138 Yield(A::Output),
139 Complete,
140 Continue,
141 Terminal(Status),
142}
143
144enum SubscriptionAttempt<A: SubscriptionAdapter> {
145 Established {
146 stream: BoxStream<A::SubscribeResponse>,
147 first_frame: LiveFrame<A::Item, Progress<A::Cursor>>,
148 },
149 Failed {
150 status: Status,
151 failure_phase: FailurePhase,
152 },
153}
154
155enum SubscribingStep<A: SubscriptionAdapter> {
156 Next(SubscribingPhase<A>),
157 Yield(SubscribingPhase<A>, A::Output),
158 Terminal(Status),
159}
160
161enum PollingStep<A: SubscriptionAdapter> {
162 Next(PollingPhase<A>),
163 Yield(PollingPhase<A>, A::Output),
164 Terminal(Status),
165}
166
167async fn dispatch_subscription<A: SubscriptionAdapter>(
168 client: &Client,
169 observability: &LedgerStreamObservability,
170 request: Request<A::SubscribeRequest>,
171 observability_stage: LedgerStreamStage,
172) -> Result<BoxStream<A::SubscribeResponse>> {
173 let future = A::dispatch_subscribe(client.clone(), request);
174 let started_at = observability.start_timer();
175 let result = future.await;
176 observability.emit_rpc_response(
177 started_at,
178 A::FAMILY,
179 LedgerStreamOperation::Subscribe,
180 observability_stage,
181 &result,
182 );
183 result
184}
185
186pub(super) struct Driver<A: SubscriptionAdapter> {
187 client: Client,
188 list_template: A::ListRequest,
189 subscribe_payload: A::SubscribeRequest,
190 item_required: bool,
191 config: LedgerStreamConfig,
192 observability: LedgerStreamObservability,
193 service_info_retry: RetryState,
194 subscription_retry: RetryState,
195 delivery: Delivery,
196 phase: Option<Phase<A>>,
197 committed_progress: Option<Progress<A::Cursor>>,
198 committed_item_position: Option<A::ItemPosition>,
199 last_covered_checkpoint_height: Option<u64>,
200}
201
202impl<A: SubscriptionAdapter> Driver<A> {
203 pub(super) fn new_stream(
204 client: Client,
205 subscribe_payload: A::SubscribeRequest,
206 mut list_template: A::ListRequest,
207 start: Start,
208 delivery: Delivery,
209 config: LedgerStreamConfig,
210 ) -> Self {
211 let observability = LedgerStreamObservability::new(config.observer());
212 let item_required = A::item_required(&subscribe_payload);
213 let requested_phase = match start {
214 Start::Tip => match delivery {
215 Delivery::Subscribe => {
216 Phase::Subscribing(SubscribingPhase::Connect(ConnectMachine {
217 context: ConnectContext::LiveTipStartup,
218 phase: ConnectPhase::Attempt,
219 }))
220 }
221 Delivery::Poll => Phase::Polling(PollingPhase::ReadBaselineTip),
222 },
223 Start::Checkpoint(checkpoint) => {
224 A::set_start_checkpoint(&mut list_template, Some(checkpoint));
225 Phase::Prologue(ProloguePhase::ReadInitialTip)
226 }
227 Start::After(cursor) => {
228 A::options_mut(&mut list_template).after = Some(cursor);
229 Phase::Prologue(ProloguePhase::ReadInitialTip)
230 }
231 };
232 let phase = if config.ledger_tip_poll_interval == Duration::ZERO {
233 Phase::Terminal(Status::invalid_argument(
234 "ledger_tip_poll_interval must be greater than zero",
235 ))
236 } else if let Err(status) =
237 validate_caller_read_mask::<A>(A::list_read_mask(&list_template))
238 {
239 Phase::Terminal(status)
240 } else {
241 requested_phase
242 };
243 Self {
244 client,
245 list_template,
246 subscribe_payload,
247 item_required,
248 config,
249 observability,
250 service_info_retry: RetryState::new(A::FAMILY, LedgerStreamOperation::GetServiceInfo),
251 subscription_retry: RetryState::new(A::FAMILY, LedgerStreamOperation::Subscribe),
252 delivery,
253 phase: Some(phase),
254 committed_progress: None,
255 committed_item_position: None,
256 last_covered_checkpoint_height: None,
257 }
258 }
259
260 pub(super) async fn next(&mut self) -> Option<Result<A::Output>> {
261 loop {
262 let phase = self.phase.take().unwrap_or(Phase::Done);
263 match phase {
264 Phase::Done => {
265 self.phase = Some(Phase::Done);
266 return None;
267 }
268 Phase::Terminal(status) => {
269 self.observability
270 .emit(|| LedgerStreamEvent::TerminalError {
271 family: A::FAMILY,
272 status: status.clone(),
273 });
274 self.phase = Some(Phase::Done);
275 return Some(Err(status));
276 }
277 Phase::Prologue(phase) => {
278 let (next_phase, output) = self.step_prologue(phase).await;
279 self.phase = Some(next_phase);
280 if let Some(output) = output {
281 return Some(Ok(output));
282 }
283 }
284 Phase::Subscribing(phase) => match self.step_subscribing(phase).await {
285 SubscribingStep::Next(next) => {
286 self.phase = Some(Phase::Subscribing(next));
287 }
288 SubscribingStep::Yield(next, output) => {
289 self.phase = Some(Phase::Subscribing(next));
290 return Some(Ok(output));
291 }
292 SubscribingStep::Terminal(status) => {
293 self.phase = Some(Phase::Terminal(status));
294 }
295 },
296 Phase::Polling(phase) => match self.step_polling(phase).await {
297 PollingStep::Next(next) => {
298 self.phase = Some(Phase::Polling(next));
299 }
300 PollingStep::Yield(next, output) => {
301 self.phase = Some(Phase::Polling(next));
302 return Some(Ok(output));
303 }
304 PollingStep::Terminal(status) => {
305 self.phase = Some(Phase::Terminal(status));
306 }
307 },
308 }
309 }
310 }
311
312 async fn step_prologue(&mut self, phase: ProloguePhase<A>) -> (Phase<A>, Option<A::Output>) {
313 match phase {
314 ProloguePhase::ReadInitialTip => match self
315 .read_service_checkpoint_height(LedgerStreamStage::InitialReplay)
316 .await
317 {
318 Err(status) => (Phase::Terminal(status), None),
319 Ok(checkpoint_height)
322 if A::start_checkpoint(&self.list_template)
323 .is_some_and(|start_checkpoint| checkpoint_height < start_checkpoint) =>
324 {
325 tokio::time::sleep(self.config.ledger_tip_poll_interval).await;
326 (Phase::Prologue(ProloguePhase::ReadInitialTip), None)
327 }
328 Ok(checkpoint_height) => {
329 match build_initial_list_request::<A>(
330 &self.list_template,
331 checkpoint_height,
332 self.config.list_page_limit,
333 ) {
334 Ok((payload, expected_end)) => (
335 Phase::Prologue(ProloguePhase::Replay {
336 list_machine: Box::new(ListMachine::new(
337 self.client.clone(),
338 payload,
339 expected_end,
340 ListScanDirection::Ascending,
341 LedgerTipPolicy::WaitForExpectedBound,
342 self.observability.clone(),
343 LedgerStreamStage::InitialReplay,
344 )),
345 target_checkpoint: checkpoint_height,
346 }),
347 None,
348 ),
349 Err(status) => (Phase::Terminal(status), None),
350 }
351 }
352 },
353 ProloguePhase::Replay {
354 mut list_machine,
355 target_checkpoint,
356 } => {
357 let rpc_event = list_machine.poll_rpc().await;
358 let action = list_machine.process_event(rpc_event, &self.config);
359 let complete = matches!(&action, ListAction::Frame { complete: true, .. });
360 match self.process_replay_frame(action, target_checkpoint, ReplayOutput::Yield) {
361 ReplayStep::Yield(output) => {
362 let next = if complete {
363 self.finish_prologue_replay(target_checkpoint)
364 } else {
365 Phase::Prologue(ProloguePhase::Replay {
366 list_machine,
367 target_checkpoint,
368 })
369 };
370 (next, Some(output))
371 }
372 ReplayStep::Complete => (self.finish_prologue_replay(target_checkpoint), None),
373 ReplayStep::Continue => (
374 Phase::Prologue(ProloguePhase::Replay {
375 list_machine,
376 target_checkpoint,
377 }),
378 None,
379 ),
380 ReplayStep::Terminal(status) => (Phase::Terminal(status), None),
381 }
382 }
383 }
384 }
385
386 async fn step_subscribing(&mut self, phase: SubscribingPhase<A>) -> SubscribingStep<A> {
387 match phase {
388 SubscribingPhase::Connect(machine) => self.step_connect(machine).await,
389 SubscribingPhase::MuteUntilCommitted(mut live) => {
390 match live.stream.next().await {
393 Some(Ok(response)) => match A::parse_live(response, self.item_required) {
394 Err(status) => SubscribingStep::Terminal(status),
395 Ok(frame) => match live
396 .last_seen
397 .classify_consecutive_subscription_progress(
398 &frame.progress,
399 frame.item.is_some(),
400 ) {
401 Err(status) => SubscribingStep::Terminal(status),
402 Ok(ProgressAdvance::Unchanged) => {
403 SubscribingStep::Next(SubscribingPhase::MuteUntilCommitted(live))
404 }
405 Ok(
406 ProgressAdvance::CheckpointCoverageAdvanced
407 | ProgressAdvance::CursorAdvanced,
408 ) => {
409 live.last_seen = frame.progress.clone();
410 self.subscription_retry.reset(&self.observability);
411 let Some(last_committed_progress) = self.committed_progress.clone()
412 else {
413 return SubscribingStep::Terminal(Status::internal(
414 "muted subscription has no committed progress",
415 ));
416 };
417 match last_committed_progress.plan_recovery(
418 &frame.progress,
419 self.last_covered_checkpoint_height,
420 ) {
421 Err(status) => SubscribingStep::Terminal(status),
422 Ok(Recovery::Live) => {
423 SubscribingStep::Next(SubscribingPhase::Live(live))
424 }
425 Ok(Recovery::MuteUntilCommitted) => SubscribingStep::Next(
426 SubscribingPhase::MuteUntilCommitted(live),
427 ),
428 Ok(Recovery::Replay(gap)) => {
429 self.enter_gap_replay(gap, live.stream, frame)
430 }
431 }
432 }
433 },
434 },
435 Some(Err(status)) => {
436 self.emit_subscription_interruption(
437 &status,
438 LedgerStreamStage::LiveSubscription,
439 );
440 self.schedule_connect_retry(
441 status,
442 FailurePhase::Body,
443 LedgerStreamStage::LiveSubscription,
444 ConnectContext::Resume,
445 )
446 }
447 None => {
448 let status = Status::unavailable("subscription stream ended unexpectedly");
449 self.emit_subscription_interruption(
450 &status,
451 LedgerStreamStage::LiveSubscription,
452 );
453 self.schedule_connect_retry(
454 status,
455 FailurePhase::Body,
456 LedgerStreamStage::LiveSubscription,
457 ConnectContext::Resume,
458 )
459 }
460 }
461 }
462 SubscribingPhase::GapReplay(mut gap) => {
463 let rpc_event =
464 if let BufferedSubscriptionState::Active(live) = &mut gap.subscription_state {
465 tokio::select! {
466 event = gap.list_machine.poll_rpc() => Some(event),
467 result = live.stream.next() => {
468 if gap.buffer_subscription_result(
469 result,
470 self.item_required,
471 &self.config,
472 ) {
473 self.subscription_retry.reset(&self.observability);
474 }
475 None
476 }
477 }
478 } else {
479 Some(gap.list_machine.poll_rpc().await)
480 };
481 let Some(rpc_event) = rpc_event else {
482 return SubscribingStep::Next(SubscribingPhase::GapReplay(gap));
483 };
484 match gap.list_machine.process_event(rpc_event, &self.config) {
485 ListAction::Frame {
486 response,
487 progress,
488 complete,
489 } => {
490 let frame = match parse_replay_frame::<A>(response, progress) {
491 Ok(frame) => frame,
492 Err(status) => return SubscribingStep::Terminal(status),
493 };
494 let Some(progress) = frame.progress else {
495 return SubscribingStep::Next(if complete {
496 SubscribingPhase::DrainGapBuffer(
497 (*gap).into_buffered_subscription_drain(),
498 )
499 } else {
500 SubscribingPhase::GapReplay(gap)
501 });
502 };
503 if frame.item.is_none()
504 && (gap.has_deferred_progress(&progress)
505 || self.progress_is_committed(&progress))
506 {
507 return SubscribingStep::Next(if complete {
508 SubscribingPhase::DrainGapBuffer(
509 (*gap).into_buffered_subscription_drain(),
510 )
511 } else {
512 SubscribingPhase::GapReplay(gap)
513 });
514 }
515 let mut item = frame.item;
516 if item
517 .as_ref()
518 .is_some_and(|item| gap.replayed_item_was_already_emitted(item))
519 {
520 item = None;
521 } else if let Some(item) = &item {
522 gap.record_replayed_item(item);
523 }
524 self.commit_progress(progress.clone());
525 let next = if complete {
526 SubscribingPhase::DrainGapBuffer(
527 (*gap).into_buffered_subscription_drain(),
528 )
529 } else {
530 SubscribingPhase::GapReplay(gap)
531 };
532 SubscribingStep::Yield(next, self.make_output(item, progress))
533 }
534 ListAction::Continue => SubscribingStep::Next(SubscribingPhase::GapReplay(gap)),
535 ListAction::Terminal(status) => SubscribingStep::Terminal(status),
536 }
537 }
538 SubscribingPhase::DrainGapBuffer(mut delivery) => {
539 if let Some(frame) = delivery.buffered_subscription_frames.pop_front() {
540 let cursor_advanced = self
541 .committed_progress
542 .as_ref()
543 .is_none_or(|committed| !committed.same_position(&frame.progress));
544 let checkpoint_coverage_advanced = self
545 .committed_progress
546 .as_ref()
547 .and_then(|committed| committed.checkpoint)
548 .zip(frame.progress.checkpoint)
549 .is_some_and(|(committed, next)| next > committed);
550 let duplicate = frame.item.as_ref().is_some_and(|item| {
551 delivery
552 .replay_item_frontier
553 .as_ref()
554 .is_some_and(|frontier| A::item_position(item) <= frontier)
555 });
556 let item = if duplicate { None } else { frame.item };
557 let next = SubscribingPhase::DrainGapBuffer(delivery);
558 if duplicate {
559 SubscribingStep::Next(next)
560 } else {
561 let yield_frame = item.is_some() || cursor_advanced;
562 if yield_frame || checkpoint_coverage_advanced {
563 self.commit_progress(frame.progress.clone());
564 }
565 if yield_frame {
566 SubscribingStep::Yield(next, self.make_output(item, frame.progress))
567 } else {
568 SubscribingStep::Next(next)
569 }
570 }
571 } else {
572 match delivery.subscription_state {
573 BufferedSubscriptionState::Failed(status) => self.schedule_connect_retry(
574 status,
575 FailurePhase::Body,
576 LedgerStreamStage::GapRecovery,
577 ConnectContext::Resume,
578 ),
579 BufferedSubscriptionState::DroppedAtBufferLimit => {
580 SubscribingStep::Next(SubscribingPhase::Connect(ConnectMachine {
581 context: ConnectContext::Resume,
582 phase: ConnectPhase::Attempt,
583 }))
584 }
585 BufferedSubscriptionState::Active(live) => {
586 SubscribingStep::Next(SubscribingPhase::Live(live))
587 }
588 }
589 }
590 }
591 SubscribingPhase::Live(mut live) => match live.stream.next().await {
592 Some(Ok(response)) => match A::parse_live(response, self.item_required) {
593 Err(status) => SubscribingStep::Terminal(status),
594 Ok(frame) => match live.last_seen.classify_consecutive_subscription_progress(
595 &frame.progress,
596 frame.item.is_some(),
597 ) {
598 Err(status) => SubscribingStep::Terminal(status),
599 Ok(ProgressAdvance::Unchanged) => {
600 SubscribingStep::Next(SubscribingPhase::Live(live))
601 }
602 Ok(ProgressAdvance::CheckpointCoverageAdvanced) => {
603 live.last_seen = frame.progress.clone();
604 self.commit_progress(frame.progress);
605 self.subscription_retry.reset(&self.observability);
606 SubscribingStep::Next(SubscribingPhase::Live(live))
607 }
608 Ok(ProgressAdvance::CursorAdvanced) => {
609 let LiveFrame { item, progress } = frame;
610 live.last_seen = progress.clone();
611 self.commit_progress(progress.clone());
612 self.subscription_retry.reset(&self.observability);
613 SubscribingStep::Yield(
614 SubscribingPhase::Live(live),
615 self.make_output(item, progress),
616 )
617 }
618 },
619 },
620 Some(Err(status)) => {
621 self.emit_subscription_interruption(
622 &status,
623 LedgerStreamStage::LiveSubscription,
624 );
625 self.schedule_connect_retry(
626 status,
627 FailurePhase::Body,
628 LedgerStreamStage::LiveSubscription,
629 ConnectContext::Resume,
630 )
631 }
632 None => {
633 let status = Status::unavailable("subscription stream ended unexpectedly");
634 self.emit_subscription_interruption(
635 &status,
636 LedgerStreamStage::LiveSubscription,
637 );
638 self.schedule_connect_retry(
639 status,
640 FailurePhase::Body,
641 LedgerStreamStage::LiveSubscription,
642 ConnectContext::Resume,
643 )
644 }
645 },
646 }
647 }
648
649 async fn step_polling(&mut self, phase: PollingPhase<A>) -> PollingStep<A> {
650 match phase {
651 PollingPhase::ReadBaselineTip => match self
652 .read_service_checkpoint_height(LedgerStreamStage::PollingBaseline)
653 .await
654 {
655 Err(status) => PollingStep::Terminal(status),
656 Ok(checkpoint_height) => {
657 match build_live_tip_baseline_list_request::<A>(
658 &self.list_template,
659 checkpoint_height,
660 self.config.list_page_limit,
661 ) {
662 Ok((payload, expected_end)) => PollingStep::Next(PollingPhase::Replay {
663 list_machine: Box::new(ListMachine::new(
664 self.client.clone(),
665 payload,
666 expected_end,
667 ListScanDirection::Ascending,
668 LedgerTipPolicy::WaitForExpectedBound,
669 self.observability.clone(),
670 LedgerStreamStage::PollingBaseline,
671 )),
672 target_checkpoint: checkpoint_height,
673 output: ReplayOutput::Suppress,
674 }),
675 Err(status) => PollingStep::Terminal(status),
676 }
677 }
678 },
679 PollingPhase::Replay {
680 mut list_machine,
681 target_checkpoint,
682 output,
683 } => {
684 let rpc_event = list_machine.poll_rpc().await;
685 let action = list_machine.process_event(rpc_event, &self.config);
686 let complete = matches!(&action, ListAction::Frame { complete: true, .. });
687 match self.process_replay_frame(action, target_checkpoint, output) {
688 ReplayStep::Yield(frame_output) => {
689 if complete {
690 match self.finish_polling_replay(target_checkpoint) {
691 Ok(next) => PollingStep::Yield(next, frame_output),
692 Err(status) => PollingStep::Terminal(status),
693 }
694 } else {
695 PollingStep::Yield(
696 PollingPhase::Replay {
697 list_machine,
698 target_checkpoint,
699 output,
700 },
701 frame_output,
702 )
703 }
704 }
705 ReplayStep::Complete => match self.finish_polling_replay(target_checkpoint) {
706 Ok(next) => PollingStep::Next(next),
707 Err(status) => PollingStep::Terminal(status),
708 },
709 ReplayStep::Continue if complete => {
710 match self.finish_polling_replay(target_checkpoint) {
711 Ok(next) => PollingStep::Next(next),
712 Err(status) => PollingStep::Terminal(status),
713 }
714 }
715 ReplayStep::Continue => PollingStep::Next(PollingPhase::Replay {
716 list_machine,
717 target_checkpoint,
718 output,
719 }),
720 ReplayStep::Terminal(status) => PollingStep::Terminal(status),
721 }
722 }
723 PollingPhase::Sleep => {
724 tokio::time::sleep(self.config.ledger_tip_poll_interval).await;
725 PollingStep::Next(PollingPhase::ReadTip)
726 }
727 PollingPhase::ReadTip => match self
728 .read_service_checkpoint_height(LedgerStreamStage::PollingTail)
729 .await
730 {
731 Err(status) => PollingStep::Terminal(status),
732 Ok(checkpoint_height)
733 if self
734 .last_covered_checkpoint_height
735 .is_some_and(|covered| checkpoint_height <= covered) =>
736 {
737 PollingStep::Next(PollingPhase::Sleep)
738 }
739 Ok(checkpoint_height) => {
740 let Some(committed_progress) = self.committed_progress.as_ref() else {
741 return PollingStep::Terminal(Status::data_loss(
742 "subscription recovery has no committed progress marker",
743 ));
744 };
745 match build_polling_list_request::<A>(
746 &self.list_template,
747 committed_progress,
748 checkpoint_height,
749 self.config.list_page_limit,
750 ) {
751 Ok((payload, expected_end)) => PollingStep::Next(PollingPhase::Replay {
752 list_machine: Box::new(ListMachine::new(
753 self.client.clone(),
754 payload,
755 expected_end,
756 ListScanDirection::Ascending,
757 LedgerTipPolicy::WaitForExpectedBound,
758 self.observability.clone(),
759 LedgerStreamStage::PollingTail,
760 )),
761 target_checkpoint: checkpoint_height,
762 output: ReplayOutput::Yield,
763 }),
764 Err(status) => PollingStep::Terminal(status),
765 }
766 }
767 },
768 }
769 }
770
771 async fn read_service_checkpoint_height(
772 &mut self,
773 observability_stage: LedgerStreamStage,
774 ) -> Result<u64> {
775 loop {
776 let request = Request::new(GetServiceInfoRequest::default());
777 let mut client = self.client.ledger_client();
778 let future = client.get_service_info(request);
779 let started_at = self.observability.start_timer();
780 let result = future.await;
781 self.observability.emit_rpc_response(
782 started_at,
783 A::FAMILY,
784 LedgerStreamOperation::GetServiceInfo,
785 observability_stage,
786 &result,
787 );
788 match result {
789 Ok(response) => {
790 let checkpoint_height =
791 response.into_inner().checkpoint_height.ok_or_else(|| {
792 Status::data_loss(
793 "GetServiceInfo response is missing checkpoint_height",
794 )
795 })?;
796 self.service_info_retry.reset(&self.observability);
797 return Ok(checkpoint_height);
798 }
799 Err(status) => {
800 let Some(delay) = self.service_info_retry.retry_delay(
801 &status,
802 FailurePhase::Dispatch,
805 &self.config,
806 &self.observability,
807 observability_stage,
808 ) else {
809 return Err(status);
810 };
811 tokio::time::sleep(delay).await;
812 }
813 }
814 }
815 }
816
817 fn emit_subscription_interruption(
818 &self,
819 status: &Status,
820 observability_stage: LedgerStreamStage,
821 ) {
822 self.observability
823 .emit(|| LedgerStreamEvent::SubscriptionStreamInterrupted {
824 family: A::FAMILY,
825 stage: observability_stage,
826 status: status.clone(),
827 });
828 }
829
830 fn process_replay_frame(
831 &mut self,
832 action: ListAction<A::ListResponse, Progress<A::Cursor>>,
833 target_checkpoint: u64,
834 output: ReplayOutput,
835 ) -> ReplayStep<A> {
836 match action {
837 ListAction::Frame {
838 response,
839 progress,
840 complete,
841 } => {
842 let frame = match parse_replay_frame::<A>(response, progress) {
843 Ok(frame) => frame,
844 Err(status) => return ReplayStep::Terminal(status),
845 };
846 let Some(progress) = frame.progress else {
847 if !complete {
848 return ReplayStep::Continue;
849 }
850 let Some(cursor) = frame.watermark.cursor else {
851 return ReplayStep::Terminal(Status::data_loss(
852 "subscription recovery has no committed progress marker",
853 ));
854 };
855 let Some(progress) = A::Cursor::position(&cursor, Some(target_checkpoint))
856 else {
857 return ReplayStep::Terminal(Status::internal(
858 "checkpoint-bound watermark names no position",
859 ));
860 };
861 let duplicate = self.progress_is_committed(&progress);
862 self.commit_progress(progress.clone());
863 return if duplicate || matches!(output, ReplayOutput::Suppress) {
864 ReplayStep::Complete
865 } else {
866 ReplayStep::Yield(self.make_output(None, progress))
867 };
868 };
869 let duplicate = frame.item.is_none() && self.progress_is_committed(&progress);
870 self.commit_progress(progress.clone());
871 if duplicate || matches!(output, ReplayOutput::Suppress) {
872 self.commit_item_position(&frame.item);
873 if complete {
874 ReplayStep::Complete
875 } else {
876 ReplayStep::Continue
877 }
878 } else {
879 ReplayStep::Yield(self.make_output(frame.item, progress))
880 }
881 }
882 ListAction::Continue => ReplayStep::Continue,
883 ListAction::Terminal(status) => ReplayStep::Terminal(status),
884 }
885 }
886
887 fn finish_prologue_replay(&mut self, target_checkpoint: u64) -> Phase<A> {
888 self.last_covered_checkpoint_height = Some(
889 self.last_covered_checkpoint_height
890 .map_or(target_checkpoint, |covered| covered.max(target_checkpoint)),
891 );
892 if self.committed_progress.is_none() {
893 return Phase::Terminal(Status::data_loss(
894 "subscription recovery has no committed progress marker",
895 ));
896 }
897 match self.delivery {
898 Delivery::Subscribe => Phase::Subscribing(SubscribingPhase::Connect(ConnectMachine {
899 context: ConnectContext::Resume,
900 phase: ConnectPhase::Attempt,
901 })),
902 Delivery::Poll => Phase::Polling(PollingPhase::Sleep),
903 }
904 }
905
906 fn finish_polling_replay(&mut self, target_checkpoint: u64) -> Result<PollingPhase<A>> {
907 self.last_covered_checkpoint_height = Some(
908 self.last_covered_checkpoint_height
909 .map_or(target_checkpoint, |covered| covered.max(target_checkpoint)),
910 );
911 if self.committed_progress.is_none() {
912 return Err(Status::data_loss(
913 "subscription recovery has no committed progress marker",
914 ));
915 }
916 Ok(PollingPhase::Sleep)
917 }
918
919 fn finish_subscription_recovery_replay(&mut self, target_checkpoint: u64) -> ConnectMachine<A> {
920 self.last_covered_checkpoint_height = Some(
921 self.last_covered_checkpoint_height
922 .map_or(target_checkpoint, |covered| covered.max(target_checkpoint)),
923 );
924 ConnectMachine {
925 context: ConnectContext::Resume,
926 phase: ConnectPhase::Attempt,
927 }
928 }
929
930 fn enter_gap_replay(
931 &mut self,
932 gap: RecoveryGap,
933 new_subscription_stream: BoxStream<A::SubscribeResponse>,
934 new_subscription_frame: LiveFrame<A::Item, Progress<A::Cursor>>,
935 ) -> SubscribingStep<A> {
936 let recovery_list_template = A::list_request_from_subscribe(&self.subscribe_payload);
938 let (payload, expected_end) = match build_recovery_list_request::<A>(
939 recovery_list_template,
940 &gap,
941 self.config.list_page_limit,
942 ) {
943 Ok(request) => request,
944 Err(status) => return SubscribingStep::Terminal(status),
945 };
946 self.observability
947 .emit(|| LedgerStreamEvent::GapRecoveryStarted { family: A::FAMILY });
948 let new_live_subscription = LiveSubscription {
949 stream: new_subscription_stream,
950 last_seen: new_subscription_frame.progress.clone(),
951 };
952 let recovery_list_machine = ListMachine::new(
953 self.client.clone(),
954 payload,
955 expected_end,
956 ListScanDirection::Ascending,
957 LedgerTipPolicy::WaitForExpectedBound,
958 self.observability.clone(),
959 LedgerStreamStage::GapRecovery,
960 );
961 SubscribingStep::Next(SubscribingPhase::GapReplay(Box::new(GapReplay::new(
962 recovery_list_machine,
963 new_live_subscription,
964 new_subscription_frame,
965 self.committed_item_position.clone(),
966 &self.config,
967 gap.replays_boundary_item(),
968 ))))
969 }
970
971 async fn step_connect(&mut self, machine: ConnectMachine<A>) -> SubscribingStep<A> {
972 let ConnectMachine { context, phase } = machine;
973 match phase {
974 ConnectPhase::Attempt => {
975 let observability_stage = match context {
976 ConnectContext::LiveTipStartup => LedgerStreamStage::LiveTipStartup,
977 ConnectContext::Resume => LedgerStreamStage::LiveSubscription,
978 };
979 match self.subscription_attempt(observability_stage).await {
980 Err(status) => SubscribingStep::Terminal(status),
981 Ok(SubscriptionAttempt::Failed {
982 status,
983 failure_phase,
984 }) => self.schedule_connect_retry(
985 status,
986 failure_phase,
987 observability_stage,
988 context,
989 ),
990 Ok(SubscriptionAttempt::Established {
991 stream,
992 first_frame,
993 }) => {
994 self.subscription_retry.reset(&self.observability);
995 match context {
996 ConnectContext::LiveTipStartup => {
997 let LiveFrame { item, progress } = first_frame;
998 let live = LiveSubscription {
999 stream,
1000 last_seen: progress.clone(),
1001 };
1002 self.commit_progress(progress.clone());
1003 SubscribingStep::Yield(
1004 SubscribingPhase::Live(live),
1005 self.make_output(item, progress),
1006 )
1007 }
1008 ConnectContext::Resume => {
1009 let Some(last_committed_progress) = self.committed_progress.clone()
1010 else {
1011 return SubscribingStep::Terminal(Status::data_loss(
1012 "subscription recovery has no committed progress marker",
1013 ));
1014 };
1015 match last_committed_progress.plan_recovery(
1016 &first_frame.progress,
1017 self.last_covered_checkpoint_height,
1018 ) {
1019 Err(status) => SubscribingStep::Terminal(status),
1020 Ok(Recovery::Live) => SubscribingStep::Next(
1021 SubscribingPhase::Live(LiveSubscription {
1022 stream,
1023 last_seen: first_frame.progress,
1024 }),
1025 ),
1026 Ok(Recovery::MuteUntilCommitted) => SubscribingStep::Next(
1027 SubscribingPhase::MuteUntilCommitted(LiveSubscription {
1028 stream,
1029 last_seen: first_frame.progress,
1030 }),
1031 ),
1032 Ok(Recovery::Replay(gap)) => {
1033 self.enter_gap_replay(gap, stream, first_frame)
1034 }
1035 }
1036 }
1037 }
1038 }
1039 }
1040 }
1041 ConnectPhase::RetryDelay { delay } => {
1042 tokio::time::sleep(delay).await;
1043 SubscribingStep::Next(SubscribingPhase::Connect(ConnectMachine {
1044 context,
1045 phase: ConnectPhase::ReadRecoveryTip,
1046 }))
1047 }
1048 ConnectPhase::ReadRecoveryTip => {
1049 let committed_progress = self.committed_progress.clone();
1050 match self
1051 .read_service_checkpoint_height(LedgerStreamStage::GapRecovery)
1052 .await
1053 {
1054 Err(status) => SubscribingStep::Terminal(status),
1055 Ok(checkpoint_height)
1056 if committed_progress
1057 .as_ref()
1058 .and_then(|progress| progress.checkpoint)
1059 .is_some_and(|committed| checkpoint_height <= committed) =>
1060 {
1061 SubscribingStep::Next(SubscribingPhase::Connect(ConnectMachine {
1062 context: ConnectContext::Resume,
1063 phase: ConnectPhase::Attempt,
1064 }))
1065 }
1066 Ok(checkpoint_height) => {
1067 let (request, output) =
1068 if let Some(committed_progress) = &committed_progress {
1069 (
1070 build_polling_list_request::<A>(
1071 &self.list_template,
1072 committed_progress,
1073 checkpoint_height,
1074 self.config.list_page_limit,
1075 ),
1076 ReplayOutput::Yield,
1077 )
1078 } else {
1079 (
1080 build_live_tip_baseline_list_request::<A>(
1081 &self.list_template,
1082 checkpoint_height,
1083 self.config.list_page_limit,
1084 ),
1085 ReplayOutput::Suppress,
1086 )
1087 };
1088 match request {
1089 Err(status) => SubscribingStep::Terminal(status),
1090 Ok((payload, expected_end)) => {
1091 self.observability
1092 .emit(|| LedgerStreamEvent::GapRecoveryStarted {
1093 family: A::FAMILY,
1094 });
1095 SubscribingStep::Next(SubscribingPhase::Connect(ConnectMachine {
1096 context,
1097 phase: ConnectPhase::ReplayToRecoveryTip {
1098 list_machine: Box::new(ListMachine::new(
1099 self.client.clone(),
1100 payload,
1101 expected_end,
1102 ListScanDirection::Ascending,
1103 LedgerTipPolicy::WaitForExpectedBound,
1104 self.observability.clone(),
1105 LedgerStreamStage::GapRecovery,
1106 )),
1107 target_checkpoint: checkpoint_height,
1108 output,
1109 },
1110 }))
1111 }
1112 }
1113 }
1114 }
1115 }
1116 ConnectPhase::ReplayToRecoveryTip {
1117 mut list_machine,
1118 target_checkpoint,
1119 output,
1120 } => {
1121 let rpc_event = list_machine.poll_rpc().await;
1122 let action = list_machine.process_event(rpc_event, &self.config);
1123 let complete = matches!(&action, ListAction::Frame { complete: true, .. });
1124 match self.process_replay_frame(action, target_checkpoint, output) {
1125 ReplayStep::Yield(frame_output) => {
1126 let next = if complete {
1127 self.finish_subscription_recovery_replay(target_checkpoint)
1128 } else {
1129 ConnectMachine {
1130 context,
1131 phase: ConnectPhase::ReplayToRecoveryTip {
1132 list_machine,
1133 target_checkpoint,
1134 output,
1135 },
1136 }
1137 };
1138 SubscribingStep::Yield(SubscribingPhase::Connect(next), frame_output)
1139 }
1140 ReplayStep::Complete => SubscribingStep::Next(SubscribingPhase::Connect(
1141 self.finish_subscription_recovery_replay(target_checkpoint),
1142 )),
1143 ReplayStep::Continue if complete => {
1144 SubscribingStep::Next(SubscribingPhase::Connect(
1145 self.finish_subscription_recovery_replay(target_checkpoint),
1146 ))
1147 }
1148 ReplayStep::Continue => {
1149 SubscribingStep::Next(SubscribingPhase::Connect(ConnectMachine {
1150 context,
1151 phase: ConnectPhase::ReplayToRecoveryTip {
1152 list_machine,
1153 target_checkpoint,
1154 output,
1155 },
1156 }))
1157 }
1158 ReplayStep::Terminal(status) => SubscribingStep::Terminal(status),
1159 }
1160 }
1161 }
1162 }
1163
1164 async fn subscription_attempt(
1165 &mut self,
1166 observability_stage: LedgerStreamStage,
1167 ) -> Result<SubscriptionAttempt<A>> {
1168 let request = Request::new(self.subscribe_payload.clone());
1169 let mut stream = match dispatch_subscription::<A>(
1170 &self.client,
1171 &self.observability,
1172 request,
1173 observability_stage,
1174 )
1175 .await
1176 {
1177 Ok(stream) => stream,
1178 Err(status) => {
1179 return Ok(SubscriptionAttempt::Failed {
1180 status,
1181 failure_phase: FailurePhase::Dispatch,
1182 });
1183 }
1184 };
1185 let first_frame = match stream.next().await {
1186 Some(Ok(response)) => A::parse_live(response, self.item_required)?,
1187 Some(Err(status)) => {
1188 self.emit_subscription_interruption(&status, observability_stage);
1189 return Ok(SubscriptionAttempt::Failed {
1190 status,
1191 failure_phase: FailurePhase::Body,
1192 });
1193 }
1194 None => {
1195 let status = Status::unavailable("subscription stream ended unexpectedly");
1196 self.emit_subscription_interruption(&status, observability_stage);
1197 return Ok(SubscriptionAttempt::Failed {
1198 status,
1199 failure_phase: FailurePhase::Body,
1200 });
1201 }
1202 };
1203 if !first_frame.progress.has_checkpoint_coverage() {
1204 return Err(Status::data_loss(
1205 "subscription initial frame is missing checkpoint coverage",
1206 ));
1207 }
1208 Ok(SubscriptionAttempt::Established {
1209 stream,
1210 first_frame,
1211 })
1212 }
1213
1214 fn schedule_connect_retry(
1215 &mut self,
1216 status: Status,
1217 failure_phase: FailurePhase,
1218 observability_stage: LedgerStreamStage,
1219 context: ConnectContext,
1220 ) -> SubscribingStep<A> {
1221 let Some(delay) = self.subscription_retry.retry_delay(
1222 &status,
1223 failure_phase,
1224 &self.config,
1225 &self.observability,
1226 observability_stage,
1227 ) else {
1228 return SubscribingStep::Terminal(status);
1229 };
1230 SubscribingStep::Next(SubscribingPhase::Connect(ConnectMachine {
1231 context,
1232 phase: ConnectPhase::RetryDelay { delay },
1233 }))
1234 }
1235
1236 fn progress_is_committed(&self, progress: &Progress<A::Cursor>) -> bool {
1237 self.committed_progress
1238 .as_ref()
1239 .is_some_and(|committed| committed.same_position(progress))
1240 || self.committed_progress.is_none()
1241 && A::request_resume_position(&self.list_template)
1242 .as_ref()
1243 .is_some_and(|resume| resume.same_position(progress))
1244 }
1245
1246 fn commit_item_position(&mut self, item: &Option<A::Item>) {
1247 if let Some(item) = item {
1248 self.committed_item_position = Some(A::item_position(item).clone());
1249 }
1250 }
1251
1252 fn make_output(&mut self, item: Option<A::Item>, progress: Progress<A::Cursor>) -> A::Output {
1253 self.commit_item_position(&item);
1254 A::into_output(item, progress)
1255 }
1256
1257 fn commit_progress(&mut self, mut progress: Progress<A::Cursor>) {
1258 if let Some(committed) = &self.committed_progress {
1259 progress.inherit_checkpoint_coverage(committed);
1261 }
1262 self.committed_progress = Some(progress);
1263 }
1264}