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 let datadir = datadir.into();
65 let setup = |source: anyhow::Error| PidLockError::SetupFailed {
66 datadir: datadir.clone(),
67 source,
68 };
69
70 fs::create_dir_all(&datadir)
71 .with_context(|| format!("failed to create datadir {}", datadir.display()))
72 .map_err(setup)?;
73
74 let path = datadir.join(LOCK_FILE);
75 let mut file = open_lock_file(&path).map_err(setup)?;
76
77 match file.try_lock() {
78 Ok(()) => {}
79 Err(TryLockError::WouldBlock) => {
80 let mut holder = String::new();
81 let _ = file.read_to_string(&mut holder);
82 let pid = holder.trim().parse::<u32>().ok();
83 return Err(PidLockError::AlreadyHeld { datadir, pid });
84 }
85 Err(TryLockError::Error(e)) => {
86 return Err(setup(anyhow::Error::from(e)
87 .context(format!("failed to acquire pid lock at {}", path.display()))));
88 }
89 }
90
91 file.set_len(0).context("failed to truncate pid lock").map_err(setup)?;
94 write!(file, "{}", std::process::id())
95 .context("failed to write pid lock").map_err(&setup)?;
96 file.flush().context("failed to flush pid lock").map_err(setup)?;
97
98 Ok(Self {
99 _pid_file: file,
100 datadir,
101 in_process: MemoryLockManager::new(),
102 })
103 }
104
105 pub fn datadir(&self) -> &Path {
106 &self.datadir
107 }
108}
109
110impl std::fmt::Debug for FlockPidLockManager {
111 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 f.debug_struct("FlockPidLockManager").field("datadir", &self.datadir).finish()
113 }
114}
115
116#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
117#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
118impl LockManager for FlockPidLockManager {
119 async fn try_lock(&self, key: &str) -> Option<Box<dyn LockGuard>> {
120 self.in_process.try_lock(key).await
121 }
122
123 async fn lock(
124 &self,
125 key: &str,
126 timeout: std::time::Duration,
127 ) -> anyhow::Result<Box<dyn LockGuard>> {
128 self.in_process.lock(key, timeout).await
129 }
130}
131
132#[cfg(test)]
133mod test {
134 use super::*;
135
136 fn tmp_dir() -> PathBuf {
137 let dir = std::env::temp_dir()
138 .join(format!("bark-pid-lockmgr-{}", std::process::id()))
139 .join(format!("{}", std::time::SystemTime::now()
140 .duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
141 let _ = fs::remove_dir_all(&dir);
142 dir
143 }
144
145 #[tokio::test]
146 async fn acquire_writes_holder_pid() {
147 let dir = tmp_dir();
148 let mgr = FlockPidLockManager::new(&dir).unwrap();
149 let contents = fs::read_to_string(dir.join(LOCK_FILE)).unwrap();
150 assert_eq!(contents, std::process::id().to_string());
151 drop(mgr);
152 let _ = fs::remove_dir_all(&dir);
153 }
154
155 #[tokio::test]
156 async fn second_acquire_in_same_process_is_refused() {
157 let dir = tmp_dir();
158 let _held = FlockPidLockManager::new(&dir).unwrap();
159 let err = FlockPidLockManager::new(&dir).unwrap_err();
160 assert!(
161 err.to_string().contains("another process is already using datadir"),
162 "unexpected error: {}", err,
163 );
164 drop(_held);
165 let _ = fs::remove_dir_all(&dir);
166 }
167
168 #[tokio::test]
169 async fn reacquire_after_drop_succeeds() {
170 let dir = tmp_dir();
171 let first = FlockPidLockManager::new(&dir).unwrap();
172 drop(first);
173 let _second = FlockPidLockManager::new(&dir).unwrap();
174 let _ = fs::remove_dir_all(&dir);
175 }
176
177 #[tokio::test]
178 async fn per_key_locking_works_in_process() {
179 let dir = tmp_dir();
180 let mgr = FlockPidLockManager::new(&dir).unwrap();
181
182 let g = mgr.try_lock("foo").await;
183 assert!(g.is_some());
184
185 let busy = mgr.try_lock("foo").await;
186 assert!(busy.is_none(), "same key should be blocked");
187
188 let g2 = mgr.try_lock("bar").await;
189 assert!(g2.is_some(), "different key should be free");
190
191 let _ = fs::remove_dir_all(&dir);
192 }
193}