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 for command in terminal.commands() {
82 if command.journal() != journal || !operations.insert(command.operation_sequence()) {
83 return Err(RestoreReferenceReleaseError::InvalidCommandSet);
84 }
85 }
86 let mut quiescence = Vec::with_capacity(terminal.commands().len());
87 for command in terminal.commands() {
88 quiescence.push(CommandQuiescenceGuard::acquire(command)?);
89 }
90 let mut references = self.restore_references()?;
91 let released = references
92 .release(&reference)
93 .map_err(PersistenceError::from)?;
94 if read_terminal(journal)? != bytes {
96 return Err(RestoreReferenceReleaseError::JournalChanged);
97 }
98 self.check_root()?;
99 if released {
100 let encoded = encode_json(&references, MAX_RESTORE_REFERENCE_BYTES)?;
101 write_encoded(&self.root().join("restore-references.json"), &encoded)?;
102 }
103 self.sync_references()?;
104 drop(quiescence);
105 Ok(if released {
106 RestoreReferenceReleaseOutcome::Released
107 } else {
108 RestoreReferenceReleaseOutcome::AlreadyReleased
109 })
110 }
111}
112
113fn read_terminal(path: &Path) -> Result<Vec<u8>, PersistenceError> {
114 ic_host_fs::read::read_file_no_follow(path, MAX_RESTORE_TERMINAL_BYTES).map_err(|error| {
115 match error {
116 ic_host_artifacts::artifact::ArtifactError::LimitExceeded { .. } => {
117 PersistenceError::RecordTooLarge {
118 limit: MAX_RESTORE_TERMINAL_BYTES as u64,
119 }
120 }
121 error => PersistenceError::Io(error.into()),
122 }
123 })
124}
125
126#[derive(Debug, Error)]
128pub enum RestoreReferenceReleaseError<E: std::error::Error + 'static> {
129 #[error("restore terminal admission failed: {0}")]
131 Terminal(#[source] E),
132 #[error("restore terminal command set is invalid")]
134 InvalidCommandSet,
135 #[error("restore terminal journal changed during qualification")]
137 JournalChanged,
138 #[error(transparent)]
140 Custody(#[from] CommandLifetimeLockError),
141 #[error(transparent)]
143 Persistence(#[from] PersistenceError),
144}
145
146#[cfg(all(test, unix))]
147mod tests;