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