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}