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#[async_trait::async_trait]
117impl LockManager for FlockPidLockManager {
118 async fn try_lock(&self, key: &str) -> Option<Box<dyn LockGuard>> {
119 self.in_process.try_lock(key).await
120 }
121
122 async fn lock(
123 &self,
124 key: &str,
125 timeout: std::time::Duration,
126 ) -> anyhow::Result<Box<dyn LockGuard>> {
127 self.in_process.lock(key, timeout).await
128 }
129}
130
131#[cfg(test)]
132mod test {
133 use super::*;
134
135 fn tmp_dir() -> PathBuf {
136 let dir = std::env::temp_dir()
137 .join(format!("bark-pid-lockmgr-{}", std::process::id()))
138 .join(format!("{}", std::time::SystemTime::now()
139 .duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
140 let _ = fs::remove_dir_all(&dir);
141 dir
142 }
143
144 #[tokio::test]
145 async fn acquire_writes_holder_pid() {
146 let dir = tmp_dir();
147 let mgr = FlockPidLockManager::new(&dir).unwrap();
148 let contents = fs::read_to_string(dir.join(LOCK_FILE)).unwrap();
149 assert_eq!(contents, std::process::id().to_string());
150 drop(mgr);
151 let _ = fs::remove_dir_all(&dir);
152 }
153
154 #[tokio::test]
155 async fn second_acquire_in_same_process_is_refused() {
156 let dir = tmp_dir();
157 let _held = FlockPidLockManager::new(&dir).unwrap();
158 let err = FlockPidLockManager::new(&dir).unwrap_err();
159 assert!(
160 err.to_string().contains("another process is already using datadir"),
161 "unexpected error: {}", err,
162 );
163 drop(_held);
164 let _ = fs::remove_dir_all(&dir);
165 }
166
167 #[tokio::test]
168 async fn reacquire_after_drop_succeeds() {
169 let dir = tmp_dir();
170 let first = FlockPidLockManager::new(&dir).unwrap();
171 drop(first);
172 let _second = FlockPidLockManager::new(&dir).unwrap();
173 let _ = fs::remove_dir_all(&dir);
174 }
175
176 #[tokio::test]
177 async fn per_key_locking_works_in_process() {
178 let dir = tmp_dir();
179 let mgr = FlockPidLockManager::new(&dir).unwrap();
180
181 let g = mgr.try_lock("foo").await;
182 assert!(g.is_some());
183
184 let busy = mgr.try_lock("foo").await;
185 assert!(busy.is_none(), "same key should be blocked");
186
187 let g2 = mgr.try_lock("bar").await;
188 assert!(g2.is_some(), "different key should be free");
189
190 let _ = fs::remove_dir_all(&dir);
191 }
192}