stackless_core/
lockfile.rs1use std::hash::{Hash, Hasher};
8use std::io::Write;
9use std::path::{Path, PathBuf};
10use std::time::{Duration, Instant};
11
12use crate::process::ProcessStamp;
13use crate::state::Store;
14use crate::types::{Pid, ProcessStartTime};
15
16const DEFAULT_POLL: Duration = Duration::from_millis(100);
17
18#[derive(Debug)]
20pub struct FileLock {
21 path: PathBuf,
22}
23
24impl Drop for FileLock {
25 fn drop(&mut self) {
26 let _ = std::fs::remove_file(&self.path);
27 }
28}
29
30impl FileLock {
31 pub fn try_acquire(path: &Path) -> Result<Self, LockError> {
33 if let Some(parent) = path.parent() {
34 std::fs::create_dir_all(parent).map_err(|err| LockError::CreateParent {
35 path: path.to_path_buf(),
36 detail: err.to_string(),
37 })?;
38 }
39 for _ in 0..2 {
40 match std::fs::OpenOptions::new()
41 .write(true)
42 .create_new(true)
43 .open(path)
44 {
45 Ok(mut file) => {
46 let me = ProcessStamp::current();
47 let _ = writeln!(file, "{} {}", me.pid.get(), me.start_time.get());
48 return Ok(Self {
49 path: path.to_path_buf(),
50 });
51 }
52 Err(_) => {
53 let stale = std::fs::read_to_string(path)
54 .ok()
55 .and_then(|content| {
56 let mut parts = content.split_whitespace();
57 let pid = parts.next()?.parse().ok()?;
58 let start_time = parts.next()?.parse().ok()?;
59 Some(ProcessStamp {
60 pid: Pid::from_os(pid),
61 start_time: ProcessStartTime::from_os(start_time),
62 })
63 })
64 .is_none_or(|stamp| !stamp.is_alive());
65 if stale {
66 let _ = std::fs::remove_file(path);
67 continue;
68 }
69 return Err(LockError::Held {
70 path: path.to_path_buf(),
71 });
72 }
73 }
74 }
75 Err(LockError::Held {
76 path: path.to_path_buf(),
77 })
78 }
79
80 pub fn acquire_with_wait(path: &Path, budget: Duration) -> Result<Self, LockError> {
83 let start = Instant::now();
84 loop {
85 match Self::try_acquire(path) {
86 Ok(lock) => return Ok(lock),
87 Err(LockError::Held { .. }) if start.elapsed() < budget => {
88 std::thread::sleep(DEFAULT_POLL);
89 }
90 Err(err) => return Err(err),
91 }
92 }
93 }
94
95 pub fn path_key(path: &Path) -> String {
97 let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
98 let mut hasher = std::collections::hash_map::DefaultHasher::new();
99 canonical.hash(&mut hasher);
100 format!("{:016x}", hasher.finish())
101 }
102
103 pub fn stripe_lock_path(definition_dir: &Path) -> PathBuf {
105 Store::state_dir()
106 .join("locks/stripe")
107 .join(format!("{}.lock", Self::path_key(definition_dir)))
108 }
109
110 pub fn git_cache_lock_path(cache_key: &str) -> PathBuf {
112 Store::state_dir()
113 .join("locks/git-cache")
114 .join(format!("{cache_key}.lock"))
115 }
116}
117
118#[derive(Debug, thiserror::Error)]
119pub enum LockError {
120 #[error("lock {path} is held by a live process")]
121 Held { path: PathBuf },
122 #[error("could not create lock parent for {path}: {detail}")]
123 CreateParent { path: PathBuf, detail: String },
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use std::sync::{Arc, Barrier};
130 use std::thread;
131
132 #[test]
133 fn second_holder_blocks_until_release() {
134 let dir = tempfile::tempdir().unwrap();
135 let path = dir.path().join("test.lock");
136
137 let lock = FileLock::try_acquire(&path).unwrap();
138 assert!(matches!(
139 FileLock::try_acquire(&path),
140 Err(LockError::Held { .. })
141 ));
142 drop(lock);
143 assert!(FileLock::try_acquire(&path).is_ok());
144 }
145
146 #[test]
147 fn concurrent_waiters_serialize() {
148 let dir = Arc::new(tempfile::tempdir().unwrap());
149 let n = 4;
150 let start = Arc::new(Barrier::new(n));
151 let mut handles = Vec::new();
152 for _ in 0..n {
153 let dir = Arc::clone(&dir);
154 let start = Arc::clone(&start);
155 handles.push(thread::spawn(move || {
156 start.wait();
157 let _lock = FileLock::acquire_with_wait(
158 &dir.path().join("queue.lock"),
159 Duration::from_secs(5),
160 )
161 .unwrap();
162 }));
163 }
164 for handle in handles {
165 handle.join().unwrap();
166 }
167 }
168}