Skip to main content

ferrum_interfaces/vnext/resource/
execution_session.rs

1//! Step admission and bound execution-stream lifecycle.
2
3use super::{
4    acquire_session_frames_with_backing, enter_sequence_dispatch, fmt, invalid_resource,
5    issue_batch_step_id, record_step_admission_profile, sequence_dispatch_is_poisoned,
6    sequence_slot_active, sequence_slot_is_poisoned, sequence_slot_poisoned_drained,
7    sequence_slot_poisoned_undrained, session_frame_capture_candidates,
8    step_admission_profile_start, AbandonedSequenceMetadata, AbandonedSequenceRecoveryError,
9    ActiveSequenceAbortDisposition, ActiveSequenceAbortReceipt, AdmissionFitPolicy,
10    AdmittedSequenceResources, AdmittedStepParticipant, AllocationLifetime, Arc, AtomicU64,
11    BatchCapacityClaimDecision, BatchParticipantAuthority, BoundExecutionStream,
12    BoundExecutionStreamState, ClaimedBackingTransaction, DeviceRuntime,
13    ExecutionBatchParticipants, ExecutionLane, ExecutionStreamCreationError,
14    LaneBackingPrepareDecision, LogicalAdmissionCoordinatorId, LogicalBackingSliceAuthority,
15    Ordering, RequestIdentity, RunId, SequenceAuthorityId, SequenceBackingSnapshot,
16    SequenceExecutionAuthoritySource, Serialize, StepAdmissionBackingDeferral,
17    StepResourceAdmissionDecision, StepResourceAdmissionProfilePhase, StepResourceAdmissionRequest,
18    StepResourceLease, StreamState, TrustedPlanRuntimeEvidence, VNextError,
19    SEQUENCE_DISPATCH_POISONED_BIT,
20};
21use std::time::Duration;
22
23impl<R> ExecutionBatchParticipants<R>
24where
25    R: DeviceRuntime,
26{
27    pub fn try_begin_step(
28        &self,
29        request: StepResourceAdmissionRequest,
30        lane: &Arc<ExecutionLane<R>>,
31    ) -> Result<StepResourceAdmissionDecision<R>, VNextError> {
32        self.try_begin_step_inner::<false, _>(request, lane, |_, _| {})
33    }
34
35    pub fn try_begin_step_profiled<F>(
36        &self,
37        request: StepResourceAdmissionRequest,
38        lane: &Arc<ExecutionLane<R>>,
39        observer: F,
40    ) -> Result<StepResourceAdmissionDecision<R>, VNextError>
41    where
42        F: FnMut(StepResourceAdmissionProfilePhase, Duration),
43    {
44        self.try_begin_step_inner::<true, _>(request, lane, observer)
45    }
46
47    fn try_begin_step_inner<const PROFILE: bool, F>(
48        &self,
49        request: StepResourceAdmissionRequest,
50        lane: &Arc<ExecutionLane<R>>,
51        mut observer: F,
52    ) -> Result<StepResourceAdmissionDecision<R>, VNextError>
53    where
54        F: FnMut(StepResourceAdmissionProfilePhase, Duration),
55    {
56        let phase_started = step_admission_profile_start::<PROFILE>();
57        let _lifecycle = self.sessions[0]
58            .resources()
59            .request
60            .plan
61            .resources
62            .read_lifecycle("begin an execution step")?;
63        let StepResourceAdmissionRequest {
64            work_shape,
65            fit_policy,
66            pressure_action,
67            reusable_execution_bucket_id,
68        } = request;
69        let work_fingerprint = work_shape.fingerprint().to_owned();
70        let expected_participants = self
71            .sessions
72            .iter()
73            .map(|session| {
74                BatchParticipantAuthority::new(
75                    session.sequence_authority(),
76                    session.request_authority(),
77                )
78            })
79            .collect::<Vec<_>>();
80        if work_shape.participants() != expected_participants {
81            return Err(invalid_resource(
82                "step work authority differs from its exact participant set",
83            ));
84        }
85        let immediate_shape = work_shape.immediate_shape();
86        let fit_shape = match fit_policy {
87            AdmissionFitPolicy::ImmediateOnly => immediate_shape,
88            AdmissionFitPolicy::FullInputMustFit => work_shape.fit_shape(),
89        };
90        let plan = &self.sessions[0].resources().request.plan;
91        if !Arc::ptr_eq(plan.runtime(), lane.runtime_arc())
92            || plan.runtime().descriptor() != lane.descriptor()
93            || !lane.is_reusable()
94        {
95            return Err(invalid_resource(
96                "step admission requires the reusable execution lane bound to its plan runtime",
97            ));
98        }
99        let reusable_execution_bucket = reusable_execution_bucket_id
100            .as_ref()
101            .map(|bucket_id| {
102                plan.reusable_execution_bucket(bucket_id)
103                    .map(|resolved| resolved.bucket().clone())
104                    .ok_or_else(|| {
105                        invalid_resource(
106                            "step reusable execution bucket is not owned by its immutable plan",
107                        )
108                    })
109            })
110            .transpose()?;
111        if reusable_execution_bucket.as_ref().is_some_and(|bucket| {
112            let capacity = bucket.capacity();
113            !capacity.covers(fit_shape.sequences(), fit_shape.tokens(), fit_shape.pages())
114        }) {
115            return Err(invalid_resource(
116                "step work shape exceeds its selected reusable execution bucket",
117            ));
118        }
119        record_step_admission_profile::<PROFILE, _>(
120            &mut observer,
121            StepResourceAdmissionProfilePhase::AuthorityAndPolicyValidate,
122            phase_started,
123        );
124
125        let phase_started = step_admission_profile_start::<PROFILE>();
126        let (demand, requested_slices) = plan.scoped_demand(
127            AllocationLifetime::Step,
128            None,
129            immediate_shape,
130            fit_shape,
131            reusable_execution_bucket.as_ref(),
132            fit_policy,
133            pressure_action,
134        )?;
135        record_step_admission_profile::<PROFILE, _>(
136            &mut observer,
137            StepResourceAdmissionProfilePhase::DemandEvaluate,
138            phase_started,
139        );
140
141        let phase_started = step_admission_profile_start::<PROFILE>();
142        let prepared = match plan.prepare_lane_stable_backing_slices(lane, requested_slices)? {
143            LaneBackingPrepareDecision::Prepared(prepared) => prepared,
144            LaneBackingPrepareDecision::Deferred(deferred) => {
145                record_step_admission_profile::<PROFILE, _>(
146                    &mut observer,
147                    StepResourceAdmissionProfilePhase::BackingClaim,
148                    phase_started,
149                );
150                return Ok(StepResourceAdmissionDecision::BackingDeferred(
151                    StepAdmissionBackingDeferral::new(
152                        deferred,
153                        self.sessions.clone(),
154                        work_fingerprint,
155                    )?,
156                ));
157            }
158        };
159        record_step_admission_profile::<PROFILE, _>(
160            &mut observer,
161            StepResourceAdmissionProfilePhase::BackingClaim,
162            phase_started,
163        );
164
165        let phase_started = step_admission_profile_start::<PROFILE>();
166        let logical_capacity = if demand.immediate_claim().is_empty() {
167            None
168        } else {
169            let parents = self
170                .sessions
171                .iter()
172                .map(|session| session.resources().logical_lease())
173                .collect::<Vec<_>>();
174            match plan
175                .logical_admission()
176                .try_claim_for_sequences(&parents, &demand)?
177            {
178                BatchCapacityClaimDecision::Claimed(capacity) => {
179                    let parents_match = capacity
180                        .parents()
181                        .iter()
182                        .map(|parent| (parent.sequence(), parent.request()))
183                        .eq(self.sessions.iter().map(|session| {
184                            (session.sequence_authority(), session.request_authority())
185                        }));
186                    if !plan
187                        .logical_admission()
188                        .owns_batch_capacity_claim(&capacity)
189                        || !parents_match
190                    {
191                        return Err(invalid_resource(
192                            "step admission returned capacity for another participant set",
193                        ));
194                    }
195                    Some(capacity)
196                }
197                BatchCapacityClaimDecision::Deferred(deferred) => {
198                    record_step_admission_profile::<PROFILE, _>(
199                        &mut observer,
200                        StepResourceAdmissionProfilePhase::LogicalCapacityClaim,
201                        phase_started,
202                    );
203                    return Ok(StepResourceAdmissionDecision::Deferred(deferred));
204                }
205                BatchCapacityClaimDecision::PermanentRejected(rejected) => {
206                    record_step_admission_profile::<PROFILE, _>(
207                        &mut observer,
208                        StepResourceAdmissionProfilePhase::LogicalCapacityClaim,
209                        phase_started,
210                    );
211                    return Ok(StepResourceAdmissionDecision::PermanentRejected(rejected));
212                }
213            }
214        };
215        record_step_admission_profile::<PROFILE, _>(
216            &mut observer,
217            StepResourceAdmissionProfilePhase::LogicalCapacityClaim,
218            phase_started,
219        );
220
221        let phase_started = step_admission_profile_start::<PROFILE>();
222        let committed_backing = prepared.commit();
223        let claimed_backing = ClaimedBackingTransaction::new_lane_stable(
224            work_shape,
225            demand,
226            logical_capacity,
227            committed_backing,
228        )?;
229        record_step_admission_profile::<PROFILE, _>(
230            &mut observer,
231            StepResourceAdmissionProfilePhase::TransactionValidateAndFingerprint,
232            phase_started,
233        );
234
235        let phase_started = step_admission_profile_start::<PROFILE>();
236        let batch_step_id = issue_batch_step_id()?;
237        let candidates = session_frame_capture_candidates(&self.sessions);
238        let captured_frames = acquire_session_frames_with_backing(&candidates, batch_step_id)?;
239        let participants = self
240            .sessions
241            .iter()
242            .cloned()
243            .zip(captured_frames)
244            .map(|(session, captured)| AdmittedStepParticipant {
245                frame: captured.hold,
246                backing_snapshot: captured.backing_snapshot,
247                session,
248            })
249            .collect();
250        let decision = StepResourceAdmissionDecision::Admitted(Arc::new(StepResourceLease::new(
251            participants,
252            Arc::clone(lane),
253            reusable_execution_bucket,
254            batch_step_id,
255            claimed_backing,
256        )?));
257        record_step_admission_profile::<PROFILE, _>(
258            &mut observer,
259            StepResourceAdmissionProfilePhase::FrameCaptureAndLease,
260            phase_started,
261        );
262        Ok(decision)
263    }
264}
265
266impl<R> AdmittedSequenceResources<R>
267where
268    R: DeviceRuntime,
269{
270    fn validate_runtime(&self, context: &'static str) -> Result<(), VNextError> {
271        let descriptor = self.request.plan.runtime().descriptor();
272        descriptor.validate()?;
273        if descriptor.id != *self.request.plan.device_id()
274            || descriptor.runtime_implementation_fingerprint
275                != self.request.plan.runtime_implementation_fingerprint()
276        {
277            return Err(invalid_resource(format!(
278                "{context} runtime differs from the trusted plan/runtime binding"
279            )));
280        }
281        Ok(())
282    }
283
284    pub fn create_execution_stream(
285        self: &Arc<Self>,
286    ) -> Result<BoundExecutionStream<R>, ExecutionStreamCreationError<R::Error>> {
287        let _lifecycle = self
288            .request
289            .plan
290            .resources
291            .read_lifecycle("create an execution stream")
292            .map_err(ExecutionStreamCreationError::Contract)?;
293        if self.is_poisoned() {
294            return Err(ExecutionStreamCreationError::Contract(invalid_resource(
295                "poisoned logical sequence cannot create an execution stream",
296            )));
297        }
298        self.validate_runtime("execution stream creation preflight")
299            .map_err(ExecutionStreamCreationError::Contract)?;
300        let stream = self
301            .request
302            .plan
303            .runtime()
304            .create_stream()
305            .map_err(ExecutionStreamCreationError::Runtime)?;
306        self.validate_runtime("execution stream creation completion")
307            .map_err(ExecutionStreamCreationError::Contract)?;
308        if self.request.plan.runtime().stream_state(&stream) != StreamState::Ready {
309            return Err(ExecutionStreamCreationError::Contract(invalid_resource(
310                "new execution stream is not ready",
311            )));
312        }
313        Ok(BoundExecutionStream {
314            runtime: Arc::clone(self.request.plan.runtime()),
315            coordinator_id: self.coordinator_id(),
316            sequence_authority: self.sequence_authority(),
317            stream: Some(stream),
318            state: BoundExecutionStreamState::Ready,
319            sequence_recovery: Arc::clone(&self.sequence_recovery),
320            sequence_dispatch_gate: Arc::clone(&self.sequence_dispatch_gate),
321            abandoned_sequence: None,
322            resources: Arc::clone(self),
323        })
324    }
325
326    pub fn activate<'resources, 'exec>(
327        &'resources self,
328        stream: &'exec mut BoundExecutionStream<R>,
329    ) -> Result<ActiveSequencePermit<'resources, 'exec, R>, VNextError> {
330        let _lifecycle = self
331            .request
332            .plan
333            .resources
334            .read_lifecycle("activate an execution stream")?;
335        if self.is_poisoned() {
336            return Err(invalid_resource(
337                "poisoned logical sequence cannot be activated",
338            ));
339        }
340        self.validate_runtime("logical sequence activation")?;
341        if !Arc::ptr_eq(self.request.plan.runtime(), &stream.runtime)
342            || !std::ptr::eq(self, Arc::as_ref(&stream.resources))
343            || stream.coordinator_id != self.coordinator_id()
344            || stream.sequence_authority != self.sequence_authority()
345            || !Arc::ptr_eq(&self.sequence_recovery, &stream.sequence_recovery)
346            || !Arc::ptr_eq(&self.sequence_dispatch_gate, &stream.sequence_dispatch_gate)
347        {
348            return Err(invalid_resource(
349                "execution stream belongs to another logical sequence authority",
350            ));
351        }
352        if stream.state != BoundExecutionStreamState::Ready
353            || stream.abandoned_sequence.is_some()
354            || self.request.plan.runtime().stream_state(stream.stream()) != StreamState::Ready
355        {
356            return Err(invalid_resource(
357                "logical sequence activation requires one core-ready stream",
358            ));
359        }
360        let mut authority_source = self.lock_authority_source()?;
361        let selecting_legacy = match *authority_source {
362            SequenceExecutionAuthoritySource::Unselected => true,
363            SequenceExecutionAuthoritySource::LegacyStream => false,
364            SequenceExecutionAuthoritySource::SequenceSession => {
365                return Err(invalid_resource(
366                    "logical sequence execution authority is permanently selected for sequence sessions",
367                ));
368            }
369            SequenceExecutionAuthoritySource::FailClosed => {
370                return Err(invalid_resource(
371                    "logical sequence execution authority selector is fail-closed",
372                ));
373            }
374        };
375        let backing_snapshot = self.backing_snapshot()?;
376        let epoch = match self.next_activation_epoch.fetch_update(
377            Ordering::AcqRel,
378            Ordering::Acquire,
379            |epoch| epoch.checked_add(1).filter(|next| *next <= (u64::MAX >> 2)),
380        ) {
381            Ok(epoch) => epoch,
382            Err(_) => {
383                *authority_source = SequenceExecutionAuthoritySource::FailClosed;
384                return Err(invalid_resource("active sequence epoch space is exhausted"));
385            }
386        };
387        let active_state = sequence_slot_active(epoch);
388        if let Err(actual) =
389            self.state
390                .compare_exchange(0, active_state, Ordering::AcqRel, Ordering::Acquire)
391        {
392            if selecting_legacy {
393                *authority_source = SequenceExecutionAuthoritySource::FailClosed;
394            }
395            return Err(if sequence_slot_is_poisoned(actual) {
396                invalid_resource("logical sequence was abandoned and is poisoned")
397            } else {
398                invalid_resource("logical sequence already owns an active stream")
399            });
400        }
401        let slot = self.sequence_authority().sparse_id();
402        let recovery_metadata = AbandonedSequenceMetadata {
403            plan: self.request.plan.evidence(),
404            sequence_authority: self.sequence_authority(),
405            run_id: self.run_id().clone(),
406            request_id: self.request_id().clone(),
407            slot,
408            activation_epoch: epoch,
409            runtime_implementation_fingerprint: self
410                .request
411                .plan
412                .runtime_implementation_fingerprint()
413                .to_owned(),
414            state: Arc::clone(&self.state),
415            sequence_dispatch_gate: Arc::clone(&self.sequence_dispatch_gate),
416            drained: false,
417        };
418        let recovery_key = recovery_metadata.key();
419        self.sequence_recovery.register(recovery_metadata);
420        stream.abandoned_sequence = Some(recovery_key);
421        stream.state = BoundExecutionStreamState::InUse;
422        *authority_source = SequenceExecutionAuthoritySource::LegacyStream;
423        Ok(ActiveSequencePermit {
424            resources: self,
425            backing_snapshot,
426            epoch,
427            state: Arc::clone(&self.state),
428            stream,
429            runtime_fingerprint: self
430                .request
431                .plan
432                .runtime_implementation_fingerprint()
433                .to_owned(),
434            stream_drained: false,
435            completed: false,
436        })
437    }
438
439    pub fn recover_abandoned_sequence(
440        &self,
441    ) -> Result<ActiveSequenceAbortReceipt, AbandonedSequenceRecoveryError<R::Error>> {
442        self.sequence_recovery.recover(
443            self.request.plan.runtime(),
444            self.sequence_authority().sparse_id(),
445        )
446    }
447}
448
449/// Non-cloneable guard for an admitted active-sequence slot. Dispatch borrows
450/// this permit; the sequence owner retains it until all asynchronous work is
451/// synchronized or cancelled.
452#[must_use = "an active sequence permit must live until asynchronous work is complete"]
453pub struct ActiveSequencePermit<'resources, 'exec, R>
454where
455    R: DeviceRuntime,
456{
457    resources: &'resources AdmittedSequenceResources<R>,
458    backing_snapshot: Arc<SequenceBackingSnapshot<R>>,
459    epoch: u64,
460    state: Arc<AtomicU64>,
461    stream: &'exec mut BoundExecutionStream<R>,
462    runtime_fingerprint: String,
463    stream_drained: bool,
464    completed: bool,
465}
466
467impl<'resources, 'exec, R> ActiveSequencePermit<'resources, 'exec, R>
468where
469    R: DeviceRuntime,
470{
471    pub fn resources(&self) -> &'resources AdmittedSequenceResources<R> {
472        self.resources
473    }
474
475    pub fn run_id(&self) -> &RunId {
476        self.resources.run_id()
477    }
478
479    pub fn request_id(&self) -> &RequestIdentity {
480        self.resources.request_id()
481    }
482
483    pub fn sequence_authority(&self) -> SequenceAuthorityId {
484        self.resources.sequence_authority()
485    }
486
487    pub fn coordinator_id(&self) -> LogicalAdmissionCoordinatorId {
488        self.resources.coordinator_id()
489    }
490
491    pub fn backing_slices(&self) -> &[LogicalBackingSliceAuthority] {
492        self.backing_snapshot.backing_slices()
493    }
494
495    pub const fn activation_epoch(&self) -> u64 {
496        self.epoch
497    }
498
499    pub fn runtime_implementation_fingerprint(&self) -> &str {
500        &self.runtime_fingerprint
501    }
502
503    pub(crate) fn with_runtime_and_stream<T>(
504        &mut self,
505        action: impl FnOnce(&R, &mut R::Stream) -> T,
506    ) -> Result<T, VNextError> {
507        if self.stream.state != BoundExecutionStreamState::InUse {
508            return Err(invalid_resource(
509                "operation dispatch requires one core-owned in-use stream",
510            ));
511        }
512        let _dispatch_guard = enter_sequence_dispatch(&self.resources.sequence_dispatch_gate)?;
513        Ok(action(
514            self.resources.request.plan.runtime(),
515            self.stream.stream_mut(),
516        ))
517    }
518
519    /// Consumes dispatch authority before draining the exact bound stream.
520    /// Successful synchronization returns a different typestate that cannot
521    /// be passed back to `OperationDispatch`.
522    pub fn synchronize(
523        mut self,
524    ) -> Result<
525        SynchronizedSequencePermit<'resources, 'exec, R>,
526        SequenceSynchronizationFailure<'resources, 'exec, R>,
527    > {
528        let preflight = self
529            .resources
530            .validate_runtime("sequence synchronization preflight")
531            .and_then(|()| {
532                if self
533                    .resources
534                    .request
535                    .plan
536                    .runtime()
537                    .descriptor()
538                    .runtime_implementation_fingerprint
539                    == self.runtime_fingerprint
540                {
541                    Ok(())
542                } else {
543                    Err(invalid_resource(
544                        "sequence synchronization runtime differs from its activation snapshot",
545                    ))
546                }
547            });
548
549        // Draining is attempted even when descriptor validation fails. The
550        // stream/runtime pair is privately bound, while skipping the drain
551        // could make later buffer quarantine unsafe.
552        let runtime_error = match self
553            .resources
554            .request
555            .plan
556            .runtime()
557            .synchronize(self.stream.stream_mut())
558        {
559            Ok(()) => None,
560            Err(error) => Some(error),
561        };
562        let stream_ready = self
563            .resources
564            .request
565            .plan
566            .runtime()
567            .stream_state(self.stream.stream())
568            == StreamState::Ready;
569        self.stream_drained = runtime_error.is_none() && stream_ready;
570        if self.stream_drained {
571            self.stream
572                .sequence_recovery
573                .set_drained((self.sequence_authority().sparse_id(), self.epoch), true);
574        }
575        let completion = self
576            .resources
577            .validate_runtime("sequence synchronization completion")
578            .and_then(|()| {
579                if stream_ready {
580                    Ok(())
581                } else {
582                    Err(invalid_resource(
583                        "sequence synchronization did not return the bound stream to ready",
584                    ))
585                }
586            });
587        let error = preflight
588            .err()
589            .map(SequenceSynchronizationError::Contract)
590            .or_else(|| runtime_error.map(SequenceSynchronizationError::Runtime))
591            .or_else(|| completion.err().map(SequenceSynchronizationError::Contract));
592        if let Some(error) = error {
593            return Err(SequenceSynchronizationFailure {
594                permit: Some(self),
595                error,
596            });
597        }
598        self.stream.state = BoundExecutionStreamState::Ready;
599        Ok(SynchronizedSequencePermit { permit: Some(self) })
600    }
601}
602
603#[derive(Debug)]
604pub enum SequenceSynchronizationError<E> {
605    Contract(VNextError),
606    Runtime(E),
607}
608
609/// Retry owner for a failed stream drain. It intentionally does not expose
610/// the active dispatch permit, so no operation can be submitted between a
611/// failed synchronization attempt and its retry.
612#[must_use = "failed sequence synchronization must be retried or retained"]
613pub struct SequenceSynchronizationFailure<'resources, 'exec, R>
614where
615    R: DeviceRuntime,
616{
617    permit: Option<ActiveSequencePermit<'resources, 'exec, R>>,
618    error: SequenceSynchronizationError<R::Error>,
619}
620
621impl<R> fmt::Debug for SequenceSynchronizationFailure<'_, '_, R>
622where
623    R: DeviceRuntime,
624{
625    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
626        formatter
627            .debug_struct("SequenceSynchronizationFailure")
628            .field("error", &self.error)
629            .finish_non_exhaustive()
630    }
631}
632
633impl<'resources, 'exec, R> SequenceSynchronizationFailure<'resources, 'exec, R>
634where
635    R: DeviceRuntime,
636{
637    pub fn error(&self) -> &SequenceSynchronizationError<R::Error> {
638        &self.error
639    }
640
641    pub fn retry(
642        mut self,
643    ) -> Result<
644        SynchronizedSequencePermit<'resources, 'exec, R>,
645        SequenceSynchronizationFailure<'resources, 'exec, R>,
646    > {
647        self.permit
648            .take()
649            .expect("synchronization failure owns its active permit")
650            .synchronize()
651    }
652}
653
654/// Stream-drained typestate. It has no dispatch API and must choose exactly
655/// one terminal slot disposition.
656#[must_use = "a synchronized sequence must be completed or aborted"]
657pub struct SynchronizedSequencePermit<'resources, 'exec, R>
658where
659    R: DeviceRuntime,
660{
661    permit: Option<ActiveSequencePermit<'resources, 'exec, R>>,
662}
663
664impl<R> SynchronizedSequencePermit<'_, '_, R>
665where
666    R: DeviceRuntime,
667{
668    pub fn complete(mut self) -> Result<ActiveSequenceCompletionReceipt, VNextError> {
669        let mut permit = self
670            .permit
671            .take()
672            .expect("synchronized sequence owns its active permit");
673        let sequence_poisoned =
674            sequence_dispatch_is_poisoned(&permit.resources.sequence_dispatch_gate);
675        let terminal_state = if sequence_poisoned {
676            sequence_slot_poisoned_drained(permit.epoch)
677        } else {
678            0
679        };
680        permit
681            .state
682            .compare_exchange(
683                sequence_slot_active(permit.epoch),
684                terminal_state,
685                Ordering::AcqRel,
686                Ordering::Acquire,
687            )
688            .map_err(|_| invalid_resource("active sequence epoch is no longer completable"))?;
689        permit
690            .stream
691            .sequence_recovery
692            .clear((permit.sequence_authority().sparse_id(), permit.epoch));
693        permit.stream.abandoned_sequence = None;
694        permit.stream.state = BoundExecutionStreamState::Ready;
695        permit.completed = true;
696        if sequence_poisoned {
697            return Err(invalid_resource(
698                "sequence cannot complete successfully after its dispatch authority was poisoned",
699            ));
700        }
701        Ok(ActiveSequenceCompletionReceipt {
702            plan: permit.resources.request.plan.evidence(),
703            sequence_authority: permit.sequence_authority(),
704            run_id: permit.run_id().clone(),
705            request_id: permit.request_id().clone(),
706            activation_epoch: permit.epoch,
707            runtime_implementation_fingerprint: permit.runtime_fingerprint.clone(),
708        })
709    }
710
711    /// Produces abort evidence only after the exact bound stream was drained.
712    /// Only this exact logical sequence remains poisoned after abort.
713    pub fn abort(mut self) -> Result<ActiveSequenceAbortReceipt, VNextError> {
714        let mut permit = self
715            .permit
716            .take()
717            .expect("synchronized sequence owns its active permit");
718        permit
719            .state
720            .compare_exchange(
721                sequence_slot_active(permit.epoch),
722                sequence_slot_poisoned_drained(permit.epoch),
723                Ordering::AcqRel,
724                Ordering::Acquire,
725            )
726            .map_err(|_| invalid_resource("active sequence epoch is no longer abortable"))?;
727        permit
728            .resources
729            .sequence_dispatch_gate
730            .fetch_or(SEQUENCE_DISPATCH_POISONED_BIT, Ordering::AcqRel);
731        permit
732            .stream
733            .sequence_recovery
734            .clear((permit.sequence_authority().sparse_id(), permit.epoch));
735        permit.stream.abandoned_sequence = None;
736        permit.stream.state = BoundExecutionStreamState::Ready;
737        permit.completed = true;
738        Ok(ActiveSequenceAbortReceipt {
739            plan: permit.resources.request.plan.evidence(),
740            sequence_authority: permit.sequence_authority(),
741            run_id: permit.run_id().clone(),
742            request_id: permit.request_id().clone(),
743            activation_epoch: permit.epoch,
744            runtime_implementation_fingerprint: permit.runtime_fingerprint.clone(),
745            disposition: ActiveSequenceAbortDisposition::SynchronizedAndPoisoned,
746        })
747    }
748}
749
750/// Core-signed evidence that synchronization succeeded and the exact active
751/// slot epoch was atomically cleared. It is trusted output and deliberately
752/// cannot be deserialized or constructed by a caller.
753#[derive(Debug, Serialize)]
754#[must_use = "sequence completion evidence must be recorded by execution"]
755pub struct ActiveSequenceCompletionReceipt {
756    plan: TrustedPlanRuntimeEvidence,
757    sequence_authority: SequenceAuthorityId,
758    run_id: RunId,
759    request_id: RequestIdentity,
760    activation_epoch: u64,
761    runtime_implementation_fingerprint: String,
762}
763
764impl ActiveSequenceCompletionReceipt {
765    pub fn plan(&self) -> &TrustedPlanRuntimeEvidence {
766        &self.plan
767    }
768
769    pub fn run_id(&self) -> &RunId {
770        &self.run_id
771    }
772
773    pub fn request_id(&self) -> &RequestIdentity {
774        &self.request_id
775    }
776
777    pub const fn sequence_authority(&self) -> SequenceAuthorityId {
778        self.sequence_authority
779    }
780
781    pub const fn activation_epoch(&self) -> u64 {
782        self.activation_epoch
783    }
784
785    pub fn runtime_implementation_fingerprint(&self) -> &str {
786        &self.runtime_implementation_fingerprint
787    }
788}
789
790impl<R> Drop for ActiveSequencePermit<'_, '_, R>
791where
792    R: DeviceRuntime,
793{
794    fn drop(&mut self) {
795        if !self.completed {
796            let poisoned_state = if self.stream_drained {
797                sequence_slot_poisoned_drained(self.epoch)
798            } else {
799                sequence_slot_poisoned_undrained(self.epoch)
800            };
801            let result = self.state.compare_exchange(
802                sequence_slot_active(self.epoch),
803                poisoned_state,
804                Ordering::AcqRel,
805                Ordering::Acquire,
806            );
807            debug_assert!(result.is_ok(), "active sequence slot guard lost ownership");
808            if result.is_ok() {
809                self.resources
810                    .sequence_dispatch_gate
811                    .fetch_or(SEQUENCE_DISPATCH_POISONED_BIT, Ordering::AcqRel);
812                self.stream.state = BoundExecutionStreamState::Poisoned;
813                self.stream.sequence_recovery.set_drained(
814                    (self.sequence_authority().sparse_id(), self.epoch),
815                    self.stream_drained,
816                );
817            }
818        }
819    }
820}