ferrum_interfaces/vnext/resource/
recovery.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
10pub enum ActiveSequenceAbortDisposition {
11 SynchronizedAndPoisoned,
12 SequenceSessionTerminalized,
13}
14
15#[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: 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 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}