use std::collections::BTreeSet;
use std::io;
use std::path::Path;
use super::Store;
use crate::catalog;
use crate::envelope;
use crate::ids::VersionId;
pub const TRIES: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Copied {
pub versions: u64,
pub tries: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackupError {
KeptChanging,
Io,
}
impl Store {
pub fn backup_to(&self, dir: &Path) -> Result<Copied, BackupError> {
let fail = |_| BackupError::Io;
let key = envelope::catalog_key(&self.tenant_key).map_err(|_| BackupError::Io)?;
let objects = dir.join("objects");
self.fs.create_dir_all(&objects).map_err(fail)?;
let catalog = dir.join("catalog.sqlite");
for tries in 1..=TRIES {
for file in ["catalog.sqlite", "catalog.sqlite-wal", "catalog.sqlite-shm"] {
remove_if_there(&dir.join(file)).map_err(fail)?;
}
catalog::backup(&self.conn(), &catalog, &key).map_err(|_| BackupError::Io)?;
#[cfg(test)]
if let Some(hook) = self.hooks.copied.lock().expect("hook").as_mut() {
hook(self);
}
let versions = versions_in(&catalog, &key)?;
let present: BTreeSet<String> = self
.fs
.list(&objects)
.map_err(fail)?
.into_iter()
.filter_map(|n| n.into_string().ok())
.collect();
let mut missing = false;
for name in &versions {
if present.contains(name) {
continue;
}
match self.fs.read(&self.objects.join(name)) {
Ok(bytes) => {
let tmp = objects.join(format!("{name}.tmp"));
if present.contains(&format!("{name}.tmp")) {
self.fs.remove(&tmp).map_err(fail)?;
}
self.fs.write_new(&tmp, &bytes).map_err(fail)?;
self.fs.sync_file(&tmp).map_err(fail)?;
self.fs.rename(&tmp, &objects.join(name)).map_err(fail)?;
}
Err(e) if e.kind() == io::ErrorKind::NotFound => missing = true,
Err(e) => return Err(fail(e)),
}
}
if missing {
continue;
}
for name in self.fs.list(&objects).map_err(fail)? {
let name = name.into_string().unwrap_or_default();
if !versions.contains(&name) {
self.fs.remove(&objects.join(&name)).map_err(fail)?;
}
}
self.fs.sync_dir(&objects).map_err(fail)?;
self.fs.sync_dir(dir).map_err(fail)?;
if let Some(parent) = dir.parent() {
self.fs.sync_dir(parent).map_err(fail)?;
}
return Ok(Copied {
versions: versions.len() as u64,
tries,
});
}
Err(BackupError::KeptChanging)
}
}
fn versions_in(path: &Path, key: &crate::envelope::Key) -> Result<BTreeSet<String>, BackupError> {
let fail = |_| BackupError::Io;
let conn = catalog::open(path, key, "").map_err(|_| BackupError::Io)?;
let mut rows = conn.prepare("SELECT version FROM version").map_err(fail)?;
rows.query_map([], |r| r.get::<_, [u8; 16]>(0))
.map_err(fail)?
.map(|row| row.map(|id| VersionId(id).to_string()).map_err(fail))
.collect()
}
fn remove_if_there(path: &Path) -> io::Result<()> {
match std::fs::remove_file(path) {
Err(e) if e.kind() != io::ErrorKind::NotFound => Err(e),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use super::*;
use crate::clock::Clock;
use crate::fs::Fs;
use crate::retention::Retention;
use crate::store::testing::vcon;
use crate::store::tests::{Fixture, fixture};
const A: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc46";
const B: &str = "01a07da8-c2bb-83e5-b9a2-279e0d16bc47";
fn stored(f: &Fixture) -> VersionId {
let (found, body) = vcon(A, "first");
let first = match f.store.create(&f.caller, &found, &body).expect("create") {
crate::store::Created::New(v) => v,
other => panic!("{other:?}"),
};
let (found, body) = vcon(A, "second");
f.store
.replace(&f.caller, first, &found, &body)
.expect("replace");
let (found, body) = vcon(B, "other");
f.store.create(&f.caller, &found, &body).expect("create");
first
}
fn examined(f: &Fixture, dir: &Path) -> (Store, crate::store::Examination) {
let store = Store::open_beside(
dir,
f.tenant(),
f.tenant_key(),
f.fs.clone(),
f.clock.clone(),
)
.expect("the backup opens under the tenant key");
let now = f.clock.now();
let found = store
.examine(Retention::Years(7), now, now)
.expect("examines");
(store, found)
}
fn body_of(store: &Store, f: &Fixture, uuid: &str) -> Vec<u8> {
store
.get(&f.caller, uuid.parse().expect("uuid"))
.expect("reads")
.body
}
#[test]
fn a_backup_opens_whole_with_every_version() {
let f = fixture();
stored(&f);
let dir = f.dir.path().join("backup");
let copied = f.store.backup_to(&dir).expect("backs up");
assert_eq!(
copied,
Copied {
versions: 3,
tries: 1
}
);
let (store, found) = examined(&f, &dir);
assert_eq!(found.versions, 3);
assert!(found.damaged.is_empty(), "{:?}", found.damaged);
assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
assert_eq!(body_of(&store, &f, A), body_of(&f.store, &f, A));
assert_eq!(body_of(&store, &f, B), body_of(&f.store, &f, B));
}
#[test]
fn a_version_purged_after_the_catalog_is_copied_makes_another_try() {
let f = fixture();
let first = stored(&f);
let calls = Arc::new(AtomicUsize::new(0));
let seen = calls.clone();
*f.store.hooks.copied.lock().expect("hook") = Some(Box::new(move |store: &Store| {
if seen.fetch_add(1, Ordering::SeqCst) == 0 {
let purged = store.purge_due(Retention::Days(0)).expect("purges");
assert_eq!(purged.purged, 1);
}
}));
let dir = f.dir.path().join("backup");
let copied = f.store.backup_to(&dir).expect("backs up");
assert_eq!(
copied,
Copied {
versions: 2,
tries: 2
}
);
let (_, found) = examined(&f, &dir);
assert_eq!(found.versions, 2);
assert!(found.damaged.is_empty(), "{:?}", found.damaged);
assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
assert!(
!dir.join("objects").join(first.to_string()).exists()
&& f.fs
.read(&dir.join("objects").join(first.to_string()))
.is_err(),
"the purged version's file is not in the backup"
);
}
#[test]
fn a_store_whose_versions_keep_being_purged_fails_after_its_tries() {
let f = fixture();
stored(&f);
let calls = Arc::new(AtomicUsize::new(0));
let seen = calls.clone();
let current = Arc::new(Mutex::new(None::<VersionId>));
*f.store.hooks.copied.lock().expect("hook") = Some(Box::new(move |store: &Store| {
let n = seen.fetch_add(1, Ordering::SeqCst);
let mut current = current.lock().expect("current");
let caller = crate::store::Caller {
key: crate::envelope::Key::generate().expect("key").id(),
};
let now = current.unwrap_or_else(|| {
store
.get(&caller, B.parse().expect("uuid"))
.expect("B")
.version
});
let (found, body) = vcon(B, &format!("change {n}"));
*current = Some(store.replace(&caller, now, &found, &body).expect("replace"));
store.purge_due(Retention::Days(0)).expect("purges");
}));
let dir = f.dir.path().join("backup");
assert_eq!(f.store.backup_to(&dir), Err(BackupError::KeptChanging));
assert_eq!(calls.load(Ordering::SeqCst), TRIES);
}
#[test]
fn files_an_earlier_try_left_are_replaced_or_removed() {
let f = fixture();
stored(&f);
let dir = f.dir.path().join("backup");
let objects = dir.join("objects");
f.fs.create_dir_all(&objects).expect("dir");
let stray = VersionId::generate().to_string();
f.fs.write_new(&objects.join(&stray), b"from an earlier try")
.expect("write");
f.fs.write_new(&objects.join(format!("{stray}.tmp")), b"half")
.expect("write");
std::fs::write(dir.join("catalog.sqlite"), b"an earlier copy").expect("write");
std::fs::write(dir.join("catalog.sqlite-wal"), b"its wal").expect("write");
f.store.backup_to(&dir).expect("backs up");
let (_, found) = examined(&f, &dir);
assert_eq!(found.versions, 3);
assert!(found.damaged.is_empty(), "{:?}", found.damaged);
assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
let names = f.fs.list(&objects).expect("list");
assert_eq!(names.len(), 3, "{names:?}");
}
#[test]
fn a_backup_survives_a_crash_once_it_returns() {
let f = fixture();
stored(&f);
let dir = f.dir.path().join("backup");
f.store.backup_to(&dir).expect("backs up");
f.fs.crash();
let (_, found) = examined(&f, &dir);
assert_eq!(found.versions, 3);
assert!(found.damaged.is_empty(), "{:?}", found.damaged);
}
#[test]
fn a_backup_taken_while_writes_go_on_opens_whole() {
let f = Arc::new(fixture());
stored(&f);
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let writer = {
let (f, stop) = (f.clone(), stop.clone());
std::thread::spawn(move || {
let mut n = 0u32;
while !stop.load(Ordering::SeqCst) {
let uuid = format!("01a07da8-c2bb-83e5-b9a2-{n:012x}");
let (found, body) = vcon(&uuid, "load");
f.store.create(&f.caller, &found, &body).expect("create");
n += 1;
}
n
})
};
let dir = f.dir.path().join("backup");
let copied = f.store.backup_to(&dir).expect("backs up");
stop.store(true, Ordering::SeqCst);
let written = writer.join().expect("writer");
assert!(written > 0, "writes went on");
let (_, found) = examined(&f, &dir);
assert_eq!(found.versions, copied.versions);
assert!(found.damaged.is_empty(), "{:?}", found.damaged);
assert!(found.leftovers.is_empty(), "{:?}", found.leftovers);
}
}