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