pub struct MutationLock { /* private fields */ }Expand description
An exclusive ownership record for one installation domain.
The default acquisition path is fail-closed: ambiguous, malformed, oversized,
or otherwise unreadable records never trigger automatic removal, and
acquire never recovers a stale record at all. PID liveness
alone cannot prove process identity across this public contract, so Core
never treats it as proof.
Recovery is available, but only when a caller explicitly supplies
StaleLockVerifier evidence through
acquire_with_recovery, and only for the
exact record that evidence classified.
Implementations§
Source§impl MutationLock
impl MutationLock
Sourcepub fn acquire(
root: &Path,
product: &ProductId,
release: &ReleaseId,
) -> Result<Self>
pub fn acquire( root: &Path, product: &ProductId, release: &ReleaseId, ) -> Result<Self>
Acquires the installation-root lock using create-new filesystem semantics.
This is the default and never recovers: an existing record always
returns Error::UpdateInProgress and is left untouched.
§Errors
Returns Error::UpdateInProgress when a lock record already exists,
and Error::Io when the record cannot be created or written.
This entry point never recovers a record: an existing record is always contention, is never displaced, and is never deleted.
Examples found in repository?
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}Sourcepub fn acquire_with_recovery(
root: &Path,
product: &ProductId,
release: &ReleaseId,
verifier: &dyn StaleLockVerifier,
) -> RecoveryResult<Self>
pub fn acquire_with_recovery( root: &Path, product: &ProductId, release: &ReleaseId, verifier: &dyn StaleLockVerifier, ) -> RecoveryResult<Self>
Acquires the lock, recovering a stale record the caller has proven stale.
The sequence is deliberately conservative:
- create-new;
- on an existing record, observe it within bounds;
- ask the caller’s verifier for a verdict on that exact observation;
- only on
StaleLockDecision::ProvenStale, re-read the record and rename the pathname into a unique Eggup-owned claim path in the same directory; - re-read the claimed object and require it to still equal the authorized observation;
- create-new the real lock, then delete the claimed stale record only after ownership is established.
A record that is malformed, oversized, a symlink, non-regular, or
unreadable is never claimed. Active and Unknown verdicts retain the
record. If the record changes before the claim, no stale deletion happens
and contention is returned. If another writer creates a new lock after the
claim, that writer wins and its record is never removed.
§Errors
Returns RecoveryError::Core carrying Error::UpdateInProgress
when the record is contended, when the caller’s verifier does not
return StaleLockDecision::ProvenStale for the exact record core
observed, or when another writer wins the race after a claim; and
carrying Error::Io when the record cannot be created, read, or
written.
Returns RecoveryError::RecoveryRequired when a displaced stale
record could not be removed or verified automatically. The real
retained evidence path is in RecoveryError::evidence.
Core never decides staleness itself: PID liveness, record age, executable name, and service state stay consumer policy.
Examples found in repository?
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}Sourcepub fn observe(root: &Path) -> Result<Option<LockObservation>>
pub fn observe(root: &Path) -> Result<Option<LockObservation>>
Observes the lock record without acquiring or removing it.
Returns Ok(None) when no record exists, and a bounded observation for a
regular, non-symlink, UTF-8, in-bounds record. Records that are oversized,
non-UTF-8, symlinked, non-regular, or unreadable yield Err, because
Core cannot prove what they are and must therefore never displace them.
§Errors
Returns Error::UpdateInProgress when a record exists that core
cannot prove safe — malformed, oversized, non-UTF-8, symlinked, or
non-regular — and Error::Io when the record cannot be read.
An observation never acquires, displaces, or removes anything.
Sourcepub fn inspect(root: &Path) -> Result<LockStatus>
pub fn inspect(root: &Path) -> Result<LockStatus>
Inspects the lock record without acquiring or removing it.
Returns LockStatus::Available when no record exists,
LockStatus::Held when a bounded readable record exists, and
LockStatus::Malformed when the record is missing, unreadable,
oversized, or non-UTF-8. This function never deletes anything.
§Errors
Returns Error::Io when the record exists but cannot be read. A
malformed, oversized, symlinked, non-regular, or non-UTF-8 record is
reported as LockStatus::Malformed, not as an error.
This function never deletes anything.
Sourcepub fn path(&self) -> &Path
pub fn path(&self) -> &Path
Returns the lock record path.
Examples found in repository?
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}