Skip to main content

ferrum_interfaces/vnext/resource/
recovery.rs

1use super::{
2    invalid_resource, sequence_slot_active, sequence_slot_poisoned_drained,
3    sequence_slot_poisoned_undrained, Arc, AtomicU64, BTreeMap, DeviceRuntime, Mutex, Ordering,
4    PlanRuntimeResources, RequestIdentity, RunId, SequenceAuthorityId, Serialize, StreamState,
5    TrustedPlanRuntimeEvidence, VNextError, SEQUENCE_DISPATCH_POISONED_BIT,
6};
7
8/// Terminal resource disposition produced by an explicit sequence abort.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
10pub enum ActiveSequenceAbortDisposition {
11    SynchronizedAndPoisoned,
12    SequenceSessionTerminalized,
13}
14
15/// Core-signed evidence that the exact active slot epoch was atomically
16/// poisoned. This type is trusted output and has no deserialization or public
17/// construction path.
18#[derive(Debug, Serialize)]
19#[must_use = "sequence abort evidence must be recorded by execution"]
20pub struct ActiveSequenceAbortReceipt {
21    pub(super) plan: TrustedPlanRuntimeEvidence,
22    pub(super) sequence_authority: SequenceAuthorityId,
23    pub(super) run_id: RunId,
24    pub(super) request_id: RequestIdentity,
25    pub(super) activation_epoch: u64,
26    pub(super) runtime_implementation_fingerprint: String,
27    pub(super) disposition: ActiveSequenceAbortDisposition,
28}
29
30impl ActiveSequenceAbortReceipt {
31    pub fn plan(&self) -> &TrustedPlanRuntimeEvidence {
32        &self.plan
33    }
34
35    pub fn run_id(&self) -> &RunId {
36        &self.run_id
37    }
38
39    pub fn request_id(&self) -> &RequestIdentity {
40        &self.request_id
41    }
42
43    pub const fn sequence_authority(&self) -> SequenceAuthorityId {
44        self.sequence_authority
45    }
46
47    pub const fn activation_epoch(&self) -> u64 {
48        self.activation_epoch
49    }
50
51    pub fn runtime_implementation_fingerprint(&self) -> &str {
52        &self.runtime_implementation_fingerprint
53    }
54
55    pub const fn disposition(&self) -> ActiveSequenceAbortDisposition {
56        self.disposition
57    }
58}
59
60#[derive(Debug)]
61pub enum AbandonedSequenceRecoveryError<E> {
62    Contract(VNextError),
63    Runtime(E),
64    StreamStillOwned { slot: u32, activation_epoch: u64 },
65}
66
67#[derive(Clone)]
68pub(super) struct AbandonedSequenceMetadata {
69    pub(super) plan: TrustedPlanRuntimeEvidence,
70    pub(super) sequence_authority: SequenceAuthorityId,
71    pub(super) run_id: RunId,
72    pub(super) request_id: RequestIdentity,
73    pub(super) slot: u32,
74    pub(super) activation_epoch: u64,
75    pub(super) runtime_implementation_fingerprint: String,
76    pub(super) state: Arc<AtomicU64>,
77    pub(super) sequence_dispatch_gate: Arc<AtomicU64>,
78    pub(super) drained: bool,
79}
80
81impl AbandonedSequenceMetadata {
82    pub(super) fn key(&self) -> (u32, u64) {
83        (self.slot, self.activation_epoch)
84    }
85
86    fn abort_receipt(&self) -> ActiveSequenceAbortReceipt {
87        ActiveSequenceAbortReceipt {
88            plan: self.plan.clone(),
89            sequence_authority: self.sequence_authority,
90            run_id: self.run_id.clone(),
91            request_id: self.request_id.clone(),
92            activation_epoch: self.activation_epoch,
93            runtime_implementation_fingerprint: self.runtime_implementation_fingerprint.clone(),
94            disposition: ActiveSequenceAbortDisposition::SynchronizedAndPoisoned,
95        }
96    }
97}
98
99struct AbandonedSequenceRecord<R>
100where
101    R: DeviceRuntime,
102{
103    metadata: AbandonedSequenceMetadata,
104    stream: AbandonedSequenceStream<R::Stream>,
105}
106
107enum AbandonedSequenceStream<S> {
108    ExternallyOwned,
109    Attached(S),
110    Recovering,
111}
112
113pub(super) struct SequenceRecoveryRegistry<R>
114where
115    R: DeviceRuntime,
116{
117    // Records (and their raw streams) must drop before the owning root.
118    records: Mutex<BTreeMap<(u32, u64), AbandonedSequenceRecord<R>>>,
119    _resources: Arc<PlanRuntimeResources<R>>,
120}
121
122impl<R> SequenceRecoveryRegistry<R>
123where
124    R: DeviceRuntime,
125{
126    pub(super) fn new(resources: Arc<PlanRuntimeResources<R>>) -> Self {
127        Self {
128            records: Mutex::new(BTreeMap::new()),
129            _resources: resources,
130        }
131    }
132
133    fn lock_records(
134        &self,
135    ) -> std::sync::MutexGuard<'_, BTreeMap<(u32, u64), AbandonedSequenceRecord<R>>> {
136        self.records
137            .lock()
138            .unwrap_or_else(std::sync::PoisonError::into_inner)
139    }
140
141    pub(super) fn is_empty(&self) -> bool {
142        self.lock_records().is_empty()
143    }
144
145    pub(super) fn register(&self, metadata: AbandonedSequenceMetadata) {
146        let key = metadata.key();
147        let mut records = self.lock_records();
148        if records.contains_key(&key) {
149            debug_assert!(false, "sequence recovery epoch registered twice");
150            return;
151        }
152        records.insert(
153            key,
154            AbandonedSequenceRecord {
155                metadata,
156                stream: AbandonedSequenceStream::ExternallyOwned,
157            },
158        );
159    }
160
161    pub(super) fn attach_stream(&self, key: (u32, u64), stream: R::Stream) {
162        let mut records = self.lock_records();
163        let Some(record) = records.get_mut(&key) else {
164            std::mem::forget(stream);
165            return;
166        };
167        record
168            .metadata
169            .sequence_dispatch_gate
170            .fetch_or(SEQUENCE_DISPATCH_POISONED_BIT, Ordering::AcqRel);
171        match &record.stream {
172            AbandonedSequenceStream::ExternallyOwned => {
173                record.stream = AbandonedSequenceStream::Attached(stream);
174            }
175            AbandonedSequenceStream::Attached(_) | AbandonedSequenceStream::Recovering => {
176                std::mem::forget(stream);
177            }
178        }
179    }
180
181    pub(super) fn set_drained(&self, key: (u32, u64), drained: bool) {
182        let mut records = self.lock_records();
183        let record = records
184            .get_mut(&key)
185            .expect("active sequence recovery metadata remains registered");
186        record.metadata.drained = drained;
187    }
188
189    pub(super) fn clear(&self, key: (u32, u64)) {
190        let removed = self.lock_records().remove(&key);
191        debug_assert!(
192            removed.is_some(),
193            "terminal sequence lost recovery metadata"
194        );
195    }
196
197    pub(super) fn recover(
198        &self,
199        runtime: &Arc<R>,
200        slot: u32,
201    ) -> Result<ActiveSequenceAbortReceipt, AbandonedSequenceRecoveryError<R::Error>> {
202        // Move the raw stream into an explicit Recovering state, then release
203        // the registry before invoking backend code. Concurrent recovery sees
204        // the state and fails closed without blocking on the backend call.
205        let (key, mut stream, was_drained) = {
206            let mut records = self.lock_records();
207            let matching = records
208                .keys()
209                .filter(|(candidate, _)| *candidate == slot)
210                .copied()
211                .collect::<Vec<_>>();
212            if matching.len() != 1 {
213                return Err(AbandonedSequenceRecoveryError::Contract(invalid_resource(
214                    "abandoned sequence recovery requires one exact registered slot epoch",
215                )));
216            }
217            let key = matching[0];
218            let record = records
219                .get_mut(&key)
220                .expect("matching recovery key remains registered");
221            let stream =
222                match std::mem::replace(&mut record.stream, AbandonedSequenceStream::Recovering) {
223                    AbandonedSequenceStream::Attached(stream) => stream,
224                    AbandonedSequenceStream::ExternallyOwned => {
225                        record.stream = AbandonedSequenceStream::ExternallyOwned;
226                        return Err(AbandonedSequenceRecoveryError::StreamStillOwned {
227                            slot,
228                            activation_epoch: record.metadata.activation_epoch,
229                        });
230                    }
231                    AbandonedSequenceStream::Recovering => {
232                        record.stream = AbandonedSequenceStream::Recovering;
233                        return Err(AbandonedSequenceRecoveryError::Contract(invalid_resource(
234                        "abandoned sequence recovery is already in progress for this slot epoch",
235                    )));
236                    }
237                };
238            (key, stream, record.metadata.drained)
239        };
240
241        let backend_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
242            if !was_drained {
243                runtime.synchronize(&mut stream)?;
244            }
245            Ok(runtime.stream_state(&stream) == StreamState::Ready)
246        }));
247        let stream_ready = match backend_result {
248            Ok(Ok(stream_ready)) => stream_ready,
249            Ok(Err(error)) => {
250                self.restore_recovery_stream(key, stream, false);
251                return Err(AbandonedSequenceRecoveryError::Runtime(error));
252            }
253            Err(payload) => {
254                self.restore_recovery_stream(key, stream, false);
255                std::panic::resume_unwind(payload);
256            }
257        };
258        if !stream_ready {
259            self.restore_recovery_stream(key, stream, false);
260            return Err(AbandonedSequenceRecoveryError::Contract(invalid_resource(
261                "abandoned sequence synchronization did not drain its stream",
262            )));
263        }
264
265        let mut records = self.lock_records();
266        let Some(record) = records.get_mut(&key) else {
267            std::mem::forget(stream);
268            return Err(AbandonedSequenceRecoveryError::Contract(invalid_resource(
269                "abandoned sequence recovery registration disappeared while synchronization was in progress",
270            )));
271        };
272        if !matches!(record.stream, AbandonedSequenceStream::Recovering) {
273            std::mem::forget(stream);
274            return Err(AbandonedSequenceRecoveryError::Contract(invalid_resource(
275                "abandoned sequence recovery ownership changed while synchronization was in progress",
276            )));
277        }
278        record.metadata.drained = true;
279        let expected_active = sequence_slot_active(record.metadata.activation_epoch);
280        let expected_undrained = sequence_slot_poisoned_undrained(record.metadata.activation_epoch);
281        let expected_drained = sequence_slot_poisoned_drained(record.metadata.activation_epoch);
282        let actual = record.metadata.state.load(Ordering::Acquire);
283        if actual == expected_active || actual == expected_undrained {
284            if record
285                .metadata
286                .state
287                .compare_exchange(
288                    actual,
289                    expected_drained,
290                    Ordering::AcqRel,
291                    Ordering::Acquire,
292                )
293                .is_err()
294            {
295                record.stream = AbandonedSequenceStream::Attached(stream);
296                return Err(AbandonedSequenceRecoveryError::Contract(invalid_resource(
297                    "abandoned sequence epoch changed during recovery",
298                )));
299            }
300        } else if actual != expected_drained {
301            record.stream = AbandonedSequenceStream::Attached(stream);
302            return Err(AbandonedSequenceRecoveryError::Contract(invalid_resource(
303                "abandoned sequence recovery does not own an active or poisoned registered slot epoch",
304            )));
305        }
306        record
307            .metadata
308            .sequence_dispatch_gate
309            .fetch_or(SEQUENCE_DISPATCH_POISONED_BIT, Ordering::AcqRel);
310
311        let receipt = record.metadata.abort_receipt();
312        let removed = records.remove(&key);
313        drop(records);
314        drop(removed);
315        drop(stream);
316        Ok(receipt)
317    }
318
319    fn restore_recovery_stream(&self, key: (u32, u64), stream: R::Stream, drained: bool) {
320        let mut records = self.lock_records();
321        let Some(record) = records.get_mut(&key) else {
322            std::mem::forget(stream);
323            return;
324        };
325        if matches!(record.stream, AbandonedSequenceStream::Recovering) {
326            record.metadata.drained = drained;
327            record.stream = AbandonedSequenceStream::Attached(stream);
328        } else {
329            std::mem::forget(stream);
330        }
331    }
332
333    pub(super) fn recover_all_for_owner_drop(
334        &self,
335        runtime: &Arc<R>,
336    ) -> Result<(), AbandonedSequenceRecoveryError<R::Error>> {
337        loop {
338            let slot = {
339                let records = self.lock_records();
340                records.keys().next().map(|(slot, _)| *slot)
341            };
342            let Some(slot) = slot else {
343                return Ok(());
344            };
345            let _ = self.recover(runtime, slot)?;
346        }
347    }
348}