Skip to main content

sui_rpc/client/ledger_streams/
stream.rs

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        // `Phase` moves by value; prologue replay allocates this boxed machine once.
61        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                // Wait for `start_checkpoint`; otherwise `[start_checkpoint, checkpoint_height + 1)`
320                // is invalid.
321                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                // This subscription began behind committed progress. Keep its frames hidden
391                // until it reaches that point; a later frame bounds the required List replay.
392                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                        // Unary dispatch and body share this await. Remote statuses retain dispatch
803                        // classification; local tonic transport sources allow reset-code retry.
804                        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        // List fills the interval through this frame before buffered live data is delivered.
937        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            // Preserve known checkpoint coverage when a later watermark omits it.
1260            progress.inherit_checkpoint_coverage(committed);
1261        }
1262        self.committed_progress = Some(progress);
1263    }
1264}