1use super::{RebaseConflict, RebaseDecision};
4use crate::foundation::{Digest, Epoch, GenerationId, Head};
5use thiserror::Error;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum LivePublicationObservation {
10 Committed {
12 generation_id: GenerationId,
14 head: Head,
16 },
17 AlreadyCommitted {
19 generation_id: GenerationId,
21 head: Head,
23 },
24 Conflict {
26 actual: Head,
28 },
29 Fenced {
31 actual_epoch: Epoch,
33 },
34 IdempotencyConflict {
36 committed_fingerprint: Digest,
38 },
39}
40
41#[derive(Clone, Debug, Eq, PartialEq)]
43pub enum LiveMutationOutcome {
44 Committed {
46 generation_id: GenerationId,
48 head: Head,
50 },
51 AlreadyCommitted {
53 generation_id: GenerationId,
55 head: Head,
57 },
58 Conflicted {
60 conflicts: Vec<RebaseConflict>,
62 truncated: bool,
64 },
65 RetryLimit {
67 actual: Head,
69 },
70 Fenced {
72 actual_epoch: Epoch,
74 },
75 IdempotencyConflict {
77 committed_fingerprint: Digest,
79 },
80}
81
82#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum LiveRetryAction {
85 Rebase,
87 Publish,
89 Complete(LiveMutationOutcome),
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94enum Phase {
95 Publication,
96 Rebase,
97 Complete,
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
102pub struct LiveRetryState {
103 attempts_remaining: u32,
104 phase: Phase,
105}
106
107impl LiveRetryState {
108 pub const fn new(maximum_attempts: u32) -> Result<Self, LiveRetryError> {
114 if maximum_attempts == 0 {
115 return Err(LiveRetryError::ZeroAttempts);
116 }
117 Ok(Self {
118 attempts_remaining: maximum_attempts,
119 phase: Phase::Publication,
120 })
121 }
122
123 pub fn observe_publication(
129 &mut self,
130 observation: LivePublicationObservation,
131 ) -> Result<LiveRetryAction, LiveRetryError> {
132 if self.phase != Phase::Publication {
133 return Err(LiveRetryError::InvalidTransition);
134 }
135 let terminal = match observation {
136 LivePublicationObservation::Committed {
137 generation_id,
138 head,
139 } => Some(LiveMutationOutcome::Committed {
140 generation_id,
141 head,
142 }),
143 LivePublicationObservation::AlreadyCommitted {
144 generation_id,
145 head,
146 } => Some(LiveMutationOutcome::AlreadyCommitted {
147 generation_id,
148 head,
149 }),
150 LivePublicationObservation::Fenced { actual_epoch } => {
151 Some(LiveMutationOutcome::Fenced { actual_epoch })
152 }
153 LivePublicationObservation::IdempotencyConflict {
154 committed_fingerprint,
155 } => Some(LiveMutationOutcome::IdempotencyConflict {
156 committed_fingerprint,
157 }),
158 LivePublicationObservation::Conflict { actual } => {
159 self.attempts_remaining -= 1;
160 if self.attempts_remaining == 0 {
161 Some(LiveMutationOutcome::RetryLimit { actual })
162 } else {
163 self.phase = Phase::Rebase;
164 return Ok(LiveRetryAction::Rebase);
165 }
166 }
167 };
168 self.phase = Phase::Complete;
169 Ok(LiveRetryAction::Complete(
170 terminal.ok_or(LiveRetryError::InvalidTransition)?,
171 ))
172 }
173
174 pub fn observe_rebase(
180 &mut self,
181 decision: RebaseDecision,
182 ) -> Result<LiveRetryAction, LiveRetryError> {
183 if self.phase != Phase::Rebase {
184 return Err(LiveRetryError::InvalidTransition);
185 }
186 match decision {
187 RebaseDecision::Safe { .. } => {
188 self.phase = Phase::Publication;
189 Ok(LiveRetryAction::Publish)
190 }
191 RebaseDecision::Conflicted {
192 conflicts,
193 truncated,
194 } => {
195 self.phase = Phase::Complete;
196 Ok(LiveRetryAction::Complete(LiveMutationOutcome::Conflicted {
197 conflicts,
198 truncated,
199 }))
200 }
201 }
202 }
203}
204
205#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
207pub enum LiveRetryError {
208 #[error("live mutation retry bound must be positive")]
210 ZeroAttempts,
211 #[error("live mutation retry transition is invalid")]
213 InvalidTransition,
214}
215
216#[cfg(test)]
217#[path = "tests/live.rs"]
218mod tests;