indexedlog/
lock.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8use std::fs;
9use std::fs::File;
10use std::io;
11use std::path::Path;
12use std::path::PathBuf;
13
14use fs2::FileExt;
15
16use crate::errors::IoResultExt;
17use crate::utils;
18
19/// RAII style file locking.
20pub struct ScopedFileLock<'a> {
21    file: &'a mut File,
22}
23
24impl<'a> ScopedFileLock<'a> {
25    pub fn new(file: &'a mut File, exclusive: bool) -> io::Result<Self> {
26        if exclusive {
27            file.lock_exclusive()?;
28        } else {
29            file.lock_shared()?;
30        }
31        Ok(ScopedFileLock { file })
32    }
33}
34
35impl<'a> AsRef<File> for ScopedFileLock<'a> {
36    fn as_ref(&self) -> &File {
37        self.file
38    }
39}
40
41impl<'a> AsMut<File> for ScopedFileLock<'a> {
42    fn as_mut(&mut self) -> &mut File {
43        self.file
44    }
45}
46
47impl<'a> Drop for ScopedFileLock<'a> {
48    fn drop(&mut self) {
49        self.file.unlock().expect("unlock");
50    }
51}
52
53/// Prove that a directory was locked.
54pub struct ScopedDirLock {
55    file: File,
56    path: PathBuf,
57}
58
59/// Options for directory locking.
60pub struct DirLockOptions {
61    pub exclusive: bool,
62    pub non_blocking: bool,
63    pub file_name: &'static str,
64}
65
66/// Lock used to indicate that a reader is alive.
67///
68/// This crate generally depends on "append-only" for lock-free reads
69/// (appending data won't invalidate existing readers' mmaps).
70///
71/// However, certain operations (ex. repair) aren't "append-only".
72/// This reader lock is used to detect if any readers are alive so
73/// non-append-only operations can know whether it's safe to go on.
74pub(crate) static READER_LOCK_OPTS: DirLockOptions = DirLockOptions {
75    exclusive: false,
76    non_blocking: false,
77    // The reader lock uses a different file name from the write lock,
78    // because readers do not block normal writes (append-only + atomic
79    // replace), and normal writes do not block readers.
80    //
81    // If this is "" (using default lock file), then active readers will
82    // prevent normal writes, which is undesirable.
83    file_name: "rlock",
84};
85
86impl ScopedDirLock {
87    /// Lock the given directory with default options (exclusive, blocking).
88    pub fn new(path: &Path) -> crate::Result<Self> {
89        const DEFAULT_OPTIONS: DirLockOptions = DirLockOptions {
90            exclusive: true,
91            non_blocking: false,
92            file_name: "",
93        };
94        Self::new_with_options(path, &DEFAULT_OPTIONS)
95    }
96
97    /// Lock the given directory with advanced options.
98    ///
99    /// - `opts.file_name`: decides the lock file name. A directory can have
100    ///   multiple locks independent from one another using different `file_name`s.
101    /// - `opts.non_blocking`: if true, do not wait and return an error if lock
102    ///   cannot be obtained; if false, wait forever for the lock to be available.
103    /// - `opts.exclusive`: if true, ensure that no other locks are present for
104    ///   for the (dir, file_name); if false, allow other non-exclusive locks
105    ///   to co-exist.
106    pub fn new_with_options(dir: &Path, opts: &DirLockOptions) -> crate::Result<Self> {
107        let (path, file) = if opts.file_name.is_empty() {
108            let file = utils::open_dir(dir).context(dir, "cannot open for locking")?;
109            (dir.to_path_buf(), file)
110        } else {
111            let path = dir.join(opts.file_name);
112
113            // Try opening witout requiring write permission first. This allows
114            // shared lock as a different user without write permission.
115            let file = match fs::OpenOptions::new().read(true).open(&path) {
116                Ok(f) => f,
117                Err(e) if e.kind() == io::ErrorKind::NotFound => {
118                    // Create the file.
119                    utils::mkdir_p(dir)?;
120                    fs::OpenOptions::new()
121                        .write(true)
122                        .create(true)
123                        .open(&path)
124                        .context(&path, "cannot create for locking")?
125                }
126                Err(e) => {
127                    return Err(e).context(&path, "cannot open for locking");
128                }
129            };
130            (path, file)
131        };
132
133        // Lock
134        match (opts.exclusive, opts.non_blocking) {
135            (true, false) => file.lock_exclusive(),
136            (true, true) => file.try_lock_exclusive(),
137            (false, false) => file.lock_shared(),
138            (false, true) => file.try_lock_shared(),
139        }
140        .context(&path, || {
141            format!(
142                "cannot lock (exclusive: {}, non_blocking: {})",
143                opts.exclusive, opts.non_blocking,
144            )
145        })?;
146
147        let result = Self { file, path };
148        Ok(result)
149    }
150
151    /// Get the path to the directory being locked.
152    pub fn path(&self) -> &Path {
153        &self.path
154    }
155}
156
157impl Drop for ScopedDirLock {
158    fn drop(&mut self) {
159        self.file.unlock().expect("unlock");
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use std::fs::OpenOptions;
166    use std::io::Read;
167    use std::io::Seek;
168    use std::io::SeekFrom;
169    use std::io::Write;
170    use std::thread;
171
172    use tempfile::tempdir;
173
174    use super::*;
175
176    #[test]
177    fn test_file_lock() {
178        let dir = tempdir().unwrap();
179        let _file = OpenOptions::new()
180            .write(true)
181            .create(true)
182            .open(dir.path().join("f"))
183            .unwrap();
184
185        const N: usize = 40;
186
187        // Spawn N threads. Half read-only, half read-write.
188        let threads: Vec<_> = (0..N)
189            .map(|i| {
190                let i = i;
191                let path = dir.path().join("f");
192                thread::spawn(move || {
193                    let write = i % 2 == 0;
194                    let mut file = OpenOptions::new()
195                        .write(write)
196                        .read(true)
197                        .open(path)
198                        .unwrap();
199                    let mut lock = ScopedFileLock::new(&mut file, write).unwrap();
200                    let len = lock.as_mut().seek(SeekFrom::End(0)).unwrap();
201                    let ptr1 = lock.as_mut() as *const File;
202                    let ptr2 = lock.as_ref() as *const File;
203                    assert_eq!(ptr1, ptr2);
204                    assert_eq!(len % 227, 0);
205                    if write {
206                        for j in 0..227 {
207                            lock.as_mut().write_all(&[j]).expect("write");
208                            lock.as_mut().flush().expect("flush");
209                        }
210                    }
211                })
212            })
213            .collect();
214
215        // Wait for them
216        for thread in threads {
217            thread.join().expect("joined");
218        }
219
220        // Verify the file still has a correct content
221        let mut file = OpenOptions::new()
222            .read(true)
223            .open(dir.path().join("f"))
224            .unwrap();
225        let mut buf = [0u8; 227];
226        let expected: Vec<u8> = (0..227).collect();
227        for _ in 0..(N / 2) {
228            file.read_exact(&mut buf).expect("read");
229            assert_eq!(&buf[..], &expected[..]);
230        }
231    }
232
233    #[test]
234    fn test_dir_lock() {
235        let dir = tempdir().unwrap();
236        let _file = OpenOptions::new()
237            .write(true)
238            .create(true)
239            .open(dir.path().join("f"))
240            .unwrap();
241
242        const N: usize = 40;
243
244        // Spawn N threads. Half read-only, half read-write.
245        let threads: Vec<_> = (0..N)
246            .map(|i| {
247                let i = i;
248                let path = dir.path().join("f");
249                let dir_path = dir.path().to_path_buf();
250                thread::spawn(move || {
251                    let write = i % 2 == 0;
252                    let mut _lock = ScopedDirLock::new(&dir_path).unwrap();
253                    let mut file = OpenOptions::new()
254                        .write(write)
255                        .read(true)
256                        .open(path)
257                        .unwrap();
258                    let len = file.seek(SeekFrom::End(0)).unwrap();
259                    assert_eq!(len % 227, 0);
260                    if write {
261                        for j in 0..227 {
262                            file.write_all(&[j]).expect("write");
263                            file.flush().expect("flush");
264                        }
265                    }
266                })
267            })
268            .collect();
269
270        // Wait for them
271        for thread in threads {
272            thread.join().expect("joined");
273        }
274
275        // Verify the file still has a correct content
276        let mut file = OpenOptions::new()
277            .read(true)
278            .open(dir.path().join("f"))
279            .unwrap();
280        let mut buf = [0u8; 227];
281        let expected: Vec<u8> = (0..227).collect();
282        for _ in 0..(N / 2) {
283            file.read_exact(&mut buf).expect("read");
284            assert_eq!(&buf[..], &expected[..]);
285        }
286    }
287
288    #[test]
289    fn test_dir_lock_with_options() {
290        let dir = tempdir().unwrap();
291        let path = dir.path();
292        let opts = DirLockOptions {
293            file_name: "foo",
294            exclusive: false,
295            non_blocking: false,
296        };
297
298        // Multiple shared locks obtained with blocking on and off.
299        let l1 = ScopedDirLock::new_with_options(path, &opts).unwrap();
300        let l2 = ScopedDirLock::new_with_options(path, &opts).unwrap();
301
302        let opts = DirLockOptions {
303            non_blocking: true,
304            ..opts
305        };
306        let l3 = ScopedDirLock::new_with_options(path, &opts).unwrap();
307
308        // Exclusive lock cannot be obtained while shared locks are present.
309        let opts = DirLockOptions {
310            exclusive: true,
311            ..opts
312        };
313        assert!(ScopedDirLock::new_with_options(path, &opts).is_err());
314
315        // Exclusive lock can be obtained after releasing shared locks.
316        drop((l1, l2, l3));
317        let l4 = ScopedDirLock::new_with_options(path, &opts).unwrap();
318
319        // Exclusive lock cannot be obtained while other locks are present.
320        assert!(ScopedDirLock::new_with_options(path, &opts).is_err());
321
322        // Exclusive lock cannot be obtained with a different file name.
323        let opts = DirLockOptions {
324            file_name: "bar",
325            ..opts
326        };
327        assert!(ScopedDirLock::new_with_options(path, &opts).is_ok());
328
329        drop(l4);
330    }
331}