sui_rpc/client/ledger_streams/adapter/
progress.rs1use prost::bytes::Bytes;
2use tonic::Status;
3
4use super::super::super::Result;
5use super::CHECKPOINT_CURSOR_OVERFLOW;
6use super::ListScanDirection;
7
8#[derive(Clone, Debug, PartialEq, Eq)]
10pub(in crate::client::ledger_streams) struct Progress<C> {
11 pub(in crate::client::ledger_streams) cursor: C,
12 pub(in crate::client::ledger_streams) checkpoint: Option<u64>,
14}
15
16pub(in crate::client::ledger_streams) trait CursorDomain:
21 Clone + Eq + Send + 'static
22{
23 fn position(cursor: &Bytes, checkpoint: Option<u64>) -> Option<Progress<Self>>;
25
26 fn gap(&self, committed_checkpoint: u64, upper: GapUpper<'_, Self>) -> Result<RecoveryGap>;
28}
29
30pub(in crate::client::ledger_streams) enum GapUpper<'a, C> {
31 Cursor(&'a C, u64),
32 EndOfCheckpoint(u64),
33}
34
35pub(in crate::client::ledger_streams) enum Recovery {
37 Live,
39 Replay(RecoveryGap),
41 MuteUntilCommitted,
43}
44
45pub(in crate::client::ledger_streams) enum RecoveryGap {
47 Checkpoints {
49 start_checkpoint: u64,
50 end_checkpoint: u64,
51 },
52 Cursors { after: Bytes, upper: CursorGapUpper },
54}
55
56pub(in crate::client::ledger_streams) enum CursorGapUpper {
57 Before(Bytes),
58 EndOfCheckpoint(u64),
59}
60
61impl RecoveryGap {
62 pub(in crate::client::ledger_streams) fn replays_boundary_item(&self) -> bool {
63 matches!(self, Self::Checkpoints { .. })
64 }
65}
66
67pub(in crate::client::ledger_streams) enum ProgressAdvance {
71 Unchanged,
72 CheckpointCoverageAdvanced,
73 CursorAdvanced,
74}
75
76impl<C: CursorDomain> Progress<C> {
77 pub(in crate::client::ledger_streams) fn has_checkpoint_coverage(&self) -> bool {
78 self.checkpoint.is_some()
79 }
80
81 pub(in crate::client::ledger_streams) fn same_position(&self, other: &Self) -> bool {
82 self.cursor == other.cursor
83 }
84
85 pub(in crate::client::ledger_streams) fn inherit_checkpoint_coverage(
86 &mut self,
87 previous: &Self,
88 ) {
89 if self.checkpoint.is_none() {
90 self.checkpoint = previous.checkpoint;
91 }
92 }
93
94 pub(in crate::client::ledger_streams) fn classify_consecutive_subscription_progress(
95 &self,
96 next: &Self,
97 item_present: bool,
98 ) -> Result<ProgressAdvance> {
99 let current_checkpoint = self.checkpoint;
100 let next_checkpoint = next.checkpoint;
101 if matches!((current_checkpoint, next_checkpoint), (Some(_), None)) {
102 Err(Status::data_loss(
103 "subscription checkpoint coverage became unavailable",
104 ))
105 } else if matches!(
106 (current_checkpoint, next_checkpoint),
107 (Some(current), Some(next)) if next < current
108 ) {
109 Err(Status::data_loss(
110 "subscription checkpoint coverage regressed",
111 ))
112 } else if self.same_position(next) && item_present {
113 Err(Status::data_loss("subscription item repeated its cursor"))
114 } else if !self.same_position(next) {
115 Ok(ProgressAdvance::CursorAdvanced)
118 } else if matches!(
119 (current_checkpoint, next_checkpoint),
120 (Some(current), Some(next)) if next > current
121 ) || matches!((current_checkpoint, next_checkpoint), (None, Some(_)))
122 {
123 Ok(ProgressAdvance::CheckpointCoverageAdvanced)
124 } else {
125 Ok(ProgressAdvance::Unchanged)
126 }
127 }
128
129 pub(in crate::client::ledger_streams) fn validate_list_successor(
130 &self,
131 next: &Self,
132 direction: ListScanDirection,
133 ) -> Result<()> {
134 let current_checkpoint = self.checkpoint;
135 let next_checkpoint = next.checkpoint;
136 if matches!((current_checkpoint, next_checkpoint), (Some(_), None)) {
137 return Err(Status::data_loss(
138 "List checkpoint coverage became unavailable",
139 ));
140 }
141 let regressed = match (current_checkpoint, next_checkpoint) {
142 (Some(current), Some(next)) => match direction {
143 ListScanDirection::Ascending => next < current,
144 ListScanDirection::Descending => next > current,
145 },
146 _ => false,
147 };
148 if regressed {
149 Err(Status::data_loss("List checkpoint coverage regressed"))
150 } else {
151 Ok(())
152 }
153 }
154
155 pub(in crate::client::ledger_streams) fn plan_recovery(
156 &self,
157 new: &Self,
158 known_coverage: Option<u64>,
159 ) -> Result<Recovery> {
160 let committed = self.checkpoint.max(known_coverage);
161 if self.same_position(new) {
162 return if matches!(
163 (committed, new.checkpoint),
164 (Some(committed), Some(new_checkpoint)) if new_checkpoint < committed
165 ) {
166 Ok(Recovery::MuteUntilCommitted)
167 } else {
168 Ok(Recovery::Live)
169 };
170 }
171 match (committed, new.checkpoint) {
172 (Some(committed), Some(new_checkpoint)) if new_checkpoint < committed => {
173 Ok(Recovery::MuteUntilCommitted)
174 }
175 (Some(committed), Some(new_checkpoint)) if new_checkpoint > committed => {
176 Ok(Recovery::Replay(self.cursor.gap(
177 committed,
178 GapUpper::Cursor(&new.cursor, new_checkpoint),
179 )?))
180 }
181 (Some(committed), Some(_)) => Ok(Recovery::Replay(
182 self.cursor
183 .gap(committed, GapUpper::EndOfCheckpoint(committed))?,
184 )),
185 _ => Ok(Recovery::MuteUntilCommitted),
186 }
187 }
188}
189
190impl CursorDomain for u64 {
191 fn position(_cursor: &Bytes, checkpoint: Option<u64>) -> Option<Progress<Self>> {
192 checkpoint.map(|checkpoint| Progress {
193 cursor: checkpoint,
194 checkpoint: Some(checkpoint),
195 })
196 }
197
198 fn gap(&self, committed_checkpoint: u64, upper: GapUpper<'_, Self>) -> Result<RecoveryGap> {
199 let upper_checkpoint = match upper {
200 GapUpper::Cursor(_, checkpoint) | GapUpper::EndOfCheckpoint(checkpoint) => checkpoint,
201 };
202 let start_checkpoint = committed_checkpoint
203 .checked_add(1)
204 .ok_or_else(|| Status::out_of_range(CHECKPOINT_CURSOR_OVERFLOW))?;
205 let end_checkpoint = upper_checkpoint
206 .checked_add(1)
207 .ok_or_else(|| Status::out_of_range(CHECKPOINT_CURSOR_OVERFLOW))?;
208 Ok(RecoveryGap::Checkpoints {
209 start_checkpoint,
210 end_checkpoint,
211 })
212 }
213}
214
215impl CursorDomain for Bytes {
216 fn position(cursor: &Bytes, checkpoint: Option<u64>) -> Option<Progress<Self>> {
217 Some(Progress {
218 cursor: cursor.clone(),
219 checkpoint,
220 })
221 }
222
223 fn gap(&self, _committed_checkpoint: u64, upper: GapUpper<'_, Self>) -> Result<RecoveryGap> {
224 let upper = match upper {
225 GapUpper::Cursor(cursor, _) => CursorGapUpper::Before(cursor.clone()),
226 GapUpper::EndOfCheckpoint(checkpoint) => CursorGapUpper::EndOfCheckpoint(checkpoint),
227 };
228 Ok(RecoveryGap::Cursors {
229 after: self.clone(),
230 upper,
231 })
232 }
233}