Skip to main content

ic_memory/
bootstrap.rs

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