Skip to main content

ic_host_fs/durable/lock_wait/
mod.rs

1//! Regular descriptor locking, release and wait observations.
2
3#[cfg(unix)]
4use std::time::Instant;
5use std::{fs::File, io, time::Duration};
6
7#[cfg(all(test, unix))]
8mod tests;
9
10/// An owned shared descriptor lock released explicitly before its file closes.
11///
12/// Duplicated or inherited descriptors must not extend this guard's lock lifetime.
13/// Independent acquisitions remain independent. Callers own any shared guard
14/// lifetime, paths, namespace custody and cleanup policy; dropping it removes no file.
15#[derive(Debug)]
16pub struct SharedFileLock(File);
17
18impl SharedFileLock {
19    /// Acquire a blocking shared lock on an already-opened regular file.
20    ///
21    /// This consumes the descriptor without reopening it or imposing a deadline.
22    ///
23    /// # Errors
24    /// Returns the original regular-descriptor admission or native lock error.
25    pub fn acquire(file: File) -> io::Result<Self> {
26        lock_shared(&file)?;
27        Ok(Self(file))
28    }
29
30    /// Borrow the locked descriptor; clones do not extend the guard's lifetime.
31    ///
32    /// Callers must coordinate explicit unlock or lock-mode changes through this
33    /// descriptor or its duplicates with their own guard lifetime contract.
34    #[must_use]
35    pub const fn file(&self) -> &File {
36        &self.0
37    }
38}
39
40impl Drop for SharedFileLock {
41    fn drop(&mut self) {
42        // Closing alone can leave a flock held by an inherited/duplicated
43        // descriptor. Release at the selected ownership boundary instead.
44        // On unlock failure, closing the owned file remains the fallback.
45        let _ = unlock(&self.0);
46    }
47}
48
49/// Acquire a blocking shared lock on a caller-owned regular descriptor.
50///
51/// Independent shared holders coexist. Opening, namespace custody, deadlines,
52/// descriptor clones and final unlock/close policy remain caller-owned.
53/// This does not reopen the file or impose an elapsed-time deadline.
54///
55/// # Errors
56/// Returns metadata, non-regular descriptor, native lock or unsupported-host errors.
57pub fn lock_shared(file: &File) -> io::Result<()> {
58    apply_lock(file, DescriptorOperation::Shared)
59}
60
61/// Acquire a blocking exclusive lock on a caller-owned regular descriptor.
62///
63/// Ownership and deadline policy are the same as [`lock_shared`]. Use
64/// [`lock_exclusive_with_wait`] when contention needs caller observations.
65///
66/// # Errors
67/// Returns metadata, non-regular descriptor, native lock or unsupported-host errors.
68pub fn lock_exclusive(file: &File) -> io::Result<()> {
69    apply_lock(file, DescriptorOperation::Exclusive)
70}
71
72/// Attempt one exclusive lock acquisition on a caller-owned regular descriptor.
73///
74/// Contention returns [`io::ErrorKind::WouldBlock`] without waiting or retrying.
75/// Success leaves the lock held by the descriptor, with the same clone, unlock
76/// and final-close obligations as [`lock_exclusive`]. Opening, durability,
77/// namespace custody and contention/retry policy remain caller-owned.
78///
79/// # Errors
80/// Returns metadata, non-regular descriptor, native lock or unsupported-host errors.
81pub fn try_lock_exclusive(file: &File) -> io::Result<()> {
82    apply_lock(file, DescriptorOperation::TryExclusive)
83}
84
85/// Release a caller-owned regular descriptor's lock without closing it.
86///
87/// Unlocking releases the shared kernel ownership of its duplicated or inherited
88/// descriptors too. It does not release independently opened holders. The caller
89/// chooses the final-owner boundary; this does not close or remove any file.
90///
91/// # Errors
92/// Returns metadata, non-regular descriptor, native unlock or unsupported-host errors.
93pub fn unlock(file: &File) -> io::Result<()> {
94    apply_lock(file, DescriptorOperation::Unlock)
95}
96
97#[derive(Clone, Copy)]
98enum DescriptorOperation {
99    Shared,
100    Exclusive,
101    TryExclusive,
102    Unlock,
103}
104
105fn apply_lock(file: &File, operation: DescriptorOperation) -> io::Result<()> {
106    require_regular_descriptor(file)?;
107    #[cfg(unix)]
108    {
109        use rustix::fs::FlockOperation;
110        let operation = match operation {
111            DescriptorOperation::Shared => FlockOperation::LockShared,
112            DescriptorOperation::Exclusive => FlockOperation::LockExclusive,
113            DescriptorOperation::TryExclusive => FlockOperation::NonBlockingLockExclusive,
114            DescriptorOperation::Unlock => FlockOperation::Unlock,
115        };
116        rustix::fs::flock(file, operation).map_err(io::Error::from)
117    }
118    #[cfg(not(unix))]
119    {
120        let _ = operation;
121        Err(io::Error::from(io::ErrorKind::Unsupported))
122    }
123}
124
125fn require_regular_descriptor(file: &File) -> io::Result<()> {
126    if file.metadata()?.is_file() {
127        Ok(())
128    } else {
129        Err(io::Error::new(
130            io::ErrorKind::InvalidInput,
131            "lock requires a regular file",
132        ))
133    }
134}
135
136/// Acquire an exclusive lock on a caller-owned regular descriptor with wait observations.
137///
138/// Each contention result calls `waiting` before sleeping for `poll_interval`.
139/// Interrupted acquisition is retried without reporting contention. Uncontended
140/// acquisition reports no wait. Returns elapsed acquisition time; the descriptor
141/// retains the lock until the caller unlocks or closes it. The caller owns opening,
142/// namespace, progress policy, deadlines and descriptor clones. This adds no path
143/// admission or descendant ownership. Callback failure stops acquisition.
144///
145/// # Errors
146/// Returns a zero polling interval, non-regular descriptor, metadata, callback,
147/// lock acquisition or unsupported-platform error.
148pub fn lock_exclusive_with_wait(
149    file: &File,
150    poll_interval: Duration,
151    waiting: impl FnMut(Duration) -> io::Result<()>,
152) -> io::Result<Duration> {
153    if poll_interval.is_zero() || !file.metadata()?.is_file() {
154        return Err(io::Error::new(
155            io::ErrorKind::InvalidInput,
156            "lock requires a regular file and nonzero polling interval",
157        ));
158    }
159    #[cfg(unix)]
160    {
161        wait_for_lock(poll_interval, waiting, || {
162            rustix::fs::flock(file, rustix::fs::FlockOperation::NonBlockingLockExclusive)
163                .map_err(|error| io::Error::from_raw_os_error(error.raw_os_error()))
164        })
165    }
166    #[cfg(not(unix))]
167    {
168        let _ = waiting;
169        Err(io::Error::new(
170            io::ErrorKind::Unsupported,
171            "descriptor lock waiting is unsupported",
172        ))
173    }
174}
175
176#[cfg(unix)]
177fn wait_for_lock(
178    poll_interval: Duration,
179    mut waiting: impl FnMut(Duration) -> io::Result<()>,
180    mut acquire: impl FnMut() -> io::Result<()>,
181) -> io::Result<Duration> {
182    let started = Instant::now();
183    loop {
184        match acquire() {
185            Ok(()) => return Ok(started.elapsed()),
186            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
187            Err(error) if error.kind() == io::ErrorKind::WouldBlock => {
188                waiting(started.elapsed())?;
189                std::thread::sleep(poll_interval);
190            }
191            Err(error) => return Err(error),
192        }
193    }
194}