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