http_cache_stream/
lock.rs

1//! Implementation of advisory file locking.
2//!
3//! An advisory file lock is used to coordinate access to a file across threads
4//! and processes.
5//!
6//! The locking is "best-effort" and will transparently succeed on file systems
7//! that do not support advisory locks.
8use std::fmt;
9use std::fs::File;
10use std::fs::OpenOptions;
11use std::io;
12use std::ops::Deref;
13use std::ops::DerefMut;
14use std::path::Path;
15
16#[cfg(unix)]
17pub(crate) mod unix;
18
19use tracing::debug;
20#[cfg(unix)]
21pub(crate) use unix as sys;
22
23#[cfg(windows)]
24pub(crate) mod windows;
25
26#[cfg(windows)]
27pub(crate) use windows as sys;
28
29use crate::runtime;
30
31/// Represents a locked file.
32#[derive(Debug)]
33pub struct LockedFile(File);
34
35impl Deref for LockedFile {
36    type Target = File;
37
38    fn deref(&self) -> &Self::Target {
39        &self.0
40    }
41}
42
43impl DerefMut for LockedFile {
44    fn deref_mut(&mut self) -> &mut Self::Target {
45        &mut self.0
46    }
47}
48
49impl Drop for LockedFile {
50    fn drop(&mut self) {
51        let _ = sys::unlock(&self.0);
52    }
53}
54
55/// An extension trait for [`OpenOptions`].
56///
57/// When the `file_lock` Rust feature stabilizes, this implementation will be
58/// removed.
59pub trait OpenOptionsExt {
60    /// Attempts to open a file with a shared lock.
61    ///
62    /// Returns the locked file upon success.
63    ///
64    /// If the lock cannot be immediately acquired, `Ok(None)` is returned.
65    fn try_open_shared(&self, path: &Path) -> io::Result<Option<LockedFile>>;
66
67    /// Attempts to open a file with a shared lock.
68    ///
69    /// Returns the locked file upon success.
70    fn open_shared(&self, path: &Path) -> impl Future<Output = io::Result<LockedFile>> + Send;
71
72    /// Attempts to open a file with an exclusive lock.
73    ///
74    /// Returns the locked file upon success.
75    ///
76    /// If the lock cannot be immediately acquired, `Ok(None)` is returned.
77    fn try_open_exclusive(&self, path: &Path) -> io::Result<Option<LockedFile>>;
78
79    /// Attempts to open a file with an exclusive lock.
80    ///
81    /// Returns the locked file upon success.
82    fn open_exclusive(&self, path: &Path) -> impl Future<Output = io::Result<LockedFile>> + Send;
83}
84
85impl OpenOptionsExt for OpenOptions {
86    fn try_open_shared(&self, path: &Path) -> io::Result<Option<LockedFile>> {
87        let file = self.open(path)?;
88
89        if try_lock(&file, Access::Shared)? {
90            Ok(Some(LockedFile(file)))
91        } else {
92            Ok(None)
93        }
94    }
95
96    async fn open_shared(&self, path: &Path) -> io::Result<LockedFile> {
97        lock(self.open(path)?, path, Access::Shared).await
98    }
99
100    fn try_open_exclusive(&self, path: &Path) -> io::Result<Option<LockedFile>> {
101        let file = self.open(path)?;
102
103        if try_lock(&file, Access::Exclusive)? {
104            Ok(Some(LockedFile(file)))
105        } else {
106            Ok(None)
107        }
108    }
109
110    async fn open_exclusive(&self, path: &Path) -> io::Result<LockedFile> {
111        lock(self.open(path)?, path, Access::Exclusive).await
112    }
113}
114
115/// Represents how a resource is accessed.
116#[derive(Debug, Copy, Clone, Eq, PartialEq)]
117enum Access {
118    /// Access to the resource is shared.
119    Shared,
120    /// Access to the resource is exclusive.
121    Exclusive,
122}
123
124impl fmt::Display for Access {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        match self {
127            Self::Shared => write!(f, "shared"),
128            Self::Exclusive => write!(f, "exclusive"),
129        }
130    }
131}
132
133/// Attempts to take a lock on a file.
134///
135/// Returns `Ok(true)` if the file was locked or `Ok(false)` if the lock was
136/// contended.
137fn try_lock(file: &File, access: Access) -> io::Result<bool> {
138    // We always try the lock on the first attempt so that we later wait for the
139    // lock using a blocking task
140    let res = match access {
141        Access::Shared => sys::try_lock_shared(file),
142        Access::Exclusive => sys::try_lock_exclusive(file),
143    };
144
145    match res {
146        Ok(_) => Ok(true),
147        Err(e) if sys::error_unsupported(&e) => Ok(true),
148        Err(e) if sys::error_contended(&e) => Ok(false),
149        Err(e) => Err(e),
150    }
151}
152
153/// Takes a lock on a file.
154async fn lock(file: File, path: &Path, access: Access) -> io::Result<LockedFile> {
155    // First try to obtain the lock so that we don't have to do a blocking spawn
156    if try_lock(&file, access)? {
157        return Ok(LockedFile(file));
158    }
159
160    // Otherwise, we'll need to block
161    debug!(
162        "waiting to acquire {access} lock on file `{path}`",
163        path = path.display()
164    );
165    match runtime::unwrap_task_output(
166        runtime::spawn_blocking(move || {
167            let res = match access {
168                Access::Shared => sys::lock_shared(&file),
169                Access::Exclusive => sys::lock_exclusive(&file),
170            };
171
172            match res {
173                Ok(_) => Ok(LockedFile(file)),
174                Err(e) if sys::error_unsupported(&e) => Ok(LockedFile(file)),
175                Err(e) => Err(e),
176            }
177        })
178        .await,
179    ) {
180        Some(res) => res,
181        None => Err(io::Error::other("failed to wait for file lock")),
182    }
183}