Skip to main content

git_zero_rustix/cache/
coordination.rs

1//! Cooperative cold-miss coordination through nonblocking advisory file locks.
2//!
3//! All producers for a namespace must use the same stable lock files. Do not
4//! unlink or replace `.lock` entries while any producer can use the directory:
5//! doing so would allow independent locks on different inodes for one key.
6//! Contended locks return without waiting. Hosts choose retry and cancellation
7//! policy; descriptor and filesystem operations remain synchronous.
8//! Filesystem and mount settings determine the scope of advisory locking.
9//! See <https://man7.org/linux/man-pages/man2/flock.2.html>.
10
11use super::{DiskCache, cstr, key_name};
12use git_zero::{Error, Result, cache::selected::Key};
13use rustix::{
14    fd::OwnedFd,
15    fs::{self, FlockOperation, Mode, OFlags},
16    io::Errno,
17};
18
19/// An exclusive producer guard. Dropping it explicitly unlocks and closes its
20/// descriptor. The empty lock file remains for future producers.
21#[must_use = "keep the guard alive while checking and publishing the cache entry"]
22pub struct Lease {
23    file: OwnedFd,
24}
25
26impl Drop for Lease {
27    fn drop(&mut self) {
28        // Release before closing even if a concurrent host fork temporarily
29        // inherited the descriptor before exec applied close-on-exec.
30        let _ = fs::flock(&self.file, FlockOperation::Unlock);
31    }
32}
33
34impl DiskCache<'_, '_> {
35    /// Attempt one nonblocking exclusive lock for a selected key. `None` means
36    /// the cache is disabled or another cooperative producer holds the lock.
37    /// After acquisition, recheck the cache before generating a pack: another
38    /// producer may have completed while this request waited to retry.
39    /// Authorization and selection remain mandatory before every lookup.
40    ///
41    /// Guards own independent close-on-exec descriptors and do not borrow the
42    /// backend, so the host can call the selected writer while holding one.
43    /// Eviction affects `.pack` files only and never removes the lock identity.
44    pub fn try_lease(&self, key: Key) -> Result<Option<Lease>> {
45        if !self.enabled {
46            return Ok(None);
47        }
48        let mut name = key_name(key);
49        name[96..101].copy_from_slice(b".lock");
50        let file = fs::openat(
51            self.directory,
52            cstr(&name),
53            OFlags::RDWR | OFlags::CREATE | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
54            Mode::RUSR | Mode::WUSR,
55        )
56        .map_err(|_| Error::Backend)?;
57        if crate::storage::file_size(&file)? != 0 {
58            return Err(Error::InvalidObject);
59        }
60        match fs::flock(&file, FlockOperation::NonBlockingLockExclusive) {
61            Ok(()) => Ok(Some(Lease { file })),
62            Err(Errno::WOULDBLOCK) => Ok(None),
63            Err(_) => Err(Error::Backend),
64        }
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    extern crate std;
71    use super::*;
72    use rustix::fd::AsFd;
73
74    #[test]
75    fn dropping_a_guard_unlocks_even_while_a_duplicate_descriptor_remains() {
76        let path = std::env::temp_dir().join(std::format!(
77            "git-zero-coordination-dup-{}",
78            std::process::id()
79        ));
80        std::fs::create_dir(&path).unwrap();
81        let directory = fs::openat(
82            fs::CWD,
83            path.as_os_str().as_encoded_bytes(),
84            OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC,
85            Mode::empty(),
86        )
87        .unwrap();
88        let repository = git_zero::EmptyRepository::new("refs/heads/main").unwrap();
89        let key = Key::derive(
90            &repository,
91            git_zero::cache::selected::Selection::All,
92            &mut [],
93        )
94        .unwrap();
95        let mut resident = [0; 1];
96        let cache = DiskCache::new(directory.as_fd(), &mut resident, [7; 16]);
97        let guard = cache.try_lease(key).unwrap().unwrap();
98        assert!(
99            rustix::io::fcntl_getfd(&guard.file)
100                .unwrap()
101                .contains(rustix::io::FdFlags::CLOEXEC)
102        );
103        // A descriptor inherited between fork and exec has the same open file
104        // description. A safe duplicate deterministically reproduces its hold.
105        let inherited = rustix::io::dup(&guard.file).unwrap();
106        assert!(cache.try_lease(key).unwrap().is_none());
107        drop(guard);
108        let retry = cache.try_lease(key).unwrap().unwrap();
109        drop(retry);
110        drop(inherited);
111        drop(directory);
112        std::fs::remove_dir_all(path).unwrap();
113    }
114}