ic_backup/ops/persistence/restore_reference_release/
mod.rs1use crate::{
4 model::{
5 artifacts::ArtifactChecksumRecord, effect_graph::MAX_EFFECT_OPERATIONS,
6 restore_references::RestoreReferenceRecord,
7 },
8 ops::persistence::{
9 BackupLayoutGuard, CommandLifetimeLockError, CommandQuiescenceGuard, JournalLock,
10 PersistenceError, write_json_durable,
11 },
12 view::restore_reference_release::TerminalRestoreEvidence,
13};
14use std::{collections::BTreeSet, path::Path};
15use thiserror::Error;
16
17pub const MAX_RESTORE_TERMINAL_BYTES: usize = 16 * 1024 * 1024;
19
20pub trait RestoreTerminalEvidenceProvider {
27 type Error: std::error::Error + 'static;
29
30 fn qualify_terminal(
35 &self,
36 journal: &Path,
37 bytes: &[u8],
38 ) -> Result<TerminalRestoreEvidence, Self::Error>;
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum RestoreReferenceReleaseOutcome {
44 Released,
46 AlreadyReleased,
48}
49
50impl BackupLayoutGuard {
51 pub fn release_restore<P: RestoreTerminalEvidenceProvider>(
63 &self,
64 journal_guard: &JournalLock,
65 provider: &P,
66 ) -> Result<RestoreReferenceReleaseOutcome, RestoreReferenceReleaseError<P::Error>> {
67 let journal = journal_guard.journal();
68 self.check_root()?;
69 let bytes = read_terminal(journal)?;
70 let terminal = provider
71 .qualify_terminal(journal, &bytes)
72 .map_err(RestoreReferenceReleaseError::Terminal)?;
73 let reference =
74 RestoreReferenceRecord::new(journal.to_path_buf(), terminal.authority().hash())
75 .map_err(PersistenceError::from)?;
76 if terminal.commands().len() > MAX_EFFECT_OPERATIONS {
77 return Err(RestoreReferenceReleaseError::InvalidCommandSet);
78 }
79 let mut operations = BTreeSet::new();
80 let mut quiescence = Vec::with_capacity(terminal.commands().len());
81 for command in terminal.commands() {
82 if command.journal() != journal || !operations.insert(command.operation_sequence()) {
83 return Err(RestoreReferenceReleaseError::InvalidCommandSet);
84 }
85 quiescence.push(CommandQuiescenceGuard::acquire(command)?);
86 }
87 let mut references = self.restore_references()?;
88 let released = references
89 .release(&reference)
90 .map_err(PersistenceError::from)?;
91 if ArtifactChecksumRecord::from_bytes(&read_terminal(journal)?)
93 != ArtifactChecksumRecord::from_bytes(&bytes)
94 {
95 return Err(RestoreReferenceReleaseError::JournalChanged);
96 }
97 self.check_root()?;
98 if released {
99 write_json_durable(&self.root().join("restore-references.json"), &references)?;
100 }
101 self.sync_references()?;
102 drop(quiescence);
103 Ok(if released {
104 RestoreReferenceReleaseOutcome::Released
105 } else {
106 RestoreReferenceReleaseOutcome::AlreadyReleased
107 })
108 }
109}
110
111fn read_terminal(path: &Path) -> Result<Vec<u8>, PersistenceError> {
112 ic_host_fs::read::read_file_no_follow(path, MAX_RESTORE_TERMINAL_BYTES).map_err(|error| {
113 match error {
114 ic_host_artifacts::artifact::ArtifactError::LimitExceeded { .. } => {
115 PersistenceError::RecordTooLarge {
116 limit: MAX_RESTORE_TERMINAL_BYTES as u64,
117 }
118 }
119 error => PersistenceError::Io(error.into()),
120 }
121 })
122}
123
124#[derive(Debug, Error)]
126pub enum RestoreReferenceReleaseError<E: std::error::Error + 'static> {
127 #[error("restore terminal admission failed: {0}")]
129 Terminal(#[source] E),
130 #[error("restore terminal command set is invalid")]
132 InvalidCommandSet,
133 #[error("restore terminal journal changed during qualification")]
135 JournalChanged,
136 #[error(transparent)]
138 Custody(#[from] CommandLifetimeLockError),
139 #[error(transparent)]
141 Persistence(#[from] PersistenceError),
142}
143
144#[cfg(all(test, unix))]
145mod tests;