1use serde::{Deserialize, Serialize};
10
11use super::container::LOCK;
12use super::record::WallTime;
13use super::storage::{Storage, ready_now};
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
35#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum Holding {
43 Process(LockHolder),
44 Elsewhere,
45}
46
47impl std::fmt::Display for Holding {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Holding::Process(held) => write!(f, "{held}"),
51 Holding::Elsewhere => f.write_str("another view of this document"),
52 }
53 }
54}
55
56pub enum Claim {
58 Taken,
59 Held(Holding),
60}
61
62pub async fn claim<S: Storage + ?Sized>(storage: &S, now: WallTime) -> std::io::Result<Claim> {
73 claim_probing(storage, now, holder).await
74}
75
76async fn claim_probing<S: Storage + ?Sized>(
79 storage: &S,
80 now: WallTime,
81 probe: impl Fn(u32) -> Holder,
82) -> std::io::Result<Claim> {
83 if let Ok(Some(bytes)) = storage.read(&LOCK).await {
84 let held = serde_json::from_slice::<LockHolder>(&bytes)
85 .ok()
86 .filter(|held| probe(held.pid) == Holder::Alive);
87 if let Some(held) = held {
88 return Ok(Claim::Held(Holding::Process(held)));
89 }
90 tracing::info!(
91 lock = storage.names(&LOCK),
92 "breaking a stale lock: the process that held it is gone",
93 );
94 }
95
96 let held = LockHolder {
97 pid: std::process::id(),
98 since: now,
99 };
100 let mut line = serde_json::to_vec(&held).map_err(std::io::Error::other)?;
101 line.push(b'\n');
102 storage.write(&LOCK, &line).await?;
103 Ok(Claim::Taken)
104}
105
106pub fn release<S: Storage + ?Sized>(storage: &S) {
110 if let Err(why) = ready_now(storage.remove(&LOCK)) {
111 tracing::warn!("the lock on {} was not released: {why}", storage.name());
112 }
113}
114
115#[cfg(target_os = "linux")]
121fn holder(pid: u32) -> Holder {
122 if std::path::Path::new(&format!("/proc/{pid}")).exists() {
123 Holder::Alive
124 } else {
125 Holder::Gone
126 }
127}
128
129#[cfg(not(target_os = "linux"))]
130fn holder(_pid: u32) -> Holder {
131 Holder::Alive
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::storage::Memory;
138
139 fn read_holder<S: Storage>(storage: &S) -> LockHolder {
140 let bytes = ready_now(storage.read(&LOCK))
141 .expect("the read answers")
142 .expect("the lock entry exists");
143 serde_json::from_slice(&bytes).expect("and parses")
144 }
145
146 #[test]
147 fn a_claim_is_held_until_it_is_released() {
148 let storage = Memory::new("doc.bwx");
149
150 match ready_now(claim(&storage, WallTime::from_unix_millis(7)))
151 .expect("the lock is written")
152 {
153 Claim::Taken => {}
154 Claim::Held(held) => panic!("a fresh container cannot be held by {held}"),
155 }
156 assert_eq!(
157 read_holder(&storage),
158 LockHolder {
159 pid: std::process::id(),
160 since: WallTime::from_unix_millis(7),
161 },
162 );
163
164 match ready_now(storage.claim(WallTime::from_unix_millis(8)))
165 .expect("the second claim answers")
166 {
167 Claim::Held(Holding::Process(held)) => assert_eq!(held.pid, std::process::id()),
168 Claim::Held(held) => panic!("a lock entry names a process, not {held}"),
169 Claim::Taken => panic!("a live holder's lock must not be broken"),
170 }
171
172 release(&storage);
173 assert_eq!(
174 ready_now(storage.read(&LOCK)).expect("the read answers"),
175 None,
176 "releasing the lock leaves the entry behind",
177 );
178 }
179
180 #[test]
183 fn a_lock_whose_holder_is_gone_is_broken() {
184 let storage = Memory::new("doc.bwx");
185 let stale = LockHolder {
186 pid: 4242,
187 since: WallTime::from_unix_millis(1),
188 };
189 ready_now(storage.write(
190 &LOCK,
191 &serde_json::to_vec(&stale).expect("a holder serializes"),
192 ))
193 .expect("the stale lock is planted");
194 assert_eq!(
195 read_holder(&storage),
196 stale,
197 "precondition: someone holds it"
198 );
199
200 let claim = ready_now(claim_probing(
201 &storage,
202 WallTime::from_unix_millis(9),
203 |_| Holder::Gone,
204 ))
205 .expect("the claim answers");
206 assert!(matches!(claim, Claim::Taken));
207 assert_eq!(read_holder(&storage).pid, std::process::id());
208 }
209
210 #[test]
212 fn an_unreadable_lock_is_broken() {
213 let storage = Memory::new("doc.bwx");
214 ready_now(storage.write(&LOCK, b"not json at all")).expect("the garbage is planted");
215
216 let claim =
217 ready_now(claim(&storage, WallTime::from_unix_millis(9))).expect("the claim answers");
218 assert!(matches!(claim, Claim::Taken));
219 }
220
221 #[cfg(target_os = "linux")]
223 #[test]
224 fn the_probe_tells_this_process_from_one_that_cannot_exist() {
225 assert_eq!(holder(std::process::id()), Holder::Alive);
226 assert_eq!(holder(i32::MAX as u32), Holder::Gone);
229 }
230}