1use crate::{
2 capability::{CommittedAllocations, ValidatedAllocations},
3 declaration::DeclarationSnapshot,
4 ledger::{
5 AllocationStageError, LedgerCommitError, LedgerCommitStore, stage_validated_generation,
6 },
7 policy::AllocationPolicy,
8 validation::{AllocationValidationError, validate_allocations},
9};
10use std::borrow::Cow;
11
12#[derive(Debug)]
21pub struct AllocationBootstrap<'store> {
22 store: &'store mut LedgerCommitStore,
23}
24
25impl<'store> AllocationBootstrap<'store> {
26 pub const fn new(store: &'store mut LedgerCommitStore) -> Self {
28 Self { store }
29 }
30
31 pub(crate) fn validate_against<P>(
32 &mut self,
33 prior: crate::RecoveredLedger,
34 snapshot: DeclarationSnapshot,
35 policy: &P,
36 ) -> Result<PendingBootstrapCommit, BootstrapError<P::Error>>
37 where
38 P: AllocationPolicy,
39 {
40 let validated =
41 validate_allocations(&prior, snapshot, policy).map_err(BootstrapError::Validation)?;
42 let staged = stage_validated_generation(Cow::Owned(prior.into_ledger()), &validated)
43 .map_err(BootstrapError::Staging)?;
44 self.store
45 .commit_generation(&staged)
46 .map_err(BootstrapError::Ledger)?;
47
48 Ok(PendingBootstrapCommit {
49 validated,
50 generation: staged.current_generation(),
51 })
52 }
53}
54
55#[derive(Debug, Eq, PartialEq)]
67#[must_use = "persist the owning ledger record, then confirm_persisted before opening allocations"]
68pub struct PendingBootstrapCommit {
69 generation: u64,
71 validated: ValidatedAllocations,
73}
74
75impl PendingBootstrapCommit {
76 #[must_use]
82 pub fn confirm_persisted(self) -> CommittedAllocations {
83 self.validated.confirm_persisted(self.generation)
84 }
85}
86
87#[non_exhaustive]
92#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
93pub enum BootstrapError<P> {
94 #[error(transparent)]
96 Ledger(LedgerCommitError),
97 #[error(transparent)]
99 Validation(AllocationValidationError<P>),
100 #[error(transparent)]
102 Staging(AllocationStageError),
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::{
109 AllocationDeclaration, AllocationLedger, GenericAllocationPolicy, MemoryManagerSlot,
110 SchemaMetadata,
111 };
112 #[derive(Debug, Eq, PartialEq)]
113 struct RejectActivePolicy;
114
115 impl AllocationPolicy for RejectActivePolicy {
116 type Error = &'static str;
117
118 fn validate_key(&self, _key: &crate::StableKey) -> Result<(), Self::Error> {
119 Ok(())
120 }
121
122 fn validate_slot(
123 &self,
124 _key: &crate::StableKey,
125 _slot: &MemoryManagerSlot,
126 ) -> Result<(), Self::Error> {
127 Err("active slot rejected")
128 }
129 }
130
131 fn ledger() -> AllocationLedger {
132 AllocationLedger::empty_genesis()
133 }
134
135 fn declaration() -> AllocationDeclaration {
136 AllocationDeclaration::new(
137 "app.users.v1",
138 MemoryManagerSlot::new(100).expect("usable slot"),
139 None,
140 SchemaMetadata::default(),
141 )
142 .expect("declaration")
143 }
144
145 #[test]
146 fn validated_commit_matches_recovered_generation() {
147 let mut store = LedgerCommitStore::default();
148 let recovered = store.recover_or_initialize(&ledger()).unwrap();
149 let snapshot = DeclarationSnapshot::new(vec![declaration()]).unwrap();
150 let commit = AllocationBootstrap::new(&mut store)
151 .validate_against(recovered, snapshot, &GenericAllocationPolicy)
152 .unwrap();
153 let recovered = store.recover().unwrap();
154 assert_eq!(recovered.current_generation(), 1);
155 assert_eq!(recovered.ledger().records().len(), 1);
156 assert_eq!(
157 recovered.ledger().records()[0],
158 crate::AllocationRecord::active(&declaration())
159 );
160 assert_eq!(commit.confirm_persisted().generation(), 1);
161 }
162
163 #[test]
164 fn reservation_policy_alone_does_not_activate_reserved_allocation() {
165 let mut store = LedgerCommitStore::default();
166 let reserved = ledger()
167 .stage_reservation_generation(&[declaration()])
168 .unwrap();
169 store.commit(&reserved).unwrap();
170 let before = store.clone();
171 let recovered = store.recover().unwrap();
172 let snapshot = DeclarationSnapshot::new(vec![declaration()]).unwrap();
173 let err = AllocationBootstrap::new(&mut store)
174 .validate_against(recovered, snapshot, &RejectActivePolicy)
175 .expect_err("active validation must run");
176 assert!(matches!(
177 err,
178 BootstrapError::Validation(AllocationValidationError::Policy("active slot rejected"))
179 ));
180 assert_eq!(store, before);
181 assert_eq!(
182 store.recover().unwrap().ledger().records()[0].state(),
183 crate::AllocationState::Reserved
184 );
185 }
186}