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