1use crate::ProcessLock;
6use std::time::{Duration, Instant};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10#[cfg_attr(kani, derive(kani::Arbitrary))]
11pub enum Mode {
12 Overlap,
14 Exclusive,
16}
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum Step {
21 Recheck,
23 Prepare,
25 Drain,
27 Start,
29 Verify,
31 Validate,
33 Commit,
35 Retire,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41#[cfg_attr(kani, derive(kani::Arbitrary))]
42pub enum Progress {
43 Done,
45 Pending,
47 Unchanged,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum Outcome {
55 Unchanged,
57 Complete,
59 RetirementPending,
61}
62
63#[derive(Debug)]
65#[non_exhaustive]
66pub struct Failure<E> {
67 pub step: Step,
69 pub committed: bool,
71 pub reason: Reason<E>,
73}
74
75#[derive(Debug)]
77#[non_exhaustive]
78pub enum Reason<E> {
79 Deadline,
81 Adapter(E),
83 InvalidProgress,
85}
86
87impl<E: std::fmt::Display> std::fmt::Display for Failure<E> {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(f, "replacement {:?} failed: ", self.step)?;
90 match &self.reason {
91 Reason::Deadline => f.write_str("deadline expired"),
92 Reason::Adapter(error) => error.fmt(f),
93 Reason::InvalidProgress => f.write_str("invalid adapter progress"),
94 }
95 }
96}
97impl<E: std::error::Error + 'static> std::error::Error for Failure<E> {
98 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
99 match &self.reason {
100 Reason::Adapter(error) => Some(error),
101 Reason::Deadline | Reason::InvalidProgress => None,
102 }
103 }
104}
105
106#[derive(Clone, Copy, Debug)]
108pub struct Budgets {
109 pub setup: Duration,
111 pub drain: Option<Duration>,
113}
114
115pub trait Driver {
117 type Error;
119 fn perform(&mut self, step: Step, deadline: Option<Instant>) -> Result<Progress, Self::Error>;
121 fn warmup_paths(&self) -> Vec<std::path::PathBuf> {
126 Vec::new()
127 }
128}
129
130#[cfg(feature = "async")]
132pub trait AsyncDriver {
133 type Error;
135 fn perform(
137 &mut self,
138 step: Step,
139 deadline: Option<tokio::time::Instant>,
140 ) -> impl std::future::Future<Output = Result<Progress, Self::Error>> + Send;
141 fn warmup_paths(&self) -> Vec<std::path::PathBuf> {
143 Vec::new()
144 }
145}
146
147struct Machine {
148 mode: Mode,
149 step: Step,
150 committed: bool,
151}
152impl Machine {
153 fn new(mode: Mode) -> Self {
154 Self {
155 mode,
156 step: Step::Recheck,
157 committed: false,
158 }
159 }
160 fn failure<E>(&self, reason: Reason<E>) -> Failure<E> {
161 Failure {
162 step: self.step,
163 committed: self.committed,
164 reason,
165 }
166 }
167 fn advance<E>(&mut self, progress: Progress) -> Result<Option<Outcome>, Failure<E>> {
168 use Step::*;
169 match (self.step, progress) {
170 (Recheck, Progress::Unchanged) => return Ok(Some(Outcome::Unchanged)),
171 (Recheck | Verify | Drain, Progress::Pending) => return Ok(None),
172 (Retire, Progress::Pending) => return Ok(Some(Outcome::RetirementPending)),
173 (_, Progress::Done) => {}
174 _ => return Err(self.failure(Reason::InvalidProgress)),
175 }
176 self.step = match self.step {
177 Recheck => Prepare,
178 Prepare if self.mode == Mode::Exclusive => Drain,
179 Prepare | Drain => Start,
180 Start => Verify,
181 Verify => Validate,
182 Validate => Commit,
183 Commit => {
184 self.committed = true;
185 if self.mode == Mode::Exclusive {
186 return Ok(Some(Outcome::Complete));
187 }
188 Retire
189 }
190 Retire => return Ok(Some(Outcome::Complete)),
191 };
192 Ok(None)
193 }
194}
195
196pub fn run<D: Driver>(
201 _ownership: &ProcessLock,
202 mode: Mode,
203 budgets: Budgets,
204 driver: &mut D,
205) -> Result<Outcome, Failure<D::Error>> {
206 let mut machine = Machine::new(mode);
207 let mut deadline = Some(Instant::now() + budgets.setup);
208 loop {
209 if deadline.is_some_and(|limit| Instant::now() >= limit) {
210 return Err(machine.failure(Reason::Deadline));
211 }
212 let step = machine.step;
213 let progress = driver
214 .perform(step, deadline)
215 .map_err(|e| machine.failure(Reason::Adapter(e)))?;
216 if step != Step::Commit && deadline.is_some_and(|limit| Instant::now() >= limit) {
218 return Err(machine.failure(Reason::Deadline));
219 }
220 let committed_before = machine.committed;
221 if let Some(outcome) = machine.advance(progress)? {
222 if machine.committed && !committed_before {
223 crate::warmup::prefault_all(driver.warmup_paths());
224 }
225 return Ok(outcome);
226 }
227 if machine.committed && !committed_before {
228 crate::warmup::prefault_all(driver.warmup_paths());
229 }
230 if machine.step != step {
231 if machine.step == Step::Drain || machine.step == Step::Retire {
232 deadline = budgets.drain.map(|budget| Instant::now() + budget);
233 } else if step == Step::Drain {
234 deadline = Some(Instant::now() + budgets.setup);
235 }
236 } else {
237 let wait = deadline.map_or(crate::readiness::POLL_INTERVAL, |limit| {
238 crate::readiness::POLL_INTERVAL.min(limit.saturating_duration_since(Instant::now()))
239 });
240 #[expect(
241 clippy::disallowed_methods,
242 reason = "The replacement driver returned Pending without a wake source; retry within the phase budget."
243 )]
244 std::thread::sleep(wait);
245 }
246 }
247}
248
249#[cfg(feature = "async")]
251pub async fn run_async<D: AsyncDriver>(
252 _ownership: &ProcessLock,
253 mode: Mode,
254 budgets: Budgets,
255 driver: &mut D,
256) -> Result<Outcome, Failure<D::Error>> {
257 let mut machine = Machine::new(mode);
258 let mut deadline = Some(tokio::time::Instant::now() + budgets.setup);
259 loop {
260 if deadline.is_some_and(|limit| tokio::time::Instant::now() >= limit) {
261 return Err(machine.failure(Reason::Deadline));
262 }
263 let step = machine.step;
264 let result = if let Some(limit) = deadline.filter(|_| step != Step::Commit) {
267 tokio::time::timeout_at(limit, driver.perform(step, deadline))
268 .await
269 .map_err(|_| machine.failure(Reason::Deadline))?
270 } else {
271 driver.perform(step, deadline).await
272 };
273 let progress = result.map_err(|e| machine.failure(Reason::Adapter(e)))?;
274 if step != Step::Commit
275 && deadline.is_some_and(|limit| tokio::time::Instant::now() >= limit)
276 {
277 return Err(machine.failure(Reason::Deadline));
278 }
279 let committed_before = machine.committed;
280 if let Some(outcome) = machine.advance(progress)? {
281 if machine.committed && !committed_before {
282 prefault_async(driver.warmup_paths()).await;
283 }
284 return Ok(outcome);
285 }
286 if machine.committed && !committed_before {
287 prefault_async(driver.warmup_paths()).await;
288 }
289 if machine.step != step {
290 if machine.step == Step::Drain || machine.step == Step::Retire {
291 deadline = budgets
292 .drain
293 .map(|budget| tokio::time::Instant::now() + budget);
294 } else if step == Step::Drain {
295 deadline = Some(tokio::time::Instant::now() + budgets.setup);
296 }
297 } else {
298 let wake = tokio::time::Instant::now() + crate::readiness::POLL_INTERVAL;
299 #[expect(
300 clippy::disallowed_methods,
301 reason = "The replacement driver returned Pending without a wake source; retry within the phase budget."
302 )]
303 tokio::time::sleep_until(deadline.map_or(wake, |limit| limit.min(wake))).await;
304 }
305 }
306}
307
308#[cfg(feature = "async")]
311async fn prefault_async(paths: Vec<std::path::PathBuf>) {
312 if !paths.is_empty() {
313 let _ = tokio::task::spawn_blocking(move || crate::warmup::prefault_all(paths)).await;
314 }
315}
316
317#[cfg(kani)]
321mod proofs {
322 use super::*;
323
324 const ANSWERS: usize = 17;
328
329 #[kani::proof]
330 #[kani::unwind(18)]
331 fn replacement_never_claims_what_it_has_not_done() {
332 let mode: Mode = kani::any();
333 let mut machine = Machine::new(mode);
334 let mut drained = false;
335 let mut verified = false;
336 let mut validated = false;
337
338 for _ in 0..ANSWERS {
339 let step = machine.step;
340 let was_committed = machine.committed;
341 let progress: Progress = kani::any();
342 let result = machine.advance::<()>(progress);
343
344 assert!(!was_committed || machine.committed);
346
347 if progress == Progress::Done {
348 drained |= step == Step::Drain;
349 verified |= step == Step::Verify;
350 validated |= step == Step::Validate;
351 }
352
353 if mode == Mode::Overlap {
355 assert!(machine.step != Step::Drain);
356 }
357 if mode == Mode::Exclusive && machine.step == Step::Start {
359 assert!(drained);
360 }
361 if machine.committed {
363 assert!(verified && validated);
364 }
365 if machine.step == Step::Retire {
367 assert!(machine.committed);
368 }
369
370 match result {
371 Ok(Some(Outcome::Unchanged)) => {
372 assert!(step == Step::Recheck && !machine.committed);
375 return;
376 }
377 Ok(Some(Outcome::Complete | Outcome::RetirementPending)) => {
378 assert!(machine.committed);
381 return;
382 }
383 Ok(None) => {}
384 Err(failure) => {
385 assert!(failure.committed == machine.committed);
388 assert!(failure.step == step);
389 return;
390 }
391 }
392 }
393 }
394}
395
396#[cfg(test)]
397mod error_tests {
398 use super::*;
399
400 #[test]
401 fn an_adapter_failure_preserves_its_source() {
402 let failure = Failure {
403 step: Step::Start,
404 committed: false,
405 reason: Reason::Adapter(std::io::Error::other("failed to spawn")),
406 };
407 assert_eq!(
408 std::error::Error::source(&failure).unwrap().to_string(),
409 "failed to spawn"
410 );
411 for reason in [Reason::Deadline, Reason::InvalidProgress] {
412 let failure: Failure<std::io::Error> = Failure {
413 step: Step::Start,
414 committed: false,
415 reason,
416 };
417 assert!(std::error::Error::source(&failure).is_none());
418 }
419 }
420}