use super::{
SqliteStore,
integrity::{check_history_blocking, integrity_check_blocking},
lineage::{self, Seal},
load_blocking,
replay::replay_blocking,
runs::{self, RunStore, RunStoreReport},
snapshot::genesis_blocking,
};
use crate::{
StoreError,
error::database_error,
schema::{self, Layout},
};
use dpm_model::{LineageId, WorkspaceId};
use files::{
ReadMode, canonical_blocking, create_target_blocking, discard_blocking, fingerprint_blocking,
open_read_only_blocking, read_mode_blocking, target_path_blocking,
};
use rusqlite::{
Connection,
backup::{Backup, StepResult},
};
use serde::Serialize;
use std::{
path::{Path, PathBuf},
time::Duration,
};
pub(super) mod files;
const BUSY_TIMEOUT: Duration = Duration::from_secs(5);
const BUSY_RETRY_DELAY: Duration = Duration::from_millis(100);
const BUSY_RETRIES: u32 = 50;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct IntegrityReport {
pub path: PathBuf,
pub schema_version: i64,
pub workspace_id: WorkspaceId,
pub workspace_name: String,
pub lineage_id: LineageId,
pub archived: bool,
pub revision: u64,
pub operation_count: u64,
pub genesis_revision: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub runs: Option<RunStoreReport>,
}
impl SqliteStore {
pub fn backup_blocking(&self, to: &Path) -> Result<IntegrityReport, StoreError> {
let to = target_path_blocking(to)?;
let runs_to = RunStore::sidecar_path(&to);
refuse_existing_blocking(&runs_to)?;
let runs_from = RunStore::sidecar_path(&self.path);
let has_runs = nonempty_blocking(&runs_from)?;
create_target_blocking(&to)?;
if has_runs {
runs::recovery::create_target(&runs_to).inspect_err(|_| discard_blocking(&to))?;
}
let result = self
.copy_runs_blocking(&runs_from, &runs_to, has_runs)
.and_then(|()| {
copy_blocking(&self.connection, &to, "delete", |target| {
lineage::seal_blocking(target, &to, Seal::Archive)
})
})
.and_then(|()| verify_store_blocking(&to));
discard_all_on_error_blocking(&to, has_runs, result)
}
fn copy_runs_blocking(&self, from: &Path, to: &Path, has_runs: bool) -> Result<(), StoreError> {
if !has_runs {
return Ok(());
}
let workspace = self
.load_blocking()?
.ok_or_else(|| StoreError::NotInitialized(self.path.clone()))?
.workspace
.id;
RunStore::open_existing_blocking(from, workspace)
.map_err(run_store_error(from))?
.ok_or_else(|| missing_runs(from))?
.backup_to_blocking(to)
.map_err(run_store_error(to))
.map(drop)
}
}
pub fn restore_store_blocking(from: &Path, to: &Path) -> Result<IntegrityReport, StoreError> {
let from = canonical_blocking(from)?;
let to = target_path_blocking(to)?;
let source_report = verify_store_blocking(&from)?;
if !source_report.archived {
return Err(crate::LineageError::NotAnArchive { path: from }.into());
}
let runs_from = RunStore::sidecar_path(&from);
let runs_to = RunStore::sidecar_path(&to);
refuse_existing_blocking(&runs_to)?;
let has_runs = nonempty_blocking(&runs_from)?;
let mode = read_mode_blocking(&from)?;
let before = fingerprint_blocking(&from)?;
let source = open_read_only_blocking(&from, mode)?;
create_target_blocking(&to)?;
if has_runs {
runs::recovery::create_target(&runs_to).inspect_err(|_| discard_blocking(&to))?;
}
let fork = LineageId::new();
let result = copy_blocking(&source, &to, "wal", |target| {
lineage::seal_blocking(target, &to, Seal::Fork(fork))
})
.and_then(|()| {
if mode == ReadMode::Immutable && fingerprint_blocking(&from)? != before {
return Err(StoreError::BackupIncomplete {
path: to.clone(),
state: format!("{} changed during the restore", from.display()),
});
}
Ok(())
})
.and_then(|()| {
if has_runs {
runs::recovery::restore_blocking(
&runs_from,
&runs_to,
source_report.workspace_id,
fork,
source_report.revision,
)
.map_err(run_store_error(&runs_to))?;
}
verify_store_blocking(&to)
});
discard_all_on_error_blocking(&to, has_runs, result)
}
pub fn verify_store_blocking(path: &Path) -> Result<IntegrityReport, StoreError> {
let path = canonical_blocking(path)?;
let runs_path = RunStore::sidecar_path(&path);
if !nonempty_blocking(&runs_path)? {
return verify_project_blocking(&path, None);
}
let (runs, pair) = runs::recovery::verify_with_facts_blocking(&runs_path)
.map_err(run_store_error(&runs_path))?;
let report = verify_project_blocking(&path, Some(&pair))?;
Ok(IntegrityReport {
runs: Some(runs),
..report
})
}
fn verify_project_blocking(
path: &Path,
pair: Option<&runs::pairing::PairFacts>,
) -> Result<IntegrityReport, StoreError> {
let path = canonical_blocking(path)?;
if read_mode_blocking(&path)? == ReadMode::Immutable {
let before = fingerprint_blocking(&path)?;
let result = verify_with_blocking(&path, ReadMode::Immutable, pair);
if fingerprint_blocking(&path)? == before {
return result;
}
}
verify_with_blocking(&path, ReadMode::Shared, pair)
}
fn verify_with_blocking(
path: &Path,
mode: ReadMode,
pair: Option<&runs::pairing::PairFacts>,
) -> Result<IntegrityReport, StoreError> {
let mut connection = open_read_only_blocking(path, mode)?;
let transaction = connection
.transaction()
.map_err(database_error(path, "begin verification"))?;
let schema_version = schema::stored_version_blocking(&transaction, path)?;
let layout = schema::check_blocking(&transaction, path)?;
if layout == Layout::Empty {
return Err(StoreError::UnrecognizedSchema {
path: path.to_path_buf(),
detail: "no DPM tables".into(),
});
}
integrity_check_blocking(&transaction, path)?;
let plan = load_blocking(&transaction, path)?
.ok_or_else(|| StoreError::NotInitialized(path.to_path_buf()))?;
let genesis =
genesis_blocking(&transaction, path)?.ok_or_else(|| StoreError::MissingGenesis {
path: path.to_path_buf(),
})?;
let genesis_revision = genesis.revision;
let store_lineage = lineage::read_blocking(&transaction, path)?;
let history = check_history_blocking(
&transaction,
path,
genesis.workspace.id,
(genesis_revision, plan.revision),
)?;
replay_blocking(&transaction, path, genesis, &plan)?;
if let Some(pair) = pair {
let identity = runs::pairing::ProjectIdentity {
workspace: plan.workspace.id,
lineage: store_lineage.lineage_id,
snapshot_revision: plan.revision,
genesis_revision,
};
runs::pairing::cross_check_blocking(&transaction, path, &identity, pair)
.map_err(run_store_error(&RunStore::sidecar_path(path)))?;
}
Ok(IntegrityReport {
path: path.to_path_buf(),
schema_version,
workspace_id: plan.workspace.id,
workspace_name: plan.workspace.name,
lineage_id: store_lineage.lineage_id,
archived: store_lineage.archived,
revision: plan.revision,
operation_count: history.count,
genesis_revision,
runs: None,
})
}
pub(in crate::sqlite) fn copy_blocking(
source: &Connection,
to: &Path,
journal_mode: &str,
seal: impl FnOnce(&Connection) -> Result<(), StoreError>,
) -> Result<(), StoreError> {
let mut target = Connection::open(to).map_err(database_error(to, "open backup target"))?;
target
.busy_timeout(BUSY_TIMEOUT)
.map_err(database_error(to, "set busy timeout"))?;
{
let backup =
Backup::new(source, &mut target).map_err(database_error(to, "start backup"))?;
let mut retries = 0;
loop {
match backup.step(-1).map_err(database_error(to, "copy pages"))? {
StepResult::Done => break,
StepResult::Busy | StepResult::Locked if retries < BUSY_RETRIES => {
retries += 1;
std::thread::sleep(BUSY_RETRY_DELAY);
}
state => {
return Err(StoreError::BackupIncomplete {
path: to.to_path_buf(),
state: format!("{state:?}"),
});
}
}
}
}
seal(&target)?;
let mode: String = target
.query_row(
&format!("PRAGMA journal_mode = {journal_mode}"),
[],
|row| row.get(0),
)
.map_err(database_error(to, "set journal mode"))?;
if !mode.eq_ignore_ascii_case(journal_mode) {
return Err(StoreError::BackupIncomplete {
path: to.to_path_buf(),
state: format!("journal mode {mode} instead of {journal_mode}"),
});
}
target
.close()
.map_err(|(_, source)| database_error(to, "close backup target")(source))
}
fn discard_all_on_error_blocking(
to: &Path,
with_runs: bool,
result: Result<IntegrityReport, StoreError>,
) -> Result<IntegrityReport, StoreError> {
if result.is_err() {
discard_blocking(to);
if with_runs {
runs::recovery::discard(&RunStore::sidecar_path(to));
}
}
result
}
fn nonempty_blocking(path: &Path) -> Result<bool, StoreError> {
match std::fs::metadata(path) {
Ok(metadata) => Ok(metadata.len() > 0),
Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(source) => Err(StoreError::Io {
action: "inspect",
path: path.to_path_buf(),
source,
}),
}
}
fn refuse_existing_blocking(path: &Path) -> Result<(), StoreError> {
let exists = path.try_exists().map_err(|source| StoreError::Io {
action: "inspect",
path: path.to_path_buf(),
source,
})?;
if exists {
return Err(StoreError::TargetExists {
path: path.to_path_buf(),
});
}
Ok(())
}
fn run_store_error(path: &Path) -> impl FnOnce(crate::RunStoreError) -> StoreError {
let path = path.to_path_buf();
move |source| match source {
crate::RunStoreError::Store(error) => error,
other => StoreError::IntegrityCheckFailed {
path,
problems: vec![other.to_string()],
},
}
}
fn missing_runs(path: &Path) -> StoreError {
run_store_error(path)(crate::RunStoreError::Corrupt {
path: path.to_path_buf(),
detail: "the run store disappeared while it was being backed up".into(),
})
}
#[cfg(test)]
mod tests;