Skip to main content

ic_backup/ops/persistence/restore_reference_release/
mod.rs

1//! Release exact source dependencies under terminal journal and command custody.
2
3use 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
14/// Maximum bytes supplied to the integration's terminal-journal validator.
15pub const MAX_RESTORE_TERMINAL_BYTES: usize = 16 * 1024 * 1024;
16
17/// Validate complete original restore settlement and return its exact command set.
18///
19/// Implementations authenticate their maintained journal schema, original intent,
20/// complete application/effect obligations and every external command identity.
21/// An all-Applied projection alone is insufficient. The library supplies freshly
22/// read journal bytes under its held mutation lock; no cached completion flag is used.
23pub trait RestoreTerminalEvidenceProvider {
24    /// Product-owned typed terminal qualification failure.
25    type Error: std::error::Error + 'static;
26
27    /// Qualify the current terminal bytes and complete retained command-custody set.
28    ///
29    /// # Errors
30    /// Reject incomplete, changed, unauthenticated or unsettled original evidence.
31    fn qualify_terminal(
32        &self,
33        journal: &Path,
34        bytes: &[u8],
35    ) -> Result<TerminalRestoreEvidence, Self::Error>;
36}
37
38/// Observable durable reference transition; neither outcome dispatches IC effects.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum RestoreReferenceReleaseOutcome {
41    /// The exact dependency was durably removed.
42    Released,
43    /// Current terminal/custody evidence passes and the dependency is already absent.
44    AlreadyReleased,
45}
46
47impl BackupLayoutGuard {
48    /// Release one exact restore dependency after fresh terminal and command checks.
49    ///
50    /// Keep the original journal mutation guard held. Every declared command sidecar
51    /// must already exist with its retained inode/device identity and be exclusively
52    /// quiescent. Guards remain held through reference publication. Failures leave
53    /// dependencies retained; lost publication responses reconcile by the same call.
54    /// This performs no application fence release, retry or paid effect.
55    ///
56    /// # Errors
57    /// Refuses changed layouts/journals, terminal admission, changed intent, invalid
58    /// or conflicting custody identities, active descendants and publication failures.
59    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        // The validator may perform product reads; preserve the exact bytes it admitted.
89        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/// Typed refusal preserves original dependency and custody evidence.
120#[derive(Debug, Error)]
121pub enum RestoreReferenceReleaseError<E: std::error::Error + 'static> {
122    /// The product did not qualify its complete original terminal obligations.
123    #[error("restore terminal admission failed: {0}")]
124    Terminal(#[source] E),
125    /// A command belongs to another journal or repeats an original identity.
126    #[error("restore terminal command set is invalid")]
127    InvalidCommandSet,
128    /// Journal bytes changed after product qualification.
129    #[error("restore terminal journal changed during qualification")]
130    JournalChanged,
131    /// Actual command exclusion or retained sidecar identity could not be established.
132    #[error(transparent)]
133    Custody(#[from] CommandLifetimeLockError),
134    /// Layout/reference/byte admission or durable publication failed.
135    #[error(transparent)]
136    Persistence(#[from] PersistenceError),
137}
138
139#[cfg(all(test, unix))]
140mod tests;