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 ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
59 where
60 P: AllocationPolicy,
61 {
62 let prior = self.store.recover().map_err(BootstrapError::Ledger)?;
63 self.validate_against(prior, snapshot, policy)
64 }
65
66 pub fn initialize_validate_and_commit<P>(
78 &mut self,
79 genesis: &AllocationLedger,
80 snapshot: DeclarationSnapshot,
81 policy: &P,
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)
91 }
92
93 pub fn reserve_and_commit<P>(
98 &mut self,
99 reservations: &[AllocationDeclaration],
100 policy: &P,
101 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
102 where
103 P: AllocationPolicy,
104 {
105 let prior = self
106 .store
107 .recover()
108 .map_err(BootstrapReservationError::Ledger)?;
109 self.reserve_against(prior.into_ledger(), reservations, policy)
110 }
111
112 pub fn initialize_reserve_and_commit<P>(
120 &mut self,
121 genesis: &AllocationLedger,
122 reservations: &[AllocationDeclaration],
123 policy: &P,
124 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
125 where
126 P: AllocationPolicy,
127 {
128 let prior = self
129 .store
130 .recover_or_initialize(genesis)
131 .map_err(BootstrapReservationError::Ledger)?;
132 self.reserve_against(prior.into_ledger(), reservations, policy)
133 }
134
135 pub fn retire_and_commit(
137 &mut self,
138 retirement: &AllocationRetirement,
139 ) -> Result<AllocationLedger, BootstrapRetirementError> {
140 let prior = self
141 .store
142 .recover()
143 .map_err(BootstrapRetirementError::Ledger)?;
144 let staged = stage_retirement_generation(Cow::Owned(prior.into_ledger()), retirement)
145 .map_err(BootstrapRetirementError::Retirement)?;
146 self.store
147 .commit_generation(&staged)
148 .map_err(BootstrapRetirementError::Ledger)?;
149 Ok(staged)
150 }
151
152 fn reserve_against<P>(
153 &mut self,
154 prior: AllocationLedger,
155 reservations: &[AllocationDeclaration],
156 policy: &P,
157 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
158 where
159 P: AllocationPolicy,
160 {
161 checked_reservation_count(reservations.len())
162 .map_err(BootstrapReservationError::Reservation)?;
163 for reservation in reservations {
164 policy
165 .validate_key(&reservation.stable_key)
166 .map_err(BootstrapReservationError::Policy)?;
167 policy
168 .validate_reserved_slot(&reservation.stable_key, &reservation.slot)
169 .map_err(BootstrapReservationError::Policy)?;
170 }
171
172 let staged = stage_reservation_generation(Cow::Owned(prior), reservations)
173 .map_err(BootstrapReservationError::Reservation)?;
174 self.store
175 .commit_generation(&staged)
176 .map_err(BootstrapReservationError::Ledger)?;
177 Ok(staged)
178 }
179
180 pub(crate) fn validate_against<P>(
181 &mut self,
182 prior: crate::RecoveredLedger,
183 snapshot: DeclarationSnapshot,
184 policy: &P,
185 ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
186 where
187 P: AllocationPolicy,
188 {
189 let validated =
190 validate_allocations(&prior, snapshot, policy).map_err(BootstrapError::Validation)?;
191 let staged = stage_validated_generation(Cow::Owned(prior.into_ledger()), &validated)
192 .map_err(BootstrapError::Staging)?;
193 self.store
194 .commit_generation(&staged)
195 .map_err(BootstrapError::Ledger)?;
196
197 Ok(PendingBootstrapCommit {
198 validated,
199 ledger: staged,
200 })
201 }
202}
203
204#[derive(Debug, Eq, PartialEq)]
216#[must_use = "persist the owning ledger record, then confirm_persisted before opening allocations"]
217pub struct PendingBootstrapCommit {
218 ledger: AllocationLedger,
220 validated: ValidatedAllocations,
222}
223
224impl PendingBootstrapCommit {
225 #[must_use]
231 pub const fn ledger(&self) -> &AllocationLedger {
232 &self.ledger
233 }
234
235 #[must_use]
237 pub const fn validated(&self) -> &ValidatedAllocations {
238 &self.validated
239 }
240
241 #[must_use]
247 pub fn confirm_persisted(self) -> CommittedAllocations {
248 self.validated
249 .confirm_persisted(self.ledger.current_generation())
250 }
251}
252
253#[non_exhaustive]
258#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
259pub enum BootstrapError<P> {
260 #[error(transparent)]
262 Ledger(LedgerCommitError),
263 #[error(transparent)]
265 Validation(AllocationValidationError<P>),
266 #[error(transparent)]
268 Staging(AllocationStageError),
269}
270
271#[non_exhaustive]
276#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
277pub enum BootstrapReservationError<P> {
278 #[error(transparent)]
280 Ledger(LedgerCommitError),
281 #[error("allocation policy rejected a reservation")]
283 Policy(P),
284 #[error(transparent)]
286 Reservation(AllocationReservationError),
287}
288
289#[non_exhaustive]
294#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
295pub enum BootstrapRetirementError {
296 #[error(transparent)]
298 Ledger(LedgerCommitError),
299 #[error(transparent)]
301 Retirement(AllocationRetirementError),
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use crate::{
308 declaration::AllocationDeclaration,
309 ledger::{AllocationLedger, AllocationState},
310 schema::SchemaMetadata,
311 slot::MemoryManagerSlot,
312 };
313
314 #[derive(Debug, Eq, PartialEq)]
315 struct TestPolicy;
316
317 impl AllocationPolicy for TestPolicy {
318 type Error = &'static str;
319
320 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
321 Ok(())
322 }
323
324 fn validate_slot(
325 &self,
326 _key: &crate::StableKey,
327 _slot: &MemoryManagerSlot,
328 ) -> Result<(), Self::Error> {
329 Ok(())
330 }
331
332 fn validate_reserved_slot(
333 &self,
334 _key: &crate::StableKey,
335 _slot: &MemoryManagerSlot,
336 ) -> Result<(), Self::Error> {
337 Ok(())
338 }
339 }
340
341 #[derive(Debug, Eq, PartialEq)]
342 struct RejectReservedPolicy;
343
344 impl AllocationPolicy for RejectReservedPolicy {
345 type Error = &'static str;
346
347 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
348 Ok(())
349 }
350
351 fn validate_slot(
352 &self,
353 _key: &crate::StableKey,
354 _slot: &MemoryManagerSlot,
355 ) -> Result<(), Self::Error> {
356 Ok(())
357 }
358
359 fn validate_reserved_slot(
360 &self,
361 _key: &crate::StableKey,
362 _slot: &MemoryManagerSlot,
363 ) -> Result<(), Self::Error> {
364 Err("reserved slot rejected")
365 }
366 }
367
368 #[derive(Debug, Eq, PartialEq)]
369 struct RejectActivePolicy;
370
371 impl AllocationPolicy for RejectActivePolicy {
372 type Error = &'static str;
373
374 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
375 Ok(())
376 }
377
378 fn validate_slot(
379 &self,
380 _key: &crate::StableKey,
381 _slot: &MemoryManagerSlot,
382 ) -> Result<(), Self::Error> {
383 Err("active slot rejected")
384 }
385
386 fn validate_reserved_slot(
387 &self,
388 _key: &crate::StableKey,
389 _slot: &MemoryManagerSlot,
390 ) -> Result<(), Self::Error> {
391 Ok(())
392 }
393 }
394
395 struct PolicyMustNotRun;
396
397 impl AllocationPolicy for PolicyMustNotRun {
398 type Error = &'static str;
399
400 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
401 panic!("policy received an invalid reservation")
402 }
403
404 fn validate_slot(
405 &self,
406 _key: &crate::StableKey,
407 _slot: &MemoryManagerSlot,
408 ) -> Result<(), Self::Error> {
409 panic!("policy received an invalid reservation")
410 }
411
412 fn validate_reserved_slot(
413 &self,
414 _key: &crate::StableKey,
415 _slot: &MemoryManagerSlot,
416 ) -> Result<(), Self::Error> {
417 panic!("policy received an invalid reservation")
418 }
419 }
420
421 fn ledger() -> AllocationLedger {
422 AllocationLedger {
423 current_generation: 0,
424 records: Vec::new(),
425 }
426 }
427
428 fn declaration() -> AllocationDeclaration {
429 AllocationDeclaration::new(
430 "app.users.v1",
431 MemoryManagerSlot::new(100).expect("usable slot"),
432 None,
433 SchemaMetadata::default(),
434 )
435 .expect("declaration")
436 }
437
438 #[test]
439 fn validate_and_commit_publishes_committed_generation() {
440 let mut store = LedgerCommitStore::default();
441 store.commit(&ledger()).expect("initial ledger");
442 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
443
444 let commit = AllocationBootstrap::new(&mut store)
445 .validate_and_commit(snapshot, &TestPolicy)
446 .expect("bootstrap commit");
447
448 assert_eq!(commit.ledger().current_generation, 1);
449 assert_eq!(commit.ledger().records().len(), 1);
450
451 assert_eq!(store.recover().unwrap().ledger(), commit.ledger());
452 assert_eq!(commit.confirm_persisted().generation(), 1);
453 }
454
455 #[test]
456 fn initialize_validate_and_commit_seeds_empty_ledger_store() {
457 let mut store = LedgerCommitStore::default();
458 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
459
460 let commit = AllocationBootstrap::new(&mut store)
461 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy)
462 .expect("bootstrap commit");
463
464 assert_eq!(commit.ledger().current_generation, 1);
465 assert_eq!(commit.ledger().records().len(), 1);
466 assert_eq!(commit.confirm_persisted().generation(), 1);
467 }
468
469 #[test]
470 fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
471 let mut store = LedgerCommitStore::default();
472 store
473 .write_corrupt_inactive_ledger(&ledger())
474 .expect("corrupt ledger");
475 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
476
477 let err = AllocationBootstrap::new(&mut store)
478 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy)
479 .expect_err("corrupt state");
480
481 assert!(matches!(err, BootstrapError::Ledger(_)));
482 }
483
484 #[test]
485 fn reserve_and_commit_policy_checks_and_commits_reservation() {
486 let mut store = LedgerCommitStore::default();
487 store.commit(&ledger()).expect("initial ledger");
488 let reservation = declaration();
489
490 let committed = AllocationBootstrap::new(&mut store)
491 .reserve_and_commit(&[reservation], &TestPolicy)
492 .expect("reservation commit");
493
494 assert_eq!(committed.current_generation, 1);
495 assert_eq!(committed.records().len(), 1);
496 assert_eq!(committed.records()[0].state(), AllocationState::Reserved);
497 assert_eq!(store.recover().unwrap().ledger(), &committed);
498
499 let reservations = [
503 AllocationDeclaration::memory_manager_unlabeled("app.future.v1", 101).unwrap(),
504 AllocationDeclaration::memory_manager_unlabeled("app.users.v1", 102).unwrap(),
505 ];
506 let before = store.clone();
507 let expected = committed
508 .stage_reservation_generation(&reservations)
509 .unwrap_err();
510 assert!(matches!(
511 expected,
512 AllocationReservationError::StableKeySlotConflict { .. }
513 ));
514 assert_eq!(committed, *store.recover().unwrap().ledger());
515 let error = AllocationBootstrap::new(&mut store)
516 .reserve_and_commit(&reservations, &TestPolicy)
517 .unwrap_err();
518 assert_eq!(error, BootstrapReservationError::Reservation(expected));
519 assert_eq!(store, before);
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)
529 .expect("reservation commit");
530
531 assert_eq!(committed.current_generation, 1);
532 assert_eq!(committed.records()[0].state(), AllocationState::Reserved);
533 }
534
535 #[test]
536 fn reserve_and_commit_rejects_policy_failure_before_commit() {
537 let mut store = LedgerCommitStore::default();
538 store.commit(&ledger()).expect("initial ledger");
539 let reservation = declaration();
540
541 let err = AllocationBootstrap::new(&mut store)
542 .reserve_and_commit(&[reservation], &RejectReservedPolicy)
543 .expect_err("policy failure");
544 let recovered = store.recover().expect("recovered");
545
546 assert!(matches!(err, BootstrapReservationError::Policy(_)));
547 assert_eq!(recovered.current_generation(), 0);
548 assert_eq!(recovered.ledger().records(), []);
549 }
550
551 #[test]
552 fn reservation_pipeline_accepts_empty_and_full_slot_domain_batches() {
553 for count in [0_u8, 255] {
554 let reservations = (0..count)
555 .map(|id| {
556 AllocationDeclaration::memory_manager_unlabeled(
557 format!("app.future{id}.v1"),
558 id,
559 )
560 .expect("reservation")
561 })
562 .collect::<Vec<_>>();
563 let mut store = LedgerCommitStore::default();
564 store.commit(&ledger()).expect("initial ledger");
565
566 let committed = AllocationBootstrap::new(&mut store)
567 .reserve_and_commit(&reservations, &TestPolicy)
568 .expect("bounded batch commits");
569
570 assert_eq!(committed.current_generation(), 1);
571 assert_eq!(committed.records().len(), usize::from(count));
572
573 assert_eq!(store.recover().unwrap().ledger(), &committed);
574 }
575 }
576
577 #[test]
578 fn oversized_reservations_reject_before_policy_and_preserve_existing_store() {
579 let reservations = vec![declaration(); 256];
580 for initialize in [false, true] {
581 let mut store = LedgerCommitStore::default();
582 store.commit(&ledger()).expect("initial ledger");
583 let before = store.clone();
584 let mut bootstrap = AllocationBootstrap::new(&mut store);
585 let error = if initialize {
586 bootstrap.initialize_reserve_and_commit(&ledger(), &reservations, &PolicyMustNotRun)
587 } else {
588 bootstrap.reserve_and_commit(&reservations, &PolicyMustNotRun)
589 }
590 .expect_err("oversized batch must fail before policy");
591
592 assert_eq!(
593 error,
594 BootstrapReservationError::Reservation(
595 AllocationReservationError::TooManyReservations { count: 256 }
596 )
597 );
598 assert_eq!(store, before);
599 }
600 }
601
602 #[test]
603 fn oversized_initial_reservations_preserve_genesis_before_policy() {
604 let reservations = vec![declaration(); 256];
605 let mut store = LedgerCommitStore::default();
606 let mut expected = LedgerCommitStore::default();
607 expected.commit(&ledger()).expect("expected genesis");
608
609 let error = AllocationBootstrap::new(&mut store)
610 .initialize_reserve_and_commit(&ledger(), &reservations, &PolicyMustNotRun)
611 .expect_err("size rejection precedes policy checks");
612
613 assert_eq!(
614 error,
615 BootstrapReservationError::Reservation(
616 AllocationReservationError::TooManyReservations { count: 256 }
617 )
618 );
619 assert_eq!(store, expected);
620 }
621
622 #[test]
623 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
624 let mut store = LedgerCommitStore::default();
625 store.commit(&ledger()).expect("initial ledger");
626 let reservation = declaration();
627 AllocationBootstrap::new(&mut store)
628 .reserve_and_commit(&[reservation], &TestPolicy)
629 .expect("reservation commit");
630 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
631
632 let err = AllocationBootstrap::new(&mut store)
633 .validate_and_commit(snapshot, &RejectActivePolicy)
634 .expect_err("active validation must run");
635 let recovered = store.recover().expect("recovered");
636
637 assert!(matches!(
638 err,
639 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
640 ));
641 assert_eq!(
642 recovered.ledger().records()[0].state(),
643 AllocationState::Reserved
644 );
645 }
646
647 #[test]
648 fn retire_and_commit_tombstones_through_protected_commit() {
649 let mut store = LedgerCommitStore::default();
650 store.commit(&ledger()).expect("initial ledger");
651 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
652 let _pending = AllocationBootstrap::new(&mut store)
653 .validate_and_commit(snapshot, &TestPolicy)
654 .expect("active commit");
655 let retirement = AllocationRetirement::new(
656 "app.users.v1",
657 MemoryManagerSlot::new(100).expect("usable slot"),
658 )
659 .expect("retirement");
660
661 let committed = AllocationBootstrap::new(&mut store)
662 .retire_and_commit(&retirement)
663 .expect("retirement commit");
664
665 assert_eq!(committed.current_generation, 2);
666 assert_eq!(committed.records()[0].state(), AllocationState::Retired);
667 assert_eq!(store.recover().unwrap().ledger(), &committed);
668 }
669
670 #[test]
671 fn retire_and_commit_rejects_unknown_key_before_commit() {
672 let mut store = LedgerCommitStore::default();
673 store.commit(&ledger()).expect("initial ledger");
674 let retirement = AllocationRetirement::new(
675 "app.users.v1",
676 MemoryManagerSlot::new(100).expect("usable slot"),
677 )
678 .expect("retirement");
679
680 let err = AllocationBootstrap::new(&mut store)
681 .retire_and_commit(&retirement)
682 .expect_err("unknown key");
683 let recovered = store.recover().expect("recovered");
684
685 assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
686 assert_eq!(recovered.current_generation(), 0);
687 assert_eq!(recovered.ledger().records(), []);
688 }
689}