Skip to main content

ic_memory/
bootstrap.rs

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