1use std::path::{Path, PathBuf};
10
11use serde::{Deserialize, Serialize};
12
13use super::record::WallTime;
14
15#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
17pub struct LockHolder {
18 pub pid: u32,
19 pub since: WallTime,
20}
21
22impl std::fmt::Display for LockHolder {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 write!(f, "process {}", self.pid)
25 }
26}
27
28#[derive(Clone, Copy, PartialEq, Eq, Debug)]
30pub enum Holder {
31 Alive,
32 Gone,
33}
34
35pub struct LockGuard {
39 path: PathBuf,
40}
41
42impl LockGuard {
43 pub fn follow(&mut self, root: &Path) {
48 self.path = root.join(super::container::LOCK);
49 }
50}
51
52impl Drop for LockGuard {
53 fn drop(&mut self) {
54 let _ = std::fs::remove_file(&self.path);
55 }
56}
57
58pub enum Claim {
60 Taken(LockGuard),
61 Held(LockHolder),
62}
63
64pub fn claim(root: &Path, now: WallTime) -> std::io::Result<Claim> {
74 claim_probing(root, now, holder)
75}
76
77fn claim_probing(
80 root: &Path,
81 now: WallTime,
82 probe: impl Fn(u32) -> Holder,
83) -> std::io::Result<Claim> {
84 let path = root.join(super::container::LOCK);
85 if let Ok(text) = std::fs::read_to_string(&path) {
86 let held = serde_json::from_str::<LockHolder>(&text)
87 .ok()
88 .filter(|held| probe(held.pid) == Holder::Alive);
89 if let Some(held) = held {
90 return Ok(Claim::Held(held));
91 }
92 tracing::info!(
93 lock = %path.display(),
94 "breaking a stale lock: the process that held it is gone",
95 );
96 }
97
98 let held = LockHolder {
99 pid: std::process::id(),
100 since: now,
101 };
102 let mut line = serde_json::to_vec(&held).map_err(std::io::Error::other)?;
103 line.push(b'\n');
104 crate::atomic::write_atomically(&path, &line)?;
105 Ok(Claim::Taken(LockGuard { path }))
106}
107
108#[cfg(target_os = "linux")]
114fn holder(pid: u32) -> Holder {
115 if Path::new(&format!("/proc/{pid}")).exists() {
116 Holder::Alive
117 } else {
118 Holder::Gone
119 }
120}
121
122#[cfg(not(target_os = "linux"))]
123fn holder(_pid: u32) -> Holder {
124 Holder::Alive
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::store::tests::fixture;
131
132 fn read_holder(root: &Path) -> LockHolder {
133 let text = std::fs::read_to_string(root.join(super::super::container::LOCK))
134 .expect("the lock file exists");
135 serde_json::from_str(&text).expect("and parses")
136 }
137
138 #[test]
139 fn a_claim_is_held_until_the_guard_is_dropped() {
140 let dir = fixture::dir("lock-lifetime");
141 let root = dir.join("doc.bwx");
142 std::fs::create_dir_all(&root).expect("the container directory");
143
144 let guard = match claim(&root, WallTime::from_unix_millis(7)).expect("the lock is written")
145 {
146 Claim::Taken(guard) => guard,
147 Claim::Held(held) => panic!("a fresh container cannot be held by {held}"),
148 };
149 assert_eq!(
150 read_holder(&root),
151 LockHolder {
152 pid: std::process::id(),
153 since: WallTime::from_unix_millis(7),
154 },
155 );
156
157 match claim(&root, WallTime::from_unix_millis(8)).expect("the second claim answers") {
158 Claim::Held(held) => assert_eq!(held.pid, std::process::id()),
159 Claim::Taken(_) => panic!("a live holder's lock must not be broken"),
160 }
161
162 drop(guard);
163 assert!(
164 !root.join(super::super::container::LOCK).exists(),
165 "releasing the lock removes the file",
166 );
167 }
168
169 #[test]
172 fn a_lock_whose_holder_is_gone_is_broken() {
173 let dir = fixture::dir("lock-stale");
174 let root = dir.join("doc.bwx");
175 std::fs::create_dir_all(&root).expect("the container directory");
176 let stale = LockHolder {
177 pid: 4242,
178 since: WallTime::from_unix_millis(1),
179 };
180 std::fs::write(
181 root.join(super::super::container::LOCK),
182 serde_json::to_vec(&stale).expect("a holder serializes"),
183 )
184 .expect("the stale lock is planted");
185 assert_eq!(read_holder(&root), stale, "precondition: someone holds it");
186
187 let claim = claim_probing(&root, WallTime::from_unix_millis(9), |_| Holder::Gone)
188 .expect("the claim answers");
189 assert!(matches!(claim, Claim::Taken(_)));
190 assert_eq!(read_holder(&root).pid, std::process::id());
191 }
192
193 #[test]
195 fn an_unreadable_lock_is_broken() {
196 let dir = fixture::dir("lock-garbage");
197 let root = dir.join("doc.bwx");
198 std::fs::create_dir_all(&root).expect("the container directory");
199 std::fs::write(root.join(super::super::container::LOCK), b"not json at all")
200 .expect("the garbage is planted");
201
202 let claim = claim(&root, WallTime::from_unix_millis(9)).expect("the claim answers");
203 assert!(matches!(claim, Claim::Taken(_)));
204 }
205
206 #[cfg(target_os = "linux")]
208 #[test]
209 fn the_probe_tells_this_process_from_one_that_cannot_exist() {
210 assert_eq!(holder(std::process::id()), Holder::Alive);
211 assert_eq!(holder(i32::MAX as u32), Holder::Gone);
214 }
215}