1use crate::{
2 declaration::AllocationDeclaration,
3 declaration::DeclarationSnapshot,
4 ledger::{
5 AllocationLedger, AllocationReservationError, AllocationRetirement,
6 AllocationRetirementError, AllocationStageError, LedgerCommitError, LedgerCommitStore,
7 },
8 policy::AllocationPolicy,
9 session::ValidatedAllocations,
10 validation::{AllocationValidationError, validate_allocations},
11};
12
13#[derive(Debug)]
38pub struct AllocationBootstrap<'store> {
39 store: &'store mut LedgerCommitStore,
40}
41
42impl<'store> AllocationBootstrap<'store> {
43 pub const fn new(store: &'store mut LedgerCommitStore) -> Self {
45 Self { store }
46 }
47
48 pub fn validate_and_commit<P>(
50 &mut self,
51 snapshot: DeclarationSnapshot,
52 policy: &P,
53 committed_at: Option<u64>,
54 ) -> Result<BootstrapCommit, BootstrapError<P::Error>>
55 where
56 P: AllocationPolicy,
57 {
58 let prior = self.store.recover().map_err(BootstrapError::Ledger)?;
59 self.validate_against(prior, snapshot, policy, committed_at)
60 }
61
62 pub fn initialize_validate_and_commit<P>(
73 &mut self,
74 genesis: &AllocationLedger,
75 snapshot: DeclarationSnapshot,
76 policy: &P,
77 committed_at: Option<u64>,
78 ) -> Result<BootstrapCommit, BootstrapError<P::Error>>
79 where
80 P: AllocationPolicy,
81 {
82 let prior = self
83 .store
84 .recover_or_initialize(genesis)
85 .map_err(BootstrapError::Ledger)?;
86 self.validate_against(prior, snapshot, policy, committed_at)
87 }
88
89 pub fn reserve_and_commit<P>(
91 &mut self,
92 reservations: &[AllocationDeclaration],
93 policy: &P,
94 committed_at: Option<u64>,
95 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
96 where
97 P: AllocationPolicy,
98 {
99 let prior = self
100 .store
101 .recover()
102 .map_err(BootstrapReservationError::Ledger)?;
103 self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
104 }
105
106 pub fn initialize_reserve_and_commit<P>(
112 &mut self,
113 genesis: &AllocationLedger,
114 reservations: &[AllocationDeclaration],
115 policy: &P,
116 committed_at: Option<u64>,
117 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
118 where
119 P: AllocationPolicy,
120 {
121 let prior = self
122 .store
123 .recover_or_initialize(genesis)
124 .map_err(BootstrapReservationError::Ledger)?;
125 self.reserve_against(prior.into_ledger(), reservations, policy, committed_at)
126 }
127
128 pub fn retire_and_commit(
130 &mut self,
131 retirement: &AllocationRetirement,
132 committed_at: Option<u64>,
133 ) -> Result<AllocationLedger, BootstrapRetirementError> {
134 let prior = self
135 .store
136 .recover()
137 .map_err(BootstrapRetirementError::Ledger)?;
138 self.retire_against(prior.into_ledger(), retirement, committed_at)
139 }
140
141 fn reserve_against<P>(
142 &mut self,
143 prior: AllocationLedger,
144 reservations: &[AllocationDeclaration],
145 policy: &P,
146 committed_at: Option<u64>,
147 ) -> Result<AllocationLedger, BootstrapReservationError<P::Error>>
148 where
149 P: AllocationPolicy,
150 {
151 for reservation in reservations {
152 policy
153 .validate_key(&reservation.stable_key)
154 .map_err(BootstrapReservationError::Policy)?;
155 policy
156 .validate_reserved_slot(&reservation.stable_key, &reservation.slot)
157 .map_err(BootstrapReservationError::Policy)?;
158 }
159
160 let staged = prior
161 .stage_reservation_generation(reservations, committed_at)
162 .map_err(BootstrapReservationError::Reservation)?;
163 self.store
164 .commit(&staged)
165 .map(crate::RecoveredLedger::into_ledger)
166 .map_err(BootstrapReservationError::Ledger)
167 }
168
169 fn retire_against(
170 &mut self,
171 prior: AllocationLedger,
172 retirement: &AllocationRetirement,
173 committed_at: Option<u64>,
174 ) -> Result<AllocationLedger, BootstrapRetirementError> {
175 let staged = prior
176 .stage_retirement_generation(retirement, committed_at)
177 .map_err(BootstrapRetirementError::Retirement)?;
178 self.store
179 .commit(&staged)
180 .map(crate::RecoveredLedger::into_ledger)
181 .map_err(BootstrapRetirementError::Ledger)
182 }
183
184 fn validate_against<P>(
185 &mut self,
186 prior: crate::RecoveredLedger,
187 snapshot: DeclarationSnapshot,
188 policy: &P,
189 committed_at: Option<u64>,
190 ) -> Result<BootstrapCommit, BootstrapError<P::Error>>
191 where
192 P: AllocationPolicy,
193 {
194 let validated =
195 validate_allocations(&prior, snapshot, policy).map_err(BootstrapError::Validation)?;
196 let prior_ledger = prior.into_ledger();
197 let staged = prior_ledger
198 .stage_validated_generation(&validated, committed_at)
199 .map_err(BootstrapError::Staging)?;
200 let committed = self.store.commit(&staged).map_err(BootstrapError::Ledger)?;
201
202 Ok(BootstrapCommit {
203 validated: validated.with_generation(committed.current_generation()),
204 ledger: committed.into_ledger(),
205 })
206 }
207}
208
209#[derive(Clone, Debug, Eq, PartialEq)]
214pub struct BootstrapCommit {
215 pub ledger: AllocationLedger,
217 pub validated: ValidatedAllocations,
219}
220
221#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
226pub enum BootstrapError<P> {
227 #[error(transparent)]
229 Ledger(LedgerCommitError),
230 #[error(transparent)]
232 Validation(AllocationValidationError<P>),
233 #[error(transparent)]
235 Staging(AllocationStageError),
236}
237
238#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
243pub enum BootstrapReservationError<P> {
244 #[error(transparent)]
246 Ledger(LedgerCommitError),
247 #[error("allocation policy rejected a reservation")]
249 Policy(P),
250 #[error(transparent)]
252 Reservation(AllocationReservationError),
253}
254
255#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
260pub enum BootstrapRetirementError {
261 #[error(transparent)]
263 Ledger(LedgerCommitError),
264 #[error(transparent)]
266 Retirement(AllocationRetirementError),
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use crate::{
273 declaration::AllocationDeclaration,
274 ledger::{AllocationHistory, AllocationLedger, AllocationState},
275 schema::SchemaMetadata,
276 slot::AllocationSlotDescriptor,
277 };
278
279 #[derive(Debug, Eq, PartialEq)]
280 struct TestPolicy;
281
282 impl AllocationPolicy for TestPolicy {
283 type Error = &'static str;
284
285 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
286 Ok(())
287 }
288
289 fn validate_slot(
290 &self,
291 _key: &crate::StableKey,
292 _slot: &AllocationSlotDescriptor,
293 ) -> Result<(), Self::Error> {
294 Ok(())
295 }
296
297 fn validate_reserved_slot(
298 &self,
299 _key: &crate::StableKey,
300 _slot: &AllocationSlotDescriptor,
301 ) -> Result<(), Self::Error> {
302 Ok(())
303 }
304 }
305
306 #[derive(Debug, Eq, PartialEq)]
307 struct RejectReservedPolicy;
308
309 impl AllocationPolicy for RejectReservedPolicy {
310 type Error = &'static str;
311
312 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
313 Ok(())
314 }
315
316 fn validate_slot(
317 &self,
318 _key: &crate::StableKey,
319 _slot: &AllocationSlotDescriptor,
320 ) -> Result<(), Self::Error> {
321 Ok(())
322 }
323
324 fn validate_reserved_slot(
325 &self,
326 _key: &crate::StableKey,
327 _slot: &AllocationSlotDescriptor,
328 ) -> Result<(), Self::Error> {
329 Err("reserved slot rejected")
330 }
331 }
332
333 #[derive(Debug, Eq, PartialEq)]
334 struct RejectActivePolicy;
335
336 impl AllocationPolicy for RejectActivePolicy {
337 type Error = &'static str;
338
339 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
340 Ok(())
341 }
342
343 fn validate_slot(
344 &self,
345 _key: &crate::StableKey,
346 _slot: &AllocationSlotDescriptor,
347 ) -> Result<(), Self::Error> {
348 Err("active slot rejected")
349 }
350
351 fn validate_reserved_slot(
352 &self,
353 _key: &crate::StableKey,
354 _slot: &AllocationSlotDescriptor,
355 ) -> Result<(), Self::Error> {
356 Ok(())
357 }
358 }
359
360 fn ledger() -> AllocationLedger {
361 AllocationLedger {
362 current_generation: 0,
363 allocation_history: AllocationHistory::default(),
364 }
365 }
366
367 fn declaration() -> AllocationDeclaration {
368 AllocationDeclaration::new(
369 "app.users.v1",
370 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
371 None,
372 SchemaMetadata::default(),
373 )
374 .expect("declaration")
375 }
376
377 #[test]
378 fn validate_and_commit_publishes_committed_generation() {
379 let mut store = LedgerCommitStore::default();
380 store.commit(&ledger()).expect("initial ledger");
381 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
382
383 let commit = AllocationBootstrap::new(&mut store)
384 .validate_and_commit(snapshot, &TestPolicy, Some(42))
385 .expect("bootstrap commit");
386
387 assert_eq!(commit.ledger.current_generation, 1);
388 assert_eq!(commit.validated.generation(), 1);
389 assert_eq!(commit.ledger.allocation_history.records().len(), 1);
390 assert_eq!(commit.ledger.allocation_history.generations().len(), 1);
391 }
392
393 #[test]
394 fn initialize_validate_and_commit_seeds_empty_ledger_store() {
395 let mut store = LedgerCommitStore::default();
396 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
397
398 let commit = AllocationBootstrap::new(&mut store)
399 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
400 .expect("bootstrap commit");
401
402 assert_eq!(commit.ledger.current_generation, 1);
403 assert_eq!(commit.validated.generation(), 1);
404 assert_eq!(commit.ledger.allocation_history.records().len(), 1);
405 }
406
407 #[test]
408 fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
409 let mut store = LedgerCommitStore::default();
410 store
411 .write_corrupt_inactive_ledger(&ledger())
412 .expect("corrupt ledger");
413 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
414
415 let err = AllocationBootstrap::new(&mut store)
416 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
417 .expect_err("corrupt state");
418
419 assert!(matches!(err, BootstrapError::Ledger(_)));
420 }
421
422 #[test]
423 fn reserve_and_commit_policy_checks_and_commits_reservation() {
424 let mut store = LedgerCommitStore::default();
425 store.commit(&ledger()).expect("initial ledger");
426 let reservation = declaration();
427
428 let committed = AllocationBootstrap::new(&mut store)
429 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
430 .expect("reservation commit");
431
432 assert_eq!(committed.current_generation, 1);
433 assert_eq!(committed.allocation_history.records().len(), 1);
434 assert_eq!(
435 committed.allocation_history.records()[0].state(),
436 AllocationState::Reserved
437 );
438 }
439
440 #[test]
441 fn initialize_reserve_and_commit_seeds_empty_store() {
442 let mut store = LedgerCommitStore::default();
443 let reservation = declaration();
444
445 let committed = AllocationBootstrap::new(&mut store)
446 .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
447 .expect("reservation commit");
448
449 assert_eq!(committed.current_generation, 1);
450 assert_eq!(
451 committed.allocation_history.records()[0].state(),
452 AllocationState::Reserved
453 );
454 }
455
456 #[test]
457 fn reserve_and_commit_rejects_policy_failure_before_commit() {
458 let mut store = LedgerCommitStore::default();
459 store.commit(&ledger()).expect("initial ledger");
460 let reservation = declaration();
461
462 let err = AllocationBootstrap::new(&mut store)
463 .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
464 .expect_err("policy failure");
465 let recovered = store.recover().expect("recovered");
466
467 assert!(matches!(err, BootstrapReservationError::Policy(_)));
468 assert_eq!(recovered.current_generation(), 0);
469 assert!(recovered.ledger().allocation_history().records().is_empty());
470 }
471
472 #[test]
473 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
474 let mut store = LedgerCommitStore::default();
475 store.commit(&ledger()).expect("initial ledger");
476 let reservation = declaration();
477 AllocationBootstrap::new(&mut store)
478 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
479 .expect("reservation commit");
480 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
481
482 let err = AllocationBootstrap::new(&mut store)
483 .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
484 .expect_err("active validation must run");
485 let recovered = store.recover().expect("recovered");
486
487 assert!(matches!(
488 err,
489 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
490 ));
491 assert_eq!(
492 recovered.ledger().allocation_history().records()[0].state(),
493 AllocationState::Reserved
494 );
495 }
496
497 #[test]
498 fn retire_and_commit_tombstones_through_protected_commit() {
499 let mut store = LedgerCommitStore::default();
500 store.commit(&ledger()).expect("initial ledger");
501 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
502 AllocationBootstrap::new(&mut store)
503 .validate_and_commit(snapshot, &TestPolicy, Some(42))
504 .expect("active commit");
505 let retirement = AllocationRetirement::new(
506 "app.users.v1",
507 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
508 )
509 .expect("retirement");
510
511 let committed = AllocationBootstrap::new(&mut store)
512 .retire_and_commit(&retirement, Some(43))
513 .expect("retirement commit");
514
515 assert_eq!(committed.current_generation, 2);
516 assert_eq!(
517 committed.allocation_history.records()[0].state(),
518 AllocationState::Retired
519 );
520 assert_eq!(
521 committed.allocation_history.records()[0].retired_generation(),
522 Some(2)
523 );
524 }
525
526 #[test]
527 fn retire_and_commit_rejects_unknown_key_before_commit() {
528 let mut store = LedgerCommitStore::default();
529 store.commit(&ledger()).expect("initial ledger");
530 let retirement = AllocationRetirement::new(
531 "app.users.v1",
532 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
533 )
534 .expect("retirement");
535
536 let err = AllocationBootstrap::new(&mut store)
537 .retire_and_commit(&retirement, Some(43))
538 .expect_err("unknown key");
539 let recovered = store.recover().expect("recovered");
540
541 assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
542 assert_eq!(recovered.current_generation(), 0);
543 assert!(recovered.ledger().allocation_history().records().is_empty());
544 }
545}