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