Skip to main content

ferrum_interfaces/vnext/event/
resource_maintenance.rs

1use 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/// Allocator-issued proof for one successful post-admission backing mutation.
67///
68/// This event is plan/batch scoped instead of belonging to one request
69/// lifecycle cursor. A multi-request wave emits one event with a canonical
70/// participant set and one allocator receipt, preventing physical growth or
71/// reclaim from being counted once per participant.
72#[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}