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