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