1use std::pin::Pin;
2use std::time::Duration;
3
4use futures::StreamExt;
5use prost::bytes::Bytes;
6use tonic::Request;
7use tonic::Status;
8use tonic::codegen::BoxStream;
9
10use super::super::Client;
11use super::super::Result;
12use super::adapter::CursorDomain;
13use super::adapter::CursorGapUpper;
14use super::adapter::ListScanDirection;
15use super::adapter::Progress;
16use super::adapter::RecoveryGap;
17use super::adapter::RpcFuture;
18use super::adapter::SubscriptionAdapter;
19use super::observability::LedgerStreamEvent;
20use super::observability::LedgerStreamObservability;
21use super::observability::LedgerStreamOperation;
22use super::observability::LedgerStreamStage;
23use super::retry::FailurePhase;
24use super::retry::RetryState;
25use super::retry::backoff_delay;
26use super::types::LedgerStreamConfig;
27use crate::proto::sui::rpc::v2::QueryEnd;
28use crate::proto::sui::rpc::v2::QueryEndReason;
29use crate::proto::sui::rpc::v2::Watermark;
30
31#[derive(Clone, Copy)]
33pub(super) struct ExpectedEnd {
34 checkpoint_after_progress: bool,
35 cursor_after_progress: bool,
36 checkpoint_without_prior_progress: bool,
37 cursor_without_prior_progress: bool,
38}
39
40impl ExpectedEnd {
41 fn from_bounds(checkpoint: bool, cursor: bool) -> Self {
42 Self {
43 checkpoint_after_progress: checkpoint,
44 cursor_after_progress: cursor,
45 checkpoint_without_prior_progress: checkpoint,
46 cursor_without_prior_progress: cursor,
47 }
48 }
49
50 fn accepts(
51 self,
52 reason: QueryEndReason,
53 received_prior_frame: bool,
54 request_started_from_resume_bound: bool,
55 ) -> bool {
56 let (checkpoint, cursor) = if received_prior_frame {
57 (self.checkpoint_after_progress, self.cursor_after_progress)
58 } else {
59 (
61 self.checkpoint_without_prior_progress,
62 self.cursor_without_prior_progress || request_started_from_resume_bound,
63 )
64 };
65 (checkpoint && reason == QueryEndReason::CheckpointBound)
66 || (cursor && reason == QueryEndReason::CursorBound)
67 }
68}
69
70fn validate_query_end_item(reason: QueryEndReason, item_present: bool) -> Result<()> {
71 if reason == QueryEndReason::ItemLimit && !item_present {
72 Err(Status::data_loss("ItemLimit QueryEnd is missing its item"))
73 } else if reason != QueryEndReason::ItemLimit && item_present {
74 Err(Status::data_loss(
75 "non-ItemLimit QueryEnd unexpectedly contains an item",
76 ))
77 } else {
78 Ok(())
79 }
80}
81
82fn to_exclusive_end_bound(checkpoint_height: u64) -> Result<u64> {
83 checkpoint_height.checked_add(1).ok_or_else(|| {
84 Status::out_of_range("checkpoint height cannot be converted to an exclusive end bound")
85 })
86}
87
88fn apply_internal_list_page_limit<A: SubscriptionAdapter>(
90 request: &mut A::ListRequest,
91 list_page_limit: Option<u32>,
92) {
93 A::options_mut(request).limit = list_page_limit;
94}
95
96pub(super) fn build_initial_list_request<A: SubscriptionAdapter>(
97 template: &A::ListRequest,
98 checkpoint_height: u64,
99 list_page_limit: Option<u32>,
100) -> Result<(A::ListRequest, ExpectedEnd)> {
101 let mut request = template.clone();
102 apply_internal_list_page_limit::<A>(&mut request, list_page_limit);
103 A::set_end_checkpoint(
104 &mut request,
105 Some(to_exclusive_end_bound(checkpoint_height)?),
106 );
107 Ok((request, ExpectedEnd::from_bounds(true, false)))
108}
109
110pub(super) fn build_live_tip_baseline_list_request<A: SubscriptionAdapter>(
113 template: &A::ListRequest,
114 checkpoint_height: u64,
115 list_page_limit: Option<u32>,
116) -> Result<(A::ListRequest, ExpectedEnd)> {
117 let mut request = template.clone();
118 apply_internal_list_page_limit::<A>(&mut request, list_page_limit);
119 A::set_start_checkpoint(&mut request, Some(checkpoint_height));
120 A::set_end_checkpoint(
121 &mut request,
122 Some(to_exclusive_end_bound(checkpoint_height)?),
123 );
124 let options = A::options_mut(&mut request);
125 options.after = None;
126 options.before = None;
127 Ok((request, ExpectedEnd::from_bounds(true, false)))
128}
129
130pub(super) fn build_polling_list_request<A: SubscriptionAdapter>(
131 template: &A::ListRequest,
132 committed_progress: &Progress<A::Cursor>,
133 checkpoint_height: u64,
134 list_page_limit: Option<u32>,
135) -> Result<(A::ListRequest, ExpectedEnd)> {
136 let mut request = template.clone();
137 apply_internal_list_page_limit::<A>(&mut request, list_page_limit);
138 A::set_ascending_resume(&mut request, committed_progress)?;
139 A::options_mut(&mut request).before = None;
140 A::set_end_checkpoint(
141 &mut request,
142 Some(to_exclusive_end_bound(checkpoint_height)?),
143 );
144 Ok((request, ExpectedEnd::from_bounds(true, false)))
145}
146
147pub(super) fn build_recovery_list_request<A: SubscriptionAdapter>(
148 template: A::ListRequest,
149 gap: &RecoveryGap,
150 list_page_limit: Option<u32>,
151) -> Result<(A::ListRequest, ExpectedEnd)> {
152 let mut request = template;
153 apply_internal_list_page_limit::<A>(&mut request, list_page_limit);
154 match gap {
155 RecoveryGap::Checkpoints {
156 start_checkpoint,
157 end_checkpoint,
158 } => {
159 A::set_start_checkpoint(&mut request, Some(*start_checkpoint));
160 A::set_end_checkpoint(&mut request, Some(*end_checkpoint));
161 let options = A::options_mut(&mut request);
162 options.after = None;
163 options.before = None;
164 Ok((request, ExpectedEnd::from_bounds(true, false)))
165 }
166 RecoveryGap::Cursors {
167 after,
168 upper: CursorGapUpper::Before(before),
169 } => {
170 let options = A::options_mut(&mut request);
171 options.after = Some(after.clone());
172 options.before = Some(before.clone());
173 Ok((request, ExpectedEnd::from_bounds(false, true)))
174 }
175 RecoveryGap::Cursors {
176 after,
177 upper: CursorGapUpper::EndOfCheckpoint(checkpoint),
178 } => {
179 let options = A::options_mut(&mut request);
180 options.after = Some(after.clone());
181 options.before = None;
182 A::set_end_checkpoint(&mut request, Some(to_exclusive_end_bound(*checkpoint)?));
183 Ok((request, ExpectedEnd::from_bounds(true, false)))
184 }
185 }
186}
187
188pub(super) fn determine_expected_end<A: SubscriptionAdapter>(
189 request: &A::ListRequest,
190 direction: ListScanDirection,
191) -> ExpectedEnd {
192 let options = A::options(request);
193 let checkpoint_without_prior_progress = A::start_checkpoint(request).is_some()
194 || A::end_checkpoint(request).is_some()
195 || direction == ListScanDirection::Descending;
196 let cursor_without_prior_progress =
197 options.is_some_and(|options| options.after.is_some() || options.before.is_some());
198 let (checkpoint_after_progress, cursor_after_progress) = match direction {
199 ListScanDirection::Ascending => (
200 A::end_checkpoint(request).is_some(),
201 options.is_some_and(|options| options.before.is_some()),
202 ),
203 ListScanDirection::Descending => {
204 (true, options.is_some_and(|options| options.after.is_some()))
205 }
206 };
207 ExpectedEnd {
208 checkpoint_after_progress,
209 cursor_after_progress,
210 checkpoint_without_prior_progress,
211 cursor_without_prior_progress,
212 }
213}
214
215fn set_resume_bound<A: SubscriptionAdapter>(
216 direction: ListScanDirection,
217 request: &mut A::ListRequest,
218 cursor: Bytes,
219) {
220 direction.set_resume_bound(A::options_mut(request), cursor);
221}
222
223enum RpcState<A: SubscriptionAdapter> {
224 Idle,
225 Dispatch {
226 future: RpcFuture<A::ListResponse>,
227 started_at: Option<tokio::time::Instant>,
228 },
229 Stream(BoxStream<A::ListResponse>),
230 Sleep(Pin<Box<tokio::time::Sleep>>),
231}
232
233pub(super) enum RpcEvent<R> {
235 Frame(R),
236 Status(Status, FailurePhase),
237 Eof,
238 Wake,
239}
240
241pub(super) enum ListAction<R, P> {
243 Frame {
244 response: R,
245 progress: Option<P>,
246 complete: bool,
247 },
248 Continue,
249 Terminal(Status),
250}
251
252#[derive(Clone, Copy)]
254pub(super) enum LedgerTipPolicy {
255 Complete,
257 WaitForExpectedBound,
259}
260
261pub(super) struct ListMachine<A: SubscriptionAdapter> {
262 client: Client,
263 payload: A::ListRequest,
264 expected_end: ExpectedEnd,
265 direction: ListScanDirection,
266 ledger_tip_policy: LedgerTipPolicy,
267 rpc_state: RpcState<A>,
268 request_start_resume_bound: Option<Bytes>,
270 latest_cursor: Option<Bytes>,
272 latest_progress: Option<Progress<A::Cursor>>,
274 polling_attempt: u32,
275 received_frame_in_request: bool,
276 request_has_checkpoint_coverage: bool,
277 received_any_frame: bool,
278 retry: RetryState,
279 pub(super) observability: LedgerStreamObservability,
280 stage: LedgerStreamStage,
281}
282
283impl<A: SubscriptionAdapter> ListMachine<A> {
284 pub(super) fn new(
285 client: Client,
286 payload: A::ListRequest,
287 expected_end: ExpectedEnd,
288 direction: ListScanDirection,
289 ledger_tip_policy: LedgerTipPolicy,
290 observability: LedgerStreamObservability,
291 stage: LedgerStreamStage,
292 ) -> Self {
293 let mut machine = Self {
294 client,
295 payload,
296 expected_end,
297 direction,
298 ledger_tip_policy,
299 rpc_state: RpcState::Idle,
300 request_start_resume_bound: None,
301 latest_cursor: None,
302 latest_progress: None,
303 polling_attempt: 0,
304 received_frame_in_request: false,
305 request_has_checkpoint_coverage: false,
306 received_any_frame: false,
307 retry: RetryState::new(A::FAMILY, LedgerStreamOperation::List),
308 observability,
309 stage,
310 };
311 machine.start_dispatch();
312 machine
313 }
314
315 fn start_dispatch(&mut self) {
316 self.received_frame_in_request = false;
317 self.request_has_checkpoint_coverage = false;
318 self.request_start_resume_bound = A::options(&self.payload)
319 .and_then(|options| self.direction.resume_bound(options).clone());
320 let request = Request::new(self.payload.clone());
321 self.rpc_state = RpcState::Dispatch {
322 future: A::dispatch_list(self.client.clone(), request),
323 started_at: None,
324 };
325 }
326
327 fn sleep(&mut self, delay: Duration) {
328 self.rpc_state = RpcState::Sleep(Box::pin(tokio::time::sleep(delay)));
329 }
330
331 pub(super) async fn poll_rpc(&mut self) -> RpcEvent<A::ListResponse> {
334 loop {
335 match &mut self.rpc_state {
336 RpcState::Idle => {
337 return RpcEvent::Status(
338 Status::internal("idle RPC state polled"),
339 FailurePhase::Dispatch,
340 );
341 }
342 RpcState::Dispatch { future, started_at } => {
343 if started_at.is_none() {
345 *started_at = self.observability.start_timer();
346 }
347 let rpc_started_at = started_at.as_ref().cloned();
348 let result = future.await;
349 self.observability.emit_rpc_response(
350 rpc_started_at,
351 A::FAMILY,
352 LedgerStreamOperation::List,
353 self.stage,
354 &result,
355 );
356 match result {
357 Ok(stream) => self.rpc_state = RpcState::Stream(stream),
358 Err(status) => {
359 self.rpc_state = RpcState::Idle;
360 return RpcEvent::Status(status, FailurePhase::Dispatch);
361 }
362 }
363 }
364 RpcState::Stream(stream) => match stream.next().await {
365 Some(Ok(response)) => return RpcEvent::Frame(response),
366 Some(Err(status)) => {
367 self.rpc_state = RpcState::Idle;
368 return RpcEvent::Status(status, FailurePhase::Body);
369 }
370 None => {
371 self.rpc_state = RpcState::Idle;
372 return RpcEvent::Eof;
373 }
374 },
375 RpcState::Sleep(sleep) => {
376 sleep.as_mut().await;
377 self.rpc_state = RpcState::Idle;
378 return RpcEvent::Wake;
379 }
380 }
381 }
382 }
383
384 pub(super) fn process_event(
386 &mut self,
387 event: RpcEvent<A::ListResponse>,
388 config: &LedgerStreamConfig,
389 ) -> ListAction<A::ListResponse, Progress<A::Cursor>> {
390 match event {
391 RpcEvent::Wake => {
392 self.start_dispatch();
393 ListAction::Continue
394 }
395 RpcEvent::Eof => ListAction::Terminal(Status::data_loss(
397 "List stream ended before its QueryEnd frame",
398 )),
399 RpcEvent::Status(status, phase) => match self.process_status(status, phase, config) {
400 Ok(()) => ListAction::Continue,
401 Err(status) => ListAction::Terminal(status),
402 },
403 RpcEvent::Frame(response) => {
404 let (item_present, watermark, end) = A::extract_metadata(&response);
405 match self.process_frame_metadata(item_present, watermark, end, config) {
406 Ok((progress, complete)) => ListAction::Frame {
407 response,
408 progress,
409 complete,
410 },
411 Err(status) => ListAction::Terminal(status),
412 }
413 }
414 }
415 }
416
417 fn process_status(
418 &mut self,
419 status: Status,
420 phase: FailurePhase,
421 config: &LedgerStreamConfig,
422 ) -> Result<()> {
423 if let Some(cursor) = self.latest_cursor.clone() {
424 set_resume_bound::<A>(self.direction, &mut self.payload, cursor);
425 }
426 if let Some(delay) =
427 self.retry
428 .retry_delay(&status, phase, config, &self.observability, self.stage)
429 {
430 self.sleep(delay);
431 Ok(())
432 } else {
433 Err(status)
434 }
435 }
436
437 fn process_frame_metadata(
438 &mut self,
439 item_present: bool,
440 watermark: Option<&Watermark>,
441 end: Option<&QueryEnd>,
442 config: &LedgerStreamConfig,
443 ) -> Result<(Option<Progress<A::Cursor>>, bool)> {
444 let watermark =
445 watermark.ok_or_else(|| Status::data_loss("List frame is missing its watermark"))?;
446 let cursor = watermark
447 .cursor
448 .as_ref()
449 .cloned()
450 .ok_or_else(|| Status::data_loss("List watermark is missing its cursor"))?;
451
452 let checkpoint = watermark.checkpoint;
453 if checkpoint.is_none() && self.request_has_checkpoint_coverage {
454 return Err(Status::data_loss(
455 "List checkpoint coverage became unavailable",
456 ));
457 }
458 if checkpoint.is_some() {
459 self.request_has_checkpoint_coverage = true;
460 }
461 let received_prior_frame_in_request = self.received_frame_in_request;
462 let received_prior_frame_ever = self.received_any_frame;
463 if item_present
464 && end.is_none()
465 && !received_prior_frame_in_request
466 && self.request_start_resume_bound.as_ref() == Some(&cursor)
467 {
468 return Err(Status::data_loss(
469 "List item cursor equals its exclusive request resume bound",
470 ));
471 }
472 let mut frame_progress = A::Cursor::position(&cursor, checkpoint);
473 if frame_progress.is_none() && item_present {
474 return Err(Status::data_loss(
475 "List item watermark names no resumable position",
476 ));
477 }
478 if let (Some(previous), Some(next)) = (&self.latest_progress, &mut frame_progress) {
479 next.inherit_checkpoint_coverage(previous);
480 previous.validate_list_successor(next, self.direction)?;
481 }
482 self.received_frame_in_request = true;
483 self.received_any_frame = true;
484 let cursor_changed = self.latest_cursor.as_ref() != Some(&cursor);
485 if item_present && !cursor_changed {
486 return Err(Status::data_loss("List item repeated its cursor"));
487 }
488 if cursor_changed {
489 self.latest_cursor = Some(cursor.clone());
490 self.polling_attempt = 0;
491 self.retry.reset(&self.observability);
492 }
493 if let Some(progress) = &frame_progress {
494 self.latest_progress = Some(progress.clone());
495 }
496
497 let complete = match end {
498 None => false,
499 Some(end) => {
500 let reason = end
501 .reason
502 .and_then(|reason| QueryEndReason::try_from(reason).ok())
503 .filter(|reason| *reason != QueryEndReason::Unknown)
504 .ok_or_else(|| {
505 Status::data_loss("List QueryEnd has a missing or unknown reason")
506 })?;
507 validate_query_end_item(reason, item_present)?;
508 if reason == QueryEndReason::CheckpointBound {
509 A::validate_checkpoint_bound(&self.payload, self.direction, checkpoint)?;
510 }
511
512 match reason {
513 QueryEndReason::ItemLimit => {
514 if self.request_start_resume_bound.as_ref() == Some(&cursor) {
515 return Err(Status::data_loss(
516 "ItemLimit QueryEnd did not advance its cursor",
517 ));
518 }
519 set_resume_bound::<A>(self.direction, &mut self.payload, cursor.clone());
520 self.start_dispatch();
521 false
522 }
523 QueryEndReason::ScanLimit => {
524 if self.request_start_resume_bound.as_ref() == Some(&cursor) {
525 return Err(Status::data_loss(
526 "ScanLimit QueryEnd did not advance its cursor",
527 ));
528 }
529 set_resume_bound::<A>(self.direction, &mut self.payload, cursor.clone());
530 self.start_dispatch();
531 false
532 }
533 QueryEndReason::CheckpointBound | QueryEndReason::CursorBound => {
534 if !self.expected_end.accepts(
535 reason,
536 received_prior_frame_in_request,
537 self.request_start_resume_bound.is_some(),
538 ) {
539 return Err(Status::data_loss("List ended at an unexpected bound"));
540 }
541 self.rpc_state = RpcState::Idle;
542 true
543 }
544 QueryEndReason::LedgerTip
546 if self.direction == ListScanDirection::Descending
547 && !received_prior_frame_ever =>
548 {
549 self.rpc_state = RpcState::Idle;
550 true
551 }
552 QueryEndReason::LedgerTip
554 if self.direction == ListScanDirection::Descending =>
555 {
556 return Err(Status::data_loss(
557 "descending List reached LedgerTip after prior scan progress",
558 ));
559 }
560 QueryEndReason::LedgerTip
561 if matches!(self.ledger_tip_policy, LedgerTipPolicy::Complete) =>
562 {
563 self.rpc_state = RpcState::Idle;
564 true
565 }
566 QueryEndReason::LedgerTip => {
567 set_resume_bound::<A>(self.direction, &mut self.payload, cursor.clone());
568 let delay = backoff_delay(config, self.polling_attempt);
569 self.polling_attempt = self.polling_attempt.saturating_add(1);
570 self.sleep(delay);
571 false
572 }
573 QueryEndReason::Unknown => {
574 return Err(Status::data_loss("List QueryEnd has an unknown reason"));
575 }
576 }
577 }
578 };
579
580 Ok((frame_progress, complete))
581 }
582}
583
584pub(super) struct ListDriver<A: SubscriptionAdapter> {
586 machine: Option<ListMachine<A>>,
587 config: LedgerStreamConfig,
588 observability: LedgerStreamObservability,
589 terminal: Option<Status>,
590 done: bool,
591}
592
593impl<A: SubscriptionAdapter> ListDriver<A> {
594 pub(super) fn new(client: Client, request: A::ListRequest, config: LedgerStreamConfig) -> Self {
595 let observability = LedgerStreamObservability::new(config.observer());
596 let payload = request;
597 let direction = ListScanDirection::from_request::<A>(&payload);
598 let terminal = direction.as_ref().err().cloned();
599 let machine = direction.ok().map(|direction| {
600 let expected_end = determine_expected_end::<A>(&payload, direction);
601 ListMachine::new(
602 client,
603 payload,
604 expected_end,
605 direction,
606 LedgerTipPolicy::Complete,
607 observability.clone(),
608 LedgerStreamStage::List,
609 )
610 });
611 Self {
612 machine,
613 config,
614 observability,
615 terminal,
616 done: false,
617 }
618 }
619
620 pub(super) async fn next(&mut self) -> Option<Result<A::ListResponse>> {
621 if self.done {
622 return None;
623 }
624 if let Some(status) = self.terminal.take() {
625 return Some(self.finish_terminal(status));
626 }
627
628 loop {
629 let machine = self.machine.as_mut()?;
630 let event = machine.poll_rpc().await;
631 match machine.process_event(event, &self.config) {
632 ListAction::Frame {
633 response, complete, ..
634 } => {
635 if complete {
636 self.done = true;
637 self.machine = None;
638 }
639 return Some(Ok(response));
640 }
641 ListAction::Continue => {}
642 ListAction::Terminal(status) => return Some(self.finish_terminal(status)),
643 }
644 }
645 }
646
647 fn finish_terminal(&mut self, status: Status) -> Result<A::ListResponse> {
648 self.observability
649 .emit(|| LedgerStreamEvent::TerminalError {
650 family: A::FAMILY,
651 status: status.clone(),
652 });
653 self.done = true;
654 self.machine = None;
655 Err(status)
656 }
657}