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)]
225#[must_use = "persist the owning ledger record, then confirm_persisted before opening allocations"]
226pub struct PendingBootstrapCommit {
227 ledger: AllocationLedger,
229 validated: ValidatedAllocations,
231}
232
233impl PendingBootstrapCommit {
234 #[must_use]
240 pub const fn ledger(&self) -> &AllocationLedger {
241 &self.ledger
242 }
243
244 #[must_use]
246 pub const fn validated(&self) -> &ValidatedAllocations {
247 &self.validated
248 }
249
250 #[must_use]
256 pub fn confirm_persisted(self) -> CommittedAllocations {
257 self.validated
258 .confirm_persisted(self.ledger.current_generation())
259 }
260}
261
262#[non_exhaustive]
267#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
268pub enum BootstrapError<P> {
269 #[error(transparent)]
271 Ledger(LedgerCommitError),
272 #[error(transparent)]
274 Validation(AllocationValidationError<P>),
275 #[error(transparent)]
277 Staging(AllocationStageError),
278}
279
280#[non_exhaustive]
285#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
286pub enum BootstrapReservationError<P> {
287 #[error(transparent)]
289 Ledger(LedgerCommitError),
290 #[error("allocation policy rejected a reservation")]
292 Policy(P),
293 #[error(transparent)]
295 Reservation(AllocationReservationError),
296}
297
298#[non_exhaustive]
303#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
304pub enum BootstrapRetirementError {
305 #[error(transparent)]
307 Ledger(LedgerCommitError),
308 #[error(transparent)]
310 Retirement(AllocationRetirementError),
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use crate::{
317 declaration::AllocationDeclaration,
318 ledger::{AllocationHistory, AllocationLedger, AllocationState},
319 schema::SchemaMetadata,
320 slot::MemoryManagerSlot,
321 };
322
323 #[derive(Debug, Eq, PartialEq)]
324 struct TestPolicy;
325
326 impl AllocationPolicy for TestPolicy {
327 type Error = &'static str;
328
329 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
330 Ok(())
331 }
332
333 fn validate_slot(
334 &self,
335 _key: &crate::StableKey,
336 _slot: &MemoryManagerSlot,
337 ) -> Result<(), Self::Error> {
338 Ok(())
339 }
340
341 fn validate_reserved_slot(
342 &self,
343 _key: &crate::StableKey,
344 _slot: &MemoryManagerSlot,
345 ) -> Result<(), Self::Error> {
346 Ok(())
347 }
348 }
349
350 #[derive(Debug, Eq, PartialEq)]
351 struct RejectReservedPolicy;
352
353 impl AllocationPolicy for RejectReservedPolicy {
354 type Error = &'static str;
355
356 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
357 Ok(())
358 }
359
360 fn validate_slot(
361 &self,
362 _key: &crate::StableKey,
363 _slot: &MemoryManagerSlot,
364 ) -> Result<(), Self::Error> {
365 Ok(())
366 }
367
368 fn validate_reserved_slot(
369 &self,
370 _key: &crate::StableKey,
371 _slot: &MemoryManagerSlot,
372 ) -> Result<(), Self::Error> {
373 Err("reserved slot rejected")
374 }
375 }
376
377 #[derive(Debug, Eq, PartialEq)]
378 struct RejectActivePolicy;
379
380 impl AllocationPolicy for RejectActivePolicy {
381 type Error = &'static str;
382
383 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
384 Ok(())
385 }
386
387 fn validate_slot(
388 &self,
389 _key: &crate::StableKey,
390 _slot: &MemoryManagerSlot,
391 ) -> Result<(), Self::Error> {
392 Err("active slot rejected")
393 }
394
395 fn validate_reserved_slot(
396 &self,
397 _key: &crate::StableKey,
398 _slot: &MemoryManagerSlot,
399 ) -> Result<(), Self::Error> {
400 Ok(())
401 }
402 }
403
404 struct PolicyMustNotRun;
405
406 impl AllocationPolicy for PolicyMustNotRun {
407 type Error = &'static str;
408
409 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
410 panic!("policy received an invalid reservation")
411 }
412
413 fn validate_slot(
414 &self,
415 _key: &crate::StableKey,
416 _slot: &MemoryManagerSlot,
417 ) -> Result<(), Self::Error> {
418 panic!("policy received an invalid reservation")
419 }
420
421 fn validate_reserved_slot(
422 &self,
423 _key: &crate::StableKey,
424 _slot: &MemoryManagerSlot,
425 ) -> Result<(), Self::Error> {
426 panic!("policy received an invalid reservation")
427 }
428 }
429
430 fn ledger() -> AllocationLedger {
431 AllocationLedger {
432 current_generation: 0,
433 allocation_history: AllocationHistory::default(),
434 }
435 }
436
437 fn declaration() -> AllocationDeclaration {
438 AllocationDeclaration::new(
439 "app.users.v1",
440 MemoryManagerSlot::new(100).expect("usable slot"),
441 None,
442 SchemaMetadata::default(),
443 )
444 .expect("declaration")
445 }
446
447 #[test]
448 fn validate_and_commit_publishes_committed_generation() {
449 let mut store = LedgerCommitStore::default();
450 store.commit(&ledger()).expect("initial ledger");
451 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
452
453 let commit = AllocationBootstrap::new(&mut store)
454 .validate_and_commit(snapshot, &TestPolicy, Some(42))
455 .expect("bootstrap commit");
456
457 assert_eq!(commit.ledger().current_generation, 1);
458 assert_eq!(commit.ledger().allocation_history.records().len(), 1);
459 assert_eq!(commit.ledger().allocation_history.generations().len(), 1);
460 assert_eq!(store.recover().unwrap().ledger(), commit.ledger());
461 assert_eq!(commit.confirm_persisted().generation(), 1);
462 }
463
464 #[test]
465 fn initialize_validate_and_commit_seeds_empty_ledger_store() {
466 let mut store = LedgerCommitStore::default();
467 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
468
469 let commit = AllocationBootstrap::new(&mut store)
470 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
471 .expect("bootstrap commit");
472
473 assert_eq!(commit.ledger().current_generation, 1);
474 assert_eq!(commit.ledger().allocation_history.records().len(), 1);
475 assert_eq!(commit.confirm_persisted().generation(), 1);
476 }
477
478 #[test]
479 fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
480 let mut store = LedgerCommitStore::default();
481 store
482 .write_corrupt_inactive_ledger(&ledger())
483 .expect("corrupt ledger");
484 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
485
486 let err = AllocationBootstrap::new(&mut store)
487 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
488 .expect_err("corrupt state");
489
490 assert!(matches!(err, BootstrapError::Ledger(_)));
491 }
492
493 #[test]
494 fn reserve_and_commit_policy_checks_and_commits_reservation() {
495 let mut store = LedgerCommitStore::default();
496 store.commit(&ledger()).expect("initial ledger");
497 let reservation = declaration();
498
499 let committed = AllocationBootstrap::new(&mut store)
500 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
501 .expect("reservation commit");
502
503 assert_eq!(committed.current_generation, 1);
504 assert_eq!(committed.allocation_history.records().len(), 1);
505 assert_eq!(
506 committed.allocation_history.records()[0].state(),
507 AllocationState::Reserved
508 );
509 assert_eq!(store.recover().unwrap().ledger(), &committed);
510
511 let reservations = [
515 AllocationDeclaration::memory_manager_unlabeled("app.future.v1", 101).unwrap(),
516 AllocationDeclaration::memory_manager_unlabeled("app.users.v1", 102).unwrap(),
517 ];
518 let before = store.clone();
519 let expected = committed
520 .stage_reservation_generation(&reservations, None)
521 .unwrap_err();
522 assert!(matches!(
523 expected,
524 AllocationReservationError::StableKeySlotConflict { .. }
525 ));
526 assert_eq!(committed, *store.recover().unwrap().ledger());
527 let error = AllocationBootstrap::new(&mut store)
528 .reserve_and_commit(&reservations, &TestPolicy, None)
529 .unwrap_err();
530 assert_eq!(error, BootstrapReservationError::Reservation(expected));
531 assert_eq!(store, before);
532 }
533
534 #[test]
535 fn initialize_reserve_and_commit_seeds_empty_store() {
536 let mut store = LedgerCommitStore::default();
537 let reservation = declaration();
538
539 let committed = AllocationBootstrap::new(&mut store)
540 .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
541 .expect("reservation commit");
542
543 assert_eq!(committed.current_generation, 1);
544 assert_eq!(
545 committed.allocation_history.records()[0].state(),
546 AllocationState::Reserved
547 );
548 }
549
550 #[test]
551 fn reserve_and_commit_rejects_policy_failure_before_commit() {
552 let mut store = LedgerCommitStore::default();
553 store.commit(&ledger()).expect("initial ledger");
554 let reservation = declaration();
555
556 let err = AllocationBootstrap::new(&mut store)
557 .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
558 .expect_err("policy failure");
559 let recovered = store.recover().expect("recovered");
560
561 assert!(matches!(err, BootstrapReservationError::Policy(_)));
562 assert_eq!(recovered.current_generation(), 0);
563 assert_eq!(recovered.ledger().allocation_history().records(), []);
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_before_policy() {
630 let reservations = vec![declaration(); 256];
631 let mut store = LedgerCommitStore::default();
632 let mut expected = LedgerCommitStore::default();
633 expected.commit(&ledger()).expect("expected genesis");
634
635 let error = AllocationBootstrap::new(&mut store)
636 .initialize_reserve_and_commit(&ledger(), &reservations, &PolicyMustNotRun, None)
637 .expect_err("size rejection precedes policy checks");
638
639 assert_eq!(
640 error,
641 BootstrapReservationError::Reservation(
642 AllocationReservationError::TooManyReservations { count: 256 }
643 )
644 );
645 assert_eq!(store, expected);
646 }
647
648 #[test]
649 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
650 let mut store = LedgerCommitStore::default();
651 store.commit(&ledger()).expect("initial ledger");
652 let reservation = declaration();
653 AllocationBootstrap::new(&mut store)
654 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
655 .expect("reservation commit");
656 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
657
658 let err = AllocationBootstrap::new(&mut store)
659 .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
660 .expect_err("active validation must run");
661 let recovered = store.recover().expect("recovered");
662
663 assert!(matches!(
664 err,
665 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
666 ));
667 assert_eq!(
668 recovered.ledger().allocation_history().records()[0].state(),
669 AllocationState::Reserved
670 );
671 }
672
673 #[test]
674 fn retire_and_commit_tombstones_through_protected_commit() {
675 let mut store = LedgerCommitStore::default();
676 store.commit(&ledger()).expect("initial ledger");
677 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
678 let _pending = AllocationBootstrap::new(&mut store)
679 .validate_and_commit(snapshot, &TestPolicy, Some(42))
680 .expect("active commit");
681 let retirement = AllocationRetirement::new(
682 "app.users.v1",
683 MemoryManagerSlot::new(100).expect("usable slot"),
684 )
685 .expect("retirement");
686
687 let committed = AllocationBootstrap::new(&mut store)
688 .retire_and_commit(&retirement, Some(43))
689 .expect("retirement commit");
690
691 assert_eq!(committed.current_generation, 2);
692 assert_eq!(
693 committed.allocation_history.records()[0].state(),
694 AllocationState::Retired { generation: 2 }
695 );
696 assert_eq!(store.recover().unwrap().ledger(), &committed);
697 }
698
699 #[test]
700 fn retire_and_commit_rejects_unknown_key_before_commit() {
701 let mut store = LedgerCommitStore::default();
702 store.commit(&ledger()).expect("initial ledger");
703 let retirement = AllocationRetirement::new(
704 "app.users.v1",
705 MemoryManagerSlot::new(100).expect("usable slot"),
706 )
707 .expect("retirement");
708
709 let err = AllocationBootstrap::new(&mut store)
710 .retire_and_commit(&retirement, Some(43))
711 .expect_err("unknown key");
712 let recovered = store.recover().expect("recovered");
713
714 assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
715 assert_eq!(recovered.current_generation(), 0);
716 assert_eq!(recovered.ledger().allocation_history().records(), []);
717 }
718}