~asayers/multilockfile

1e2db66ad7e69a5cf1ab65040486c050eb8edb57 — Alex Sayers 4 years ago
Initial implementation
3 files changed, 143 insertions(+), 0 deletions(-)

A .gitignore
A Cargo.toml
A src/lib.rs
A  => .gitignore +3 -0
@@ 1,3 @@
/target
**/*.rs.bk
Cargo.lock

A  => Cargo.toml +12 -0
@@ 1,12 @@
[package]
name = "lockfile"
version = "0.1.0"
authors = ["Alex Sayers <alex@asayers.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
lazy_static = "1.3"
nix = "0.14"
libc = "0.2.60"

A  => src/lib.rs +128 -0
@@ 1,128 @@
use lazy_static::*;
use nix::errno::Errno;
use nix::fcntl::*;
use std::collections::HashSet;
use std::fs::File;
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::sync::Mutex;

lazy_static! {
    /// We need to keep a process-wide fd open for all access to the lock file.
    /// Because POSIX lock files are insane, if you close *one* fd pointing
    /// at a given inode, it will immediately release *all* locks on that inode from
    /// your pid, even if those locks are on a different fd.  This is literally
    /// never what you want.  To avoid the problem, always use just a single fd.
    static ref LOCKFILE: Mutex<File> = {
        let file = std::fs::OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .mode(0o666)
            .open(".redo/locks")
            .unwrap();
        Mutex::new(file)
    };
    static ref LOCKS: Mutex<HashSet<LockId>> = Mutex::new(HashSet::default());
}

pub type LockId = u32;

/// An object representing a lock on a redo target file.
//
// FIXME: I really want to use fcntl F_SETLK, F_SETLKW, etc here.  But python
// doesn't do the lockdata structure in a portable way, so we have to use
// fcntl.lockf() instead.  Usually this is just a wrapper for fcntl, so it's
// ok, but it doesn't have F_GETLK, so we can't report which pid owns the lock.
// The makes debugging a bit harder.  When we someday port to C, we can do that.
pub struct Lock {
    owned: bool,
    id: LockId,
}

impl Lock {
    /// Initialize a lock, given the target's `state.File.id`.
    pub fn new(id: LockId) -> Lock {
        assert!(LOCKS.lock().unwrap().insert(id));
        Lock {
            owned: false,
            id: id,
        }
    }

    /// Non-blocking try to acquire our lock; returns true if it worked.
    pub fn trylock(&mut self, shared: bool) -> bool {
        self.lock(shared, false);
        self.owned
    }

    /// Try to acquire our lock, and wait if it's currently locked.
    ///
    /// If shared=True, acquires a shared lock (which can be shared with
    /// other shared locks; used by redo-log).  Otherwise, acquires an
    /// exclusive lock.
    pub fn waitlock(&mut self, shared: bool) {
        self.lock(shared, true);
    }

    fn lock(&mut self, shared: bool, block: bool) {
        assert!(!self.owned);
        let lockfile = LOCKFILE.lock().unwrap();
        let args = libc::flock {
            l_type: if shared {
                libc::F_RDLCK as i16
            } else {
                libc::F_WRLCK as i16
            },
            l_whence: 0,
            l_start: i64::from(self.id),
            l_len: 1,
            l_pid: std::process::id() as i32,
        };
        let args = if block {
            FcntlArg::F_SETLKW(&args)
        } else {
            FcntlArg::F_SETLK(&args)
        };
        match fcntl((*lockfile).as_raw_fd(), args) {
            Err(nix::Error::Sys(Errno::EAGAIN)) if !block => (), // someone else has it locked
            Err(nix::Error::Sys(Errno::EACCES)) if !block => (), // someone else has it locked
            Err(e) => panic!(e),
            Ok(_) => self.owned = true,
        }
    }

    /// Release the lock, which we must currently own.
    fn unlock(&mut self) {
        if !self.owned {
            panic!("can't unlock {} - we don't own it", self.id);
        }
        let lockfile = LOCKFILE.lock().unwrap();
        let args = libc::flock {
            l_type: libc::F_UNLCK as i16,
            l_whence: 0,
            l_start: i64::from(self.id),
            l_len: 1,
            l_pid: std::process::id() as i32,
        };
        fcntl((*lockfile).as_raw_fd(), FcntlArg::F_SETLK(&args)).unwrap();
        self.owned = false;
    }
}

impl Drop for Lock {
    fn drop(&mut self) {
        LOCKS.lock().unwrap().remove(&self.id);
        if self.owned {
            self.unlock();
        }
    }
}

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}