1use std::{
20 collections::HashMap,
21 fs::File,
22 io,
23 path::{Path, PathBuf},
24 sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock},
25 thread::{self, ThreadId},
26};
27
28use fs2::FileExt;
29pub use heddle_object_model::error::LockError;
30
31pub type Result<T> = std::result::Result<T, LockError>;
32
33struct GateState {
36 owner: Option<ThreadId>,
37 depth: usize,
38 flock: Option<File>,
39}
40
41struct Entry {
42 gate: Mutex<GateState>,
43 cv: Condvar,
44}
45
46impl Entry {
47 fn new() -> Self {
48 Self {
49 gate: Mutex::new(GateState {
50 owner: None,
51 depth: 0,
52 flock: None,
53 }),
54 cv: Condvar::new(),
55 }
56 }
57}
58
59static REGISTRY: OnceLock<Mutex<HashMap<PathBuf, Arc<Entry>>>> = OnceLock::new();
63
64fn registry() -> &'static Mutex<HashMap<PathBuf, Arc<Entry>>> {
65 REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
66}
67
68fn entry_for(key: PathBuf) -> Arc<Entry> {
69 let mut map = registry().lock().unwrap_or_else(|e| e.into_inner());
70 Arc::clone(map.entry(key).or_insert_with(|| Arc::new(Entry::new())))
71}
72
73fn lock_gate(entry: &Entry) -> MutexGuard<'_, GateState> {
74 entry.gate.lock().unwrap_or_else(|e| e.into_inner())
75}
76
77pub struct ReadLockGuard {
78 _file: Option<File>,
81}
82
83impl Drop for ReadLockGuard {
84 fn drop(&mut self) {
85 if let Some(file) = &self._file {
86 let _ = file.unlock();
87 }
88 }
89}
90
91pub struct WriteLockGuard {
92 entry: Arc<Entry>,
93}
94
95impl WriteLockGuard {
96 #[doc(hidden)]
105 pub fn handoff_to_current_thread(&mut self) -> Result<()> {
106 let mut state = lock_gate(&self.entry);
107 let current = thread::current().id();
108 if state.owner == Some(current) {
109 return Ok(());
110 }
111 if state.owner.is_none() || state.depth != 1 || state.flock.is_none() {
112 return Err(LockError::Acquire(io::Error::other(
113 "write-lock handoff requires one outermost held guard",
114 )));
115 }
116 state.owner = Some(current);
117 Ok(())
118 }
119}
120
121impl Drop for WriteLockGuard {
122 fn drop(&mut self) {
123 let mut state = lock_gate(&self.entry);
124 if state.depth > 0 {
125 state.depth -= 1;
126 }
127 if state.depth == 0 {
128 state.owner = None;
129 state.flock = None;
131 self.entry.cv.notify_one();
132 }
133 }
134}
135
136pub struct RepoLock {
137 lock_path: PathBuf,
138}
139
140impl RepoLock {
141 pub fn new(repo_root: &Path) -> Self {
142 let lock_path = repo_root.join(".heddle/locks/repo.lock");
143 Self { lock_path }
144 }
145
146 pub fn at(lock_path: PathBuf) -> Self {
147 Self { lock_path }
148 }
149
150 pub fn read(&self) -> Result<ReadLockGuard> {
151 self.ensure_lock_dir()?;
152 let entry = entry_for(self.registry_key());
153
154 {
158 let state = lock_gate(&entry);
159 if state.owner == Some(thread::current().id()) {
160 return Ok(ReadLockGuard { _file: None });
161 }
162 }
163
164 let file = self.open_lock_file()?;
165 file.lock_shared().map_err(LockError::Acquire)?;
166 Ok(ReadLockGuard { _file: Some(file) })
167 }
168
169 pub fn write(&self) -> Result<WriteLockGuard> {
170 self.ensure_lock_dir()?;
171 let entry = entry_for(self.registry_key());
172 let tid = thread::current().id();
173 let mut state = lock_gate(&entry);
174 loop {
175 match state.owner {
176 Some(owner) if owner == tid => {
177 state.depth += 1;
178 return Ok(WriteLockGuard {
179 entry: Arc::clone(&entry),
180 });
181 }
182 None => {
183 let file = self.open_lock_file()?;
188 file.lock_exclusive().map_err(LockError::Acquire)?;
189 state.owner = Some(tid);
190 state.depth = 1;
191 state.flock = Some(file);
192 return Ok(WriteLockGuard {
193 entry: Arc::clone(&entry),
194 });
195 }
196 Some(_) => {
197 state = entry.cv.wait(state).unwrap_or_else(|e| e.into_inner());
198 }
199 }
200 }
201 }
202
203 pub fn try_read(&self) -> Result<Option<ReadLockGuard>> {
204 self.ensure_lock_dir()?;
205 let file = self.open_lock_file()?;
206
207 match file.try_lock_shared() {
208 Ok(()) => Ok(Some(ReadLockGuard { _file: Some(file) })),
209 Err(_) => Ok(None),
210 }
211 }
212
213 pub fn try_write(&self) -> Result<Option<WriteLockGuard>> {
214 self.ensure_lock_dir()?;
215 let entry = entry_for(self.registry_key());
216 let mut state = lock_gate(&entry);
217 match state.owner {
224 Some(_) => Ok(None),
225 None => {
226 let file = self.open_lock_file()?;
227 match file.try_lock_exclusive() {
228 Ok(()) => {
229 state.owner = Some(thread::current().id());
230 state.depth = 1;
231 state.flock = Some(file);
232 Ok(Some(WriteLockGuard {
233 entry: Arc::clone(&entry),
234 }))
235 }
236 Err(_) => Ok(None),
237 }
238 }
239 }
240 }
241
242 fn ensure_lock_dir(&self) -> Result<()> {
243 if let Some(parent) = self.lock_path.parent() {
244 crate::fs_atomic::create_dir_all_durable(parent).map_err(LockError::Io)?;
245 }
246 Ok(())
247 }
248
249 fn registry_key(&self) -> PathBuf {
253 match self.lock_path.parent() {
254 Some(parent) => {
255 let canon_parent = parent
256 .canonicalize()
257 .unwrap_or_else(|_| parent.to_path_buf());
258 match self.lock_path.file_name() {
259 Some(name) => canon_parent.join(name),
260 None => canon_parent,
261 }
262 }
263 None => self.lock_path.clone(),
264 }
265 }
266
267 fn open_lock_file(&self) -> Result<File> {
268 File::create(&self.lock_path).map_err(LockError::Io)
269 }
270}
271
272pub trait RepositoryLockExt {
273 fn locker(&self) -> RepoLock;
274}
275
276#[cfg(test)]
277mod tests {
278 use std::{
279 sync::{
280 Arc,
281 mpsc::{self},
282 },
283 thread,
284 };
285
286 use tempfile::TempDir;
287
288 use super::*;
289
290 #[test]
291 fn test_read_lock_acquired() {
292 let temp = TempDir::new().unwrap();
293 let lock = RepoLock::new(temp.path());
294
295 let guard = lock.read().unwrap();
296 assert!(std::mem::size_of_val(&guard) > 0);
297 }
298
299 #[test]
300 fn test_write_lock_acquired() {
301 let temp = TempDir::new().unwrap();
302 let lock = RepoLock::new(temp.path());
303
304 let guard = lock.write().unwrap();
305 assert!(std::mem::size_of_val(&guard) > 0);
306 }
307
308 #[test]
309 fn test_multiple_readers() {
310 let temp = TempDir::new().unwrap();
311 let lock = Arc::new(RepoLock::new(temp.path()));
312
313 let mut handles = vec![];
314 for _ in 0..10 {
315 let lock = Arc::clone(&lock);
316 let handle = thread::spawn(move || {
317 let _guard = lock.read().unwrap();
318 thread::sleep(std::time::Duration::from_millis(10));
319 });
320 handles.push(handle);
321 }
322
323 for handle in handles {
324 handle.join().unwrap();
325 }
326 }
327
328 #[test]
329 fn test_writer_excludes_reader() {
330 let temp = TempDir::new().unwrap();
331 let lock = Arc::new(RepoLock::new(temp.path()));
332
333 let _write_guard = lock.write().unwrap();
334 let read_result = lock.try_read().unwrap();
335 assert!(read_result.is_none(), "Reader should be blocked by writer");
336 }
337
338 #[test]
339 fn test_reader_excludes_writer() {
340 let temp = TempDir::new().unwrap();
341 let lock = Arc::new(RepoLock::new(temp.path()));
342
343 let _read_guard = lock.read().unwrap();
344 let write_result = lock.try_write().unwrap();
345 assert!(write_result.is_none(), "Writer should be blocked by reader");
346 }
347
348 #[test]
349 fn test_lock_released_on_drop() {
350 let temp = TempDir::new().unwrap();
351 let lock = RepoLock::new(temp.path());
352
353 {
354 let _guard = lock.write().unwrap();
355 }
356
357 let _guard2 = lock.read().unwrap();
358 }
359
360 #[test]
363 fn same_thread_write_is_reentrant() {
364 let temp = TempDir::new().unwrap();
365 let lock = RepoLock::new(temp.path());
366
367 let _a = lock.write().unwrap();
368 let _b = lock.write().unwrap();
369 }
372
373 #[test]
376 fn same_thread_read_under_write_does_not_deadlock() {
377 let temp = TempDir::new().unwrap();
378 let lock = RepoLock::new(temp.path());
379
380 let _w = lock.write().unwrap();
381 let _r = lock.read().unwrap();
382 }
383
384 #[test]
387 fn distinct_threads_still_exclude() {
388 let temp = TempDir::new().unwrap();
389 let lock = Arc::new(RepoLock::new(temp.path()));
390
391 let (acquired_tx, acquired_rx) = mpsc::channel();
392 let (release_tx, release_rx) = mpsc::channel();
393 let lock_a = Arc::clone(&lock);
394 let handle = thread::spawn(move || {
395 let _g = lock_a.write().unwrap();
396 acquired_tx.send(()).unwrap();
397 release_rx.recv().unwrap();
398 });
399
400 acquired_rx.recv().unwrap();
401 assert!(
402 lock.try_write().unwrap().is_none(),
403 "a second thread must not acquire the write lock"
404 );
405
406 release_tx.send(()).unwrap();
407 handle.join().unwrap();
408
409 assert!(
410 lock.try_write().unwrap().is_some(),
411 "write lock is available once the owning thread releases"
412 );
413 }
414
415 #[test]
418 fn reentrant_release_keeps_lock_until_outermost_drop() {
419 let temp = TempDir::new().unwrap();
420 let lock = Arc::new(RepoLock::new(temp.path()));
421
422 let a1 = lock.write().unwrap();
423 let a2 = lock.write().unwrap();
424
425 let other = |lock: &Arc<RepoLock>| {
426 let lock = Arc::clone(lock);
427 thread::spawn(move || lock.try_write().unwrap().is_none())
428 .join()
429 .unwrap()
430 };
431
432 assert!(other(&lock), "excluded while held at depth 2");
433 drop(a2);
434 assert!(other(&lock), "still excluded while held at depth 1");
435 drop(a1);
436
437 let lock_b = Arc::clone(&lock);
438 let now_available = thread::spawn(move || lock_b.try_write().unwrap().is_some())
439 .join()
440 .unwrap();
441 assert!(now_available, "available after the outermost guard drops");
442 }
443
444 #[test]
445 fn moved_guard_handoff_changes_the_reentrant_owner() {
446 let temp = TempDir::new().unwrap();
447 let lock = RepoLock::new(temp.path());
448 let guard = lock.write().unwrap();
449 let original_owner = thread::current().id();
450 let entry = entry_for(lock.registry_key());
451 let (ready_tx, ready_rx) = mpsc::channel();
452 let (release_tx, release_rx) = mpsc::channel();
453
454 let worker = thread::spawn(move || {
455 let mut guard = guard;
456 guard.handoff_to_current_thread().unwrap();
457 ready_tx.send(thread::current().id()).unwrap();
458 release_rx.recv().unwrap();
459 drop(guard);
460 });
461
462 let worker_owner = ready_rx.recv().unwrap();
463 assert_ne!(worker_owner, original_owner);
464 assert_eq!(lock_gate(&entry).owner, Some(worker_owner));
465 assert!(
466 lock.try_write().unwrap().is_none(),
467 "the original thread must not re-enter a guard owned by the worker"
468 );
469
470 release_tx.send(()).unwrap();
471 worker.join().unwrap();
472 assert!(lock.try_write().unwrap().is_some());
473 }
474
475 #[test]
483 fn try_write_is_non_reentrant_even_for_owner() {
484 let temp = TempDir::new().unwrap();
485 let lock = RepoLock::new(temp.path());
486
487 let _held = lock.write().unwrap();
488 assert!(
489 lock.try_write().unwrap().is_none(),
490 "try_write must report contention even for the lock's own owner thread"
491 );
492 }
493}