use crate::*;
pub static GC_LOCK: &str = "GC_LOCK";
#[derive(Debug)]
pub struct GarbageCollectionLock {
archive: Archive,
band_id: Option<BandId>,
}
impl GarbageCollectionLock {
pub fn new(archive: &Archive) -> Result<GarbageCollectionLock> {
let archive = archive.clone();
let band_id = archive.last_band_id()?;
if let Some(band_id) = band_id {
if !archive.band_is_closed(band_id)? {
return Err(Error::DeleteWithIncompleteBackup { band_id });
}
}
if archive.transport().is_file(GC_LOCK).unwrap_or(true) {
return Err(Error::GarbageCollectionLockHeld);
}
archive.transport().write_file(GC_LOCK, b"{}\n")?;
Ok(GarbageCollectionLock { archive, band_id })
}
pub fn break_lock(archive: &Archive) -> Result<GarbageCollectionLock> {
if GarbageCollectionLock::is_locked(archive)? {
archive.transport().remove_file(GC_LOCK)?;
}
GarbageCollectionLock::new(archive)
}
pub fn is_locked(archive: &Archive) -> Result<bool> {
archive.transport().is_file(GC_LOCK).map_err(Error::from)
}
pub fn check(&self) -> Result<()> {
let current_last_band_id = self.archive.last_band_id()?;
if self.band_id == current_last_band_id {
Ok(())
} else {
Err(Error::GarbageCollectionLockHeldDuringBackup)
}
}
}
impl Drop for GarbageCollectionLock {
fn drop(&mut self) {
if let Err(err) = self.archive.transport().remove_file(GC_LOCK) {
eprintln!("Failed to delete GC_LOCK: {err:?}")
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::monitor::test::TestMonitor;
use crate::test_fixtures::{ScratchArchive, TreeFixture};
#[test]
fn empty_archive_ok() {
let archive = ScratchArchive::new();
let delete_guard = GarbageCollectionLock::new(&archive).unwrap();
assert!(archive.transport().is_file("GC_LOCK").unwrap());
delete_guard.check().unwrap();
drop(delete_guard);
assert!(!archive.transport().is_file("GC_LOCK").unwrap());
}
#[test]
fn completed_backup_ok() {
let archive = ScratchArchive::new();
let source = TreeFixture::new();
backup(
&archive,
source.path(),
&BackupOptions::default(),
TestMonitor::arc(),
)
.unwrap();
let delete_guard = GarbageCollectionLock::new(&archive).unwrap();
delete_guard.check().unwrap();
}
#[test]
fn concurrent_complete_backup_denied() {
let archive = ScratchArchive::new();
let source = TreeFixture::new();
let _delete_guard = GarbageCollectionLock::new(&archive).unwrap();
let backup_result = backup(
&archive,
source.path(),
&BackupOptions::default(),
TestMonitor::arc(),
);
assert_eq!(
backup_result.expect_err("backup fails").to_string(),
"Archive is locked for garbage collection"
);
}
#[test]
fn incomplete_backup_denied() {
let archive = ScratchArchive::new();
Band::create(&archive).unwrap();
let result = GarbageCollectionLock::new(&archive);
assert!(result.is_err());
assert_eq!(
result.err().unwrap().to_string(),
"Can't delete blocks because the last band (b0000) is incomplete and may be in use"
);
}
#[test]
fn concurrent_gc_prevented() {
let archive = ScratchArchive::new();
let _lock1 = GarbageCollectionLock::new(&archive).unwrap();
let lock2_result = GarbageCollectionLock::new(&archive);
assert_eq!(
lock2_result.unwrap_err().to_string(),
"Archive is locked for garbage collection"
);
}
#[test]
fn sequential_gc_allowed() {
let archive = ScratchArchive::new();
let _lock1 = GarbageCollectionLock::new(&archive).unwrap();
drop(_lock1);
let _lock2 = GarbageCollectionLock::new(&archive).unwrap();
drop(_lock2);
}
#[test]
fn break_lock() {
let archive = ScratchArchive::new();
let lock1 = GarbageCollectionLock::new(&archive).unwrap();
std::mem::forget(lock1);
let _lock2 = GarbageCollectionLock::break_lock(&archive).unwrap();
}
}