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