dotlock/
lib.rs

1//! Create ".lock" files atomically on any filesystem.
2//!
3//! This crate contains support for creating lock files as are used on
4//! various UNIX type systems. This is similar to the `lockfile` program
5//! from [procmail](http://www.procmail.org) or the `dotlockfile`
6//! program from [liblockfile](https://github.com/miquels/liblockfile).
7//!
8//! They are called ".lock" files, because they are traditionally named
9//! the same as the file they are referencing with the extension of
10//! `.lock`.
11//!
12//! The algorithm that is used to create a lock file in an atomic way is
13//! as follows:
14//!
15//! 1. A unique file is created using
16//! [`tempfile`](https://docs.rs/tempfile).
17//!
18//! 2. The destination lock file is created using the `link` system
19//! call. This operation is atomic across all filesystems including
20//! NFS. The result of this operation is ignored, as success is based on
21//! subsequent results.
22//!
23//! 3. Delete the temporary file.
24//!
25//! 4. The metadata of the destination is retrieved. If this fails,
26//! repeat the process.
27//!
28//! 5. The metadata of the temporary file and the destination lock file
29//! are compared. If they are the same file, then we have successfully
30//! locked the file. Return the opened file.
31//!
32//! 6. If the lock file is stale (older than a configured age), delete
33//! the existing lock file and retry immediately.
34//!
35//! 7. Before retrying, sleep briefly (defaults to 5 seconds).
36//!
37//! # Examples
38//!
39//! ```no_run
40//! use dotlock::DotlockOptions;
41//! use std::time::Duration;
42//!
43//! let _lock = DotlockOptions::new()
44//!     .tries(10)
45//!     .pause(Duration::from_secs(1))
46//!     .create("database.lock").unwrap();
47//! ```
48
49extern crate tempfile;
50
51use std::fs::{remove_file, File, Metadata, Permissions};
52use std::io::{Error, ErrorKind, Read, Result, Seek, SeekFrom, Write};
53use std::os::unix::fs::MetadataExt;
54use std::path::{Path, PathBuf};
55use std::thread::sleep;
56use std::time::{Duration, SystemTime};
57use tempfile::Builder;
58
59const DEFAULT_PAUSE: Duration = Duration::from_secs(5);
60const DEFAULT_TRIES: usize = 10;
61
62// Do the two Metadata reference the same file?
63fn meta_eq(a: &Metadata, b: &Metadata) -> bool {
64    a.dev() == b.dev() && a.ino() == b.ino()
65}
66
67/// A created ".lock" file.
68#[derive(Debug)]
69pub struct Dotlock {
70    file: File,
71    path: Option<PathBuf>,
72}
73
74impl Dotlock {
75    fn create_in(path: &Path, options: DotlockOptions, tempdir: &Path) -> Result<File> {
76        let mut trynum = 0;
77        loop {
78            // Create a unique temporary file in the same directory
79            let temp = Builder::new().tempfile_in(tempdir)?;
80            let tempmeta = temp.as_file().metadata()?;
81            // link temporary file to destination, ignore the result
82            std::fs::hard_link(temp.path(), &path).ok();
83            // Drop the temporary file
84            let temp = temp.into_file();
85            // stat the destination lock file
86            let destmeta = match std::fs::metadata(&path) {
87                Ok(meta) => meta,
88                Err(_) => continue,
89            };
90            // Compare result of stat to temporary file
91            if meta_eq(&destmeta, &tempmeta) {
92                if let Some(perm) = options.permissions {
93                    temp.set_permissions(perm)?;
94                }
95                break Ok(temp);
96            }
97            // Is the existing lock stale?
98            if let Some(stale_age) = options.stale_age {
99                let now = SystemTime::now();
100                if let Ok(modtime) = destmeta.modified() {
101                    if let Ok(age) = now.duration_since(modtime) {
102                        if age >= stale_age {
103                            remove_file(&path).ok();
104                            continue;
105                        }
106                    }
107                }
108            }
109            trynum += 1;
110            if trynum >= options.tries {
111                break Err(Error::new(ErrorKind::TimedOut, "Timed out"));
112            }
113            // Pause only before retrying
114            sleep(options.pause);
115        }
116    }
117
118    fn create_with(path: PathBuf, options: DotlockOptions) -> Result<Self> {
119        let file = Self::create_in(&path, options, &path.parent().unwrap_or(Path::new(".")))?;
120        Ok(Self {
121            file,
122            path: Some(path),
123        })
124    }
125
126    /// Attempts to create the named lock file using the default options.
127    pub fn create<T: Into<PathBuf>>(path: T) -> Result<Self> {
128        DotlockOptions::new().create(path.into())
129    }
130
131    /// Unlocks the lock by removing the file. The lock will be
132    /// automatically removed when this `Dotlock` is dropped.
133    pub fn unlock(&mut self) -> Result<()> {
134        self.path.take().map_or(Ok(()), |path| remove_file(path))
135    }
136
137    /// Attempts to sync all OS-internal metadata to disk. Calls
138    /// [`File::sync_all`](https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_all).
139    pub fn sync_all(&self) -> Result<()> {
140        self.file.sync_all()
141    }
142
143    /// Attempts to sync all OS-internal data to disk except
144    /// metadata. Calls
145    /// [`File::sync_data`](https://doc.rust-lang.org/std/fs/struct.File.html#method.sync_data).
146    pub fn sync_data(&self) -> Result<()> {
147        self.file.sync_all()
148    }
149
150    /// Truncates or extends the underlying file, updating the size of
151    /// this file to become `size`. Calls
152    /// [`File::set_len`](https://doc.rust-lang.org/std/fs/struct.File.html#method.set_len).
153    pub fn set_len(&self, size: u64) -> Result<()> {
154        self.file.set_len(size)
155    }
156
157    /// Queries metadata about the underlying file. Calls
158    /// [`File::metadata`](https://doc.rust-lang.org/std/fs/struct.File.html#method.metadata).
159    pub fn metadata(&self) -> Result<Metadata> {
160        self.file.metadata()
161    }
162
163    /// Changes the permissions on the underlying file. Calls
164    /// [`File::set_permissions`](https://doc.rust-lang.org/std/fs/struct.File.html#method.set_permissions).
165    pub fn set_permissions(&self, perm: Permissions) -> Result<()> {
166        self.file.set_permissions(perm)
167    }
168}
169
170impl Drop for Dotlock {
171    fn drop(&mut self) {
172        self.unlock().ok();
173    }
174}
175
176impl Read for Dotlock {
177    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
178        self.file.read(buf)
179    }
180}
181
182impl Seek for Dotlock {
183    fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
184        self.file.seek(pos)
185    }
186}
187
188impl Write for Dotlock {
189    fn write(&mut self, buf: &[u8]) -> Result<usize> {
190        self.file.write(buf)
191    }
192    fn flush(&mut self) -> Result<()> {
193        self.file.flush()
194    }
195}
196
197/// Options which can be used to configure how a lock file is created.
198///
199/// This builder exposes the ability to configure how a lock file is
200/// created. The [`Dotlock::create`] method is an alias for the
201/// [`create`] method here.
202///
203/// To use `DotlockOptions`, first call [`new`], then chain calls to
204/// methods to set each option required, and finally call [`create`]
205/// with the full path of the lock file to create. This will give you a
206/// `io::Result` with a [`Dotlock`] inside.
207///
208/// [`new`]: struct.DotlockOptions.html#method.new
209/// [`create`]: struct.DotlockOptions.html#method.create
210/// [`Dotlock`]: struct.Dotlock.html
211/// [`Dotlock::create`]: struct.Dotlock.html#method.create
212///
213/// # Examples
214///
215/// Create a lock file using the defaults:
216///
217/// ```no_run
218/// use dotlock::DotlockOptions;
219///
220/// DotlockOptions::new().create("database.lock").unwrap();
221/// ```
222///
223/// Create a lock file, but failing immediately if creating it fails,
224/// and remove lock files older than 5 minutes.
225///
226/// ```no_run
227/// use dotlock::DotlockOptions;
228/// use std::time::Duration;
229///
230/// DotlockOptions::new()
231///     .tries(1)
232///     .stale_age(Duration::from_secs(300))
233///     .create("database.lock").unwrap();
234/// ```
235#[derive(Debug)]
236pub struct DotlockOptions {
237    pause: Duration,
238    tries: usize,
239    permissions: Option<Permissions>,
240    stale_age: Option<Duration>,
241}
242
243impl DotlockOptions {
244    /// Create a new set of options.
245    pub fn new() -> Self {
246        Self {
247            pause: DEFAULT_PAUSE,
248            tries: DEFAULT_TRIES,
249            permissions: None,
250            stale_age: None,
251        }
252    }
253
254    /// Set the time `Dotlock` will pause between attempts to create the
255    /// lock file. Defaults to 5 seconds.
256    pub fn pause<T: Into<Duration>>(mut self, pause: T) -> Self {
257        self.pause = pause.into();
258        self
259    }
260
261    /// Set the number of times `Dotlock` will try to create the lock
262    /// file. Defaults to 10 times.
263    pub fn tries(mut self, tries: usize) -> Self {
264        self.tries = tries.max(1);
265        self
266    }
267
268    /// Set the permissions on the newly created lock file. If not set,
269    /// the lock file permissions will be based on the current umask.
270    pub fn permissions(mut self, perm: Permissions) -> Self {
271        self.permissions = Some(perm);
272        self
273    }
274
275    /// Set the age at which a lock file is considered stale. If not
276    /// set, the existing file age will not be considered for staleness.
277    pub fn stale_age<T: Into<Duration>>(mut self, age: T) -> Self {
278        self.stale_age = Some(age.into());
279        self
280    }
281
282    /// Create the lock file at `path` with the options in `self`.
283    pub fn create<T: Into<PathBuf>>(self, path: T) -> Result<Dotlock> {
284        Dotlock::create_with(path.into(), self)
285    }
286}