Skip to main content

blockworx_store/
lock.rs

1//! The advisory single-writer lock: a `lock` entry naming the process that
2//! holds the container open for writing.
3//!
4//! Advisory on purpose. It stops the second *blockworx* from writing a log
5//! the first is also appending to; it does not — and is not meant to —
6//! stop the user's text editor, which is the point of a container made of
7//! plain files.
8
9use serde::{Deserialize, Serialize};
10
11use super::container::LOCK;
12use super::record::WallTime;
13use super::storage::{Storage, ready_now};
14
15/// Who holds the lock, as the entry records it.
16#[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/// Whether the process named in a lock entry is still running.
29#[derive(Clone, Copy, PartialEq, Eq, Debug)]
30pub enum Holder {
31    Alive,
32    Gone,
33}
34
35/// Who holds a container open for writing, in the words the reader is told
36/// it in.
37///
38/// Not every lock names a process: a host with a lock manager of its own
39/// answers whether a lock is held and nothing more, so what the reader is
40/// owed is the *fact*, with a name on it where there is one.
41#[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
56/// The answer to "may I write this container?".
57pub enum Claim {
58    Taken,
59    Held(Holding),
60}
61
62/// Claim the writer's lock on `storage`'s container — what
63/// [`Storage::claim`] does for a storage with no lock of its own.
64///
65/// A lock entry whose process is gone is *stale* — a writer that died
66/// without releasing it — and is broken rather than left to make the
67/// container unopenable forever. So is one that does not parse: it names
68/// nobody, and a container cannot be held hostage by a corrupt byte.
69///
70/// # Errors
71/// Failure to write the lock entry.
72pub async fn claim<S: Storage + ?Sized>(storage: &S, now: WallTime) -> std::io::Result<Claim> {
73    claim_probing(storage, now, holder).await
74}
75
76/// The seam the liveness probe enters by: a test can state "the holder is
77/// gone" without needing a process that reliably is.
78async 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
106/// Give the lock back. A release that does not land leaves a stale entry,
107/// which the next claim breaks — so there is nothing here for a caller to
108/// do about it.
109pub 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/// Linux answers this from `/proc`, which needs no dependency and no
116/// `unsafe`. Everywhere else the honest answer is that we cannot tell, and
117/// the safe one is to leave the lock alone: a lock wrongly broken means
118/// two writers appending to one log, while a lock wrongly kept means one
119/// read-only session and a file the user can delete.
120#[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    /// The stale case, stated without needing a process that is reliably
181    /// dead: the holder is gone, so the lock is broken and re-taken.
182    #[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    /// A lock entry that names nobody readable is not a lock.
211    #[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    /// The production probe, on the one platform that can answer it.
222    #[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        // Above every platform's pid ceiling, so it names no process now
227        // and cannot come to name one.
228        assert_eq!(holder(i32::MAX as u32), Holder::Gone);
229    }
230}