ic_backup/ops/persistence/restore_reference_release/
mod.rs1use super::{
4 json::{encode_json, write_encoded},
5 layout_lifetime::MAX_RESTORE_REFERENCE_BYTES,
6};
7use crate::{
8 model::{effect_graph::MAX_EFFECT_OPERATIONS, restore_references::RestoreReferenceRecord},
9 ops::persistence::{
10 BackupLayoutGuard, CommandLifetimeLockError, CommandQuiescenceGuard, JournalLock,
11 PersistenceError,
12 },
13 view::restore_reference_release::TerminalRestoreEvidence,
14};
15use std::{collections::BTreeSet, path::Path};
16use thiserror::Error;
17
18pub const MAX_RESTORE_TERMINAL_BYTES: usize = 16 * 1024 * 1024;
20
21pub trait RestoreTerminalEvidenceProvider {
28 type Error: std::error::Error + 'static;
30
31 fn qualify_terminal(
36 &self,
37 journal: &Path,
38 bytes: &[u8],
39 ) -> Result<TerminalRestoreEvidence, Self::Error>;
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
44pub enum RestoreReferenceReleaseOutcome {
45 Released,
47 AlreadyReleased,
49}
50
51impl BackupLayoutGuard {
52 pub fn release_restore<P: RestoreTerminalEvidenceProvider>(
64 &self,
65 journal_guard: &JournalLock,
66 provider: &P,
67 ) -> Result<RestoreReferenceReleaseOutcome, RestoreReferenceReleaseError<P::Error>> {
68 let journal = journal_guard.journal();
69 self.check_root()?;
70 let bytes = read_terminal(journal)?;
71 let terminal = provider
72 .qualify_terminal(journal, &bytes)
73 .map_err(RestoreReferenceReleaseError::Terminal)?;
74 let reference =
75 RestoreReferenceRecord::new(journal.to_path_buf(), terminal.authority().hash())
76 .map_err(PersistenceError::from)?;
77 if terminal.commands().len() > MAX_EFFECT_OPERATIONS {
78 return Err(RestoreReferenceReleaseError::InvalidCommandSet);
79 }
80 let mut operations = BTreeSet::new();
81 let mut quiescence = Vec::with_capacity(terminal.commands().len());
82 for command in terminal.commands() {
83 if command.journal() != journal || !operations.insert(command.operation_sequence()) {
84 return Err(RestoreReferenceReleaseError::InvalidCommandSet);
85 }
86 quiescence.push(CommandQuiescenceGuard::acquire(command)?);
87 }
88 let mut references = self.restore_references()?;
89 let released = references
90 .release(&reference)
91 .map_err(PersistenceError::from)?;
92 if read_terminal(journal)? != bytes {
94 return Err(RestoreReferenceReleaseError::JournalChanged);
95 }
96 self.check_root()?;
97 if released {
98 let encoded = encode_json(&references, MAX_RESTORE_REFERENCE_BYTES)?;
99 write_encoded(&self.root().join("restore-references.json"), &encoded)?;
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;