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 pub(crate) 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
263#[non_exhaustive]
268#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
269pub enum BootstrapError<P> {
270 #[error(transparent)]
272 Ledger(LedgerCommitError),
273 #[error(transparent)]
275 Validation(AllocationValidationError<P>),
276 #[error(transparent)]
278 Staging(AllocationStageError),
279}
280
281#[non_exhaustive]
286#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
287pub enum BootstrapReservationError<P> {
288 #[error(transparent)]
290 Ledger(LedgerCommitError),
291 #[error("allocation policy rejected a reservation")]
293 Policy(P),
294 #[error(transparent)]
296 Reservation(AllocationReservationError),
297}
298
299#[non_exhaustive]
304#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
305pub enum BootstrapRetirementError {
306 #[error(transparent)]
308 Ledger(LedgerCommitError),
309 #[error(transparent)]
311 Retirement(AllocationRetirementError),
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use crate::{
318 declaration::AllocationDeclaration,
319 ledger::{AllocationHistory, AllocationLedger, AllocationState},
320 schema::SchemaMetadata,
321 slot::AllocationSlotDescriptor,
322 };
323
324 #[derive(Debug, Eq, PartialEq)]
325 struct TestPolicy;
326
327 impl AllocationPolicy for TestPolicy {
328 type Error = &'static str;
329
330 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
331 Ok(())
332 }
333
334 fn validate_slot(
335 &self,
336 _key: &crate::StableKey,
337 _slot: &AllocationSlotDescriptor,
338 ) -> Result<(), Self::Error> {
339 Ok(())
340 }
341
342 fn validate_reserved_slot(
343 &self,
344 _key: &crate::StableKey,
345 _slot: &AllocationSlotDescriptor,
346 ) -> Result<(), Self::Error> {
347 Ok(())
348 }
349 }
350
351 #[derive(Debug, Eq, PartialEq)]
352 struct RejectReservedPolicy;
353
354 impl AllocationPolicy for RejectReservedPolicy {
355 type Error = &'static str;
356
357 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
358 Ok(())
359 }
360
361 fn validate_slot(
362 &self,
363 _key: &crate::StableKey,
364 _slot: &AllocationSlotDescriptor,
365 ) -> Result<(), Self::Error> {
366 Ok(())
367 }
368
369 fn validate_reserved_slot(
370 &self,
371 _key: &crate::StableKey,
372 _slot: &AllocationSlotDescriptor,
373 ) -> Result<(), Self::Error> {
374 Err("reserved slot rejected")
375 }
376 }
377
378 #[derive(Debug, Eq, PartialEq)]
379 struct RejectActivePolicy;
380
381 impl AllocationPolicy for RejectActivePolicy {
382 type Error = &'static str;
383
384 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
385 Ok(())
386 }
387
388 fn validate_slot(
389 &self,
390 _key: &crate::StableKey,
391 _slot: &AllocationSlotDescriptor,
392 ) -> Result<(), Self::Error> {
393 Err("active slot rejected")
394 }
395
396 fn validate_reserved_slot(
397 &self,
398 _key: &crate::StableKey,
399 _slot: &AllocationSlotDescriptor,
400 ) -> Result<(), Self::Error> {
401 Ok(())
402 }
403 }
404
405 struct PolicyMustNotRun;
406
407 impl AllocationPolicy for PolicyMustNotRun {
408 type Error = &'static str;
409
410 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
411 panic!("policy received an invalid reservation")
412 }
413
414 fn validate_slot(
415 &self,
416 _key: &crate::StableKey,
417 _slot: &AllocationSlotDescriptor,
418 ) -> Result<(), Self::Error> {
419 panic!("policy received an invalid reservation")
420 }
421
422 fn validate_reserved_slot(
423 &self,
424 _key: &crate::StableKey,
425 _slot: &AllocationSlotDescriptor,
426 ) -> Result<(), Self::Error> {
427 panic!("policy received an invalid reservation")
428 }
429 }
430
431 fn ledger() -> AllocationLedger {
432 AllocationLedger {
433 current_generation: 0,
434 allocation_history: AllocationHistory::default(),
435 }
436 }
437
438 fn declaration() -> AllocationDeclaration {
439 AllocationDeclaration::new(
440 "app.users.v1",
441 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
442 None,
443 SchemaMetadata::default(),
444 )
445 .expect("declaration")
446 }
447
448 #[test]
449 fn validate_and_commit_publishes_committed_generation() {
450 let mut store = LedgerCommitStore::default();
451 store.commit(&ledger()).expect("initial ledger");
452 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
453
454 let commit = AllocationBootstrap::new(&mut store)
455 .validate_and_commit(snapshot, &TestPolicy, Some(42))
456 .expect("bootstrap commit");
457
458 assert_eq!(commit.ledger().current_generation, 1);
459 assert_eq!(commit.ledger().allocation_history.records().len(), 1);
460 assert_eq!(commit.ledger().allocation_history.generations().len(), 1);
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 }
510
511 #[test]
512 fn initialize_reserve_and_commit_seeds_empty_store() {
513 let mut store = LedgerCommitStore::default();
514 let reservation = declaration();
515
516 let committed = AllocationBootstrap::new(&mut store)
517 .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
518 .expect("reservation commit");
519
520 assert_eq!(committed.current_generation, 1);
521 assert_eq!(
522 committed.allocation_history.records()[0].state(),
523 AllocationState::Reserved
524 );
525 }
526
527 #[test]
528 fn reserve_and_commit_rejects_policy_failure_before_commit() {
529 let mut store = LedgerCommitStore::default();
530 store.commit(&ledger()).expect("initial ledger");
531 let reservation = declaration();
532
533 let err = AllocationBootstrap::new(&mut store)
534 .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
535 .expect_err("policy failure");
536 let recovered = store.recover().expect("recovered");
537
538 assert!(matches!(err, BootstrapReservationError::Policy(_)));
539 assert_eq!(recovered.current_generation(), 0);
540 assert_eq!(recovered.ledger().allocation_history().records(), []);
541 }
542
543 #[test]
544 fn reserve_and_commit_validates_reservation_before_policy() {
545 let mut store = LedgerCommitStore::default();
546 store.commit(&ledger()).expect("initial ledger");
547 let mut reservation = declaration();
548 reservation.slot =
549 AllocationSlotDescriptor::memory_manager_unchecked(crate::MEMORY_MANAGER_INVALID_ID);
550
551 let err = AllocationBootstrap::new(&mut store)
552 .reserve_and_commit(&[reservation], &PolicyMustNotRun, Some(42))
553 .expect_err("invalid reservation must fail before policy");
554
555 assert!(matches!(
556 err,
557 BootstrapReservationError::Reservation(AllocationReservationError::InvalidDeclaration(
558 _
559 ))
560 ));
561 }
562
563 #[test]
564 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
565 let mut store = LedgerCommitStore::default();
566 store.commit(&ledger()).expect("initial ledger");
567 let reservation = declaration();
568 AllocationBootstrap::new(&mut store)
569 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
570 .expect("reservation commit");
571 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
572
573 let err = AllocationBootstrap::new(&mut store)
574 .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
575 .expect_err("active validation must run");
576 let recovered = store.recover().expect("recovered");
577
578 assert!(matches!(
579 err,
580 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
581 ));
582 assert_eq!(
583 recovered.ledger().allocation_history().records()[0].state(),
584 AllocationState::Reserved
585 );
586 }
587
588 #[test]
589 fn retire_and_commit_tombstones_through_protected_commit() {
590 let mut store = LedgerCommitStore::default();
591 store.commit(&ledger()).expect("initial ledger");
592 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
593 AllocationBootstrap::new(&mut store)
594 .validate_and_commit(snapshot, &TestPolicy, Some(42))
595 .expect("active commit");
596 let retirement = AllocationRetirement::new(
597 "app.users.v1",
598 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
599 )
600 .expect("retirement");
601
602 let committed = AllocationBootstrap::new(&mut store)
603 .retire_and_commit(&retirement, Some(43))
604 .expect("retirement commit");
605
606 assert_eq!(committed.current_generation, 2);
607 assert_eq!(
608 committed.allocation_history.records()[0].state(),
609 AllocationState::Retired { generation: 2 }
610 );
611 }
612
613 #[test]
614 fn retire_and_commit_rejects_unknown_key_before_commit() {
615 let mut store = LedgerCommitStore::default();
616 store.commit(&ledger()).expect("initial ledger");
617 let retirement = AllocationRetirement::new(
618 "app.users.v1",
619 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
620 )
621 .expect("retirement");
622
623 let err = AllocationBootstrap::new(&mut store)
624 .retire_and_commit(&retirement, Some(43))
625 .expect_err("unknown key");
626 let recovered = store.recover().expect("recovered");
627
628 assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
629 assert_eq!(recovered.current_generation(), 0);
630 assert_eq!(recovered.ledger().allocation_history().records(), []);
631 }
632}