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_generation(&staged)
178 .map_err(BootstrapReservationError::Ledger)?;
179 Ok(staged)
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_generation(&staged)
193 .map_err(BootstrapRetirementError::Ledger)?;
194 Ok(staged)
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 self.store
214 .commit_generation(&staged)
215 .map_err(BootstrapError::Ledger)?;
216
217 Ok(PendingBootstrapCommit {
218 validated,
219 ledger: staged,
220 })
221 }
222}
223
224#[derive(Debug, Eq, PartialEq)]
236pub struct PendingBootstrapCommit {
237 ledger: AllocationLedger,
239 validated: ValidatedAllocations,
241}
242
243impl PendingBootstrapCommit {
244 #[must_use]
250 pub const fn ledger(&self) -> &AllocationLedger {
251 &self.ledger
252 }
253
254 #[must_use]
256 pub const fn validated(&self) -> &ValidatedAllocations {
257 &self.validated
258 }
259
260 #[must_use]
266 pub fn confirm_persisted(self) -> CommittedAllocations {
267 self.validated
268 .confirm_persisted(self.ledger.current_generation())
269 }
270}
271
272#[non_exhaustive]
277#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
278pub enum BootstrapError<P> {
279 #[error(transparent)]
281 Ledger(LedgerCommitError),
282 #[error(transparent)]
284 Validation(AllocationValidationError<P>),
285 #[error(transparent)]
287 Staging(AllocationStageError),
288}
289
290#[non_exhaustive]
295#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
296pub enum BootstrapReservationError<P> {
297 #[error(transparent)]
299 Ledger(LedgerCommitError),
300 #[error("allocation policy rejected a reservation")]
302 Policy(P),
303 #[error(transparent)]
305 Reservation(AllocationReservationError),
306}
307
308#[non_exhaustive]
313#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
314pub enum BootstrapRetirementError {
315 #[error(transparent)]
317 Ledger(LedgerCommitError),
318 #[error(transparent)]
320 Retirement(AllocationRetirementError),
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use crate::{
327 declaration::AllocationDeclaration,
328 ledger::{AllocationHistory, AllocationLedger, AllocationState},
329 schema::SchemaMetadata,
330 slot::AllocationSlotDescriptor,
331 };
332
333 #[derive(Debug, Eq, PartialEq)]
334 struct TestPolicy;
335
336 impl AllocationPolicy for TestPolicy {
337 type Error = &'static str;
338
339 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
340 Ok(())
341 }
342
343 fn validate_slot(
344 &self,
345 _key: &crate::StableKey,
346 _slot: &AllocationSlotDescriptor,
347 ) -> Result<(), Self::Error> {
348 Ok(())
349 }
350
351 fn validate_reserved_slot(
352 &self,
353 _key: &crate::StableKey,
354 _slot: &AllocationSlotDescriptor,
355 ) -> Result<(), Self::Error> {
356 Ok(())
357 }
358 }
359
360 #[derive(Debug, Eq, PartialEq)]
361 struct RejectReservedPolicy;
362
363 impl AllocationPolicy for RejectReservedPolicy {
364 type Error = &'static str;
365
366 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
367 Ok(())
368 }
369
370 fn validate_slot(
371 &self,
372 _key: &crate::StableKey,
373 _slot: &AllocationSlotDescriptor,
374 ) -> Result<(), Self::Error> {
375 Ok(())
376 }
377
378 fn validate_reserved_slot(
379 &self,
380 _key: &crate::StableKey,
381 _slot: &AllocationSlotDescriptor,
382 ) -> Result<(), Self::Error> {
383 Err("reserved slot rejected")
384 }
385 }
386
387 #[derive(Debug, Eq, PartialEq)]
388 struct RejectActivePolicy;
389
390 impl AllocationPolicy for RejectActivePolicy {
391 type Error = &'static str;
392
393 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
394 Ok(())
395 }
396
397 fn validate_slot(
398 &self,
399 _key: &crate::StableKey,
400 _slot: &AllocationSlotDescriptor,
401 ) -> Result<(), Self::Error> {
402 Err("active slot rejected")
403 }
404
405 fn validate_reserved_slot(
406 &self,
407 _key: &crate::StableKey,
408 _slot: &AllocationSlotDescriptor,
409 ) -> Result<(), Self::Error> {
410 Ok(())
411 }
412 }
413
414 struct PolicyMustNotRun;
415
416 impl AllocationPolicy for PolicyMustNotRun {
417 type Error = &'static str;
418
419 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
420 panic!("policy received an invalid reservation")
421 }
422
423 fn validate_slot(
424 &self,
425 _key: &crate::StableKey,
426 _slot: &AllocationSlotDescriptor,
427 ) -> Result<(), Self::Error> {
428 panic!("policy received an invalid reservation")
429 }
430
431 fn validate_reserved_slot(
432 &self,
433 _key: &crate::StableKey,
434 _slot: &AllocationSlotDescriptor,
435 ) -> Result<(), Self::Error> {
436 panic!("policy received an invalid reservation")
437 }
438 }
439
440 fn ledger() -> AllocationLedger {
441 AllocationLedger {
442 current_generation: 0,
443 allocation_history: AllocationHistory::default(),
444 }
445 }
446
447 fn declaration() -> AllocationDeclaration {
448 AllocationDeclaration::new(
449 "app.users.v1",
450 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
451 None,
452 SchemaMetadata::default(),
453 )
454 .expect("declaration")
455 }
456
457 #[test]
458 fn validate_and_commit_publishes_committed_generation() {
459 let mut store = LedgerCommitStore::default();
460 store.commit(&ledger()).expect("initial ledger");
461 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
462
463 let commit = AllocationBootstrap::new(&mut store)
464 .validate_and_commit(snapshot, &TestPolicy, Some(42))
465 .expect("bootstrap commit");
466
467 assert_eq!(commit.ledger().current_generation, 1);
468 assert_eq!(commit.ledger().allocation_history.records().len(), 1);
469 assert_eq!(commit.ledger().allocation_history.generations().len(), 1);
470 assert_eq!(store.recover().unwrap().ledger(), commit.ledger());
471 assert_eq!(commit.confirm_persisted().generation(), 1);
472 }
473
474 #[test]
475 fn initialize_validate_and_commit_seeds_empty_ledger_store() {
476 let mut store = LedgerCommitStore::default();
477 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
478
479 let commit = AllocationBootstrap::new(&mut store)
480 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
481 .expect("bootstrap commit");
482
483 assert_eq!(commit.ledger().current_generation, 1);
484 assert_eq!(commit.ledger().allocation_history.records().len(), 1);
485 assert_eq!(commit.confirm_persisted().generation(), 1);
486 }
487
488 #[test]
489 fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
490 let mut store = LedgerCommitStore::default();
491 store
492 .write_corrupt_inactive_ledger(&ledger())
493 .expect("corrupt ledger");
494 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
495
496 let err = AllocationBootstrap::new(&mut store)
497 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
498 .expect_err("corrupt state");
499
500 assert!(matches!(err, BootstrapError::Ledger(_)));
501 }
502
503 #[test]
504 fn reserve_and_commit_policy_checks_and_commits_reservation() {
505 let mut store = LedgerCommitStore::default();
506 store.commit(&ledger()).expect("initial ledger");
507 let reservation = declaration();
508
509 let committed = AllocationBootstrap::new(&mut store)
510 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
511 .expect("reservation commit");
512
513 assert_eq!(committed.current_generation, 1);
514 assert_eq!(committed.allocation_history.records().len(), 1);
515 assert_eq!(
516 committed.allocation_history.records()[0].state(),
517 AllocationState::Reserved
518 );
519 assert_eq!(store.recover().unwrap().ledger(), &committed);
520 }
521
522 #[test]
523 fn initialize_reserve_and_commit_seeds_empty_store() {
524 let mut store = LedgerCommitStore::default();
525 let reservation = declaration();
526
527 let committed = AllocationBootstrap::new(&mut store)
528 .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
529 .expect("reservation commit");
530
531 assert_eq!(committed.current_generation, 1);
532 assert_eq!(
533 committed.allocation_history.records()[0].state(),
534 AllocationState::Reserved
535 );
536 }
537
538 #[test]
539 fn reserve_and_commit_rejects_policy_failure_before_commit() {
540 let mut store = LedgerCommitStore::default();
541 store.commit(&ledger()).expect("initial ledger");
542 let reservation = declaration();
543
544 let err = AllocationBootstrap::new(&mut store)
545 .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
546 .expect_err("policy failure");
547 let recovered = store.recover().expect("recovered");
548
549 assert!(matches!(err, BootstrapReservationError::Policy(_)));
550 assert_eq!(recovered.current_generation(), 0);
551 assert_eq!(recovered.ledger().allocation_history().records(), []);
552 }
553
554 #[test]
555 fn reserve_and_commit_validates_reservation_before_policy() {
556 let mut store = LedgerCommitStore::default();
557 store.commit(&ledger()).expect("initial ledger");
558 let mut reservation = declaration();
559 reservation.slot =
560 AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
561
562 let err = AllocationBootstrap::new(&mut store)
563 .reserve_and_commit(&[reservation], &PolicyMustNotRun, Some(42))
564 .expect_err("invalid reservation must fail before policy");
565
566 assert!(matches!(
567 err,
568 BootstrapReservationError::Reservation(AllocationReservationError::InvalidDeclaration(
569 _
570 ))
571 ));
572 }
573
574 #[test]
575 fn reservation_pipeline_accepts_empty_and_full_slot_domain_batches() {
576 for count in [0_u8, 255] {
577 let reservations = (0..count)
578 .map(|id| {
579 AllocationDeclaration::memory_manager_unlabeled(
580 format!("app.future{id}.v1"),
581 id,
582 )
583 .expect("reservation")
584 })
585 .collect::<Vec<_>>();
586 let mut store = LedgerCommitStore::default();
587 store.commit(&ledger()).expect("initial ledger");
588
589 let committed = AllocationBootstrap::new(&mut store)
590 .reserve_and_commit(&reservations, &TestPolicy, Some(42))
591 .expect("bounded batch commits");
592
593 assert_eq!(committed.current_generation(), 1);
594 assert_eq!(
595 committed.allocation_history().records().len(),
596 usize::from(count)
597 );
598 assert_eq!(
599 committed.allocation_history().generations()[0].declaration_count(),
600 u32::from(count)
601 );
602 assert_eq!(store.recover().unwrap().ledger(), &committed);
603 }
604 }
605
606 #[test]
607 fn oversized_reservations_reject_before_policy_and_preserve_existing_store() {
608 let reservations = vec![declaration(); 256];
609 for initialize in [false, true] {
610 let mut store = LedgerCommitStore::default();
611 store.commit(&ledger()).expect("initial ledger");
612 let before = store.clone();
613 let mut bootstrap = AllocationBootstrap::new(&mut store);
614 let error = if initialize {
615 bootstrap.initialize_reserve_and_commit(
616 &ledger(),
617 &reservations,
618 &PolicyMustNotRun,
619 None,
620 )
621 } else {
622 bootstrap.reserve_and_commit(&reservations, &PolicyMustNotRun, None)
623 }
624 .expect_err("oversized batch must fail before policy");
625
626 assert_eq!(
627 error,
628 BootstrapReservationError::Reservation(
629 AllocationReservationError::TooManyReservations { count: 256 }
630 )
631 );
632 assert_eq!(store, before);
633 }
634 }
635
636 #[test]
637 fn oversized_initial_reservations_preserve_genesis_and_precede_invalid_declarations() {
638 let mut reservations = vec![declaration(); 256];
639 reservations[0].slot =
640 AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
641 let mut store = LedgerCommitStore::default();
642 let mut expected = LedgerCommitStore::default();
643 expected.commit(&ledger()).expect("expected genesis");
644
645 let error = AllocationBootstrap::new(&mut store)
646 .initialize_reserve_and_commit(&ledger(), &reservations, &PolicyMustNotRun, None)
647 .expect_err("size rejection precedes declaration and policy checks");
648
649 assert_eq!(
650 error,
651 BootstrapReservationError::Reservation(
652 AllocationReservationError::TooManyReservations { count: 256 }
653 )
654 );
655 assert_eq!(store, expected);
656 }
657
658 #[test]
659 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
660 let mut store = LedgerCommitStore::default();
661 store.commit(&ledger()).expect("initial ledger");
662 let reservation = declaration();
663 AllocationBootstrap::new(&mut store)
664 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
665 .expect("reservation commit");
666 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
667
668 let err = AllocationBootstrap::new(&mut store)
669 .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
670 .expect_err("active validation must run");
671 let recovered = store.recover().expect("recovered");
672
673 assert!(matches!(
674 err,
675 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
676 ));
677 assert_eq!(
678 recovered.ledger().allocation_history().records()[0].state(),
679 AllocationState::Reserved
680 );
681 }
682
683 #[test]
684 fn retire_and_commit_tombstones_through_protected_commit() {
685 let mut store = LedgerCommitStore::default();
686 store.commit(&ledger()).expect("initial ledger");
687 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
688 AllocationBootstrap::new(&mut store)
689 .validate_and_commit(snapshot, &TestPolicy, Some(42))
690 .expect("active commit");
691 let retirement = AllocationRetirement::new(
692 "app.users.v1",
693 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
694 )
695 .expect("retirement");
696
697 let committed = AllocationBootstrap::new(&mut store)
698 .retire_and_commit(&retirement, Some(43))
699 .expect("retirement commit");
700
701 assert_eq!(committed.current_generation, 2);
702 assert_eq!(
703 committed.allocation_history.records()[0].state(),
704 AllocationState::Retired { generation: 2 }
705 );
706 assert_eq!(store.recover().unwrap().ledger(), &committed);
707 }
708
709 #[test]
710 fn retire_and_commit_rejects_unknown_key_before_commit() {
711 let mut store = LedgerCommitStore::default();
712 store.commit(&ledger()).expect("initial ledger");
713 let retirement = AllocationRetirement::new(
714 "app.users.v1",
715 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
716 )
717 .expect("retirement");
718
719 let err = AllocationBootstrap::new(&mut store)
720 .retire_and_commit(&retirement, Some(43))
721 .expect_err("unknown key");
722 let recovered = store.recover().expect("recovered");
723
724 assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
725 assert_eq!(recovered.current_generation(), 0);
726 assert_eq!(recovered.ledger().allocation_history().records(), []);
727 }
728}