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