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