Skip to main content

bark/lock_manager/
pid_flock.rs

1//! Named locks for a single-process-per-datadir deployment.
2//!
3//! # Safety scope
4//!
5//! Prevents concurrent access by callers within the **current OS
6//! process**. Construction additionally **refuses to start a second
7//! process** holding the same datadir: an exclusive OS-level lock is
8//! acquired on `<datadir>/LOCK` via `std::fs::File::try_lock`
9//! (`flock(2)` on Unix, `LockFileEx` on Windows). The OS releases that
10//! lock when the process exits, even on SIGKILL or a crash.
11//!
12//! From that point on, all per-key locking is in-memory: by
13//! construction, this is the only process touching `datadir`, so the
14//! cross-process semantics of file-based per-key locking would be
15//! redundant.
16//!
17//! # Platform support
18//!
19//! Linux, macOS, Android, Windows. Not available on `wasm32`.
20//!
21//! # When to use
22//!
23//! - You run a single-process-per-datadir deployment (CLIs, daemons).
24//! - You want that constraint expressed as a type rather than as a
25//!   separate setup step that callers might forget.
26
27use 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
36/// File name used for the datadir-level PID lock.
37pub 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	// Holds the OS lock for the lifetime of the manager. Dropped on
52	// drop of the manager — at which point the OS releases the lock.
53	_pid_file: File,
54	datadir: PathBuf,
55	in_process: MemoryLockManager,
56}
57
58impl FlockPidLockManager {
59	/// Take the pid lock on `datadir` and construct a manager. Fails
60	/// with [`PidLockError::AlreadyHeld`] if another process already
61	/// holds the lock, or [`PidLockError::SetupFailed`] for any other
62	/// I/O failure.
63	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		// Stamp the holder PID for diagnostics. Truncate first to drop
92		// any stale content left by a previous holder.
93		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}