1use crate::wire::{AckCommitted, BindingEpoch, DeliverySeq, ParticipantId};
4
5use super::{
6 ClosureState, CursorEpisodeBuildError, FrontierBinding, LiveFrontierOwner, LiveMember,
7 NonzeroDebtCursorEpisode, NonzeroParticipantAckCommit, NonzeroParticipantAckCommitError,
8};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum ObligationDebtOwnerError {
13 Episode(CursorEpisodeBuildError),
15 NonzeroAck(NonzeroParticipantAckCommitError),
17 NonzeroAckState,
19}
20
21#[derive(Debug, PartialEq, Eq)]
23pub struct CoupledObligationDebtOwner {
24 frontier: LiveFrontierOwner,
25 episode: NonzeroDebtCursorEpisode,
26}
27
28#[derive(Debug, PartialEq, Eq)]
30pub enum ObligationDebtDispatchState {
31 Clear(LiveFrontierOwner),
33 Owed(CoupledObligationDebtOwner),
35}
36
37#[derive(Debug)]
40pub struct ObligationDebtDispatchTransition {
41 prior_episode: Option<NonzeroDebtCursorEpisode>,
42}
43
44impl ObligationDebtDispatchState {
45 pub fn from_frontier(
52 frontier: LiveFrontierOwner,
53 observer_progress: DeliverySeq,
54 ) -> Result<Self, ObligationDebtOwnerError> {
55 match frontier.closure_accounting().state() {
56 ClosureState::Clear => Ok(Self::Clear(frontier)),
57 ClosureState::Owed { debt, .. } => {
58 let episode = NonzeroDebtCursorEpisode::from_claim_frontiers(
59 frontier.frontiers(),
60 debt,
61 observer_progress,
62 )
63 .map_err(ObligationDebtOwnerError::Episode)?;
64 Ok(Self::Owed(CoupledObligationDebtOwner { frontier, episode }))
65 }
66 }
67 }
68
69 #[must_use]
71 pub const fn frontier(&self) -> &LiveFrontierOwner {
72 match self {
73 Self::Clear(frontier) => frontier,
74 Self::Owed(coupled) => &coupled.frontier,
75 }
76 }
77
78 #[must_use]
80 pub const fn episode(&self) -> Option<&NonzeroDebtCursorEpisode> {
81 match self {
82 Self::Clear(_) => None,
83 Self::Owed(coupled) => Some(&coupled.episode),
84 }
85 }
86
87 #[must_use]
90 pub fn frontier_participant(
91 &self,
92 participant_id: ParticipantId,
93 ) -> Option<(FrontierBinding, DeliverySeq)> {
94 self.frontier()
95 .frontiers()
96 .active_identities()
97 .participants()
98 .iter()
99 .find(|participant| participant.participant_index() == participant_id)
100 .map(|participant| (participant.binding(), participant.cursor()))
101 }
102
103 #[must_use]
105 pub fn participant(
106 &self,
107 participant_id: ParticipantId,
108 ) -> Option<(FrontierBinding, DeliverySeq)> {
109 self.episode()?.participant_binding(participant_id)
110 }
111
112 #[must_use]
115 pub fn begin_transition(self) -> (LiveFrontierOwner, ObligationDebtDispatchTransition) {
116 match self {
117 Self::Clear(frontier) => (
118 frontier,
119 ObligationDebtDispatchTransition {
120 prior_episode: None,
121 },
122 ),
123 Self::Owed(coupled) => (
124 coupled.frontier,
125 ObligationDebtDispatchTransition {
126 prior_episode: Some(coupled.episode),
127 },
128 ),
129 }
130 }
131}
132
133impl ObligationDebtDispatchTransition {
134 pub fn complete(
143 self,
144 frontier: LiveFrontierOwner,
145 observer_progress: DeliverySeq,
146 ) -> Result<ObligationDebtDispatchState, ObligationDebtOwnerError> {
147 match frontier.closure_accounting().state() {
148 ClosureState::Clear => Ok(ObligationDebtDispatchState::Clear(frontier)),
149 ClosureState::Owed { debt, .. } => {
150 let episode = self
151 .prior_episode
152 .map_or_else(
153 || {
154 NonzeroDebtCursorEpisode::from_claim_frontiers(
155 frontier.frontiers(),
156 debt,
157 observer_progress,
158 )
159 },
160 |episode| {
161 episode.reconcile_claim_frontiers(
162 frontier.frontiers(),
163 debt,
164 observer_progress,
165 )
166 },
167 )
168 .map_err(ObligationDebtOwnerError::Episode)?;
169 Ok(ObligationDebtDispatchState::Owed(
170 CoupledObligationDebtOwner { frontier, episode },
171 ))
172 }
173 }
174 }
175
176 pub fn complete_nonzero_ack<F>(
187 mut self,
188 frontier: LiveFrontierOwner,
189 commit: NonzeroParticipantAckCommit,
190 member: &mut LiveMember<F>,
191 observer_progress: DeliverySeq,
192 ) -> Result<(ObligationDebtDispatchState, AckCommitted), ObligationDebtOwnerError> {
193 let ClosureState::Owed { debt, .. } = frontier.closure_accounting().state() else {
194 return Err(ObligationDebtOwnerError::NonzeroAckState);
195 };
196 let mut episode = self
197 .prior_episode
198 .take()
199 .ok_or(ObligationDebtOwnerError::NonzeroAckState)?;
200 let outcome = commit
201 .apply_to(member, &mut episode)
202 .map_err(ObligationDebtOwnerError::NonzeroAck)?;
203 if episode.debt() != debt {
204 return Err(ObligationDebtOwnerError::NonzeroAckState);
205 }
206 let reconciled = episode
207 .clone()
208 .reconcile_claim_frontiers(frontier.frontiers(), debt, observer_progress)
209 .map_err(ObligationDebtOwnerError::Episode)?;
210 if reconciled != episode {
211 return Err(ObligationDebtOwnerError::NonzeroAckState);
212 }
213 Ok((
214 ObligationDebtDispatchState::Owed(CoupledObligationDebtOwner { frontier, episode }),
215 outcome,
216 ))
217 }
218}
219
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
222pub enum DebtDispatchDeferral {
223 NoObligation,
225 NoCurrentBinding,
227 BeyondDebtHighWatermark,
229}
230
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
233pub enum DebtDispatchInvariant {
234 CoupledState,
236 ParticipantAuthority,
238 OutboxSelection,
240 ScalarDivergence,
242}
243
244#[derive(Debug, PartialEq, Eq)]
246pub enum ObligationDebtDispatchDecision<T> {
247 Permit(T),
249 Defer(DebtDispatchDeferral),
251 Invariant(DebtDispatchInvariant),
253}
254
255pub fn decide_obligation_debt_dispatch<T>(
264 state: &ObligationDebtDispatchState,
265 participant_id: ParticipantId,
266 binding_epoch: BindingEpoch,
267 dispatch_after: DeliverySeq,
268 select_next: impl FnOnce(ParticipantId, BindingEpoch, DeliverySeq) -> Option<(DeliverySeq, T)>,
269) -> ObligationDebtDispatchDecision<T> {
270 let frontier_participant = state
271 .frontier()
272 .frontiers()
273 .active_identities()
274 .participants()
275 .iter()
276 .find(|participant| participant.participant_index() == participant_id)
277 .copied();
278 let Some(frontier_participant) = frontier_participant else {
279 return ObligationDebtDispatchDecision::Invariant(
280 DebtDispatchInvariant::ParticipantAuthority,
281 );
282 };
283 let FrontierBinding::Bound(frontier_epoch) = frontier_participant.binding() else {
284 return ObligationDebtDispatchDecision::Defer(DebtDispatchDeferral::NoCurrentBinding);
285 };
286 if frontier_epoch != binding_epoch || frontier_participant.cursor() > dispatch_after {
287 return ObligationDebtDispatchDecision::Invariant(
288 DebtDispatchInvariant::ParticipantAuthority,
289 );
290 }
291
292 let owed_high_watermark = match state {
293 ObligationDebtDispatchState::Clear(frontier) => {
294 if !matches!(frontier.closure_accounting().state(), ClosureState::Clear) {
295 return ObligationDebtDispatchDecision::Invariant(
296 DebtDispatchInvariant::CoupledState,
297 );
298 }
299 None
300 }
301 ObligationDebtDispatchState::Owed(coupled) => {
302 let ClosureState::Owed { debt, .. } = coupled.frontier.closure_accounting().state()
303 else {
304 return ObligationDebtDispatchDecision::Invariant(
305 DebtDispatchInvariant::CoupledState,
306 );
307 };
308 if coupled.episode.debt() != debt {
309 return ObligationDebtDispatchDecision::Invariant(
310 DebtDispatchInvariant::CoupledState,
311 );
312 }
313 if coupled.episode.participant_binding(participant_id)
314 != Some((
315 FrontierBinding::Bound(binding_epoch),
316 frontier_participant.cursor(),
317 ))
318 {
319 return ObligationDebtDispatchDecision::Invariant(
320 DebtDispatchInvariant::ParticipantAuthority,
321 );
322 }
323 Some(coupled.episode.candidate_high_watermark())
324 }
325 };
326
327 classify_selected_obligation(
328 dispatch_after,
329 owed_high_watermark,
330 select_next(participant_id, binding_epoch, dispatch_after),
331 )
332}
333
334fn classify_selected_obligation<T>(
335 dispatch_after: DeliverySeq,
336 owed_high_watermark: Option<DeliverySeq>,
337 selected: Option<(DeliverySeq, T)>,
338) -> ObligationDebtDispatchDecision<T> {
339 let Some((delivery_seq, selected)) = selected else {
340 return ObligationDebtDispatchDecision::Defer(DebtDispatchDeferral::NoObligation);
341 };
342 if delivery_seq <= dispatch_after {
343 return ObligationDebtDispatchDecision::Invariant(DebtDispatchInvariant::OutboxSelection);
344 }
345 if owed_high_watermark.is_some_and(|high_watermark| delivery_seq > high_watermark) {
346 return ObligationDebtDispatchDecision::Defer(
347 DebtDispatchDeferral::BeyondDebtHighWatermark,
348 );
349 }
350 ObligationDebtDispatchDecision::Permit(selected)
351}
352
353#[cfg(test)]
354mod tests {
355 use alloc::vec;
356
357 use crate::{
358 algebra::WideResourceVector,
359 wire::{BindingEpoch, ConnectionIncarnation, Generation},
360 };
361
362 use super::{
363 super::{BoundParticipantCursor, ClosureDebt, NonzeroDebtCursorEpisode},
364 DebtDispatchDeferral, DebtDispatchInvariant, ObligationDebtDispatchDecision,
365 classify_selected_obligation,
366 };
367
368 fn binding_epoch() -> BindingEpoch {
369 BindingEpoch::new(
370 ConnectionIncarnation::new(1, 1),
371 Generation::new(1).unwrap_or(Generation::ONE),
372 )
373 }
374
375 #[test]
376 fn nonzero_debt_permits_testified_below_floor_and_defers_above_high_watermark() {
377 let cursor = 0;
378 let high_watermark = 100;
379 let physical_floor = 25;
380 let capacity_floor = 25;
381 let below_floor_endpoint = 10;
382 let above_high_watermark = high_watermark + 1;
383 let episode = NonzeroDebtCursorEpisode::new(
384 1,
385 ClosureDebt::new(WideResourceVector::new(1, 1))
386 .unwrap_or_else(|| unreachable!("fixture debt is nonzero")),
387 high_watermark,
388 high_watermark,
389 physical_floor,
390 capacity_floor,
391 vec![BoundParticipantCursor::new(0, binding_epoch(), cursor)],
392 )
393 .unwrap_or_else(|error| unreachable!("fixture episode must be valid: {error:?}"));
394
395 assert_eq!(episode.candidate_high_watermark(), high_watermark);
396 assert_eq!(episode.observer_progress(), high_watermark);
397 assert_eq!(episode.cap_floor(), capacity_floor);
398 assert_eq!(episode.retained_suffix_start(), Some(25));
399 assert!(!episode.retains(below_floor_endpoint));
400 assert_eq!(
401 classify_selected_obligation(
402 cursor,
403 Some(episode.candidate_high_watermark()),
404 Some((below_floor_endpoint, below_floor_endpoint)),
405 ),
406 ObligationDebtDispatchDecision::Permit(below_floor_endpoint)
407 );
408 assert_eq!(
409 classify_selected_obligation(
410 cursor,
411 Some(episode.candidate_high_watermark()),
412 Some((above_high_watermark, above_high_watermark)),
413 ),
414 ObligationDebtDispatchDecision::Defer(DebtDispatchDeferral::BeyondDebtHighWatermark)
415 );
416 assert_eq!(
417 classify_selected_obligation::<u64>(
418 cursor,
419 Some(episode.candidate_high_watermark()),
420 None,
421 ),
422 ObligationDebtDispatchDecision::Defer(DebtDispatchDeferral::NoObligation)
423 );
424 }
425
426 #[test]
427 fn debt_dispatch_invariant_never_falls_back_or_fabricates_wire_refusal() {
428 let cursor = 7;
429 let selected_payload = "durable publication";
430 let decision =
431 classify_selected_obligation(cursor, Some(100), Some((cursor, selected_payload)));
432
433 assert_eq!(
434 decision,
435 ObligationDebtDispatchDecision::Invariant(DebtDispatchInvariant::OutboxSelection)
436 );
437 assert!(!matches!(
438 decision,
439 ObligationDebtDispatchDecision::Permit(_)
440 ));
441 assert!(!matches!(
442 decision,
443 ObligationDebtDispatchDecision::Defer(_)
444 ));
445 }
446}