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(
239            &candidates,
240            batch_step_id,
241            claimed_backing.work_shape(),
242        )?;
243        let participants = self
244            .sessions
245            .iter()
246            .cloned()
247            .zip(captured_frames)
248            .map(|(session, captured)| AdmittedStepParticipant {
249                frame: captured.hold,
250                backing_snapshot: captured.backing_snapshot,
251                session,
252            })
253            .collect();
254        let decision = StepResourceAdmissionDecision::Admitted(Arc::new(StepResourceLease::new(
255            participants,
256            Arc::clone(lane),
257            reusable_execution_bucket,
258            batch_step_id,
259            claimed_backing,
260        )?));
261        record_step_admission_profile::<PROFILE, _>(
262            &mut observer,
263            StepResourceAdmissionProfilePhase::FrameCaptureAndLease,
264            phase_started,
265        );
266        Ok(decision)
267    }
268}
269
270impl<R> AdmittedSequenceResources<R>
271where
272    R: DeviceRuntime,
273{
274    fn validate_runtime(&self, context: &'static str) -> Result<(), VNextError> {
275        let descriptor = self.request.plan.runtime().descriptor();
276        descriptor.validate()?;
277        if descriptor.id != *self.request.plan.device_id()
278            || descriptor.runtime_implementation_fingerprint
279                != self.request.plan.runtime_implementation_fingerprint()
280        {
281            return Err(invalid_resource(format!(
282                "{context} runtime differs from the trusted plan/runtime binding"
283            )));
284        }
285        Ok(())
286    }
287
288    pub fn create_execution_stream(
289        self: &Arc<Self>,
290    ) -> Result<BoundExecutionStream<R>, ExecutionStreamCreationError<R::Error>> {
291        let _lifecycle = self
292            .request
293            .plan
294            .resources
295            .read_lifecycle("create an execution stream")
296            .map_err(ExecutionStreamCreationError::Contract)?;
297        if self.is_poisoned() {
298            return Err(ExecutionStreamCreationError::Contract(invalid_resource(
299                "poisoned logical sequence cannot create an execution stream",
300            )));
301        }
302        self.validate_runtime("execution stream creation preflight")
303            .map_err(ExecutionStreamCreationError::Contract)?;
304        let stream = self
305            .request
306            .plan
307            .runtime()
308            .create_stream()
309            .map_err(ExecutionStreamCreationError::Runtime)?;
310        self.validate_runtime("execution stream creation completion")
311            .map_err(ExecutionStreamCreationError::Contract)?;
312        if self.request.plan.runtime().stream_state(&stream) != StreamState::Ready {
313            return Err(ExecutionStreamCreationError::Contract(invalid_resource(
314                "new execution stream is not ready",
315            )));
316        }
317        Ok(BoundExecutionStream {
318            runtime: Arc::clone(self.request.plan.runtime()),
319            coordinator_id: self.coordinator_id(),
320            sequence_authority: self.sequence_authority(),
321            stream: Some(stream),
322            state: BoundExecutionStreamState::Ready,
323            sequence_recovery: Arc::clone(&self.sequence_recovery),
324            sequence_dispatch_gate: Arc::clone(&self.sequence_dispatch_gate),
325            abandoned_sequence: None,
326            resources: Arc::clone(self),
327        })
328    }
329
330    pub fn activate<'resources, 'exec>(
331        &'resources self,
332        stream: &'exec mut BoundExecutionStream<R>,
333    ) -> Result<ActiveSequencePermit<'resources, 'exec, R>, VNextError> {
334        let _lifecycle = self
335            .request
336            .plan
337            .resources
338            .read_lifecycle("activate an execution stream")?;
339        if self.is_poisoned() {
340            return Err(invalid_resource(
341                "poisoned logical sequence cannot be activated",
342            ));
343        }
344        self.validate_runtime("logical sequence activation")?;
345        if !Arc::ptr_eq(self.request.plan.runtime(), &stream.runtime)
346            || !std::ptr::eq(self, Arc::as_ref(&stream.resources))
347            || stream.coordinator_id != self.coordinator_id()
348            || stream.sequence_authority != self.sequence_authority()
349            || !Arc::ptr_eq(&self.sequence_recovery, &stream.sequence_recovery)
350            || !Arc::ptr_eq(&self.sequence_dispatch_gate, &stream.sequence_dispatch_gate)
351        {
352            return Err(invalid_resource(
353                "execution stream belongs to another logical sequence authority",
354            ));
355        }
356        if stream.state != BoundExecutionStreamState::Ready
357            || stream.abandoned_sequence.is_some()
358            || self.request.plan.runtime().stream_state(stream.stream()) != StreamState::Ready
359        {
360            return Err(invalid_resource(
361                "logical sequence activation requires one core-ready stream",
362            ));
363        }
364        let mut authority_source = self.lock_authority_source()?;
365        let selecting_legacy = match *authority_source {
366            SequenceExecutionAuthoritySource::Unselected => true,
367            SequenceExecutionAuthoritySource::LegacyStream => false,
368            SequenceExecutionAuthoritySource::SequenceSession => {
369                return Err(invalid_resource(
370                    "logical sequence execution authority is permanently selected for sequence sessions",
371                ));
372            }
373            SequenceExecutionAuthoritySource::FailClosed => {
374                return Err(invalid_resource(
375                    "logical sequence execution authority selector is fail-closed",
376                ));
377            }
378        };
379        let backing_snapshot = self.backing_snapshot()?;
380        let epoch = match self.next_activation_epoch.fetch_update(
381            Ordering::AcqRel,
382            Ordering::Acquire,
383            |epoch| epoch.checked_add(1).filter(|next| *next <= (u64::MAX >> 2)),
384        ) {
385            Ok(epoch) => epoch,
386            Err(_) => {
387                *authority_source = SequenceExecutionAuthoritySource::FailClosed;
388                return Err(invalid_resource("active sequence epoch space is exhausted"));
389            }
390        };
391        let active_state = sequence_slot_active(epoch);
392        if let Err(actual) =
393            self.state
394                .compare_exchange(0, active_state, Ordering::AcqRel, Ordering::Acquire)
395        {
396            if selecting_legacy {
397                *authority_source = SequenceExecutionAuthoritySource::FailClosed;
398            }
399            return Err(if sequence_slot_is_poisoned(actual) {
400                invalid_resource("logical sequence was abandoned and is poisoned")
401            } else {
402                invalid_resource("logical sequence already owns an active stream")
403            });
404        }
405        let slot = self.sequence_authority().sparse_id();
406        let recovery_metadata = AbandonedSequenceMetadata {
407            plan: self.request.plan.evidence(),
408            sequence_authority: self.sequence_authority(),
409            run_id: self.run_id().clone(),
410            request_id: self.request_id().clone(),
411            slot,
412            activation_epoch: epoch,
413            runtime_implementation_fingerprint: self
414                .request
415                .plan
416                .runtime_implementation_fingerprint()
417                .to_owned(),
418            state: Arc::clone(&self.state),
419            sequence_dispatch_gate: Arc::clone(&self.sequence_dispatch_gate),
420            drained: false,
421        };
422        let recovery_key = recovery_metadata.key();
423        self.sequence_recovery.register(recovery_metadata);
424        stream.abandoned_sequence = Some(recovery_key);
425        stream.state = BoundExecutionStreamState::InUse;
426        *authority_source = SequenceExecutionAuthoritySource::LegacyStream;
427        Ok(ActiveSequencePermit {
428            resources: self,
429            backing_snapshot,
430            epoch,
431            state: Arc::clone(&self.state),
432            stream,
433            runtime_fingerprint: self
434                .request
435                .plan
436                .runtime_implementation_fingerprint()
437                .to_owned(),
438            stream_drained: false,
439            completed: false,
440        })
441    }
442
443    pub fn recover_abandoned_sequence(
444        &self,
445    ) -> Result<ActiveSequenceAbortReceipt, AbandonedSequenceRecoveryError<R::Error>> {
446        self.sequence_recovery.recover(
447            self.request.plan.runtime(),
448            self.sequence_authority().sparse_id(),
449        )
450    }
451}
452
453/// Non-cloneable guard for an admitted active-sequence slot. Dispatch borrows
454/// this permit; the sequence owner retains it until all asynchronous work is
455/// synchronized or cancelled.
456#[must_use = "an active sequence permit must live until asynchronous work is complete"]
457pub struct ActiveSequencePermit<'resources, 'exec, R>
458where
459    R: DeviceRuntime,
460{
461    resources: &'resources AdmittedSequenceResources<R>,
462    backing_snapshot: Arc<SequenceBackingSnapshot<R>>,
463    epoch: u64,
464    state: Arc<AtomicU64>,
465    stream: &'exec mut BoundExecutionStream<R>,
466    runtime_fingerprint: String,
467    stream_drained: bool,
468    completed: bool,
469}
470
471impl<'resources, 'exec, R> ActiveSequencePermit<'resources, 'exec, R>
472where
473    R: DeviceRuntime,
474{
475    pub fn resources(&self) -> &'resources AdmittedSequenceResources<R> {
476        self.resources
477    }
478
479    pub fn run_id(&self) -> &RunId {
480        self.resources.run_id()
481    }
482
483    pub fn request_id(&self) -> &RequestIdentity {
484        self.resources.request_id()
485    }
486
487    pub fn sequence_authority(&self) -> SequenceAuthorityId {
488        self.resources.sequence_authority()
489    }
490
491    pub fn coordinator_id(&self) -> LogicalAdmissionCoordinatorId {
492        self.resources.coordinator_id()
493    }
494
495    pub fn backing_slices(&self) -> &[LogicalBackingSliceAuthority] {
496        self.backing_snapshot.backing_slices()
497    }
498
499    pub const fn activation_epoch(&self) -> u64 {
500        self.epoch
501    }
502
503    pub fn runtime_implementation_fingerprint(&self) -> &str {
504        &self.runtime_fingerprint
505    }
506
507    pub(crate) fn with_runtime_and_stream<T>(
508        &mut self,
509        action: impl FnOnce(&R, &mut R::Stream) -> T,
510    ) -> Result<T, VNextError> {
511        if self.stream.state != BoundExecutionStreamState::InUse {
512            return Err(invalid_resource(
513                "operation dispatch requires one core-owned in-use stream",
514            ));
515        }
516        let _dispatch_guard = enter_sequence_dispatch(&self.resources.sequence_dispatch_gate)?;
517        Ok(action(
518            self.resources.request.plan.runtime(),
519            self.stream.stream_mut(),
520        ))
521    }
522
523    /// Consumes dispatch authority before draining the exact bound stream.
524    /// Successful synchronization returns a different typestate that cannot
525    /// be passed back to `OperationDispatch`.
526    pub fn synchronize(
527        mut self,
528    ) -> Result<
529        SynchronizedSequencePermit<'resources, 'exec, R>,
530        SequenceSynchronizationFailure<'resources, 'exec, R>,
531    > {
532        let preflight = self
533            .resources
534            .validate_runtime("sequence synchronization preflight")
535            .and_then(|()| {
536                if self
537                    .resources
538                    .request
539                    .plan
540                    .runtime()
541                    .descriptor()
542                    .runtime_implementation_fingerprint
543                    == self.runtime_fingerprint
544                {
545                    Ok(())
546                } else {
547                    Err(invalid_resource(
548                        "sequence synchronization runtime differs from its activation snapshot",
549                    ))
550                }
551            });
552
553        // Draining is attempted even when descriptor validation fails. The
554        // stream/runtime pair is privately bound, while skipping the drain
555        // could make later buffer quarantine unsafe.
556        let runtime_error = match self
557            .resources
558            .request
559            .plan
560            .runtime()
561            .synchronize(self.stream.stream_mut())
562        {
563            Ok(()) => None,
564            Err(error) => Some(error),
565        };
566        let stream_ready = self
567            .resources
568            .request
569            .plan
570            .runtime()
571            .stream_state(self.stream.stream())
572            == StreamState::Ready;
573        self.stream_drained = runtime_error.is_none() && stream_ready;
574        if self.stream_drained {
575            self.stream
576                .sequence_recovery
577                .set_drained((self.sequence_authority().sparse_id(), self.epoch), true);
578        }
579        let completion = self
580            .resources
581            .validate_runtime("sequence synchronization completion")
582            .and_then(|()| {
583                if stream_ready {
584                    Ok(())
585                } else {
586                    Err(invalid_resource(
587                        "sequence synchronization did not return the bound stream to ready",
588                    ))
589                }
590            });
591        let error = preflight
592            .err()
593            .map(SequenceSynchronizationError::Contract)
594            .or_else(|| runtime_error.map(SequenceSynchronizationError::Runtime))
595            .or_else(|| completion.err().map(SequenceSynchronizationError::Contract));
596        if let Some(error) = error {
597            return Err(SequenceSynchronizationFailure {
598                permit: Some(self),
599                error,
600            });
601        }
602        self.stream.state = BoundExecutionStreamState::Ready;
603        Ok(SynchronizedSequencePermit { permit: Some(self) })
604    }
605}
606
607#[derive(Debug)]
608pub enum SequenceSynchronizationError<E> {
609    Contract(VNextError),
610    Runtime(E),
611}
612
613/// Retry owner for a failed stream drain. It intentionally does not expose
614/// the active dispatch permit, so no operation can be submitted between a
615/// failed synchronization attempt and its retry.
616#[must_use = "failed sequence synchronization must be retried or retained"]
617pub struct SequenceSynchronizationFailure<'resources, 'exec, R>
618where
619    R: DeviceRuntime,
620{
621    permit: Option<ActiveSequencePermit<'resources, 'exec, R>>,
622    error: SequenceSynchronizationError<R::Error>,
623}
624
625impl<R> fmt::Debug for SequenceSynchronizationFailure<'_, '_, R>
626where
627    R: DeviceRuntime,
628{
629    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
630        formatter
631            .debug_struct("SequenceSynchronizationFailure")
632            .field("error", &self.error)
633            .finish_non_exhaustive()
634    }
635}
636
637impl<'resources, 'exec, R> SequenceSynchronizationFailure<'resources, 'exec, R>
638where
639    R: DeviceRuntime,
640{
641    pub fn error(&self) -> &SequenceSynchronizationError<R::Error> {
642        &self.error
643    }
644
645    pub fn retry(
646        mut self,
647    ) -> Result<
648        SynchronizedSequencePermit<'resources, 'exec, R>,
649        SequenceSynchronizationFailure<'resources, 'exec, R>,
650    > {
651        self.permit
652            .take()
653            .expect("synchronization failure owns its active permit")
654            .synchronize()
655    }
656}
657
658/// Stream-drained typestate. It has no dispatch API and must choose exactly
659/// one terminal slot disposition.
660#[must_use = "a synchronized sequence must be completed or aborted"]
661pub struct SynchronizedSequencePermit<'resources, 'exec, R>
662where
663    R: DeviceRuntime,
664{
665    permit: Option<ActiveSequencePermit<'resources, 'exec, R>>,
666}
667
668impl<R> SynchronizedSequencePermit<'_, '_, R>
669where
670    R: DeviceRuntime,
671{
672    pub fn complete(mut self) -> Result<ActiveSequenceCompletionReceipt, VNextError> {
673        let mut permit = self
674            .permit
675            .take()
676            .expect("synchronized sequence owns its active permit");
677        let sequence_poisoned =
678            sequence_dispatch_is_poisoned(&permit.resources.sequence_dispatch_gate);
679        let terminal_state = if sequence_poisoned {
680            sequence_slot_poisoned_drained(permit.epoch)
681        } else {
682            0
683        };
684        permit
685            .state
686            .compare_exchange(
687                sequence_slot_active(permit.epoch),
688                terminal_state,
689                Ordering::AcqRel,
690                Ordering::Acquire,
691            )
692            .map_err(|_| invalid_resource("active sequence epoch is no longer completable"))?;
693        permit
694            .stream
695            .sequence_recovery
696            .clear((permit.sequence_authority().sparse_id(), permit.epoch));
697        permit.stream.abandoned_sequence = None;
698        permit.stream.state = BoundExecutionStreamState::Ready;
699        permit.completed = true;
700        if sequence_poisoned {
701            return Err(invalid_resource(
702                "sequence cannot complete successfully after its dispatch authority was poisoned",
703            ));
704        }
705        Ok(ActiveSequenceCompletionReceipt {
706            plan: permit.resources.request.plan.evidence(),
707            sequence_authority: permit.sequence_authority(),
708            run_id: permit.run_id().clone(),
709            request_id: permit.request_id().clone(),
710            activation_epoch: permit.epoch,
711            runtime_implementation_fingerprint: permit.runtime_fingerprint.clone(),
712        })
713    }
714
715    /// Produces abort evidence only after the exact bound stream was drained.
716    /// Only this exact logical sequence remains poisoned after abort.
717    pub fn abort(mut self) -> Result<ActiveSequenceAbortReceipt, VNextError> {
718        let mut permit = self
719            .permit
720            .take()
721            .expect("synchronized sequence owns its active permit");
722        permit
723            .state
724            .compare_exchange(
725                sequence_slot_active(permit.epoch),
726                sequence_slot_poisoned_drained(permit.epoch),
727                Ordering::AcqRel,
728                Ordering::Acquire,
729            )
730            .map_err(|_| invalid_resource("active sequence epoch is no longer abortable"))?;
731        permit
732            .resources
733            .sequence_dispatch_gate
734            .fetch_or(SEQUENCE_DISPATCH_POISONED_BIT, Ordering::AcqRel);
735        permit
736            .stream
737            .sequence_recovery
738            .clear((permit.sequence_authority().sparse_id(), permit.epoch));
739        permit.stream.abandoned_sequence = None;
740        permit.stream.state = BoundExecutionStreamState::Ready;
741        permit.completed = true;
742        Ok(ActiveSequenceAbortReceipt {
743            plan: permit.resources.request.plan.evidence(),
744            sequence_authority: permit.sequence_authority(),
745            run_id: permit.run_id().clone(),
746            request_id: permit.request_id().clone(),
747            activation_epoch: permit.epoch,
748            runtime_implementation_fingerprint: permit.runtime_fingerprint.clone(),
749            disposition: ActiveSequenceAbortDisposition::SynchronizedAndPoisoned,
750        })
751    }
752}
753
754/// Core-signed evidence that synchronization succeeded and the exact active
755/// slot epoch was atomically cleared. It is trusted output and deliberately
756/// cannot be deserialized or constructed by a caller.
757#[derive(Debug, Serialize)]
758#[must_use = "sequence completion evidence must be recorded by execution"]
759pub struct ActiveSequenceCompletionReceipt {
760    plan: TrustedPlanRuntimeEvidence,
761    sequence_authority: SequenceAuthorityId,
762    run_id: RunId,
763    request_id: RequestIdentity,
764    activation_epoch: u64,
765    runtime_implementation_fingerprint: String,
766}
767
768impl ActiveSequenceCompletionReceipt {
769    pub fn plan(&self) -> &TrustedPlanRuntimeEvidence {
770        &self.plan
771    }
772
773    pub fn run_id(&self) -> &RunId {
774        &self.run_id
775    }
776
777    pub fn request_id(&self) -> &RequestIdentity {
778        &self.request_id
779    }
780
781    pub const fn sequence_authority(&self) -> SequenceAuthorityId {
782        self.sequence_authority
783    }
784
785    pub const fn activation_epoch(&self) -> u64 {
786        self.activation_epoch
787    }
788
789    pub fn runtime_implementation_fingerprint(&self) -> &str {
790        &self.runtime_implementation_fingerprint
791    }
792}
793
794impl<R> Drop for ActiveSequencePermit<'_, '_, R>
795where
796    R: DeviceRuntime,
797{
798    fn drop(&mut self) {
799        if !self.completed {
800            let poisoned_state = if self.stream_drained {
801                sequence_slot_poisoned_drained(self.epoch)
802            } else {
803                sequence_slot_poisoned_undrained(self.epoch)
804            };
805            let result = self.state.compare_exchange(
806                sequence_slot_active(self.epoch),
807                poisoned_state,
808                Ordering::AcqRel,
809                Ordering::Acquire,
810            );
811            debug_assert!(result.is_ok(), "active sequence slot guard lost ownership");
812            if result.is_ok() {
813                self.resources
814                    .sequence_dispatch_gate
815                    .fetch_or(SEQUENCE_DISPATCH_POISONED_BIT, Ordering::AcqRel);
816                self.stream.state = BoundExecutionStreamState::Poisoned;
817                self.stream.sequence_recovery.set_drained(
818                    (self.sequence_authority().sparse_id(), self.epoch),
819                    self.stream_drained,
820                );
821            }
822        }
823    }
824}