Skip to main content

acyclic_fs/speculation/
controller.rs

1//! Atomic composition of residency and promotion prediction state machines.
2
3use super::{
4    ObjectResidency, PromotionAdmission, PromotionCandidate, PromotionDestination,
5    PromotionMetrics, PromotionSpeculator, PromotionSpeculatorError, PromotionSpeculatorOptions,
6    ResidencyAdmission, ResidencyCandidate, ResidencyHint, ResidencyMetrics, ResidencyPermit,
7    ResidencySpeculator, ResidencySpeculatorError, ResidencySpeculatorOptions, StorageTier,
8};
9use crate::foundation::{GenerationId, OperationId, VolumeId};
10use thiserror::Error;
11
12/// Complete policy for both correctness-inert speculation engines.
13#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
14pub struct SpeculationOptions {
15    /// Process-local immutable-object residency policy.
16    pub residency: ResidencySpeculatorOptions,
17    /// Cross-location immutable-object promotion policy.
18    pub promotion: PromotionSpeculatorOptions,
19}
20
21/// Operation identities terminalized by one foreground or generation fence.
22#[derive(Clone, Debug, Default, Eq, PartialEq)]
23pub struct SpeculationPreemption {
24    /// Cancelled local residency operations in stable identity order.
25    pub residency: Vec<OperationId>,
26    /// Cancelled cross-location promotions in stable identity order.
27    pub promotion: Vec<OperationId>,
28}
29
30/// Exact payload-free metrics from both engines at one instant.
31#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
32pub struct SpeculationMetrics {
33    /// Local residency state and cumulative decisions.
34    pub residency: ResidencyMetrics,
35    /// Cross-location promotion state and cumulative decisions.
36    pub promotion: PromotionMetrics,
37}
38
39/// Atomic composition failure. Neither engine changes when a composed
40/// transition fails.
41#[derive(Clone, Copy, Debug, Error, Eq, PartialEq)]
42pub enum SpeculationControllerError {
43    /// Residency policy, admission, feedback, or accounting failed.
44    #[error(transparent)]
45    Residency(#[from] ResidencySpeculatorError),
46    /// Promotion policy, planning, feedback, or accounting failed.
47    #[error(transparent)]
48    Promotion(#[from] PromotionSpeculatorError),
49}
50
51/// Runtime-neutral owner of one volume generation's two speculation engines.
52///
53/// The controller performs no I/O and spawns no work. It gives embedded,
54/// browser, daemon, and fleet adapters one atomic transition surface while
55/// execution remains pull-based through `execute_residency` and
56/// `execute_promotion`.
57#[derive(Clone)]
58pub struct SpeculationController {
59    residency: ResidencySpeculator,
60    promotion: PromotionSpeculator,
61}
62
63impl SpeculationController {
64    /// Creates both engines against the same exact volume generation.
65    ///
66    /// # Errors
67    ///
68    /// Rejects an invalid policy without retaining either partial engine.
69    pub fn new(
70        options: SpeculationOptions,
71        volume_id: VolumeId,
72        generation_id: GenerationId,
73    ) -> Result<Self, SpeculationControllerError> {
74        let residency = ResidencySpeculator::new(options.residency, volume_id, generation_id)?;
75        let promotion = PromotionSpeculator::new(options.promotion, volume_id, generation_id)?;
76        Ok(Self {
77            residency,
78            promotion,
79        })
80    }
81
82    /// Records foreground demand and evaluates its already-authenticated
83    /// successor as one all-or-nothing transition.
84    ///
85    /// # Errors
86    ///
87    /// Leaves foreground history and admission state unchanged on exact
88    /// accounting failure.
89    pub fn observe_hint(
90        &mut self,
91        operation_id: OperationId,
92        volume_id: VolumeId,
93        generation_id: GenerationId,
94        foreground_bytes: u64,
95        hint: ResidencyHint,
96    ) -> Result<ResidencyAdmission, SpeculationControllerError> {
97        let mut residency = self.residency.clone();
98        residency.record_foreground(foreground_bytes)?;
99        let admission = residency.admit(ResidencyCandidate::from_hint(
100            operation_id,
101            volume_id,
102            generation_id,
103            hint,
104        ))?;
105        self.residency = residency;
106        Ok(admission)
107    }
108
109    /// Converts an admitted local prediction into a bounded promotion plan.
110    ///
111    /// # Errors
112    ///
113    /// Leaves promotion state unchanged on policy or accounting failure.
114    pub fn plan_promotion(
115        &mut self,
116        permit: ResidencyPermit,
117        accepted_tiers: Vec<StorageTier>,
118        residency: &[ObjectResidency],
119        destinations: &[PromotionDestination],
120    ) -> Result<PromotionAdmission, SpeculationControllerError> {
121        Ok(self.promotion.plan(
122            PromotionCandidate::from_residency(permit, accepted_tiers),
123            residency,
124            destinations,
125        )?)
126    }
127
128    /// Records terminal local-residency usefulness exactly once.
129    ///
130    /// # Errors
131    ///
132    /// Rejects unknown identities or accounting overflow without mutation.
133    pub fn finish_residency(
134        &mut self,
135        operation_id: OperationId,
136        useful: bool,
137    ) -> Result<(), SpeculationControllerError> {
138        Ok(self.residency.finish(operation_id, useful)?)
139    }
140
141    /// Records terminal promotion usefulness exactly once.
142    ///
143    /// # Errors
144    ///
145    /// Rejects unknown identities or accounting overflow without mutation.
146    pub fn finish_promotion(
147        &mut self,
148        operation_id: OperationId,
149        useful: bool,
150    ) -> Result<(), SpeculationControllerError> {
151        Ok(self.promotion.finish(operation_id, useful)?)
152    }
153
154    /// Preempts both engines before recording new foreground traffic.
155    ///
156    /// # Errors
157    ///
158    /// Commits all cancellations and traffic together or changes neither
159    /// engine.
160    pub fn preempt_for_foreground(
161        &mut self,
162        foreground_bytes: u64,
163    ) -> Result<SpeculationPreemption, SpeculationControllerError> {
164        let mut residency = self.residency.clone();
165        let mut promotion = self.promotion.clone();
166        let residency_operations = residency.preempt_for_foreground()?;
167        let promotion_operations = promotion.preempt_for_foreground()?;
168        residency.record_foreground(foreground_bytes)?;
169        self.residency = residency;
170        self.promotion = promotion;
171        Ok(SpeculationPreemption {
172            residency: residency_operations,
173            promotion: promotion_operations,
174        })
175    }
176
177    /// Atomically fences both engines onto a new immutable generation.
178    ///
179    /// # Errors
180    ///
181    /// Leaves both engines on the old generation if either transition fails.
182    pub fn replace_generation(
183        &mut self,
184        generation_id: GenerationId,
185    ) -> Result<SpeculationPreemption, SpeculationControllerError> {
186        let mut residency = self.residency.clone();
187        let mut promotion = self.promotion.clone();
188        let residency_operations = residency.replace_generation(generation_id)?;
189        let promotion_operations = promotion.replace_generation(generation_id)?;
190        self.residency = residency;
191        self.promotion = promotion;
192        Ok(SpeculationPreemption {
193            residency: residency_operations,
194            promotion: promotion_operations,
195        })
196    }
197
198    /// Returns both exact metrics from one committed controller state.
199    #[must_use]
200    pub const fn metrics(&self) -> SpeculationMetrics {
201        SpeculationMetrics {
202            residency: self.residency.metrics(),
203            promotion: self.promotion.metrics(),
204        }
205    }
206
207    /// Borrows the local residency engine for pull-based execution.
208    #[must_use]
209    pub const fn residency(&self) -> &ResidencySpeculator {
210        &self.residency
211    }
212
213    /// Borrows the promotion engine for pull-based execution.
214    #[must_use]
215    pub const fn promotion(&self) -> &PromotionSpeculator {
216        &self.promotion
217    }
218
219    /// Returns one currently active local execution permit.
220    #[must_use]
221    pub fn active_residency_permit(&self, operation_id: OperationId) -> Option<ResidencyPermit> {
222        self.residency.active_permit(operation_id)
223    }
224}
225
226#[cfg(test)]
227#[path = "tests/controller.rs"]
228mod tests;