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#[non_exhaustive]
226#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
227pub enum BootstrapError<P> {
228 #[error(transparent)]
230 Ledger(LedgerCommitError),
231 #[error(transparent)]
233 Validation(AllocationValidationError<P>),
234 #[error(transparent)]
236 Staging(AllocationStageError),
237}
238
239#[non_exhaustive]
244#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
245pub enum BootstrapReservationError<P> {
246 #[error(transparent)]
248 Ledger(LedgerCommitError),
249 #[error("allocation policy rejected a reservation")]
251 Policy(P),
252 #[error(transparent)]
254 Reservation(AllocationReservationError),
255}
256
257#[non_exhaustive]
262#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
263pub enum BootstrapRetirementError {
264 #[error(transparent)]
266 Ledger(LedgerCommitError),
267 #[error(transparent)]
269 Retirement(AllocationRetirementError),
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use crate::{
276 declaration::AllocationDeclaration,
277 ledger::{AllocationHistory, AllocationLedger, AllocationState},
278 schema::SchemaMetadata,
279 slot::AllocationSlotDescriptor,
280 };
281
282 #[derive(Debug, Eq, PartialEq)]
283 struct TestPolicy;
284
285 impl AllocationPolicy for TestPolicy {
286 type Error = &'static str;
287
288 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
289 Ok(())
290 }
291
292 fn validate_slot(
293 &self,
294 _key: &crate::StableKey,
295 _slot: &AllocationSlotDescriptor,
296 ) -> Result<(), Self::Error> {
297 Ok(())
298 }
299
300 fn validate_reserved_slot(
301 &self,
302 _key: &crate::StableKey,
303 _slot: &AllocationSlotDescriptor,
304 ) -> Result<(), Self::Error> {
305 Ok(())
306 }
307 }
308
309 #[derive(Debug, Eq, PartialEq)]
310 struct RejectReservedPolicy;
311
312 impl AllocationPolicy for RejectReservedPolicy {
313 type Error = &'static str;
314
315 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
316 Ok(())
317 }
318
319 fn validate_slot(
320 &self,
321 _key: &crate::StableKey,
322 _slot: &AllocationSlotDescriptor,
323 ) -> Result<(), Self::Error> {
324 Ok(())
325 }
326
327 fn validate_reserved_slot(
328 &self,
329 _key: &crate::StableKey,
330 _slot: &AllocationSlotDescriptor,
331 ) -> Result<(), Self::Error> {
332 Err("reserved slot rejected")
333 }
334 }
335
336 #[derive(Debug, Eq, PartialEq)]
337 struct RejectActivePolicy;
338
339 impl AllocationPolicy for RejectActivePolicy {
340 type Error = &'static str;
341
342 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
343 Ok(())
344 }
345
346 fn validate_slot(
347 &self,
348 _key: &crate::StableKey,
349 _slot: &AllocationSlotDescriptor,
350 ) -> Result<(), Self::Error> {
351 Err("active slot rejected")
352 }
353
354 fn validate_reserved_slot(
355 &self,
356 _key: &crate::StableKey,
357 _slot: &AllocationSlotDescriptor,
358 ) -> Result<(), Self::Error> {
359 Ok(())
360 }
361 }
362
363 fn ledger() -> AllocationLedger {
364 AllocationLedger {
365 current_generation: 0,
366 allocation_history: AllocationHistory::default(),
367 }
368 }
369
370 fn declaration() -> AllocationDeclaration {
371 AllocationDeclaration::new(
372 "app.users.v1",
373 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
374 None,
375 SchemaMetadata::default(),
376 )
377 .expect("declaration")
378 }
379
380 #[test]
381 fn validate_and_commit_publishes_committed_generation() {
382 let mut store = LedgerCommitStore::default();
383 store.commit(&ledger()).expect("initial ledger");
384 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
385
386 let commit = AllocationBootstrap::new(&mut store)
387 .validate_and_commit(snapshot, &TestPolicy, Some(42))
388 .expect("bootstrap commit");
389
390 assert_eq!(commit.ledger.current_generation, 1);
391 assert_eq!(commit.validated.generation(), 1);
392 assert_eq!(commit.ledger.allocation_history.records().len(), 1);
393 assert_eq!(commit.ledger.allocation_history.generations().len(), 1);
394 }
395
396 #[test]
397 fn initialize_validate_and_commit_seeds_empty_ledger_store() {
398 let mut store = LedgerCommitStore::default();
399 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
400
401 let commit = AllocationBootstrap::new(&mut store)
402 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
403 .expect("bootstrap commit");
404
405 assert_eq!(commit.ledger.current_generation, 1);
406 assert_eq!(commit.validated.generation(), 1);
407 assert_eq!(commit.ledger.allocation_history.records().len(), 1);
408 }
409
410 #[test]
411 fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
412 let mut store = LedgerCommitStore::default();
413 store
414 .write_corrupt_inactive_ledger(&ledger())
415 .expect("corrupt ledger");
416 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
417
418 let err = AllocationBootstrap::new(&mut store)
419 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
420 .expect_err("corrupt state");
421
422 assert!(matches!(err, BootstrapError::Ledger(_)));
423 }
424
425 #[test]
426 fn reserve_and_commit_policy_checks_and_commits_reservation() {
427 let mut store = LedgerCommitStore::default();
428 store.commit(&ledger()).expect("initial ledger");
429 let reservation = declaration();
430
431 let committed = AllocationBootstrap::new(&mut store)
432 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
433 .expect("reservation commit");
434
435 assert_eq!(committed.current_generation, 1);
436 assert_eq!(committed.allocation_history.records().len(), 1);
437 assert_eq!(
438 committed.allocation_history.records()[0].state(),
439 AllocationState::Reserved
440 );
441 }
442
443 #[test]
444 fn initialize_reserve_and_commit_seeds_empty_store() {
445 let mut store = LedgerCommitStore::default();
446 let reservation = declaration();
447
448 let committed = AllocationBootstrap::new(&mut store)
449 .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
450 .expect("reservation commit");
451
452 assert_eq!(committed.current_generation, 1);
453 assert_eq!(
454 committed.allocation_history.records()[0].state(),
455 AllocationState::Reserved
456 );
457 }
458
459 #[test]
460 fn reserve_and_commit_rejects_policy_failure_before_commit() {
461 let mut store = LedgerCommitStore::default();
462 store.commit(&ledger()).expect("initial ledger");
463 let reservation = declaration();
464
465 let err = AllocationBootstrap::new(&mut store)
466 .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
467 .expect_err("policy failure");
468 let recovered = store.recover().expect("recovered");
469
470 assert!(matches!(err, BootstrapReservationError::Policy(_)));
471 assert_eq!(recovered.current_generation(), 0);
472 assert!(recovered.ledger().allocation_history().records().is_empty());
473 }
474
475 #[test]
476 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
477 let mut store = LedgerCommitStore::default();
478 store.commit(&ledger()).expect("initial ledger");
479 let reservation = declaration();
480 AllocationBootstrap::new(&mut store)
481 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
482 .expect("reservation commit");
483 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
484
485 let err = AllocationBootstrap::new(&mut store)
486 .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
487 .expect_err("active validation must run");
488 let recovered = store.recover().expect("recovered");
489
490 assert!(matches!(
491 err,
492 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
493 ));
494 assert_eq!(
495 recovered.ledger().allocation_history().records()[0].state(),
496 AllocationState::Reserved
497 );
498 }
499
500 #[test]
501 fn retire_and_commit_tombstones_through_protected_commit() {
502 let mut store = LedgerCommitStore::default();
503 store.commit(&ledger()).expect("initial ledger");
504 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
505 AllocationBootstrap::new(&mut store)
506 .validate_and_commit(snapshot, &TestPolicy, Some(42))
507 .expect("active commit");
508 let retirement = AllocationRetirement::new(
509 "app.users.v1",
510 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
511 )
512 .expect("retirement");
513
514 let committed = AllocationBootstrap::new(&mut store)
515 .retire_and_commit(&retirement, Some(43))
516 .expect("retirement commit");
517
518 assert_eq!(committed.current_generation, 2);
519 assert_eq!(
520 committed.allocation_history.records()[0].state(),
521 AllocationState::Retired
522 );
523 assert_eq!(
524 committed.allocation_history.records()[0].retired_generation(),
525 Some(2)
526 );
527 }
528
529 #[test]
530 fn retire_and_commit_rejects_unknown_key_before_commit() {
531 let mut store = LedgerCommitStore::default();
532 store.commit(&ledger()).expect("initial ledger");
533 let retirement = AllocationRetirement::new(
534 "app.users.v1",
535 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
536 )
537 .expect("retirement");
538
539 let err = AllocationBootstrap::new(&mut store)
540 .retire_and_commit(&retirement, Some(43))
541 .expect_err("unknown key");
542 let recovered = store.recover().expect("recovered");
543
544 assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
545 assert_eq!(recovered.current_generation(), 0);
546 assert!(recovered.ledger().allocation_history().records().is_empty());
547 }
548}