stale_lock_recovery/
stale_lock_recovery.rs1use eggup_core::{
22 LockObservation, MutationLock, ProductId, ReleaseId, StaleLockDecision, StaleLockVerifier,
23};
24
25#[derive(Debug)]
32struct DeploymentEvidence {
33 product: ProductId,
34 supervised: Vec<u32>,
35 alive: Vec<u32>,
36 declared_idle: bool,
38}
39
40struct 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 if observed.product() != Some(self.evidence.product.as_str()) {
62 return StaleLockDecision::Unknown;
63 }
64 if !observed.format_known() {
66 return StaleLockDecision::Unknown;
67 }
68 let Some(pid) = observed.pid() else {
69 return StaleLockDecision::Unknown;
70 };
71 if self.evidence.alive.contains(&pid) {
73 return StaleLockDecision::Active;
74 }
75 if !self.evidence.supervised.contains(&pid) {
78 return StaleLockDecision::Unknown;
79 }
80 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 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 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 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 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}