Skip to main content

ic_memory/
bootstrap.rs

1use crate::{
2    declaration::AllocationDeclaration,
3    declaration::DeclarationSnapshot,
4    ledger::{
5        AllocationLedger, AllocationReservationError, AllocationRetirement,
6        AllocationRetirementError, LedgerCodec, LedgerCommitError, LedgerCommitStore,
7    },
8    policy::AllocationPolicy,
9    session::ValidatedAllocations,
10    validation::{AllocationValidationError, validate_allocations},
11};
12
13///
14/// AllocationBootstrap
15///
16/// Generic generation bootstrap pipeline.
17///
18/// This type owns allocation-governance sequencing only. Frameworks own when
19/// the pipeline runs, how the ledger store is backed by stable memory, and when
20/// endpoint dispatch is allowed.
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    /// Recover, validate, stage, commit, and publish one allocation generation.
33    pub fn validate_and_commit<C, P>(
34        &mut self,
35        codec: &C,
36        snapshot: DeclarationSnapshot,
37        policy: &P,
38        committed_at: Option<u64>,
39    ) -> Result<BootstrapCommit, BootstrapError<C::Error, P::Error>>
40    where
41        C: LedgerCodec,
42        P: AllocationPolicy,
43    {
44        let prior = self.store.recover(codec).map_err(BootstrapError::Ledger)?;
45        self.validate_against(codec, prior, snapshot, policy, committed_at)
46    }
47
48    /// Initialize an empty ledger store explicitly, then validate and commit.
49    ///
50    /// This is the generic genesis path. The supplied `genesis` ledger is a
51    /// framework decision; the generic crate only guarantees that it is used
52    /// when the protected physical store is empty, never when recovery sees
53    /// corrupt or partially written state.
54    pub fn initialize_validate_and_commit<C, P>(
55        &mut self,
56        codec: &C,
57        genesis: &AllocationLedger,
58        snapshot: DeclarationSnapshot,
59        policy: &P,
60        committed_at: Option<u64>,
61    ) -> Result<BootstrapCommit, BootstrapError<C::Error, P::Error>>
62    where
63        C: LedgerCodec,
64        P: AllocationPolicy,
65    {
66        let prior = self
67            .store
68            .recover_or_initialize(codec, genesis)
69            .map_err(BootstrapError::Ledger)?;
70        self.validate_against(codec, prior, snapshot, policy, committed_at)
71    }
72
73    /// Recover, policy-check, reserve, and commit one reservation generation.
74    pub fn reserve_and_commit<C, P>(
75        &mut self,
76        codec: &C,
77        reservations: &[AllocationDeclaration],
78        policy: &P,
79        committed_at: Option<u64>,
80    ) -> Result<AllocationLedger, BootstrapReservationError<C::Error, P::Error>>
81    where
82        C: LedgerCodec,
83        P: AllocationPolicy,
84    {
85        let prior = self
86            .store
87            .recover(codec)
88            .map_err(BootstrapReservationError::Ledger)?;
89        self.reserve_against(codec, prior, reservations, policy, committed_at)
90    }
91
92    /// Initialize an empty ledger store, then reserve and commit.
93    pub fn initialize_reserve_and_commit<C, P>(
94        &mut self,
95        codec: &C,
96        genesis: &AllocationLedger,
97        reservations: &[AllocationDeclaration],
98        policy: &P,
99        committed_at: Option<u64>,
100    ) -> Result<AllocationLedger, BootstrapReservationError<C::Error, P::Error>>
101    where
102        C: LedgerCodec,
103        P: AllocationPolicy,
104    {
105        let prior = self
106            .store
107            .recover_or_initialize(codec, genesis)
108            .map_err(BootstrapReservationError::Ledger)?;
109        self.reserve_against(codec, prior, reservations, policy, committed_at)
110    }
111
112    /// Recover, retire, and commit one explicit retirement generation.
113    pub fn retire_and_commit<C>(
114        &mut self,
115        codec: &C,
116        retirement: &AllocationRetirement,
117        committed_at: Option<u64>,
118    ) -> Result<AllocationLedger, BootstrapRetirementError<C::Error>>
119    where
120        C: LedgerCodec,
121    {
122        let prior = self
123            .store
124            .recover(codec)
125            .map_err(BootstrapRetirementError::Ledger)?;
126        self.retire_against(codec, prior, retirement, committed_at)
127    }
128
129    fn reserve_against<C, P>(
130        &mut self,
131        codec: &C,
132        prior: AllocationLedger,
133        reservations: &[AllocationDeclaration],
134        policy: &P,
135        committed_at: Option<u64>,
136    ) -> Result<AllocationLedger, BootstrapReservationError<C::Error, P::Error>>
137    where
138        C: LedgerCodec,
139        P: AllocationPolicy,
140    {
141        for reservation in reservations {
142            policy
143                .validate_key(&reservation.stable_key)
144                .map_err(BootstrapReservationError::Policy)?;
145            policy
146                .validate_reserved_slot(&reservation.stable_key, &reservation.slot)
147                .map_err(BootstrapReservationError::Policy)?;
148        }
149
150        let staged = prior
151            .stage_reservation_generation(reservations, committed_at)
152            .map_err(BootstrapReservationError::Reservation)?;
153        self.store
154            .commit(&staged, codec)
155            .map_err(BootstrapReservationError::Ledger)
156    }
157
158    fn retire_against<C>(
159        &mut self,
160        codec: &C,
161        prior: AllocationLedger,
162        retirement: &AllocationRetirement,
163        committed_at: Option<u64>,
164    ) -> Result<AllocationLedger, BootstrapRetirementError<C::Error>>
165    where
166        C: LedgerCodec,
167    {
168        let staged = prior
169            .stage_retirement_generation(retirement, committed_at)
170            .map_err(BootstrapRetirementError::Retirement)?;
171        self.store
172            .commit(&staged, codec)
173            .map_err(BootstrapRetirementError::Ledger)
174    }
175
176    fn validate_against<C, P>(
177        &mut self,
178        codec: &C,
179        prior: AllocationLedger,
180        snapshot: DeclarationSnapshot,
181        policy: &P,
182        committed_at: Option<u64>,
183    ) -> Result<BootstrapCommit, BootstrapError<C::Error, P::Error>>
184    where
185        C: LedgerCodec,
186        P: AllocationPolicy,
187    {
188        let validated =
189            validate_allocations(&prior, snapshot, policy).map_err(BootstrapError::Validation)?;
190        let staged = prior.stage_validated_generation(&validated, committed_at);
191        let committed = self
192            .store
193            .commit(&staged, codec)
194            .map_err(BootstrapError::Ledger)?;
195
196        Ok(BootstrapCommit {
197            validated: validated.with_generation(committed.current_generation),
198            ledger: committed,
199        })
200    }
201}
202
203///
204/// BootstrapCommit
205///
206/// Result of a successful generic allocation bootstrap commit.
207#[derive(Clone, Debug, Eq, PartialEq)]
208pub struct BootstrapCommit {
209    /// Ledger recovered after the protected generation commit.
210    pub ledger: AllocationLedger,
211    /// Validated allocation declarations tied to the committed generation.
212    pub validated: ValidatedAllocations,
213}
214
215///
216/// BootstrapError
217///
218/// Failure to recover, validate, or commit an allocation generation.
219#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
220pub enum BootstrapError<C, P> {
221    /// Ledger recovery or protected commit failed.
222    #[error(transparent)]
223    Ledger(LedgerCommitError<C>),
224    /// Policy or historical allocation validation failed.
225    #[error(transparent)]
226    Validation(AllocationValidationError<P>),
227}
228
229///
230/// BootstrapReservationError
231///
232/// Failure to policy-check, stage, or commit an allocation reservation.
233#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
234pub enum BootstrapReservationError<C, P> {
235    /// Ledger recovery or protected commit failed.
236    #[error(transparent)]
237    Ledger(LedgerCommitError<C>),
238    /// Policy adapter rejected a reservation declaration.
239    #[error("allocation policy rejected a reservation")]
240    Policy(P),
241    /// Reservation conflicted with historical allocation facts.
242    #[error(transparent)]
243    Reservation(AllocationReservationError),
244}
245
246///
247/// BootstrapRetirementError
248///
249/// Failure to stage or commit an explicit allocation retirement.
250#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
251pub enum BootstrapRetirementError<C> {
252    /// Ledger recovery or protected commit failed.
253    #[error(transparent)]
254    Ledger(LedgerCommitError<C>),
255    /// Retirement conflicted with historical allocation facts.
256    #[error(transparent)]
257    Retirement(AllocationRetirementError),
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::{
264        declaration::AllocationDeclaration,
265        ledger::{AllocationHistory, AllocationLedger, AllocationState},
266        schema::SchemaMetadata,
267        slot::AllocationSlotDescriptor,
268    };
269    use std::cell::RefCell;
270
271    #[derive(Debug, Default)]
272    struct TestCodec {
273        encoded: RefCell<Option<AllocationLedger>>,
274    }
275
276    impl LedgerCodec for TestCodec {
277        type Error = &'static str;
278
279        fn encode(&self, ledger: &AllocationLedger) -> Result<Vec<u8>, Self::Error> {
280            *self.encoded.borrow_mut() = Some(ledger.clone());
281            Ok(ledger.current_generation.to_le_bytes().to_vec())
282        }
283
284        fn decode(&self, _bytes: &[u8]) -> Result<AllocationLedger, Self::Error> {
285            self.encoded
286                .borrow()
287                .clone()
288                .ok_or("ledger was not encoded")
289        }
290    }
291
292    #[derive(Debug, Eq, PartialEq)]
293    struct TestPolicy;
294
295    impl AllocationPolicy for TestPolicy {
296        type Error = &'static str;
297
298        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
299            Ok(())
300        }
301
302        fn validate_slot(
303            &self,
304            _key: &crate::StableKey,
305            _slot: &AllocationSlotDescriptor,
306        ) -> Result<(), Self::Error> {
307            Ok(())
308        }
309
310        fn validate_reserved_slot(
311            &self,
312            _key: &crate::StableKey,
313            _slot: &AllocationSlotDescriptor,
314        ) -> Result<(), Self::Error> {
315            Ok(())
316        }
317    }
318
319    #[derive(Debug, Eq, PartialEq)]
320    struct RejectReservedPolicy;
321
322    impl AllocationPolicy for RejectReservedPolicy {
323        type Error = &'static str;
324
325        fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
326            Ok(())
327        }
328
329        fn validate_slot(
330            &self,
331            _key: &crate::StableKey,
332            _slot: &AllocationSlotDescriptor,
333        ) -> Result<(), Self::Error> {
334            Ok(())
335        }
336
337        fn validate_reserved_slot(
338            &self,
339            _key: &crate::StableKey,
340            _slot: &AllocationSlotDescriptor,
341        ) -> Result<(), Self::Error> {
342            Err("reserved slot rejected")
343        }
344    }
345
346    fn ledger() -> AllocationLedger {
347        AllocationLedger {
348            ledger_schema_version: 1,
349            physical_format_id: 1,
350            current_generation: 0,
351            allocation_history: AllocationHistory::default(),
352        }
353    }
354
355    fn declaration() -> AllocationDeclaration {
356        AllocationDeclaration::new(
357            "app.users.v1",
358            AllocationSlotDescriptor::memory_manager(100),
359            None,
360            SchemaMetadata::default(),
361        )
362        .expect("declaration")
363    }
364
365    #[test]
366    fn validate_and_commit_publishes_committed_generation() {
367        let codec = TestCodec::default();
368        let mut store = LedgerCommitStore::default();
369        store.commit(&ledger(), &codec).expect("initial ledger");
370        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
371
372        let commit = AllocationBootstrap::new(&mut store)
373            .validate_and_commit(&codec, snapshot, &TestPolicy, Some(42))
374            .expect("bootstrap commit");
375
376        assert_eq!(commit.ledger.current_generation, 1);
377        assert_eq!(commit.validated.generation(), 1);
378        assert_eq!(commit.ledger.allocation_history.records.len(), 1);
379        assert_eq!(commit.ledger.allocation_history.generations.len(), 1);
380    }
381
382    #[test]
383    fn initialize_validate_and_commit_seeds_empty_ledger_store() {
384        let codec = TestCodec::default();
385        let mut store = LedgerCommitStore::default();
386        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
387
388        let commit = AllocationBootstrap::new(&mut store)
389            .initialize_validate_and_commit(&codec, &ledger(), snapshot, &TestPolicy, Some(42))
390            .expect("bootstrap commit");
391
392        assert_eq!(commit.ledger.current_generation, 1);
393        assert_eq!(commit.validated.generation(), 1);
394        assert_eq!(commit.ledger.allocation_history.records.len(), 1);
395    }
396
397    #[test]
398    fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
399        let codec = TestCodec::default();
400        let mut store = LedgerCommitStore::default();
401        store
402            .write_corrupt_inactive_ledger(&ledger(), &codec)
403            .expect("corrupt ledger");
404        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
405
406        let err = AllocationBootstrap::new(&mut store)
407            .initialize_validate_and_commit(&codec, &ledger(), snapshot, &TestPolicy, Some(42))
408            .expect_err("corrupt state");
409
410        assert!(matches!(err, BootstrapError::Ledger(_)));
411    }
412
413    #[test]
414    fn reserve_and_commit_policy_checks_and_commits_reservation() {
415        let codec = TestCodec::default();
416        let mut store = LedgerCommitStore::default();
417        store.commit(&ledger(), &codec).expect("initial ledger");
418        let reservation = declaration();
419
420        let committed = AllocationBootstrap::new(&mut store)
421            .reserve_and_commit(&codec, &[reservation], &TestPolicy, Some(42))
422            .expect("reservation commit");
423
424        assert_eq!(committed.current_generation, 1);
425        assert_eq!(committed.allocation_history.records.len(), 1);
426        assert_eq!(
427            committed.allocation_history.records[0].state,
428            AllocationState::Reserved
429        );
430    }
431
432    #[test]
433    fn initialize_reserve_and_commit_seeds_empty_store() {
434        let codec = TestCodec::default();
435        let mut store = LedgerCommitStore::default();
436        let reservation = declaration();
437
438        let committed = AllocationBootstrap::new(&mut store)
439            .initialize_reserve_and_commit(&codec, &ledger(), &[reservation], &TestPolicy, Some(42))
440            .expect("reservation commit");
441
442        assert_eq!(committed.current_generation, 1);
443        assert_eq!(
444            committed.allocation_history.records[0].state,
445            AllocationState::Reserved
446        );
447    }
448
449    #[test]
450    fn reserve_and_commit_rejects_policy_failure_before_commit() {
451        let codec = TestCodec::default();
452        let mut store = LedgerCommitStore::default();
453        store.commit(&ledger(), &codec).expect("initial ledger");
454        let reservation = declaration();
455
456        let err = AllocationBootstrap::new(&mut store)
457            .reserve_and_commit(&codec, &[reservation], &RejectReservedPolicy, Some(42))
458            .expect_err("policy failure");
459        let recovered = store.recover(&codec).expect("recovered");
460
461        assert!(matches!(err, BootstrapReservationError::Policy(_)));
462        assert_eq!(recovered.current_generation, 0);
463        assert!(recovered.allocation_history.records.is_empty());
464    }
465
466    #[test]
467    fn retire_and_commit_tombstones_through_protected_commit() {
468        let codec = TestCodec::default();
469        let mut store = LedgerCommitStore::default();
470        store.commit(&ledger(), &codec).expect("initial ledger");
471        let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
472        AllocationBootstrap::new(&mut store)
473            .validate_and_commit(&codec, snapshot, &TestPolicy, Some(42))
474            .expect("active commit");
475        let retirement = AllocationRetirement::new(
476            "app.users.v1",
477            AllocationSlotDescriptor::memory_manager(100),
478        )
479        .expect("retirement");
480
481        let committed = AllocationBootstrap::new(&mut store)
482            .retire_and_commit(&codec, &retirement, Some(43))
483            .expect("retirement commit");
484
485        assert_eq!(committed.current_generation, 2);
486        assert_eq!(
487            committed.allocation_history.records[0].state,
488            AllocationState::Retired
489        );
490        assert_eq!(
491            committed.allocation_history.records[0].retired_generation,
492            Some(2)
493        );
494    }
495
496    #[test]
497    fn retire_and_commit_rejects_unknown_key_before_commit() {
498        let codec = TestCodec::default();
499        let mut store = LedgerCommitStore::default();
500        store.commit(&ledger(), &codec).expect("initial ledger");
501        let retirement = AllocationRetirement::new(
502            "app.users.v1",
503            AllocationSlotDescriptor::memory_manager(100),
504        )
505        .expect("retirement");
506
507        let err = AllocationBootstrap::new(&mut store)
508            .retire_and_commit(&codec, &retirement, Some(43))
509            .expect_err("unknown key");
510        let recovered = store.recover(&codec).expect("recovered");
511
512        assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
513        assert_eq!(recovered.current_generation, 0);
514        assert!(recovered.allocation_history.records.is_empty());
515    }
516}