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