ferrum_interfaces/vnext/event/
resource_maintenance.rs1use serde::Serialize;
2use std::collections::BTreeSet;
3
4use crate::vnext::{
5 DynamicPoolGrowthBatchReceipt, RequestIdentity, RunId, SequenceAuthorityId,
6 TrustedActiveSequenceBinding, TrustedPlanRuntimeEvidence, VNextError,
7};
8
9use super::{canonical_fingerprint, invalid_event};
10
11pub const EXECUTION_RESOURCE_MAINTENANCE_EVENT_SCHEMA_VERSION: u32 = 2;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ExecutionResourceMaintenanceStage {
16 SequenceExtension,
17 StepAdmission,
18 SubmissionWave,
19}
20
21impl ExecutionResourceMaintenanceStage {
22 pub const fn as_str(self) -> &'static str {
23 match self {
24 Self::SequenceExtension => "sequence_extension",
25 Self::StepAdmission => "step_admission",
26 Self::SubmissionWave => "submission_wave",
27 }
28 }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)]
32pub struct ExecutionResourceMaintenanceParticipant {
33 run_id: RunId,
34 request_id: RequestIdentity,
35 sequence_authority: SequenceAuthorityId,
36 active_sequence_fingerprint: String,
37}
38
39impl ExecutionResourceMaintenanceParticipant {
40 fn from_active(active: &TrustedActiveSequenceBinding) -> Self {
41 Self {
42 run_id: active.run_id().clone(),
43 request_id: active.request_id().clone(),
44 sequence_authority: active.sequence_authority(),
45 active_sequence_fingerprint: active.fingerprint().to_owned(),
46 }
47 }
48
49 pub fn run_id(&self) -> &RunId {
50 &self.run_id
51 }
52
53 pub fn request_id(&self) -> &RequestIdentity {
54 &self.request_id
55 }
56
57 pub const fn sequence_authority(&self) -> SequenceAuthorityId {
58 self.sequence_authority
59 }
60
61 pub fn active_sequence_fingerprint(&self) -> &str {
62 &self.active_sequence_fingerprint
63 }
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73pub struct BoundExecutionResourceMaintenance {
74 schema_version: u32,
75 plan: TrustedPlanRuntimeEvidence,
76 stage: ExecutionResourceMaintenanceStage,
77 participants: Box<[ExecutionResourceMaintenanceParticipant]>,
78 receipt: DynamicPoolGrowthBatchReceipt,
79 #[serde(skip)]
80 fingerprint: String,
81}
82
83impl BoundExecutionResourceMaintenance {
84 pub fn bind<'a>(
85 stage: ExecutionResourceMaintenanceStage,
86 participants: impl IntoIterator<Item = &'a TrustedActiveSequenceBinding>,
87 receipt: DynamicPoolGrowthBatchReceipt,
88 ) -> Result<Self, VNextError> {
89 if receipt.growths().is_empty() || receipt.capacity_epoch() == 0 {
90 return Err(invalid_event(
91 "execution resource maintenance requires installed backing growth",
92 ));
93 }
94 let mut growth_pool_ids = BTreeSet::new();
95 let mut chunk_ids = BTreeSet::new();
96 if receipt.growths().iter().any(|growth| {
97 growth.chunk_bytes() == 0
98 || growth.published_capacity_bytes() < growth.chunk_bytes()
99 || growth.capacity_epoch() != receipt.capacity_epoch()
100 || growth.chunk().pool_id() != growth.pool_id()
101 || !growth_pool_ids.insert(growth.pool_id().clone())
102 || !chunk_ids.insert(growth.chunk().clone())
103 }) {
104 return Err(invalid_event(
105 "execution resource maintenance contains invalid or duplicate growth evidence",
106 ));
107 }
108 if let Some(rebalance) = receipt.rebalance() {
109 let mut reclaimed_pool_ids = BTreeSet::new();
110 let mut reclaimed_chunk_ids = BTreeSet::new();
111 let detailed_chunks = rebalance.pools().iter().try_fold(0_usize, |total, pool| {
112 total.checked_add(pool.chunks().len())
113 });
114 let detailed_bytes = rebalance.pools().iter().try_fold(0_u64, |total, pool| {
115 total.checked_add(pool.reclaimed_bytes())
116 });
117 if rebalance.pools().is_empty()
118 || rebalance.reclaimed_chunks() == 0
119 || rebalance.reclaimed_bytes() == 0
120 || rebalance.logical_capacity_epoch() == 0
121 || rebalance.plan_device_capacity_epoch() == 0
122 || rebalance.process_device_capacity_epoch() == 0
123 || detailed_chunks != Some(rebalance.reclaimed_chunks())
124 || detailed_bytes != Some(rebalance.reclaimed_bytes())
125 || rebalance.pools().iter().any(|pool| {
126 pool.chunks().is_empty()
127 || pool.reclaimed_bytes() == 0
128 || !reclaimed_pool_ids.insert(pool.pool_id().clone())
129 || pool.chunks().iter().any(|chunk| {
130 chunk.pool_id() != pool.pool_id()
131 || !reclaimed_chunk_ids.insert(chunk.clone())
132 })
133 })
134 {
135 return Err(invalid_event(
136 "execution resource maintenance contains invalid rebalance evidence",
137 ));
138 }
139 let boundary = receipt.maintenance_boundary().ok_or_else(|| {
140 invalid_event(
141 "execution resource rebalance requires its pre-mutation boundary receipt",
142 )
143 })?;
144 let selected = boundary
145 .selected_chunks()
146 .iter()
147 .cloned()
148 .collect::<BTreeSet<_>>();
149 if !boundary.reclaim_sufficient()
150 || boundary.selected_bytes() != rebalance.reclaimed_bytes()
151 || selected.len() != boundary.selected_chunks().len()
152 || selected != reclaimed_chunk_ids
153 {
154 return Err(invalid_event(
155 "execution resource rebalance differs from its maintenance boundary",
156 ));
157 }
158 } else if receipt.maintenance_boundary().is_some() {
159 return Err(invalid_event(
160 "successful execution maintenance boundary requires a rebalance receipt",
161 ));
162 }
163
164 let mut plan = None;
165 let mut bound = Vec::new();
166 for active in participants {
167 active.ensure_open_for_emission()?;
168 match &plan {
169 Some(expected) if expected != active.plan() => {
170 return Err(invalid_event(
171 "execution resource maintenance participants differ in plan authority",
172 ));
173 }
174 None => plan = Some(active.plan().clone()),
175 Some(_) => {}
176 }
177 bound.push(ExecutionResourceMaintenanceParticipant::from_active(active));
178 }
179 let plan = plan.ok_or_else(|| {
180 invalid_event("execution resource maintenance requires at least one participant")
181 })?;
182 if receipt.coordinator_id() != plan.coordinator_id() {
183 return Err(invalid_event(
184 "execution resource maintenance receipt belongs to a different coordinator",
185 ));
186 }
187 let mut participant_authorities = BTreeSet::new();
188 if bound.iter().any(|participant| {
189 !participant_authorities.insert((
190 participant.run_id.clone(),
191 participant.request_id.clone(),
192 participant.sequence_authority,
193 ))
194 }) {
195 return Err(invalid_event(
196 "execution resource maintenance contains duplicate participants",
197 ));
198 }
199 bound.sort();
200
201 let mut event = Self {
202 schema_version: EXECUTION_RESOURCE_MAINTENANCE_EVENT_SCHEMA_VERSION,
203 plan,
204 stage,
205 participants: bound.into_boxed_slice(),
206 receipt,
207 fingerprint: String::new(),
208 };
209 event.fingerprint = canonical_fingerprint(&event);
210 Ok(event)
211 }
212
213 pub const fn schema_version(&self) -> u32 {
214 self.schema_version
215 }
216
217 pub fn plan(&self) -> &TrustedPlanRuntimeEvidence {
218 &self.plan
219 }
220
221 pub const fn stage(&self) -> ExecutionResourceMaintenanceStage {
222 self.stage
223 }
224
225 pub fn participants(&self) -> &[ExecutionResourceMaintenanceParticipant] {
226 &self.participants
227 }
228
229 pub fn receipt(&self) -> &DynamicPoolGrowthBatchReceipt {
230 &self.receipt
231 }
232
233 pub fn fingerprint(&self) -> &str {
234 &self.fingerprint
235 }
236}