use crate::{
model::{
artifacts::ArtifactChecksumRecord, effect_graph::MAX_EFFECT_OPERATIONS,
restore_references::RestoreReferenceRecord,
},
ops::persistence::{
BackupLayoutGuard, CommandLifetimeLockError, CommandQuiescenceGuard, JournalLock,
PersistenceError, write_json_durable,
},
view::restore_reference_release::TerminalRestoreEvidence,
};
use std::{collections::BTreeSet, path::Path};
use thiserror::Error;
pub const MAX_RESTORE_TERMINAL_BYTES: usize = 16 * 1024 * 1024;
pub trait RestoreTerminalEvidenceProvider {
type Error: std::error::Error + 'static;
fn qualify_terminal(
&self,
journal: &Path,
bytes: &[u8],
) -> Result<TerminalRestoreEvidence, Self::Error>;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RestoreReferenceReleaseOutcome {
Released,
AlreadyReleased,
}
impl BackupLayoutGuard {
pub fn release_restore<P: RestoreTerminalEvidenceProvider>(
&self,
journal_guard: &JournalLock,
provider: &P,
) -> Result<RestoreReferenceReleaseOutcome, RestoreReferenceReleaseError<P::Error>> {
let journal = journal_guard.journal();
self.check_root()?;
let bytes = read_terminal(journal)?;
let terminal = provider
.qualify_terminal(journal, &bytes)
.map_err(RestoreReferenceReleaseError::Terminal)?;
let reference =
RestoreReferenceRecord::new(journal.to_path_buf(), terminal.authority().hash())
.map_err(PersistenceError::from)?;
if terminal.commands().len() > MAX_EFFECT_OPERATIONS {
return Err(RestoreReferenceReleaseError::InvalidCommandSet);
}
let mut operations = BTreeSet::new();
let mut quiescence = Vec::with_capacity(terminal.commands().len());
for command in terminal.commands() {
if command.journal() != journal || !operations.insert(command.operation_sequence()) {
return Err(RestoreReferenceReleaseError::InvalidCommandSet);
}
quiescence.push(CommandQuiescenceGuard::acquire(command)?);
}
let mut references = self.restore_references()?;
let released = references
.release(&reference)
.map_err(PersistenceError::from)?;
if ArtifactChecksumRecord::from_bytes(&read_terminal(journal)?)
!= ArtifactChecksumRecord::from_bytes(&bytes)
{
return Err(RestoreReferenceReleaseError::JournalChanged);
}
self.check_root()?;
if released {
write_json_durable(&self.root().join("restore-references.json"), &references)?;
}
self.sync_references()?;
drop(quiescence);
Ok(if released {
RestoreReferenceReleaseOutcome::Released
} else {
RestoreReferenceReleaseOutcome::AlreadyReleased
})
}
}
fn read_terminal(path: &Path) -> Result<Vec<u8>, PersistenceError> {
ic_host_fs::read::read_file_no_follow(path, MAX_RESTORE_TERMINAL_BYTES).map_err(|error| {
match error {
ic_host_artifacts::artifact::ArtifactError::LimitExceeded { .. } => {
PersistenceError::RecordTooLarge {
limit: MAX_RESTORE_TERMINAL_BYTES as u64,
}
}
error => PersistenceError::Io(error.into()),
}
})
}
#[derive(Debug, Error)]
pub enum RestoreReferenceReleaseError<E: std::error::Error + 'static> {
#[error("restore terminal admission failed: {0}")]
Terminal(#[source] E),
#[error("restore terminal command set is invalid")]
InvalidCommandSet,
#[error("restore terminal journal changed during qualification")]
JournalChanged,
#[error(transparent)]
Custody(#[from] CommandLifetimeLockError),
#[error(transparent)]
Persistence(#[from] PersistenceError),
}
#[cfg(all(test, unix))]
mod tests;