liminal_protocol/lifecycle/
observer_recovery.rs1use alloc::collections::{BTreeMap, BTreeSet};
2use alloc::vec::Vec;
3
4use crate::wire::{
5 ConversationId, DeliverySeq, InvalidObserverEpoch, InvalidObserverEpochList, ObserverEpoch,
6 ObserverProgressStatus, ObserverRecoveryAccepted, ObserverRecoveryHandshake,
7 ObserverRecoveryResponse,
8};
9
10#[path = "observer_recovery_errors.rs"]
11mod errors;
12pub use errors::{
13 ObserverProgressAdvanceError, ObserverProgressTrackError, ObserverRecoveryAggregateRestoreError,
14};
15
16#[path = "observer_progress_projection.rs"]
17mod projection;
18pub use projection::ObserverProgressProjection;
19
20#[path = "observer_recovery_retire.rs"]
21mod retire;
22pub use retire::{
23 ObserverProgressRetireDecision, ObserverProgressRetireError, ObserverProgressRetireTransaction,
24};
25
26#[path = "observer_recovery_transactions.rs"]
27mod transactions;
28pub use transactions::{
29 ObserverProgressAdvanceDecision, ObserverProgressAdvanceTransaction,
30 ObserverProgressTrackDecision, ObserverProgressTrackTransaction, ObserverRecoveryArm,
31 ObserverRecoveryCommit, ObserverRecoveryTransaction, ObserverRecoveryTransactionDecision,
32};
33
34#[derive(Debug, Default, PartialEq, Eq)]
70pub struct ObserverRecoveryAggregate {
71 progress: BTreeMap<ConversationId, DeliverySeq>,
72 armed: BTreeMap<ConversationId, ObserverEpoch>,
73}
74
75impl ObserverRecoveryAggregate {
76 #[must_use]
78 pub const fn new() -> Self {
79 Self {
80 progress: BTreeMap::new(),
81 armed: BTreeMap::new(),
82 }
83 }
84
85 pub fn restore(
93 progress_rows: &[(ConversationId, DeliverySeq)],
94 armed_rows: &[(ConversationId, ObserverEpoch)],
95 ) -> Result<Self, ObserverRecoveryAggregateRestoreError> {
96 let mut progress = BTreeMap::new();
97 for (conversation_id, observer_progress) in progress_rows {
98 if progress
99 .insert(*conversation_id, *observer_progress)
100 .is_some()
101 {
102 return Err(ObserverRecoveryAggregateRestoreError::DuplicateProgress {
103 conversation_id: *conversation_id,
104 });
105 }
106 }
107 let mut armed = BTreeMap::new();
108 for (conversation_id, armed_epoch) in armed_rows {
109 let Some(current_observer_progress) = progress.get(conversation_id).copied() else {
110 return Err(ObserverRecoveryAggregateRestoreError::ArmWithoutProgress {
111 conversation_id: *conversation_id,
112 });
113 };
114 if current_observer_progress != *armed_epoch {
115 return Err(ObserverRecoveryAggregateRestoreError::ArmEpochMismatch {
116 conversation_id: *conversation_id,
117 armed_epoch: *armed_epoch,
118 current_observer_progress,
119 });
120 }
121 if armed.insert(*conversation_id, *armed_epoch).is_some() {
122 return Err(ObserverRecoveryAggregateRestoreError::DuplicateArm {
123 conversation_id: *conversation_id,
124 });
125 }
126 }
127 Ok(Self { progress, armed })
128 }
129
130 pub fn install_restored_arms(
142 mut self,
143 arms: &[(ConversationId, ObserverEpoch)],
144 ) -> Result<Self, ObserverRecoveryAggregateRestoreError> {
145 for (conversation_id, armed_epoch) in arms {
146 let Some(current_observer_progress) = self.progress.get(conversation_id).copied()
147 else {
148 return Err(ObserverRecoveryAggregateRestoreError::ArmWithoutProgress {
149 conversation_id: *conversation_id,
150 });
151 };
152 if current_observer_progress != *armed_epoch {
153 return Err(ObserverRecoveryAggregateRestoreError::ArmEpochMismatch {
154 conversation_id: *conversation_id,
155 armed_epoch: *armed_epoch,
156 current_observer_progress,
157 });
158 }
159 match self.armed.get(conversation_id) {
160 Some(installed) if installed == armed_epoch => {}
161 Some(_) => {
162 return Err(ObserverRecoveryAggregateRestoreError::DuplicateArm {
163 conversation_id: *conversation_id,
164 });
165 }
166 None => {
167 self.armed.insert(*conversation_id, *armed_epoch);
168 }
169 }
170 }
171 Ok(self)
172 }
173
174 #[must_use]
176 pub fn observer_progress(&self, conversation_id: ConversationId) -> Option<DeliverySeq> {
177 self.progress.get(&conversation_id).copied()
178 }
179
180 #[must_use]
182 pub fn armed_epoch(&self, conversation_id: ConversationId) -> Option<ObserverEpoch> {
183 self.armed.get(&conversation_id).copied()
184 }
185
186 #[must_use]
188 pub fn progress_rows(&self) -> Vec<(ConversationId, DeliverySeq)> {
189 self.progress
190 .iter()
191 .map(|(conversation_id, observer_progress)| (*conversation_id, *observer_progress))
192 .collect()
193 }
194
195 #[must_use]
197 pub fn armed_rows(&self) -> Vec<(ConversationId, ObserverEpoch)> {
198 self.armed
199 .iter()
200 .map(|(conversation_id, armed_epoch)| (*conversation_id, *armed_epoch))
201 .collect()
202 }
203
204 #[must_use]
220 pub fn decide_track(
221 self,
222 conversation_id: ConversationId,
223 observer_progress: DeliverySeq,
224 ) -> ObserverProgressTrackDecision {
225 if self.progress.contains_key(&conversation_id) {
226 return ObserverProgressTrackDecision::Refuse {
227 aggregate: self,
228 error: ObserverProgressTrackError::AlreadyTracked { conversation_id },
229 };
230 }
231 ObserverProgressTrackDecision::Commit(ObserverProgressTrackTransaction {
232 aggregate: self,
233 conversation_id,
234 observer_progress,
235 })
236 }
237
238 #[must_use]
253 pub fn decide_progress_advance(
254 self,
255 conversation_id: ConversationId,
256 presented_progress: DeliverySeq,
257 ) -> ObserverProgressAdvanceDecision {
258 let Some(current) = self.progress.get(&conversation_id).copied() else {
259 return ObserverProgressAdvanceDecision::Refuse {
260 aggregate: self,
261 error: ObserverProgressAdvanceError::ConversationUnknown { conversation_id },
262 };
263 };
264 if presented_progress <= current {
265 return ObserverProgressAdvanceDecision::Refuse {
266 aggregate: self,
267 error: ObserverProgressAdvanceError::NotAdvancing {
268 conversation_id,
269 current_observer_progress: current,
270 presented_progress,
271 },
272 };
273 }
274 let fired = self
275 .armed
276 .get(&conversation_id)
277 .copied()
278 .map(|refused_epoch| ObserverRecoveryArm {
279 conversation_id,
280 refused_epoch,
281 });
282 ObserverProgressAdvanceDecision::Commit(ObserverProgressAdvanceTransaction {
283 aggregate: self,
284 conversation_id,
285 presented_progress,
286 fired,
287 })
288 }
289
290 #[must_use]
298 pub fn decide_recovery(
299 self,
300 request: &ObserverRecoveryHandshake,
301 max_entries: u64,
302 connection_conversation_limit: u64,
303 tracked_conversations: &[ConversationId],
304 ) -> ObserverRecoveryTransactionDecision {
305 let decision = apply_observer_recovery(
306 request,
307 max_entries,
308 connection_conversation_limit,
309 tracked_conversations,
310 |conversation_id| self.progress.get(&conversation_id).copied(),
311 );
312 match decision {
313 ObserverRecoveryDecision::Respond(response) => {
314 ObserverRecoveryTransactionDecision::Respond {
315 aggregate: self,
316 response,
317 }
318 }
319 ObserverRecoveryDecision::Commit(commit) => {
320 ObserverRecoveryTransactionDecision::Commit(ObserverRecoveryTransaction {
321 aggregate: self,
322 commit,
323 })
324 }
325 }
326 }
327}
328
329#[derive(Clone, Debug, PartialEq, Eq)]
331pub enum ObserverRecoveryDecision {
332 Respond(ObserverRecoveryResponse),
334 Commit(ObserverRecoveryCommit),
336}
337
338fn wire_count(value: usize) -> u64 {
339 u64::try_from(value).unwrap_or(u64::MAX)
340}
341
342#[must_use]
360pub(super) fn apply_observer_recovery<F>(
361 request: &ObserverRecoveryHandshake,
362 max_entries: u64,
363 connection_conversation_limit: u64,
364 tracked_conversations: &[ConversationId],
365 mut observer_progress: F,
366) -> ObserverRecoveryDecision
367where
368 F: FnMut(ConversationId) -> Option<DeliverySeq>,
369{
370 let presented_entries = wire_count(request.observer_refusals.len());
371 if presented_entries > max_entries {
372 return ObserverRecoveryDecision::Respond(
373 ObserverRecoveryResponse::invalid_observer_epoch_list(
374 InvalidObserverEpochList::TooManyEntries {
375 presented_entries,
376 max_entries,
377 },
378 ),
379 );
380 }
381
382 let mut first_indices = BTreeMap::new();
383 for (index, refusal) in request.observer_refusals.iter().enumerate() {
384 let request_index = wire_count(index);
385 if let Some(first_index) = first_indices.insert(refusal.conversation_id, request_index) {
386 return ObserverRecoveryDecision::Respond(
387 ObserverRecoveryResponse::invalid_observer_epoch_list(
388 InvalidObserverEpochList::DuplicateConversation {
389 conversation_id: refusal.conversation_id,
390 first_index,
391 duplicate_index: request_index,
392 },
393 ),
394 );
395 }
396 }
397
398 let mut tracked: BTreeSet<_> = tracked_conversations.iter().copied().collect();
399 for refusal in &request.observer_refusals {
400 if tracked.contains(&refusal.conversation_id) {
401 continue;
402 }
403 let occupied = wire_count(tracked.len());
404 if occupied >= connection_conversation_limit {
405 return ObserverRecoveryDecision::Respond(
406 ObserverRecoveryResponse::connection_capacity_exceeded(
407 refusal.conversation_id,
408 connection_conversation_limit,
409 ),
410 );
411 }
412 tracked.insert(refusal.conversation_id);
413 }
414
415 let mut validated = Vec::with_capacity(request.observer_refusals.len());
416 for refusal in &request.observer_refusals {
417 let Some(current_observer_progress) = observer_progress(refusal.conversation_id) else {
418 return ObserverRecoveryDecision::Respond(
419 ObserverRecoveryResponse::invalid_observer_epoch(
420 InvalidObserverEpoch::ConversationUnknown {
421 conversation_id: refusal.conversation_id,
422 presented_epoch: refusal.refused_epoch,
423 },
424 ),
425 );
426 };
427 if refusal.refused_epoch > current_observer_progress {
428 return ObserverRecoveryDecision::Respond(
429 ObserverRecoveryResponse::invalid_observer_epoch(
430 InvalidObserverEpoch::EpochAhead {
431 conversation_id: refusal.conversation_id,
432 presented_epoch: refusal.refused_epoch,
433 current_observer_progress,
434 },
435 ),
436 );
437 }
438 validated.push((refusal, current_observer_progress));
439 }
440
441 let mut arms = Vec::new();
442 let mut statuses = Vec::with_capacity(validated.len());
443 for (refusal, current_observer_progress) in validated {
444 let armed = refusal.refused_epoch == current_observer_progress;
445 if armed {
446 arms.push(ObserverRecoveryArm {
447 conversation_id: refusal.conversation_id,
448 refused_epoch: refusal.refused_epoch,
449 });
450 }
451 statuses.push(ObserverProgressStatus {
452 conversation_id: refusal.conversation_id,
453 refused_epoch: refusal.refused_epoch,
454 current_observer_progress,
455 armed,
456 progressed: !armed,
457 });
458 }
459
460 ObserverRecoveryDecision::Commit(ObserverRecoveryCommit {
461 arms,
462 outcome: ObserverRecoveryAccepted { statuses },
463 })
464}