1use crate::{
2 capability::{CommittedAllocations, ValidatedAllocations},
3 declaration::AllocationDeclaration,
4 declaration::DeclarationSnapshot,
5 ledger::{
6 AllocationLedger, AllocationReservationError, AllocationRetirement,
7 AllocationRetirementError, AllocationStageError, LedgerCommitError, LedgerCommitStore,
8 checked_reservation_count, validate_reservation_declaration,
9 },
10 policy::AllocationPolicy,
11 validation::{AllocationValidationError, validate_allocations},
12};
13
14#[derive(Debug)]
41pub struct AllocationBootstrap<'store> {
42 store: &'store mut LedgerCommitStore,
43}
44
45impl<'store> AllocationBootstrap<'store> {
46 pub const fn new(store: &'store mut LedgerCommitStore) -> Self {
48 Self { store }
49 }
50
51 pub fn validate_and_commit<P>(
53 &mut self,
54 snapshot: DeclarationSnapshot,
55 policy: &P,
56 committed_at: Option<u64>,
57 ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
58 where
59 P: AllocationPolicy,
60 {
61 let prior = self.store.recover().map_err(BootstrapError::Ledger)?;
62 self.validate_against(prior, snapshot, policy, committed_at)
63 }
64
65 pub fn initialize_validate_and_commit<P>(
77 &mut self,
78 genesis: &AllocationLedger,
79 snapshot: DeclarationSnapshot,
80 policy: &P,
81 committed_at: Option<u64>,
82 ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
83 where
84 P: AllocationPolicy,
85 {
86 let prior = self
87 .store
88 .recover_or_initialize(genesis)
89 .map_err(BootstrapError::Ledger)?;
90 self.validate_against(prior, snapshot, policy, committed_at)
91 }
92
93 pub fn reserve_and_commit<P>(
98 &mut self,
99 reservations: &[AllocationDeclaration],
100 policy: &P,
101 committed_at: Option<u64>,
102 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
103 where
104 P: AllocationPolicy,
105 {
106 let prior = self
107 .store
108 .recover()
109 .map_err(BootstrapReservationError::Ledger)?;
110 self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
111 }
112
113 pub fn initialize_reserve_and_commit<P>(
121 &mut self,
122 genesis: &AllocationLedger,
123 reservations: &[AllocationDeclaration],
124 policy: &P,
125 committed_at: Option<u64>,
126 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
127 where
128 P: AllocationPolicy,
129 {
130 let prior = self
131 .store
132 .recover_or_initialize(genesis)
133 .map_err(BootstrapReservationError::Ledger)?;
134 self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
135 }
136
137 pub fn retire_and_commit(
139 &mut self,
140 retirement: &AllocationRetirement,
141 committed_at: Option<u64>,
142 ) -> Result<AllocationLedger, BootstrapRetirementError> {
143 let prior = self
144 .store
145 .recover()
146 .map_err(BootstrapRetirementError::Ledger)?;
147 self.retire_against(prior.into_ledger(), retirement, committed_at)
148 }
149
150 fn reserve_against<P>(
151 &mut self,
152 prior: AllocationLedger,
153 reservations: &[AllocationDeclaration],
154 policy: &P,
155 committed_at: Option<u64>,
156 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
157 where
158 P: AllocationPolicy,
159 {
160 checked_reservation_count(reservations.len())
161 .map_err(BootstrapReservationError::Reservation)?;
162 for reservation in reservations {
163 validate_reservation_declaration(reservation)
164 .map_err(BootstrapReservationError::Reservation)?;
165 policy
166 .validate_key(&reservation.stable_key)
167 .map_err(BootstrapReservationError::Policy)?;
168 policy
169 .validate_reserved_slot(&reservation.stable_key, &reservation.slot)
170 .map_err(BootstrapReservationError::Policy)?;
171 }
172
173 let staged = prior
174 .stage_reservation_generation(reservations, committed_at)
175 .map_err(BootstrapReservationError::Reservation)?;
176 self.store
177 .commit(&staged)
178 .map(crate::RecoveredLedger::into_ledger)
179 .map_err(BootstrapReservationError::Ledger)
180 }
181
182 fn retire_against(
183 &mut self,
184 prior: AllocationLedger,
185 retirement: &AllocationRetirement,
186 committed_at: Option<u64>,
187 ) -> Result<AllocationLedger, BootstrapRetirementError> {
188 let staged = prior
189 .stage_retirement_generation(retirement, committed_at)
190 .map_err(BootstrapRetirementError::Retirement)?;
191 self.store
192 .commit(&staged)
193 .map(crate::RecoveredLedger::into_ledger)
194 .map_err(BootstrapRetirementError::Ledger)
195 }
196
197 pub(crate) fn validate_against<P>(
198 &mut self,
199 prior: crate::RecoveredLedger,
200 snapshot: DeclarationSnapshot,
201 policy: &P,
202 committed_at: Option<u64>,
203 ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
204 where
205 P: AllocationPolicy,
206 {
207 let validated =
208 validate_allocations(&prior, snapshot, policy).map_err(BootstrapError::Validation)?;
209 let prior_ledger = prior.into_ledger();
210 let staged = prior_ledger
211 .stage_validated_generation(&validated, committed_at)
212 .map_err(BootstrapError::Staging)?;
213 let committed = self.store.commit(&staged).map_err(BootstrapError::Ledger)?;
214
215 Ok(PendingBootstrapCommit {
216 validated,
217 ledger: committed.into_ledger(),
218 })
219 }
220}
221
222#[derive(Debug, Eq, PartialEq)]
234pub struct PendingBootstrapCommit {
235 ledger: AllocationLedger,
237 validated: ValidatedAllocations,
239}
240
241impl PendingBootstrapCommit {
242 #[must_use]
248 pub const fn ledger(&self) -> &AllocationLedger {
249 &self.ledger
250 }
251
252 #[must_use]
254 pub const fn validated(&self) -> &ValidatedAllocations {
255 &self.validated
256 }
257
258 #[must_use]
264 pub fn confirm_persisted(self) -> CommittedAllocations {
265 self.validated
266 .confirm_persisted(self.ledger.current_generation())
267 }
268}
269
270#[non_exhaustive]
275#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
276pub enum BootstrapError<P> {
277 #[error(transparent)]
279 Ledger(LedgerCommitError),
280 #[error(transparent)]
282 Validation(AllocationValidationError<P>),
283 #[error(transparent)]
285 Staging(AllocationStageError),
286}
287
288#[non_exhaustive]
293#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
294pub enum BootstrapReservationError<P> {
295 #[error(transparent)]
297 Ledger(LedgerCommitError),
298 #[error("allocation policy rejected a reservation")]
300 Policy(P),
301 #[error(transparent)]
303 Reservation(AllocationReservationError),
304}
305
306#[non_exhaustive]
311#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
312pub enum BootstrapRetirementError {
313 #[error(transparent)]
315 Ledger(LedgerCommitError),
316 #[error(transparent)]
318 Retirement(AllocationRetirementError),
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::{
325 declaration::AllocationDeclaration,
326 ledger::{AllocationHistory, AllocationLedger, AllocationState},
327 schema::SchemaMetadata,
328 slot::AllocationSlotDescriptor,
329 };
330
331 #[derive(Debug, Eq, PartialEq)]
332 struct TestPolicy;
333
334 impl AllocationPolicy for TestPolicy {
335 type Error = &'static str;
336
337 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
338 Ok(())
339 }
340
341 fn validate_slot(
342 &self,
343 _key: &crate::StableKey,
344 _slot: &AllocationSlotDescriptor,
345 ) -> Result<(), Self::Error> {
346 Ok(())
347 }
348
349 fn validate_reserved_slot(
350 &self,
351 _key: &crate::StableKey,
352 _slot: &AllocationSlotDescriptor,
353 ) -> Result<(), Self::Error> {
354 Ok(())
355 }
356 }
357
358 #[derive(Debug, Eq, PartialEq)]
359 struct RejectReservedPolicy;
360
361 impl AllocationPolicy for RejectReservedPolicy {
362 type Error = &'static str;
363
364 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
365 Ok(())
366 }
367
368 fn validate_slot(
369 &self,
370 _key: &crate::StableKey,
371 _slot: &AllocationSlotDescriptor,
372 ) -> Result<(), Self::Error> {
373 Ok(())
374 }
375
376 fn validate_reserved_slot(
377 &self,
378 _key: &crate::StableKey,
379 _slot: &AllocationSlotDescriptor,
380 ) -> Result<(), Self::Error> {
381 Err("reserved slot rejected")
382 }
383 }
384
385 #[derive(Debug, Eq, PartialEq)]
386 struct RejectActivePolicy;
387
388 impl AllocationPolicy for RejectActivePolicy {
389 type Error = &'static str;
390
391 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
392 Ok(())
393 }
394
395 fn validate_slot(
396 &self,
397 _key: &crate::StableKey,
398 _slot: &AllocationSlotDescriptor,
399 ) -> Result<(), Self::Error> {
400 Err("active slot rejected")
401 }
402
403 fn validate_reserved_slot(
404 &self,
405 _key: &crate::StableKey,
406 _slot: &AllocationSlotDescriptor,
407 ) -> Result<(), Self::Error> {
408 Ok(())
409 }
410 }
411
412 struct PolicyMustNotRun;
413
414 impl AllocationPolicy for PolicyMustNotRun {
415 type Error = &'static str;
416
417 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
418 panic!("policy received an invalid reservation")
419 }
420
421 fn validate_slot(
422 &self,
423 _key: &crate::StableKey,
424 _slot: &AllocationSlotDescriptor,
425 ) -> Result<(), Self::Error> {
426 panic!("policy received an invalid reservation")
427 }
428
429 fn validate_reserved_slot(
430 &self,
431 _key: &crate::StableKey,
432 _slot: &AllocationSlotDescriptor,
433 ) -> Result<(), Self::Error> {
434 panic!("policy received an invalid reservation")
435 }
436 }
437
438 fn ledger() -> AllocationLedger {
439 AllocationLedger {
440 current_generation: 0,
441 allocation_history: AllocationHistory::default(),
442 }
443 }
444
445 fn declaration() -> AllocationDeclaration {
446 AllocationDeclaration::new(
447 "app.users.v1",
448 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
449 None,
450 SchemaMetadata::default(),
451 )
452 .expect("declaration")
453 }
454
455 #[test]
456 fn validate_and_commit_publishes_committed_generation() {
457 let mut store = LedgerCommitStore::default();
458 store.commit(&ledger()).expect("initial ledger");
459 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
460
461 let commit = AllocationBootstrap::new(&mut store)
462 .validate_and_commit(snapshot, &TestPolicy, Some(42))
463 .expect("bootstrap commit");
464
465 assert_eq!(commit.ledger().current_generation, 1);
466 assert_eq!(commit.ledger().allocation_history.records().len(), 1);
467 assert_eq!(commit.ledger().allocation_history.generations().len(), 1);
468 assert_eq!(commit.confirm_persisted().generation(), 1);
469 }
470
471 #[test]
472 fn initialize_validate_and_commit_seeds_empty_ledger_store() {
473 let mut store = LedgerCommitStore::default();
474 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
475
476 let commit = AllocationBootstrap::new(&mut store)
477 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
478 .expect("bootstrap commit");
479
480 assert_eq!(commit.ledger().current_generation, 1);
481 assert_eq!(commit.ledger().allocation_history.records().len(), 1);
482 assert_eq!(commit.confirm_persisted().generation(), 1);
483 }
484
485 #[test]
486 fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
487 let mut store = LedgerCommitStore::default();
488 store
489 .write_corrupt_inactive_ledger(&ledger())
490 .expect("corrupt ledger");
491 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
492
493 let err = AllocationBootstrap::new(&mut store)
494 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
495 .expect_err("corrupt state");
496
497 assert!(matches!(err, BootstrapError::Ledger(_)));
498 }
499
500 #[test]
501 fn reserve_and_commit_policy_checks_and_commits_reservation() {
502 let mut store = LedgerCommitStore::default();
503 store.commit(&ledger()).expect("initial ledger");
504 let reservation = declaration();
505
506 let committed = AllocationBootstrap::new(&mut store)
507 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
508 .expect("reservation commit");
509
510 assert_eq!(committed.current_generation, 1);
511 assert_eq!(committed.allocation_history.records().len(), 1);
512 assert_eq!(
513 committed.allocation_history.records()[0].state(),
514 AllocationState::Reserved
515 );
516 }
517
518 #[test]
519 fn initialize_reserve_and_commit_seeds_empty_store() {
520 let mut store = LedgerCommitStore::default();
521 let reservation = declaration();
522
523 let committed = AllocationBootstrap::new(&mut store)
524 .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
525 .expect("reservation commit");
526
527 assert_eq!(committed.current_generation, 1);
528 assert_eq!(
529 committed.allocation_history.records()[0].state(),
530 AllocationState::Reserved
531 );
532 }
533
534 #[test]
535 fn reserve_and_commit_rejects_policy_failure_before_commit() {
536 let mut store = LedgerCommitStore::default();
537 store.commit(&ledger()).expect("initial ledger");
538 let reservation = declaration();
539
540 let err = AllocationBootstrap::new(&mut store)
541 .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
542 .expect_err("policy failure");
543 let recovered = store.recover().expect("recovered");
544
545 assert!(matches!(err, BootstrapReservationError::Policy(_)));
546 assert_eq!(recovered.current_generation(), 0);
547 assert_eq!(recovered.ledger().allocation_history().records(), []);
548 }
549
550 #[test]
551 fn reserve_and_commit_validates_reservation_before_policy() {
552 let mut store = LedgerCommitStore::default();
553 store.commit(&ledger()).expect("initial ledger");
554 let mut reservation = declaration();
555 reservation.slot =
556 AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
557
558 let err = AllocationBootstrap::new(&mut store)
559 .reserve_and_commit(&[reservation], &PolicyMustNotRun, Some(42))
560 .expect_err("invalid reservation must fail before policy");
561
562 assert!(matches!(
563 err,
564 BootstrapReservationError::Reservation(AllocationReservationError::InvalidDeclaration(
565 _
566 ))
567 ));
568 }
569
570 #[test]
571 fn reservation_pipeline_accepts_empty_and_full_slot_domain_batches() {
572 for count in [0_u8, 255] {
573 let reservations = (0..count)
574 .map(|id| {
575 AllocationDeclaration::memory_manager_unlabeled(
576 format!("app.future{id}.v1"),
577 id,
578 )
579 .expect("reservation")
580 })
581 .collect::<Vec<_>>();
582 let mut store = LedgerCommitStore::default();
583 store.commit(&ledger()).expect("initial ledger");
584
585 let committed = AllocationBootstrap::new(&mut store)
586 .reserve_and_commit(&reservations, &TestPolicy, Some(42))
587 .expect("bounded batch commits");
588
589 assert_eq!(committed.current_generation(), 1);
590 assert_eq!(
591 committed.allocation_history().records().len(),
592 usize::from(count)
593 );
594 assert_eq!(
595 committed.allocation_history().generations()[0].declaration_count(),
596 u32::from(count)
597 );
598 assert_eq!(store.recover().unwrap().ledger(), &committed);
599 }
600 }
601
602 #[test]
603 fn oversized_reservations_reject_before_policy_and_preserve_existing_store() {
604 let reservations = vec![declaration(); 256];
605 for initialize in [false, true] {
606 let mut store = LedgerCommitStore::default();
607 store.commit(&ledger()).expect("initial ledger");
608 let before = store.clone();
609 let mut bootstrap = AllocationBootstrap::new(&mut store);
610 let error = if initialize {
611 bootstrap.initialize_reserve_and_commit(
612 &ledger(),
613 &reservations,
614 &PolicyMustNotRun,
615 None,
616 )
617 } else {
618 bootstrap.reserve_and_commit(&reservations, &PolicyMustNotRun, None)
619 }
620 .expect_err("oversized batch must fail before policy");
621
622 assert_eq!(
623 error,
624 BootstrapReservationError::Reservation(
625 AllocationReservationError::TooManyReservations { count: 256 }
626 )
627 );
628 assert_eq!(store, before);
629 }
630 }
631
632 #[test]
633 fn oversized_initial_reservations_preserve_genesis_and_precede_invalid_declarations() {
634 let mut reservations = vec![declaration(); 256];
635 reservations[0].slot =
636 AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
637 let mut store = LedgerCommitStore::default();
638 let mut expected = LedgerCommitStore::default();
639 expected.commit(&ledger()).expect("expected genesis");
640
641 let error = AllocationBootstrap::new(&mut store)
642 .initialize_reserve_and_commit(&ledger(), &reservations, &PolicyMustNotRun, None)
643 .expect_err("size rejection precedes declaration and policy checks");
644
645 assert_eq!(
646 error,
647 BootstrapReservationError::Reservation(
648 AllocationReservationError::TooManyReservations { count: 256 }
649 )
650 );
651 assert_eq!(store, expected);
652 }
653
654 #[test]
655 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
656 let mut store = LedgerCommitStore::default();
657 store.commit(&ledger()).expect("initial ledger");
658 let reservation = declaration();
659 AllocationBootstrap::new(&mut store)
660 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
661 .expect("reservation commit");
662 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
663
664 let err = AllocationBootstrap::new(&mut store)
665 .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
666 .expect_err("active validation must run");
667 let recovered = store.recover().expect("recovered");
668
669 assert!(matches!(
670 err,
671 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
672 ));
673 assert_eq!(
674 recovered.ledger().allocation_history().records()[0].state(),
675 AllocationState::Reserved
676 );
677 }
678
679 #[test]
680 fn retire_and_commit_tombstones_through_protected_commit() {
681 let mut store = LedgerCommitStore::default();
682 store.commit(&ledger()).expect("initial ledger");
683 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
684 AllocationBootstrap::new(&mut store)
685 .validate_and_commit(snapshot, &TestPolicy, Some(42))
686 .expect("active commit");
687 let retirement = AllocationRetirement::new(
688 "app.users.v1",
689 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
690 )
691 .expect("retirement");
692
693 let committed = AllocationBootstrap::new(&mut store)
694 .retire_and_commit(&retirement, Some(43))
695 .expect("retirement commit");
696
697 assert_eq!(committed.current_generation, 2);
698 assert_eq!(
699 committed.allocation_history.records()[0].state(),
700 AllocationState::Retired { generation: 2 }
701 );
702 }
703
704 #[test]
705 fn retire_and_commit_rejects_unknown_key_before_commit() {
706 let mut store = LedgerCommitStore::default();
707 store.commit(&ledger()).expect("initial ledger");
708 let retirement = AllocationRetirement::new(
709 "app.users.v1",
710 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
711 )
712 .expect("retirement");
713
714 let err = AllocationBootstrap::new(&mut store)
715 .retire_and_commit(&retirement, Some(43))
716 .expect_err("unknown key");
717 let recovered = store.recover().expect("recovered");
718
719 assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
720 assert_eq!(recovered.current_generation(), 0);
721 assert_eq!(recovered.ledger().allocation_history().records(), []);
722 }
723}