bakare 0.1.0

modern and simple, yet efficient backup solution
Documentation
use anyhow::Result;
use anyhow::*;
use fail::fail_point;
use std::{io::Write, time::Instant};
use uuid::Uuid;
use vfs::VfsPath;

use rand::{rngs::OsRng, RngCore};
use std::{thread, time};

pub struct Lock {
    path: VfsPath,
}

const MAX_TIMEOUT_MILLIS: u16 = 8192;
const FILE_EXTENSION: &str = ".lock";

impl Lock {
    pub fn lock(index_directory: &VfsPath) -> Result<Self> {
        Lock::lock_with_timeout(index_directory, MAX_TIMEOUT_MILLIS)
    }

    pub fn lock_with_timeout(index_directory: &VfsPath, max_timeout_millis: u16) -> Result<Self> {
        let mut buffer = [0u8; 16];
        OsRng.fill_bytes(&mut buffer);
        let id = Uuid::from_bytes(buffer);
        Lock::wait_to_have_sole_lock(id, index_directory, max_timeout_millis)?;
        let path = Lock::lock_file_path(index_directory, id)?;
        Ok(Lock { path })
    }

    pub fn release(self) -> Result<()> {
        self.delete_lock_file()?;
        Ok(())
    }

    fn delete_lock_file(&self) -> Result<()> {
        if self.path.exists() {
            self.path.remove_file()?;
        }
        Ok(())
    }

    fn wait_to_have_sole_lock(lock_id: Uuid, index_directory: &VfsPath, max_timeout_millis: u16) -> Result<()> {
        let start_time = Instant::now();
        let _ = Lock::create_lock_file(lock_id, index_directory);
        while !Lock::sole_lock(lock_id, index_directory)? {
            let path = Lock::lock_file_path(index_directory, lock_id)?;
            if path.exists() {
                path.remove_file()?;
            }
            let sleep_duration = time::Duration::from_millis((OsRng.next_u32() % 64).into());
            thread::sleep(sleep_duration);
            let _ = Lock::create_lock_file(lock_id, index_directory);
            if start_time.elapsed().as_millis() > max_timeout_millis.into() {
                return Err(anyhow!("timed out waiting on lock"));
            }
        }
        Ok(())
    }

    fn sole_lock(lock_id: Uuid, index_directory: &VfsPath) -> Result<bool> {
        let my_lock_file_path = Lock::lock_file_path(index_directory, lock_id)?;
        let locks = Lock::all_locks(index_directory)?;
        let mut only_mine = true;
        for path in &locks {
            if path != &my_lock_file_path {
                only_mine = false;
                break;
            }
        }
        if locks.iter().count() == 0 {
            return Ok(false);
        }
        Ok(only_mine)
    }

    fn all_locks(index_directory: &VfsPath) -> Result<Vec<VfsPath>> {
        Ok(index_directory
            .read_dir()?
            .into_iter()
            .filter(|f| f.filename().ends_with(FILE_EXTENSION))
            .collect())
    }

    fn create_lock_file(lock_id: Uuid, index_directory: &VfsPath) -> Result<()> {
        let lock_file_path = Lock::lock_file_path(index_directory, lock_id)?;
        fail_point!("create-lock-file", |e: Option<String>| Err(anyhow!(e.unwrap())));
        let mut file = lock_file_path.create_file()?;
        let lock_id_text = lock_id.to_hyphenated().to_string();
        let lock_id_bytes = lock_id_text.as_bytes();
        Ok(file.write_all(lock_id_bytes)?)
    }

    fn lock_file_path(path: &VfsPath, lock_id: Uuid) -> Result<VfsPath> {
        let file_name = format!("{}.{}", lock_id, FILE_EXTENSION);
        Ok(path.join(&file_name)?)
    }
}

impl Drop for Lock {
    fn drop(&mut self) {
        let _ = self.delete_lock_file();
    }
}

#[cfg(test)]
mod must {
    use super::Lock;
    use anyhow::Result;

    #[cfg(feature = "failpoints")]
    use two_rusty_forks::rusty_fork_test;
    use vfs::{MemoryFS, VfsPath};

    #[test]
    fn be_released_when_dropped() -> Result<()> {
        let temp_dir: VfsPath = MemoryFS::new().into();
        {
            let _lock = Lock::lock(&temp_dir);
        }
        let entries = temp_dir.read_dir()?.count();

        assert_eq!(entries, 0);
        Ok(())
    }

    #[cfg(feature = "failpoints")]
    rusty_fork_test! {
        #[test]
        fn be_able_to_lock_when_creating_lock_file_fails_sometimes() {
            fail::cfg("create-lock-file", "90%10*return(some lock file creation error)->off").unwrap();
            let path = MemoryFS::new().into();

            let lock = Lock::lock(&path).unwrap();
            lock.release().unwrap();
        }
    }

    #[cfg(feature = "failpoints")]
    rusty_fork_test! {
        #[test]
        fn know_to_give_up_when_creating_lock_file_always_fails()  {
            fail::cfg("create-lock-file", "return(persistent lock file creation error)").unwrap();
            let path = MemoryFS::new().into();

            assert!(Lock::lock_with_timeout(&path, 1).is_err());
        }
    }
}