bark/lock_manager/
pid_flock.rs1use std::fs::{self, File, TryLockError};
28use std::io::{Read, Write};
29use std::path::{Path, PathBuf};
30
31use anyhow::Context;
32
33use super::{LockGuard, LockManager, PidLockError};
34use super::memory::MemoryLockManager;
35
36pub const LOCK_FILE: &str = "LOCK";
38
39fn open_lock_file(path: &Path) -> anyhow::Result<File> {
40 File::options()
41 .read(true)
42 .write(true)
43 .create(true)
44 .truncate(false)
45 .open(path)
46 .with_context(|| format!("failed to open lock file {}", path.display()))
47}
48
49
50pub struct FlockPidLockManager {
51 _pid_file: File,
54 datadir: PathBuf,
55 in_process: MemoryLockManager,
56}
57
58impl FlockPidLockManager {
59 pub fn new(datadir: impl Into<PathBuf>) -> Result<Self, PidLockError> {
64 Self::new_with_lock_file(datadir, LOCK_FILE)
65 }
66
67 pub fn new_with_lock_file(
69 datadir: impl Into<PathBuf>,
70 lock_file: &str,
71 ) -> Result<Self, PidLockError> {
72 let datadir = datadir.into();
73 let setup = |source: anyhow::Error| PidLockError::SetupFailed {
74 datadir: datadir.clone(),
75 source,
76 };
77
78 fs::create_dir_all(&datadir)
79 .with_context(|| format!("failed to create datadir {}", datadir.display()))
80 .map_err(setup)?;
81
82 let path = datadir.join(lock_file);
83 let mut file = open_lock_file(&path).map_err(setup)?;
84
85 match file.try_lock() {
86 Ok(()) => {}
87 Err(TryLockError::WouldBlock) => {
88 let mut holder = String::new();
89 let _ = file.read_to_string(&mut holder);
90 let pid = holder.trim().parse::<u32>().ok();
91 return Err(PidLockError::AlreadyHeld { datadir, pid });
92 }
93 Err(TryLockError::Error(e)) => {
94 return Err(setup(anyhow::Error::from(e)
95 .context(format!("failed to acquire pid lock at {}", path.display()))));
96 }
97 }
98
99 file.set_len(0).context("failed to truncate pid lock").map_err(setup)?;
102 write!(file, "{}", std::process::id())
103 .context("failed to write pid lock").map_err(&setup)?;
104 file.flush().context("failed to flush pid lock").map_err(setup)?;
105
106 Ok(Self {
107 _pid_file: file,
108 datadir,
109 in_process: MemoryLockManager::new(),
110 })
111 }
112
113 pub fn datadir(&self) -> &Path {
114 &self.datadir
115 }
116}
117
118impl std::fmt::Debug for FlockPidLockManager {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 f.debug_struct("FlockPidLockManager").field("datadir", &self.datadir).finish()
121 }
122}
123
124#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
125#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
126impl LockManager for FlockPidLockManager {
127 async fn try_lock(&self, key: &str) -> Option<Box<dyn LockGuard>> {
128 self.in_process.try_lock(key).await
129 }
130
131 async fn lock(
132 &self,
133 key: &str,
134 timeout: std::time::Duration,
135 ) -> anyhow::Result<Box<dyn LockGuard>> {
136 self.in_process.lock(key, timeout).await
137 }
138}
139
140#[cfg(test)]
141mod test {
142 use super::*;
143
144 fn tmp_dir() -> PathBuf {
145 let dir = std::env::temp_dir()
146 .join(format!("bark-pid-lockmgr-{}", std::process::id()))
147 .join(format!("{}", std::time::SystemTime::now()
148 .duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
149 let _ = fs::remove_dir_all(&dir);
150 dir
151 }
152
153 #[tokio::test]
154 async fn acquire_writes_holder_pid() {
155 let dir = tmp_dir();
156 let mgr = FlockPidLockManager::new(&dir).unwrap();
157 let contents = fs::read_to_string(dir.join(LOCK_FILE)).unwrap();
158 assert_eq!(contents, std::process::id().to_string());
159 drop(mgr);
160 let _ = fs::remove_dir_all(&dir);
161 }
162
163 #[tokio::test]
164 async fn second_acquire_in_same_process_is_refused() {
165 let dir = tmp_dir();
166 let _held = FlockPidLockManager::new(&dir).unwrap();
167 let err = FlockPidLockManager::new(&dir).unwrap_err();
168 assert!(
169 err.to_string().contains("another process is already using datadir"),
170 "unexpected error: {}", err,
171 );
172 drop(_held);
173 let _ = fs::remove_dir_all(&dir);
174 }
175
176 #[tokio::test]
177 async fn distinct_lock_files_dont_conflict() {
178 let dir = tmp_dir();
179 let _default = FlockPidLockManager::new(&dir).unwrap();
180 let _custom = FlockPidLockManager::new_with_lock_file(&dir, "other.lock").unwrap();
181 let err = FlockPidLockManager::new_with_lock_file(&dir, "other.lock").unwrap_err();
182 assert!(matches!(err, PidLockError::AlreadyHeld { .. }));
183 let _ = fs::remove_dir_all(&dir);
184 }
185
186 #[tokio::test]
187 async fn reacquire_after_drop_succeeds() {
188 let dir = tmp_dir();
189 let first = FlockPidLockManager::new(&dir).unwrap();
190 drop(first);
191 let _second = FlockPidLockManager::new(&dir).unwrap();
192 let _ = fs::remove_dir_all(&dir);
193 }
194
195 #[tokio::test]
196 async fn per_key_locking_works_in_process() {
197 let dir = tmp_dir();
198 let mgr = FlockPidLockManager::new(&dir).unwrap();
199
200 let g = mgr.try_lock("foo").await;
201 assert!(g.is_some());
202
203 let busy = mgr.try_lock("foo").await;
204 assert!(busy.is_none(), "same key should be blocked");
205
206 let g2 = mgr.try_lock("bar").await;
207 assert!(g2.is_some(), "different key should be free");
208
209 let _ = fs::remove_dir_all(&dir);
210 }
211}