pgdo/
lock.rs

1//! File-based locking using [`flock(2)`](https://linux.die.net/man/2/flock).
2//!
3//! You must start with an [`UnlockedFile`].
4//!
5//! ```rust
6//! let lock_dir = tempfile::tempdir()?;
7//! # use pgdo::lock::UnlockedFile;
8//! let mut lock = UnlockedFile::try_from(lock_dir.path().join("foo").as_path())?;
9//! let lock = lock.lock_shared()?;
10//! let lock = lock.lock_exclusive()?;
11//! let lock = lock.unlock()?;
12//! # Ok::<(), std::io::Error>(())
13//! ```
14//!
15//! Dropping a [`LockedFileShared`] or [`LockedFileExclusive`] will ordinarily
16//! drop the underlying `flock`-based lock by virtue of dropping the [`File`]
17//! they each wrap. However, if the file descriptor was duplicated prior to
18//! creating the initial [`UnlockedFile`], the lock will persist as long as that
19//! descriptor remains valid.
20
21// Ignore deprecation warnings, for now, regarding `nix::fcntl::flock`, since
22// the suggested replacement, `nix::fcntl::Flock`, does not provide the same
23// functionality. This change was made in the `nix` crate on 2023-12-03; see
24// https://github.com/nix-rust/nix/pull/2170. Some limitations of the new API
25// reported 2024-04-07; see https://github.com/nix-rust/nix/issues/2356.
26#![allow(deprecated)]
27
28use std::fs::File;
29use std::os::unix::io::AsRawFd;
30
31use either::{Either, Left, Right};
32use nix::errno::Errno;
33use nix::fcntl::{flock, FlockArg};
34use nix::Result;
35use uuid::Uuid;
36
37#[derive(Debug)]
38pub struct UnlockedFile(File);
39#[derive(Debug)]
40pub struct LockedFileShared(File);
41#[derive(Debug)]
42pub struct LockedFileExclusive(File);
43
44impl From<File> for UnlockedFile {
45    fn from(file: File) -> Self {
46        Self(file)
47    }
48}
49
50impl TryFrom<&std::path::Path> for UnlockedFile {
51    type Error = std::io::Error;
52
53    fn try_from(path: &std::path::Path) -> std::io::Result<Self> {
54        std::fs::OpenOptions::new()
55            .append(true)
56            .create(true)
57            .open(path)
58            .map(UnlockedFile)
59    }
60}
61
62impl TryFrom<&std::path::PathBuf> for UnlockedFile {
63    type Error = std::io::Error;
64
65    fn try_from(path: &std::path::PathBuf) -> std::io::Result<Self> {
66        Self::try_from(path.as_path())
67    }
68}
69
70impl TryFrom<&Uuid> for UnlockedFile {
71    type Error = std::io::Error;
72
73    fn try_from(uuid: &Uuid) -> std::io::Result<Self> {
74        let mut buffer = Uuid::encode_buffer();
75        let uuid = uuid.simple().encode_lower(&mut buffer);
76        let filename = ".pgdo.".to_owned() + uuid;
77        let path = std::env::temp_dir().join(filename);
78        UnlockedFile::try_from(&*path)
79    }
80}
81
82#[allow(unused)]
83impl UnlockedFile {
84    pub fn try_lock_shared(self) -> Result<Either<Self, LockedFileShared>> {
85        match flock(self.0.as_raw_fd(), FlockArg::LockSharedNonblock) {
86            Ok(()) => Ok(Right(LockedFileShared(self.0))),
87            Err(Errno::EAGAIN) => Ok(Left(self)),
88            Err(err) => Err(err),
89        }
90    }
91
92    pub fn lock_shared(self) -> Result<LockedFileShared> {
93        flock(self.0.as_raw_fd(), FlockArg::LockShared)?;
94        Ok(LockedFileShared(self.0))
95    }
96
97    pub fn try_lock_exclusive(self) -> Result<Either<Self, LockedFileExclusive>> {
98        match flock(self.0.as_raw_fd(), FlockArg::LockExclusiveNonblock) {
99            Ok(()) => Ok(Right(LockedFileExclusive(self.0))),
100            Err(Errno::EAGAIN) => Ok(Left(self)),
101            Err(err) => Err(err),
102        }
103    }
104
105    pub fn lock_exclusive(self) -> Result<LockedFileExclusive> {
106        flock(self.0.as_raw_fd(), FlockArg::LockExclusive)?;
107        Ok(LockedFileExclusive(self.0))
108    }
109}
110
111#[allow(unused)]
112impl LockedFileShared {
113    pub fn try_lock_exclusive(self) -> Result<Either<Self, LockedFileExclusive>> {
114        match flock(self.0.as_raw_fd(), FlockArg::LockExclusiveNonblock) {
115            Ok(()) => Ok(Right(LockedFileExclusive(self.0))),
116            Err(Errno::EAGAIN) => Ok(Left(self)),
117            Err(err) => Err(err),
118        }
119    }
120
121    pub fn lock_exclusive(self) -> Result<LockedFileExclusive> {
122        flock(self.0.as_raw_fd(), FlockArg::LockExclusive)?;
123        Ok(LockedFileExclusive(self.0))
124    }
125
126    pub fn try_unlock(self) -> Result<Either<Self, UnlockedFile>> {
127        match flock(self.0.as_raw_fd(), FlockArg::UnlockNonblock) {
128            Ok(()) => Ok(Right(UnlockedFile(self.0))),
129            Err(Errno::EAGAIN) => Ok(Left(self)),
130            Err(err) => Err(err),
131        }
132    }
133
134    pub fn unlock(self) -> Result<UnlockedFile> {
135        flock(self.0.as_raw_fd(), FlockArg::Unlock)?;
136        Ok(UnlockedFile(self.0))
137    }
138}
139
140#[allow(unused)]
141impl LockedFileExclusive {
142    pub fn try_lock_shared(self) -> Result<Either<Self, LockedFileShared>> {
143        match flock(self.0.as_raw_fd(), FlockArg::LockSharedNonblock) {
144            Ok(()) => Ok(Right(LockedFileShared(self.0))),
145            Err(Errno::EAGAIN) => Ok(Left(self)),
146            Err(err) => Err(err),
147        }
148    }
149
150    pub fn lock_shared(self) -> Result<LockedFileShared> {
151        flock(self.0.as_raw_fd(), FlockArg::LockShared)?;
152        Ok(LockedFileShared(self.0))
153    }
154
155    pub fn try_unlock(self) -> Result<Either<Self, UnlockedFile>> {
156        match flock(self.0.as_raw_fd(), FlockArg::UnlockNonblock) {
157            Ok(()) => Ok(Right(UnlockedFile(self.0))),
158            Err(Errno::EAGAIN) => Ok(Left(self)),
159            Err(err) => Err(err),
160        }
161    }
162
163    pub fn unlock(self) -> Result<UnlockedFile> {
164        flock(self.0.as_raw_fd(), FlockArg::Unlock)?;
165        Ok(UnlockedFile(self.0))
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::UnlockedFile;
172
173    use std::fs::OpenOptions;
174    use std::io;
175    use std::os::unix::io::AsRawFd;
176    use std::path::Path;
177
178    use either::Left;
179    use nix::fcntl::{flock, FlockArg};
180
181    fn can_lock<P: AsRef<Path>>(filename: P, exclusive: bool) -> nix::Result<()> {
182        let file = OpenOptions::new()
183            .append(true)
184            .create(true)
185            .open(filename)
186            .unwrap();
187        let mode = if exclusive {
188            FlockArg::LockExclusiveNonblock
189        } else {
190            FlockArg::LockSharedNonblock
191        };
192        flock(file.as_raw_fd(), mode)
193    }
194
195    fn can_lock_exclusive<P: AsRef<Path>>(filename: P) -> nix::Result<()> {
196        can_lock(filename, true)
197    }
198
199    fn can_lock_shared<P: AsRef<Path>>(filename: P) -> nix::Result<()> {
200        can_lock(filename, false)
201    }
202
203    #[test]
204    fn file_lock_exclusive_takes_exclusive_flock() -> io::Result<()> {
205        let lock_dir = tempfile::tempdir()?;
206        let lock_filename = lock_dir.path().join("lock");
207        let lock = OpenOptions::new()
208            .append(true)
209            .create(true)
210            .open(&lock_filename)
211            .map(UnlockedFile::from)?;
212
213        assert_eq!(Ok(()), can_lock_exclusive(&lock_filename));
214        assert_eq!(Ok(()), can_lock_shared(&lock_filename));
215
216        let lock = lock.lock_exclusive()?;
217
218        assert_ne!(Ok(()), can_lock_exclusive(&lock_filename));
219        assert_ne!(Ok(()), can_lock_shared(&lock_filename));
220
221        lock.unlock()?;
222
223        assert_eq!(Ok(()), can_lock_exclusive(&lock_filename));
224        assert_eq!(Ok(()), can_lock_shared(&lock_filename));
225
226        Ok(())
227    }
228
229    #[test]
230    fn file_try_lock_exclusive_does_not_block_on_existing_shared_lock() -> io::Result<()> {
231        let lock_dir = tempfile::tempdir()?;
232        let lock_filename = lock_dir.path().join("lock");
233        let open_lock_file = || {
234            OpenOptions::new()
235                .append(true)
236                .create(true)
237                .open(&lock_filename)
238                .map(UnlockedFile::from)
239        };
240
241        let _lock_shared = open_lock_file()?.lock_shared()?;
242
243        assert!(matches!(
244            open_lock_file()?.try_lock_exclusive(),
245            Ok(Left(_))
246        ));
247
248        Ok(())
249    }
250
251    #[test]
252    fn file_try_lock_exclusive_does_not_block_on_existing_exclusive_lock() -> io::Result<()> {
253        let lock_dir = tempfile::tempdir()?;
254        let lock_filename = lock_dir.path().join("lock");
255        let open_lock_file = || {
256            OpenOptions::new()
257                .append(true)
258                .create(true)
259                .open(&lock_filename)
260                .map(UnlockedFile::from)
261        };
262
263        let _lock_exclusive = open_lock_file()?.lock_exclusive()?;
264
265        assert!(matches!(
266            open_lock_file()?.try_lock_exclusive(),
267            Ok(Left(_)),
268        ));
269
270        Ok(())
271    }
272}