Skip to main content

ic_memory/
bootstrap.rs

1use crate::{
2    capability::{CommittedAllocations, ValidatedAllocations},
3    declaration::DeclarationSnapshot,
4    ledger::{
5        AllocationStageError, LedgerCommitError, LedgerCommitStore, stage_validated_generation,
6    },
7    policy::AllocationPolicy,
8    validation::{AllocationValidationError, validate_allocations},
9};
10use std::borrow::Cow;
11
12///
13/// AllocationBootstrap
14///
15/// Internal validation and protected commit step used by `MemoryRuntime`.
16/// The runtime persists the owning stable-cell record before confirming the
17/// pending commit.
18///
19
20#[derive(Debug)]
21pub struct AllocationBootstrap<'store> {
22    store: &'store mut LedgerCommitStore,
23}
24
25impl<'store> AllocationBootstrap<'store> {
26    /// Build a bootstrap pipeline over a protected ledger commit store.
27    pub const fn new(store: &'store mut LedgerCommitStore) -> Self {
28        Self { store }
29    }
30
31    pub(crate) fn validate_against<P>(
32        &mut self,
33        prior: crate::RecoveredLedger,
34        snapshot: DeclarationSnapshot,
35        policy: &P,
36    ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
37    where
38        P: AllocationPolicy,
39    {
40        let validated =
41            validate_allocations(&prior, snapshot, policy).map_err(BootstrapError::Validation)?;
42        let staged = stage_validated_generation(Cow::Owned(prior.into_ledger()), &validated)
43            .map_err(BootstrapError::Staging)?;
44        self.store
45            .commit_generation(&staged)
46            .map_err(BootstrapError::Ledger)?;
47
48        Ok(PendingBootstrapCommit {
49            validated,
50            generation: staged.current_generation(),
51        })
52    }
53}
54
55///
56/// PendingBootstrapCommit
57///
58/// Pending result of a successful generic allocation bootstrap commit.
59///
60/// The embedded [`crate::LedgerCommitStore`] has advanced, but this generic
61/// layer does not own stable-memory IO. Persist the owning record first, then
62/// call [`PendingBootstrapCommit::confirm_persisted`] to mint the allocation-open
63/// capability.
64///
65
66#[derive(Debug, Eq, PartialEq)]
67#[must_use = "persist the owning ledger record, then confirm_persisted before opening allocations"]
68pub struct PendingBootstrapCommit {
69    /// Generation accepted by the protected commit, awaiting stable persistence.
70    generation: u64,
71    /// Validated allocation declarations awaiting persistence confirmation.
72    validated: ValidatedAllocations,
73}
74
75impl PendingBootstrapCommit {
76    /// Confirm that the owning integration durably persisted this commit.
77    ///
78    /// Calling this method before the stable-memory write succeeds violates the
79    /// allocation protocol. The default runtime performs its stable-cell write
80    /// before confirmation.
81    #[must_use]
82    pub fn confirm_persisted(self) -> CommittedAllocations {
83        self.validated.confirm_persisted(self.generation)
84    }
85}
86
87///
88/// BootstrapError
89///
90/// Failure to recover, validate, or commit an allocation generation.
91#[non_exhaustive]
92#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
93pub enum BootstrapError<P> {
94    /// Ledger recovery or protected commit failed.
95    #[error(transparent)]
96    Ledger(LedgerCommitError),
97    /// Policy or historical allocation validation failed.
98    #[error(transparent)]
99    Validation(AllocationValidationError<P>),
100    /// Validated declarations could not be staged against the recovered ledger.
101    #[error(transparent)]
102    Staging(AllocationStageError),
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::{
109        AllocationDeclaration, AllocationLedger, GenericAllocationPolicy, MemoryManagerSlot,
110        SchemaMetadata,
111    };
112    #[derive(Debug, Eq, PartialEq)]
113    struct RejectActivePolicy;
114
115    impl AllocationPolicy for RejectActivePolicy {
116        type Error = &'static str;
117
118        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
119            Ok(())
120        }
121
122        fn validate_slot(
123            &self,
124            _key: &crate::StableKey,
125            _slot: &MemoryManagerSlot,
126        ) -> Result<(), Self::Error> {
127            Err("active slot rejected")
128        }
129    }
130
131    fn ledger() -> AllocationLedger {
132        AllocationLedger::empty_genesis()
133    }
134
135    fn declaration() -> AllocationDeclaration {
136        AllocationDeclaration::new(
137            "app.users.v1",
138            MemoryManagerSlot::new(100).expect("usable slot"),
139            None,
140            SchemaMetadata::default(),
141        )
142        .expect("declaration")
143    }
144
145    #[test]
146    fn validated_commit_matches_recovered_generation() {
147        let mut store = LedgerCommitStore::default();
148        let recovered = store.recover_or_initialize(&ledger()).unwrap();
149        let snapshot = DeclarationSnapshot::new(vec![declaration()]).unwrap();
150        let commit = AllocationBootstrap::new(&mut store)
151            .validate_against(recovered, snapshot, &GenericAllocationPolicy)
152            .unwrap();
153        let recovered = store.recover().unwrap();
154        assert_eq!(recovered.current_generation(), 1);
155        assert_eq!(recovered.ledger().records().len(), 1);
156        assert_eq!(
157            recovered.ledger().records()[0],
158            crate::AllocationRecord::active(&declaration())
159        );
160        assert_eq!(commit.confirm_persisted().generation(), 1);
161    }
162
163    #[test]
164    fn reservation_policy_alone_does_not_activate_reserved_allocation() {
165        let mut store = LedgerCommitStore::default();
166        let reserved = ledger()
167            .stage_reservation_generation(&[declaration()])
168            .unwrap();
169        store.commit(&reserved).unwrap();
170        let before = store.clone();
171        let recovered = store.recover().unwrap();
172        let snapshot = DeclarationSnapshot::new(vec![declaration()]).unwrap();
173        let err = AllocationBootstrap::new(&mut store)
174            .validate_against(recovered, snapshot, &RejectActivePolicy)
175            .expect_err("active validation must run");
176        assert!(matches!(
177            err,
178            BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
179        ));
180        assert_eq!(store, before);
181        assert_eq!(
182            store.recover().unwrap().ledger().records()[0].state(),
183            crate::AllocationState::Reserved
184        );
185    }
186}