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 ledger_schema_version: 1,
363 physical_format_id: 1,
364 current_generation: 0,
365 allocation_history: AllocationHistory::default(),
366 }
367 }
368
369 fn declaration() -> AllocationDeclaration {
370 AllocationDeclaration::new(
371 "app.users.v1",
372 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
373 None,
374 SchemaMetadata::default(),
375 )
376 .expect("declaration")
377 }
378
379 #[test]
380 fn validate_and_commit_publishes_committed_generation() {
381 let mut store = LedgerCommitStore::default();
382 store.commit(&ledger()).expect("initial ledger");
383 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
384
385 let commit = AllocationBootstrap::new(&mut store)
386 .validate_and_commit(snapshot, &TestPolicy, Some(42))
387 .expect("bootstrap commit");
388
389 assert_eq!(commit.ledger.current_generation, 1);
390 assert_eq!(commit.validated.generation(), 1);
391 assert_eq!(commit.ledger.allocation_history.records().len(), 1);
392 assert_eq!(commit.ledger.allocation_history.generations().len(), 1);
393 }
394
395 #[test]
396 fn initialize_validate_and_commit_seeds_empty_ledger_store() {
397 let mut store = LedgerCommitStore::default();
398 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
399
400 let commit = AllocationBootstrap::new(&mut store)
401 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
402 .expect("bootstrap commit");
403
404 assert_eq!(commit.ledger.current_generation, 1);
405 assert_eq!(commit.validated.generation(), 1);
406 assert_eq!(commit.ledger.allocation_history.records().len(), 1);
407 }
408
409 #[test]
410 fn initialize_validate_and_commit_fails_closed_on_corrupt_store() {
411 let mut store = LedgerCommitStore::default();
412 store
413 .write_corrupt_inactive_ledger(&ledger())
414 .expect("corrupt ledger");
415 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
416
417 let err = AllocationBootstrap::new(&mut store)
418 .initialize_validate_and_commit(&ledger(), snapshot, &TestPolicy, Some(42))
419 .expect_err("corrupt state");
420
421 assert!(matches!(err, BootstrapError::Ledger(_)));
422 }
423
424 #[test]
425 fn reserve_and_commit_policy_checks_and_commits_reservation() {
426 let mut store = LedgerCommitStore::default();
427 store.commit(&ledger()).expect("initial ledger");
428 let reservation = declaration();
429
430 let committed = AllocationBootstrap::new(&mut store)
431 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
432 .expect("reservation commit");
433
434 assert_eq!(committed.current_generation, 1);
435 assert_eq!(committed.allocation_history.records().len(), 1);
436 assert_eq!(
437 committed.allocation_history.records()[0].state(),
438 AllocationState::Reserved
439 );
440 }
441
442 #[test]
443 fn initialize_reserve_and_commit_seeds_empty_store() {
444 let mut store = LedgerCommitStore::default();
445 let reservation = declaration();
446
447 let committed = AllocationBootstrap::new(&mut store)
448 .initialize_reserve_and_commit(&ledger(), &[reservation], &TestPolicy, Some(42))
449 .expect("reservation commit");
450
451 assert_eq!(committed.current_generation, 1);
452 assert_eq!(
453 committed.allocation_history.records()[0].state(),
454 AllocationState::Reserved
455 );
456 }
457
458 #[test]
459 fn reserve_and_commit_rejects_policy_failure_before_commit() {
460 let mut store = LedgerCommitStore::default();
461 store.commit(&ledger()).expect("initial ledger");
462 let reservation = declaration();
463
464 let err = AllocationBootstrap::new(&mut store)
465 .reserve_and_commit(&[reservation], &RejectReservedPolicy, Some(42))
466 .expect_err("policy failure");
467 let recovered = store.recover().expect("recovered");
468
469 assert!(matches!(err, BootstrapReservationError::Policy(_)));
470 assert_eq!(recovered.current_generation(), 0);
471 assert!(recovered.ledger().allocation_history().records().is_empty());
472 }
473
474 #[test]
475 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
476 let mut store = LedgerCommitStore::default();
477 store.commit(&ledger()).expect("initial ledger");
478 let reservation = declaration();
479 AllocationBootstrap::new(&mut store)
480 .reserve_and_commit(&[reservation], &TestPolicy, Some(42))
481 .expect("reservation commit");
482 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
483
484 let err = AllocationBootstrap::new(&mut store)
485 .validate_and_commit(snapshot, &RejectActivePolicy, Some(43))
486 .expect_err("active validation must run");
487 let recovered = store.recover().expect("recovered");
488
489 assert!(matches!(
490 err,
491 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
492 ));
493 assert_eq!(
494 recovered.ledger().allocation_history().records()[0].state(),
495 AllocationState::Reserved
496 );
497 }
498
499 #[test]
500 fn retire_and_commit_tombstones_through_protected_commit() {
501 let mut store = LedgerCommitStore::default();
502 store.commit(&ledger()).expect("initial ledger");
503 let snapshot = DeclarationSnapshot::new(vec![declaration()]).expect("snapshot");
504 AllocationBootstrap::new(&mut store)
505 .validate_and_commit(snapshot, &TestPolicy, Some(42))
506 .expect("active commit");
507 let retirement = AllocationRetirement::new(
508 "app.users.v1",
509 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
510 )
511 .expect("retirement");
512
513 let committed = AllocationBootstrap::new(&mut store)
514 .retire_and_commit(&retirement, Some(43))
515 .expect("retirement commit");
516
517 assert_eq!(committed.current_generation, 2);
518 assert_eq!(
519 committed.allocation_history.records()[0].state(),
520 AllocationState::Retired
521 );
522 assert_eq!(
523 committed.allocation_history.records()[0].retired_generation(),
524 Some(2)
525 );
526 }
527
528 #[test]
529 fn retire_and_commit_rejects_unknown_key_before_commit() {
530 let mut store = LedgerCommitStore::default();
531 store.commit(&ledger()).expect("initial ledger");
532 let retirement = AllocationRetirement::new(
533 "app.users.v1",
534 AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
535 )
536 .expect("retirement");
537
538 let err = AllocationBootstrap::new(&mut store)
539 .retire_and_commit(&retirement, Some(43))
540 .expect_err("unknown key");
541 let recovered = store.recover().expect("recovered");
542
543 assert!(matches!(err, BootstrapRetirementError::Retirement(_)));
544 assert_eq!(recovered.current_generation(), 0);
545 assert!(recovered.ledger().allocation_history().records().is_empty());
546 }
547}