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}