Skip to main content

git_claw/infra/
lock.rs

1//! Advisory file locking on `slots.lock` with polling and timeout.
2
3use crate::infra::error::LockError;
4use fd_lock::{RwLock, RwLockWriteGuard};
5use std::fs::{self, File, OpenOptions};
6use std::path::{Path, PathBuf};
7use std::time::{Duration, Instant};
8
9/// RAII wrapper around an advisory write lock.
10pub struct SlotLock {
11    path: PathBuf,
12    // Leaked reference to RwLock<File> allows the guard to be held in the struct.
13    // Cleaned up on drop.
14    guard: Option<RwLockWriteGuard<'static, File>>,
15    raw_lock: *mut RwLock<File>,
16}
17
18impl SlotLock {
19    pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
20    pub const POLL_INTERVAL: Duration = Duration::from_millis(50);
21
22    /// Acquires an exclusive advisory file lock at `lock_path` with default 5-second timeout.
23    pub fn acquire(lock_path: &Path) -> Result<Self, LockError> {
24        Self::acquire_with_timeout(lock_path, Self::DEFAULT_TIMEOUT, Self::POLL_INTERVAL)
25    }
26
27    /// Acquires an exclusive advisory file lock with configurable timeout and poll interval.
28    pub fn acquire_with_timeout(
29        lock_path: &Path,
30        timeout: Duration,
31        poll_interval: Duration,
32    ) -> Result<Self, LockError> {
33        if let Some(parent) = lock_path.parent() {
34            fs::create_dir_all(parent).map_err(|e| LockError::IoError {
35                path: lock_path.to_string_lossy().to_string(),
36                message: e.to_string(),
37            })?;
38        }
39
40        let file = OpenOptions::new()
41            .read(true)
42            .write(true)
43            .create(true)
44            .truncate(false)
45            .open(lock_path)
46            .map_err(|e| LockError::IoError {
47                path: lock_path.to_string_lossy().to_string(),
48                message: e.to_string(),
49            })?;
50
51        let raw_lock = Box::into_raw(Box::new(RwLock::new(file)));
52        let start = Instant::now();
53
54        loop {
55            // SAFETY: raw_lock was allocated via Box::into_raw and remains valid until dropped.
56            let rwlock_ref: &'static mut RwLock<File> = unsafe { &mut *raw_lock };
57
58            match rwlock_ref.try_write() {
59                Ok(guard) => {
60                    return Ok(SlotLock {
61                        path: lock_path.to_path_buf(),
62                        guard: Some(guard),
63                        raw_lock,
64                    });
65                }
66                Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => {
67                    if start.elapsed() >= timeout {
68                        unsafe {
69                            let _ = Box::from_raw(raw_lock);
70                        }
71                        return Err(LockError::Timeout {
72                            path: lock_path.to_string_lossy().to_string(),
73                            timeout_secs: timeout.as_secs(),
74                        });
75                    }
76                    std::thread::sleep(poll_interval);
77                }
78                Err(err) => {
79                    unsafe {
80                        let _ = Box::from_raw(raw_lock);
81                    }
82                    return Err(LockError::IoError {
83                        path: lock_path.to_string_lossy().to_string(),
84                        message: err.to_string(),
85                    });
86                }
87            }
88        }
89    }
90
91    /// Returns the path to the lock file.
92    pub fn path(&self) -> &Path {
93        &self.path
94    }
95}
96
97impl Drop for SlotLock {
98    fn drop(&mut self) {
99        // Drop the guard first to release the OS advisory lock
100        self.guard.take();
101        // Deallocate the RwLock and close the underlying file
102        if !self.raw_lock.is_null() {
103            unsafe {
104                let _ = Box::from_raw(self.raw_lock);
105            }
106            self.raw_lock = std::ptr::null_mut();
107        }
108    }
109}
110
111// SlotLock is Send across threads
112unsafe impl Send for SlotLock {}