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