1use crate::{
2 capability::{CommittedAllocations, ValidatedAllocations},
3 declaration::AllocationDeclaration,
4 declaration::DeclarationSnapshot,
5 ledger::{
6 AllocationLedger, AllocationReservationError, AllocationRetirement,
7 AllocationRetirementError, AllocationStageError, LedgerCommitError, LedgerCommitStore,
8 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>(
95 &mut self,
96 reservations: &[AllocationDeclaration],
97 policy: &P,
98 committed_at: Option<u64>,
99 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
100 where
101 P: AllocationPolicy,
102 {
103 let prior = self
104 .store
105 .recover()
106 .map_err(BootstrapReservationError::Ledger)?;
107 self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
108 }
109
110 pub fn initialize_reserve_and_commit<P>(
116 &mut self,
117 genesis: &AllocationLedger,
118 reservations: &[AllocationDeclaration],
119 policy: &P,
120 committed_at: Option<u64>,
121 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
122 where
123 P: AllocationPolicy,
124 {
125 let prior = self
126 .store
127 .recover_or_initialize(genesis)
128 .map_err(BootstrapReservationError::Ledger)?;
129 self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
130 }
131
132 pub fn retire_and_commit(
134 &mut self,
135 retirement: &AllocationRetirement,
136 committed_at: Option<u64>,
137 ) -> Result<AllocationLedger, BootstrapRetirementError> {
138 let prior = self
139 .store
140 .recover()
141 .map_err(BootstrapRetirementError::Ledger)?;
142 self.retire_against(prior.into_ledger(), retirement, committed_at)
143 }
144
145 fn reserve_against<P>(
146 &mut self,
147 prior: AllocationLedger,
148 reservations: &[AllocationDeclaration],
149 policy: &P,
150 committed_at: Option<u64>,
151 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
152 where
153 P: AllocationPolicy,
154 {
155 for reservation in reservations {
156 validate_reservation_declaration(reservation)
157 .map_err(BootstrapReservationError::Reservation)?;
158 policy
159 .validate_key(&reservation.stable_key)
160 .map_err(BootstrapReservationError::Policy)?;
161 policy
162 .validate_reserved_slot(&reservation.stable_key, &reservation.slot)
163 .map_err(BootstrapReservationError::Policy)?;
164 }
165
166 let staged = prior
167 .stage_reservation_generation(reservations, committed_at)
168 .map_err(BootstrapReservationError::Reservation)?;
169 self.store
170 .commit(&staged)
171 .map(crate::RecoveredLedger::into_ledger)
172 .map_err(BootstrapReservationError::Ledger)
173 }
174
175 fn retire_against(
176 &mut self,
177 prior: AllocationLedger,
178 retirement: &AllocationRetirement,
179 committed_at: Option<u64>,
180 ) -> Result<AllocationLedger, BootstrapRetirementError> {
181 let staged = prior
182 .stage_retirement_generation(retirement, committed_at)
183 .map_err(BootstrapRetirementError::Retirement)?;
184 self.store
185 .commit(&staged)
186 .map(crate::RecoveredLedger::into_ledger)
187 .map_err(BootstrapRetirementError::Ledger)
188 }
189
190 fn validate_against<P>(
191 &mut self,
192 prior: crate::RecoveredLedger,
193 snapshot: DeclarationSnapshot,
194 policy: &P,
195 committed_at: Option<u64>,
196 ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
197 where
198 P: AllocationPolicy,
199 {
200 let validated =
201 validate_allocations(&prior, snapshot, policy).map_err(BootstrapError::Validation)?;
202 let prior_ledger = prior.into_ledger();
203 let staged = prior_ledger
204 .stage_validated_generation(&validated, committed_at)
205 .map_err(BootstrapError::Staging)?;
206 let committed = self.store.commit(&staged).map_err(BootstrapError::Ledger)?;
207
208 Ok(PendingBootstrapCommit {
209 validated,
210 ledger: committed.into_ledger(),
211 })
212 }
213}
214
215#[derive(Debug, Eq, PartialEq)]
227pub struct PendingBootstrapCommit {
228 ledger: AllocationLedger,
230 validated: ValidatedAllocations,
232}
233
234impl PendingBootstrapCommit {
235 #[must_use]
241 pub const fn ledger(&self) -> &AllocationLedger {
242 &self.ledger
243 }
244
245 #[must_use]
247 pub const fn validated(&self) -> &ValidatedAllocations {
248 &self.validated
249 }
250
251 #[must_use]
257 pub fn confirm_persisted(self) -> CommittedAllocations {
258 self.validated
259 .confirm_persisted(self.ledger.current_generation())
260 }
261
262 pub(crate) fn into_parts(self) -> (AllocationLedger, ValidatedAllocations) {
263 (self.ledger, self.validated)
264 }
265}
266
267#[non_exhaustive]
272#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
273pub enum BootstrapError<P> {
274 #[error(transparent)]
276 Ledger(LedgerCommitError),
277 #[error(transparent)]
279 Validation(AllocationValidationError<P>),
280 #[error(transparent)]
282 Staging(AllocationStageError),
283}
284
285#[non_exhaustive]
290#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
291pub enum BootstrapReservationError<P> {
292 #[error(transparent)]
294 Ledger(LedgerCommitError),
295 #[error("allocation policy rejected a reservation")]
297 Policy(P),
298 #[error(transparent)]
300 Reservation(AllocationReservationError),
301}
302
303#[non_exhaustive]
308#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
309pub enum BootstrapRetirementError {
310 #[error(transparent)]
312 Ledger(LedgerCommitError),
313 #[error(transparent)]
315 Retirement(AllocationRetirementError),
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use crate::{
322 declaration::AllocationDeclaration,
323 ledger::{AllocationHistory, AllocationLedger, AllocationState},
324 schema::SchemaMetadata,
325 slot::AllocationSlotDescriptor,
326 };
327
328 #[derive(Debug, Eq, PartialEq)]
329 struct TestPolicy;
330
331 impl AllocationPolicy for TestPolicy {
332 type Error = &'static str;
333
334 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
335 Ok(())
336 }
337
338 fn validate_slot(
339 &self,
340 _key: &crate::StableKey,
341 _slot: &AllocationSlotDescriptor,
342 ) -> Result<(), Self::Error> {
343 Ok(())
344 }
345
346 fn validate_reserved_slot(
347 &self,
348 _key: &crate::StableKey,
349 _slot: &AllocationSlotDescriptor,
350 ) -> Result<(), Self::Error> {
351 Ok(())
352 }
353 }
354
355 #[derive(Debug, Eq, PartialEq)]
356 struct RejectReservedPolicy;
357
358 impl AllocationPolicy for RejectReservedPolicy {
359 type Error = &'static str;
360
361 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
362 Ok(())
363 }
364
365 fn validate_slot(
366 &self,
367 _key: &crate::StableKey,
368 _slot: &AllocationSlotDescriptor,
369 ) -> Result<(), Self::Error> {
370 Ok(())
371 }
372
373 fn validate_reserved_slot(
374 &self,
375 _key: &crate::StableKey,
376 _slot: &AllocationSlotDescriptor,
377 ) -> Result<(), Self::Error> {
378 Err("reserved slot rejected")
379 }
380 }
381
382 #[derive(Debug, Eq, PartialEq)]
383 struct RejectActivePolicy;
384
385 impl AllocationPolicy for RejectActivePolicy {
386 type Error = &'static str;
387
388 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
389 Ok(())
390 }
391
392 fn validate_slot(
393 &self,
394 _key: &crate::StableKey,
395 _slot: &AllocationSlotDescriptor,
396 ) -> Result<(), Self::Error> {
397 Err("active slot rejected")
398 }
399
400 fn validate_reserved_slot(
401 &self,
402 _key: &crate::StableKey,
403 _slot: &AllocationSlotDescriptor,
404 ) -> Result<(), Self::Error> {
405 Ok(())
406 }
407 }
408
409 struct PolicyMustNotRun;
410
411 impl AllocationPolicy for PolicyMustNotRun {
412 type Error = &'static str;
413
414 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
415 panic!("policy received an invalid reservation")
416 }
417
418 fn validate_slot(
419 &self,
420 _key: &crate::StableKey,
421 _slot: &AllocationSlotDescriptor,
422 ) -> Result<(), Self::Error> {
423 panic!("policy received an invalid reservation")
424 }
425
426 fn validate_reserved_slot(
427 &self,
428 _key: &crate::StableKey,
429 _slot: &AllocationSlotDescriptor,
430 ) -> Result<(), Self::Error> {
431 panic!("policy received an invalid reservation")
432 }
433 }
434
435 fn ledger() -> AllocationLedger {
436 AllocationLedger {
437 current_generation: 0,
438 allocation_history: AllocationHistory::default(),
439 }
440 }
441
442 fn declaration() -> AllocationDeclaration {
443 AllocationDeclaration::new(
444 "app.users.v1",
445 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
446 None,
447 SchemaMetadata::default(),
448 )
449 .expect("declaration")
450 }
451
452 #[test]
453 fn validate_and_commit_publishes_committed_generation() {
454 let mut store = LedgerCommitStore::default();
455 store.commit(&ledger()).expect("initial ledger");
456 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
457
458 let commit = AllocationBootstrap::new(&mut store)
459 .validate_and_commit(snapshot, &TestPolicy, Some(42))
460 .expect("bootstrap commit");
461
462 assert_eq!(commit.ledger().current_generation, 1);
463 assert_eq!(commit.ledger().allocation_history.records().len(), 1);
464 assert_eq!(commit.ledger().allocation_history.generations().len(), 1);
465 assert_eq!(commit.confirm_persisted().generation(), 1);
466 }
467
468 #[test]
469 fn initialize_validate_and_commit_seeds_empty_ledger_store() {
470 let mut store = LedgerCommitStore::default();
471 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
472
473 let commit = AllocationBootstrap::new(&mut store)
474 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
475 .expect("bootstrap commit");
476
477 assert_eq!(commit.ledger().current_generation, 1);
478 assert_eq!(commit.ledger().allocation_history.records().len(), 1);
479 assert_eq!(commit.confirm_persisted().generation(), 1);
480 }
481
482 #[test]
483 fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
484 let mut store = LedgerCommitStore::default();
485 store
486 .write_corrupt_inactive_ledger(&ledger())
487 .expect("corrupt ledger");
488 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
489
490 let err = AllocationBootstrap::new(&mut store)
491 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
492 .expect_err("corrupt state");
493
494 assert!(matches!(err, BootstrapError::Ledger(_)));
495 }
496
497 #[test]
498 fn reserve_and_commit_policy_checks_and_commits_reservation() {
499 let mut store = LedgerCommitStore::default();
500 store.commit(&ledger()).expect("initial ledger");
501 let reservation = declaration();
502
503 let committed = AllocationBootstrap::new(&mut store)
504 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
505 .expect("reservation commit");
506
507 assert_eq!(committed.current_generation, 1);
508 assert_eq!(committed.allocation_history.records().len(), 1);
509 assert_eq!(
510 committed.allocation_history.records()[0].state(),
511 AllocationState::Reserved
512 );
513 }
514
515 #[test]
516 fn initialize_reserve_and_commit_seeds_empty_store() {
517 let mut store = LedgerCommitStore::default();
518 let reservation = declaration();
519
520 let committed = AllocationBootstrap::new(&mut store)
521 .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
522 .expect("reservation commit");
523
524 assert_eq!(committed.current_generation, 1);
525 assert_eq!(
526 committed.allocation_history.records()[0].state(),
527 AllocationState::Reserved
528 );
529 }
530
531 #[test]
532 fn reserve_and_commit_rejects_policy_failure_before_commit() {
533 let mut store = LedgerCommitStore::default();
534 store.commit(&ledger()).expect("initial ledger");
535 let reservation = declaration();
536
537 let err = AllocationBootstrap::new(&mut store)
538 .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
539 .expect_err("policy failure");
540 let recovered = store.recover().expect("recovered");
541
542 assert!(matches!(err, BootstrapReservationError::Policy(_)));
543 assert_eq!(recovered.current_generation(), 0);
544 assert!(recovered.ledger().allocation_history().records().is_empty());
545 }
546
547 #[test]
548 fn reserve_and_commit_validates_reservation_before_policy() {
549 let mut store = LedgerCommitStore::default();
550 store.commit(&ledger()).expect("initial ledger");
551 let mut reservation = declaration();
552 reservation.slot =
553 AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
554
555 let err = AllocationBootstrap::new(&mut store)
556 .reserve_and_commit(&[reservation], &PolicyMustNotRun, Some(42))
557 .expect_err("invalid reservation must fail before policy");
558
559 assert!(matches!(
560 err,
561 BootstrapReservationError::Reservation(AllocationReservationError::InvalidDeclaration(
562 _
563 ))
564 ));
565 }
566
567 #[test]
568 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
569 let mut store = LedgerCommitStore::default();
570 store.commit(&ledger()).expect("initial ledger");
571 let reservation = declaration();
572 AllocationBootstrap::new(&mut store)
573 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
574 .expect("reservation commit");
575 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
576
577 let err = AllocationBootstrap::new(&mut store)
578 .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
579 .expect_err("active validation must run");
580 let recovered = store.recover().expect("recovered");
581
582 assert!(matches!(
583 err,
584 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
585 ));
586 assert_eq!(
587 recovered.ledger().allocation_history().records()[0].state(),
588 AllocationState::Reserved
589 );
590 }
591
592 #[test]
593 fn retire_and_commit_tombstones_through_protected_commit() {
594 let mut store = LedgerCommitStore::default();
595 store.commit(&ledger()).expect("initial ledger");
596 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
597 AllocationBootstrap::new(&mut store)
598 .validate_and_commit(snapshot, &TestPolicy, Some(42))
599 .expect("active commit");
600 let retirement = AllocationRetirement::new(
601 "app.users.v1",
602 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
603 )
604 .expect("retirement");
605
606 let committed = AllocationBootstrap::new(&mut store)
607 .retire_and_commit(&retirement, Some(43))
608 .expect("retirement commit");
609
610 assert_eq!(committed.current_generation, 2);
611 assert_eq!(
612 committed.allocation_history.records()[0].state(),
613 AllocationState::Retired { generation: 2 }
614 );
615 }
616
617 #[test]
618 fn retire_and_commit_rejects_unknown_key_before_commit() {
619 let mut store = LedgerCommitStore::default();
620 store.commit(&ledger()).expect("initial ledger");
621 let retirement = AllocationRetirement::new(
622 "app.users.v1",
623 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
624 )
625 .expect("retirement");
626
627 let err = AllocationBootstrap::new(&mut store)
628 .retire_and_commit(&retirement, Some(43))
629 .expect_err("unknown key");
630 let recovered = store.recover().expect("recovered");
631
632 assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
633 assert_eq!(recovered.current_generation(), 0);
634 assert!(recovered.ledger().allocation_history().records().is_empty());
635 }
636}