blockworx/storage/
lock.rs1use std::fs::File;
18use std::path::Path;
19
20use serde::{Deserialize, Serialize};
21
22pub const LOCK: &str = "lock";
24
25#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
27pub struct Holder {
28 pub pid: u32,
29 pub since: u64,
31}
32
33#[derive(Debug)]
36pub struct Lock {
37 _file: File,
39}
40
41#[derive(Debug)]
43pub enum Claim {
44 Held(Lock),
45 Taken(Option<Holder>),
48}
49
50pub fn claim(dir: &Path, now: u64) -> std::io::Result<Claim> {
55 std::fs::create_dir_all(dir)?;
56 let path = dir.join(LOCK);
57 let file = File::options()
58 .read(true)
59 .write(true)
60 .create(true)
61 .truncate(false)
62 .open(&path)?;
63
64 match file.try_lock() {
65 Ok(()) => {
66 let holder = Holder {
69 pid: std::process::id(),
70 since: now,
71 };
72 file.set_len(0)?;
73 serde_json::to_writer(&file, &holder).map_err(std::io::Error::other)?;
74 Ok(Claim::Held(Lock { _file: file }))
75 }
76 Err(std::fs::TryLockError::WouldBlock) => Ok(Claim::Taken(read_holder(&path))),
78 Err(std::fs::TryLockError::Error(e)) => Err(e),
79 }
80}
81
82fn read_holder(path: &Path) -> Option<Holder> {
83 let bytes = std::fs::read(path).ok()?;
84 serde_json::from_slice(&bytes).ok()
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::storage::atomic::tests::TempDir;
91
92 #[test]
93 fn claiming_a_free_container_succeeds_and_names_the_holder() {
94 let dir = TempDir::new("lock-free");
95 let root = dir.join("d.bwx");
96 let Claim::Held(_held) = claim(&root, 1234).unwrap() else {
97 panic!("a container nobody holds should be claimable");
98 };
99 assert_eq!(
100 read_holder(&root.join(LOCK)),
101 Some(Holder {
102 pid: std::process::id(),
103 since: 1234,
104 })
105 );
106 }
107
108 #[test]
111 fn a_second_claim_is_refused_and_can_say_who_holds_it() {
112 let dir = TempDir::new("lock-contended");
113 let root = dir.join("d.bwx");
114 let Claim::Held(_first) = claim(&root, 1234).unwrap() else {
115 panic!("the first claim should succeed");
116 };
117
118 let Claim::Taken(holder) = claim(&root, 5678).unwrap() else {
119 panic!("the second claim should be refused");
120 };
121 assert_eq!(
122 holder,
123 Some(Holder {
124 pid: std::process::id(),
125 since: 1234,
126 }),
127 "the refusal should name who has it"
128 );
129 }
130
131 #[test]
134 fn releasing_the_claim_frees_the_container() {
135 let dir = TempDir::new("lock-release");
136 let root = dir.join("d.bwx");
137
138 let first = claim(&root, 1).unwrap();
139 assert!(matches!(first, Claim::Held(_)));
140 assert!(matches!(claim(&root, 2).unwrap(), Claim::Taken(_)));
141
142 drop(first);
143 let Claim::Held(_) = claim(&root, 3).unwrap() else {
144 panic!("the claim was not released");
145 };
146 }
147
148 #[test]
151 fn an_unreadable_lock_file_does_not_block_a_claim() {
152 let dir = TempDir::new("lock-garbage");
153 let root = dir.join("d.bwx");
154 std::fs::create_dir_all(&root).unwrap();
155 std::fs::write(root.join(LOCK), b"not json at all").unwrap();
156
157 let Claim::Held(_held) = claim(&root, 9).unwrap() else {
158 panic!("nobody holds this, whatever the file says");
159 };
160 assert_eq!(read_holder(&root.join(LOCK)).map(|h| h.since), Some(9));
162 }
163
164 #[test]
167 fn a_refused_claim_leaves_the_holders_identity_alone() {
168 let dir = TempDir::new("lock-identity");
169 let root = dir.join("d.bwx");
170 let Claim::Held(_first) = claim(&root, 111).unwrap() else {
171 panic!("the first claim should succeed");
172 };
173 let _ = claim(&root, 222).unwrap();
174 assert_eq!(read_holder(&root.join(LOCK)).map(|h| h.since), Some(111));
175 }
176}