Skip to main content

stale_lock_recovery/
stale_lock_recovery.rs

1//! Proof-authorized stale-lock recovery.
2//!
3//! Core owns safe lock mutation and refuses to decide staleness on its own. This
4//! example shows the shape a consumer implements: it inspects what Core can
5//! prove about the record, combines that with deployment evidence only it has,
6//! and returns `ProvenStale` only when the combination is conclusive.
7//!
8//! The evidence is deliberately deployment-shaped rather than universal. A real
9//! consumer would additionally consult its own service manager, its own lock
10//! semantics, or an operator-provisioned deadline. What matters for this crate's
11//! contract is the shape:
12//!
13//! - `Active` and `Unknown` retain the record and fail closed.
14//! - Only `ProvenStale` authorizes recovery, and only for the exact observed
15//!   record.
16//! - Core never consults process liveness, record age, executable name, or
17//!   service state on the verifier's behalf.
18//!
19//! Run with: `cargo run -p eggup-core --example stale_lock_recovery`
20
21use eggup_core::{
22    LockObservation, MutationLock, ProductId, ReleaseId, StaleLockDecision, StaleLockVerifier,
23};
24
25/// Evidence a consumer gathers from its own deployment.
26///
27/// `supervised` is what this consumer is *responsible for`; `alive` is what it
28/// can currently *see*. Keeping them apart is the point: a pid the consumer
29/// never supervised cannot be reasoned about at all, and a supervised pid it can
30/// no longer see is merely suggestive on its own.
31#[derive(Debug)]
32struct DeploymentEvidence {
33    product: ProductId,
34    supervised: Vec<u32>,
35    alive: Vec<u32>,
36    /// Whether the deployment has an out-of-band statement that it is idle.
37    declared_idle: bool,
38}
39
40/// A consumer verifier that combines a record with its own evidence.
41///
42/// Note what it does *not* do: it does not treat "the pid is not alive" as proof
43/// by itself. Pids are reused, so liveness is corroboration, never authority.
44struct DeploymentVerifier {
45    evidence: DeploymentEvidence,
46}
47
48impl std::fmt::Debug for DeploymentVerifier {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("DeploymentVerifier")
51            .field("evidence", &self.evidence)
52            .finish()
53    }
54}
55
56impl StaleLockVerifier for DeploymentVerifier {
57    fn classify(&self, observed: &LockObservation) -> StaleLockDecision {
58        // The record must belong to this consumer's domain before its fields
59        // mean anything. Core observed the bytes; attributing them is the
60        // caller's job.
61        if observed.product() != Some(self.evidence.product.as_str()) {
62            return StaleLockDecision::Unknown;
63        }
64        // An unrecognised record format carries no attributable evidence.
65        if !observed.format_known() {
66            return StaleLockDecision::Unknown;
67        }
68        let Some(pid) = observed.pid() else {
69            return StaleLockDecision::Unknown;
70        };
71        // A supervised process this consumer can still see is plainly active.
72        if self.evidence.alive.contains(&pid) {
73            return StaleLockDecision::Active;
74        }
75        // A pid this consumer never supervised is ambiguous: the number may now
76        // belong to an unrelated process entirely.
77        if !self.evidence.supervised.contains(&pid) {
78            return StaleLockDecision::Unknown;
79        }
80        // Conclusive only with the deployment's own out-of-band statement.
81        if self.evidence.declared_idle {
82            StaleLockDecision::ProvenStale
83        } else {
84            StaleLockDecision::Unknown
85        }
86    }
87}
88
89fn main() -> Result<(), Box<dyn std::error::Error>> {
90    let root =
91        std::env::temp_dir().join(format!("eggup-stale-lock-example-{}", std::process::id()));
92    let _ = std::fs::remove_dir_all(&root);
93    std::fs::create_dir_all(&root)?;
94
95    let product = ProductId::new("example-daemon")?;
96    let release = ReleaseId::new("2026.10.05")?;
97    let lock_path = root.join(".eggup-mutation.lock");
98
99    // A crashed updater left this record behind.
100    std::fs::write(
101        &lock_path,
102        "pid=4242 nonce=9 product=example-daemon release=2026.10.05\n",
103    )?;
104
105    println!("default acquisition never recovers:");
106    let error = MutationLock::acquire(&root, &product, &release).expect_err("record exists");
107    println!("  {error}\n");
108
109    // The consumer supervises pid 4242, can no longer see it, and the deployment
110    // is declared idle. That combination is its proof.
111    let conclusive = DeploymentVerifier {
112        evidence: DeploymentEvidence {
113            product: product.clone(),
114            supervised: vec![4242],
115            alive: Vec::new(),
116            declared_idle: true,
117        },
118    };
119    match MutationLock::acquire_with_recovery(&root, &product, &release, &conclusive) {
120        Ok(lock) => {
121            println!("recovered after caller proof:");
122            println!("  lock record: {}", lock.path().display());
123            println!("  our own record is removed when it is dropped");
124        }
125        Err(error) => println!("retained the record: {error}"),
126    }
127
128    // Contrast: the same record without the deployment's out-of-band statement is
129    // ambiguous, so it is left exactly where it is.
130    let _ = std::fs::remove_file(&lock_path);
131    std::fs::write(
132        &lock_path,
133        "pid=5150 nonce=1 product=example-daemon release=2026.10.05\n",
134    )?;
135    let cautious = DeploymentVerifier {
136        evidence: DeploymentEvidence {
137            product: product.clone(),
138            supervised: vec![5150],
139            alive: Vec::new(),
140            declared_idle: false,
141        },
142    };
143    let error = MutationLock::acquire_with_recovery(&root, &product, &release, &cautious)
144        .expect_err("no out-of-band proof");
145    println!("\nwithout out-of-band proof:");
146    println!("  {error}");
147    println!("  record still present: {}", lock_path.exists());
148
149    // And a record belonging to a different deployment is never claimed.
150    let _ = std::fs::remove_file(&lock_path);
151    std::fs::write(
152        &lock_path,
153        "pid=6060 nonce=1 product=somebody-else release=2026.10.05\n",
154    )?;
155    let foreign = DeploymentVerifier {
156        evidence: DeploymentEvidence {
157            product: ProductId::new("example-daemon")?,
158            supervised: vec![6060],
159            alive: Vec::new(),
160            declared_idle: true,
161        },
162    };
163    let error = MutationLock::acquire_with_recovery(&root, &product, &release, &foreign)
164        .expect_err("foreign record");
165    println!("\nfor another deployment's record:");
166    println!("  {error}");
167    println!("  record still present: {}", lock_path.exists());
168
169    let _ = std::fs::remove_dir_all(&root);
170    Ok(())
171}