Skip to main content

ferrum_interfaces/vnext/resource/
invocation.rs

1use super::{
2    begin_participant_flights_dispatch, begin_submission_wave_participant_flights_dispatch,
3    finalize_session_frames, fmt, invalid_resource, issue_batch_invocation_id,
4    poison_session_frame, prepare_participant_flights, prepare_submission_wave_participant_flights,
5    reset_participant_flights_after_definitely_not_submitted,
6    reset_submission_wave_participant_flights_after_definitely_not_submitted,
7    session_participant_key, ActiveInvocationWaveGuard, ActiveSequenceFrame, AdmissionDeferred,
8    AdmissionFitPolicy, AdmissionPressureAction, AdmissionRejected, AdmittedRequestResources,
9    AdmittedSequenceResources, AdmittedStepParticipant, AllocationLifetime, Arc, AtomicU64,
10    BackingInitializationEncodeError, BackingPrepareDecision, BatchCapacityClaimDecision,
11    BatchInvocationId, BatchParticipantAuthority, BatchParticipantTokenSpan, BatchStepId,
12    BatchWorkShape, ClaimedBackingTransaction, ClaimedSubmissionWaveBacking,
13    DeferredDeviceCleanupDomainId, DeviceCommandBatch, DeviceRuntime, Digest,
14    DynamicBackingDeferred, DynamicDeferredMaintenanceOutcome, DynamicResourceDescriptor,
15    ExecutionLane, ExecutionLaneId, InvocationRegistry, InvocationResourceAdmissionRequest,
16    LaneBackingPrepareDecision, LogicalAdmissionCoordinatorId, LogicalBackingBufferView,
17    LogicalBackingSliceAuthority, LogicalBatchCapacityLease, NodeId, Ordering,
18    ParticipantFlightCandidate, ParticipantFlightPhase, ParticipantNodeKey, PlanBackingDeferral,
19    PlanCapacityWaitRegistration, PreparedBackingInitializations, PreparedParticipantFlightHold,
20    PreparedSubmissionWaveParticipantFlightHold, RequestStateHazardAcquireDecision,
21    RequestStateHazardDeferral, RequestStateHazardParticipant, RequestStateHazardPermit,
22    RequestStateHazardPoison, RequestStateHazardSplitRequired,
23    RequestStateHazardTerminalDisposition, ResourceId, SequenceAuthorityId,
24    SequenceBackingSnapshot, SequenceRecoveryRegistry, SequenceSessionEpoch,
25    SequenceSessionFingerprint, Serialize, Sha256, StaticProvisioningLease,
26    StepFinalizationFailure, StepFrameFinalization, StepParticipantFrameAssignment,
27    StepParticipantRetirement, StepParticipantRetirementDisposition, StepResourceLease,
28    StepRetirementReceipt, TokenSpanWork, TrustedPlanRuntimeEvidence, VNextError,
29    SEQUENCE_DISPATCH_POISONED_BIT,
30};
31use crate::vnext::ReusableExecutionBucketSpec;
32use std::time::{Duration, Instant};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum StepResourceAdmissionProfilePhase {
36    AuthorityAndPolicyValidate,
37    DemandEvaluate,
38    BackingClaim,
39    LogicalCapacityClaim,
40    TransactionValidateAndFingerprint,
41    FrameCaptureAndLease,
42}
43
44#[inline(always)]
45pub(super) fn step_admission_profile_start<const PROFILE: bool>() -> Option<Instant> {
46    PROFILE.then(Instant::now)
47}
48
49#[inline(always)]
50pub(super) fn record_step_admission_profile<const PROFILE: bool, F>(
51    observer: &mut F,
52    phase: StepResourceAdmissionProfilePhase,
53    started: Option<Instant>,
54) where
55    F: FnMut(StepResourceAdmissionProfilePhase, Duration),
56{
57    if PROFILE {
58        observer(
59            phase,
60            started
61                .expect("profiled step admission phase owns a start instant")
62                .elapsed(),
63        );
64    }
65}
66
67#[derive(Debug)]
68pub enum ExecutionStreamCreationError<E> {
69    Contract(VNextError),
70    Runtime(E),
71}
72
73/// A stream created and owned by one exact admitted runtime instance. Its
74/// runtime and raw stream are private so execution can only proceed through an
75/// active sequence permit.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub(super) enum BoundExecutionStreamState {
78    Ready,
79    InUse,
80    Poisoned,
81}
82
83#[must_use = "an execution stream must be activated through its resource lease"]
84pub struct BoundExecutionStream<R>
85where
86    R: DeviceRuntime,
87{
88    // The raw stream drops or transfers into the recovery registry before this
89    // owning sequence hold can release logical capacity or backing extents.
90    pub(super) runtime: Arc<R>,
91    pub(super) coordinator_id: LogicalAdmissionCoordinatorId,
92    pub(super) sequence_authority: SequenceAuthorityId,
93    pub(super) stream: Option<R::Stream>,
94    pub(super) state: BoundExecutionStreamState,
95    pub(super) sequence_recovery: Arc<SequenceRecoveryRegistry<R>>,
96    pub(super) sequence_dispatch_gate: Arc<AtomicU64>,
97    pub(super) abandoned_sequence: Option<(u32, u64)>,
98    pub(super) resources: Arc<AdmittedSequenceResources<R>>,
99}
100
101impl<R> BoundExecutionStream<R>
102where
103    R: DeviceRuntime,
104{
105    pub(super) fn stream(&self) -> &R::Stream {
106        self.stream
107            .as_ref()
108            .expect("bound execution stream retains its raw stream")
109    }
110
111    pub(super) fn stream_mut(&mut self) -> &mut R::Stream {
112        self.stream
113            .as_mut()
114            .expect("bound execution stream retains its raw stream")
115    }
116}
117
118impl<R> Drop for BoundExecutionStream<R>
119where
120    R: DeviceRuntime,
121{
122    fn drop(&mut self) {
123        let Some(key) = self.abandoned_sequence.take() else {
124            return;
125        };
126        self.sequence_dispatch_gate
127            .fetch_or(SEQUENCE_DISPATCH_POISONED_BIT, Ordering::AcqRel);
128        if let Some(stream) = self.stream.take() {
129            self.sequence_recovery.attach_stream(key, stream);
130        }
131    }
132}
133
134impl<R> StepResourceLease<R>
135where
136    R: DeviceRuntime,
137{
138    pub(super) fn new(
139        participants: Vec<AdmittedStepParticipant<R>>,
140        execution_lane: Arc<ExecutionLane<R>>,
141        reusable_execution_bucket: Option<ReusableExecutionBucketSpec>,
142        batch_step_id: BatchStepId,
143        claimed_backing: ClaimedBackingTransaction,
144    ) -> Result<Self, VNextError> {
145        if participants.is_empty() {
146            return Err(invalid_resource(
147                "step resources require a non-empty participant set",
148            ));
149        }
150        let coordinator = participants[0]
151            .session
152            .resources()
153            .request
154            .plan
155            .logical_admission();
156        if claimed_backing.work_shape().participants().len() != participants.len()
157            || claimed_backing
158                .work_shape()
159                .participants()
160                .iter()
161                .zip(&participants)
162                .any(|(authority, participant)| {
163                    authority.sequence_authority() != participant.session.sequence_authority()
164                        || authority.request_authority() != participant.session.request_authority()
165                })
166        {
167            return Err(invalid_resource(
168                "step work shape differs from its exact batch participants",
169            ));
170        }
171        if let Some(capacity) = claimed_backing.logical_capacity() {
172            let parents_match = capacity
173                .parents()
174                .iter()
175                .map(|parent| (parent.sequence(), parent.request()))
176                .eq(participants.iter().map(|participant| {
177                    (
178                        participant.session.sequence_authority(),
179                        participant.session.request_authority(),
180                    )
181                }));
182            if !coordinator.owns_batch_capacity_claim(capacity) || !parents_match {
183                return Err(invalid_resource(
184                    "step capacity authority differs from its exact batch participants",
185                ));
186            }
187        }
188        Ok(Self {
189            claimed_backing,
190            participants,
191            invocation_registry: Arc::new(InvocationRegistry::default()),
192            execution_lane,
193            reusable_execution_bucket,
194            batch_step_id,
195            finalized: false,
196        })
197    }
198
199    pub const fn batch_step_id(&self) -> BatchStepId {
200        self.batch_step_id
201    }
202
203    pub fn execution_lane(&self) -> &Arc<ExecutionLane<R>> {
204        &self.execution_lane
205    }
206
207    pub fn reusable_execution_bucket(&self) -> Option<&ReusableExecutionBucketSpec> {
208        self.reusable_execution_bucket.as_ref()
209    }
210
211    pub fn try_retire_normal(
212        self: Arc<Self>,
213    ) -> Result<StepRetirementReceipt, StepFinalizationFailure<R>> {
214        Self::try_finalize(self, StepFrameFinalization::Commit)
215    }
216
217    pub fn try_abort(self: Arc<Self>) -> Result<StepRetirementReceipt, StepFinalizationFailure<R>> {
218        Self::try_finalize(self, StepFrameFinalization::Abort)
219    }
220
221    /// Releases a capacity-deferred step before any invocation was prepared or
222    /// submitted. Unlike `try_abort`, this leaves every participant session open
223    /// so the scheduler can retry after the exact capacity source advances.
224    pub fn try_rollback_unsubmitted(
225        self: Arc<Self>,
226    ) -> Result<StepRetirementReceipt, StepFinalizationFailure<R>> {
227        Self::try_finalize(self, StepFrameFinalization::RollbackUnsubmitted)
228    }
229
230    fn try_finalize(
231        step: Arc<Self>,
232        finalization: StepFrameFinalization,
233    ) -> Result<StepRetirementReceipt, StepFinalizationFailure<R>> {
234        let mut step = match Arc::try_unwrap(step) {
235            Ok(step) => step,
236            Err(step) => {
237                return Err(StepFinalizationFailure {
238                    step,
239                    error: invalid_resource(
240                        "step cannot finalize while an invocation or scheduler clone retains it",
241                    ),
242                });
243            }
244        };
245        if finalization == StepFrameFinalization::RollbackUnsubmitted {
246            if let Err(error) = step.invocation_registry.ensure_pristine_for_step_rollback() {
247                return Err(StepFinalizationFailure {
248                    step: Arc::new(step),
249                    error,
250                });
251            }
252        }
253        let dispositions = match step.finalize_participants(finalization) {
254            Ok(dispositions) => dispositions,
255            Err(error) => {
256                return Err(StepFinalizationFailure {
257                    step: Arc::new(step),
258                    error,
259                });
260            }
261        };
262        let participants = step
263            .participants
264            .iter()
265            .zip(dispositions)
266            .map(|(participant, disposition)| StepParticipantRetirement {
267                assignment: StepParticipantFrameAssignment::new(
268                    participant.session.sequence_authority(),
269                    participant.session.request_authority(),
270                    participant.frame.frame_id,
271                ),
272                disposition,
273            })
274            .collect();
275        Ok(StepRetirementReceipt {
276            batch_step_id: step.batch_step_id,
277            participants,
278        })
279    }
280
281    fn finalize_participants(
282        &mut self,
283        finalization: StepFrameFinalization,
284    ) -> Result<Vec<StepParticipantRetirementDisposition>, VNextError> {
285        if self.finalized {
286            return Err(invalid_resource("step resources are already finalized"));
287        }
288        let mut holds = self
289            .participants
290            .iter_mut()
291            .map(|participant| &mut participant.frame)
292            .collect::<Vec<_>>();
293        let dispositions = finalize_session_frames(&mut holds, finalization)?;
294        self.finalized = true;
295        Ok(dispositions)
296    }
297
298    pub fn participant_count(&self) -> u32 {
299        u32::try_from(self.participants.len())
300            .expect("step participant count was validated before admission")
301    }
302
303    pub fn coordinator_id(&self) -> LogicalAdmissionCoordinatorId {
304        self.participants[0].session.resources().coordinator_id()
305    }
306
307    pub fn participants(
308        &self,
309    ) -> impl ExactSizeIterator<Item = &Arc<AdmittedSequenceResources<R>>> {
310        self.participants
311            .iter()
312            .map(|participant| participant.session.resources())
313    }
314
315    pub(crate) fn participant_backing_snapshot(
316        &self,
317        participant: BatchParticipantAuthority,
318    ) -> Result<&Arc<SequenceBackingSnapshot<R>>, VNextError> {
319        let index = self
320            .participants
321            .binary_search_by_key(&participant.canonical_key(), |candidate| {
322                session_participant_key(&candidate.session)
323            })
324            .map_err(|_| invalid_resource("step does not own that backing participant"))?;
325        self.participants
326            .get(index)
327            .map(|participant| &participant.backing_snapshot)
328            .ok_or_else(|| invalid_resource("step backing participant mapping is inconsistent"))
329    }
330
331    pub(crate) fn participant_backing_view(
332        &self,
333        authority: BatchParticipantAuthority,
334        resource_id: &ResourceId,
335    ) -> Result<LogicalBackingBufferView<'_, R::Buffer>, VNextError> {
336        let index = self
337            .participants
338            .binary_search_by_key(&authority.canonical_key(), |candidate| {
339                session_participant_key(&candidate.session)
340            })
341            .map_err(|_| invalid_resource("step does not own that backing participant"))?;
342        let participant = self
343            .participants
344            .get(index)
345            .ok_or_else(|| invalid_resource("step backing participant mapping is inconsistent"))?;
346        let authorities = participant.backing_snapshot.backing_slices_for(resource_id);
347        if !authorities.is_empty() {
348            return participant
349                .session
350                .resources()
351                .request
352                .plan
353                .dynamic_pools()
354                .view_many(authorities);
355        }
356        participant
357            .session
358            .resources()
359            .request
360            .backing_view(resource_id)
361    }
362
363    pub fn participant_frames(
364        &self,
365    ) -> impl ExactSizeIterator<Item = StepParticipantFrameAssignment> + '_ {
366        self.participants.iter().map(|participant| {
367            StepParticipantFrameAssignment::new(
368                participant.session.sequence_authority(),
369                participant.session.request_authority(),
370                participant.frame.frame_id,
371            )
372        })
373    }
374
375    pub fn backing_slices(&self) -> &[LogicalBackingSliceAuthority] {
376        self.claimed_backing.backing_slices()
377    }
378
379    pub fn logical_capacity(&self) -> Option<&LogicalBatchCapacityLease> {
380        self.claimed_backing.logical_capacity()
381    }
382
383    pub fn work_shape(&self) -> &BatchWorkShape {
384        self.claimed_backing.work_shape()
385    }
386
387    pub fn bind_invocation_work_shape(
388        &self,
389        mut participant_tokens: Vec<(BatchParticipantAuthority, TokenSpanWork)>,
390    ) -> Result<BatchWorkShape, VNextError> {
391        participant_tokens.sort_by_key(|(participant, _)| participant.canonical_key());
392        if participant_tokens.is_empty()
393            || participant_tokens
394                .windows(2)
395                .any(|pair| pair[0].0.canonical_key() == pair[1].0.canonical_key())
396            || participant_tokens.iter().any(|(authority, _)| {
397                self.participants
398                    .binary_search_by_key(&authority.canonical_key(), |participant| {
399                        session_participant_key(&participant.session)
400                    })
401                    .is_err()
402            })
403        {
404            return Err(invalid_resource(
405                "invocation token work must bind a unique non-empty step participant subset",
406            ));
407        }
408        BatchWorkShape::new(
409            participant_tokens
410                .into_iter()
411                .map(|(participant, token_span)| {
412                    BatchParticipantTokenSpan::new(participant, token_span)
413                })
414                .collect(),
415        )
416    }
417
418    pub fn bind_all_invocation_work_shape(
419        &self,
420        token_spans: Vec<TokenSpanWork>,
421    ) -> Result<BatchWorkShape, VNextError> {
422        if token_spans.len() != self.participants.len() {
423            return Err(invalid_resource(
424                "invocation token work count differs from all step participants",
425            ));
426        }
427        self.bind_invocation_work_shape(
428            self.participants
429                .iter()
430                .zip(token_spans)
431                .map(|(participant, token_span)| {
432                    (
433                        BatchParticipantAuthority::new(
434                            participant.session.sequence_authority(),
435                            participant.session.request_authority(),
436                        ),
437                        token_span,
438                    )
439                })
440                .collect(),
441        )
442    }
443
444    /// Reuses the exact immutable work authority admitted for this step when a
445    /// submission wave binds the same participants and token spans.
446    pub fn shared_all_invocation_work_shape(
447        &self,
448        token_spans: &[TokenSpanWork],
449    ) -> Result<Arc<BatchWorkShape>, VNextError> {
450        let work_shape = self.claimed_backing.work_shape();
451        if token_spans.len() != self.participants.len()
452            || work_shape
453                .participant_work()
454                .iter()
455                .zip(token_spans)
456                .any(|(bound, requested)| bound.token_span() != requested)
457        {
458            return Err(invalid_resource(
459                "submission wave token work differs from its admitted step work authority",
460            ));
461        }
462        Ok(Arc::clone(self.claimed_backing.work_shape_arc()))
463    }
464
465    pub fn claimed_backing(&self) -> &ClaimedBackingTransaction {
466        &self.claimed_backing
467    }
468
469    pub fn static_provisioning(&self) -> Option<&StaticProvisioningLease<R>> {
470        self.participants[0]
471            .session
472            .resources()
473            .static_provisioning()
474    }
475
476    pub fn plan_evidence(&self) -> TrustedPlanRuntimeEvidence {
477        self.participants[0].session.resources().plan_evidence()
478    }
479
480    pub(crate) fn backing_view(
481        &self,
482        resource_id: &ResourceId,
483    ) -> Result<LogicalBackingBufferView<'_, R::Buffer>, VNextError> {
484        if let Some(authority) = self
485            .claimed_backing
486            .backing_slices()
487            .iter()
488            .find(|authority| authority.resource_id() == resource_id)
489        {
490            return self.participants[0]
491                .session
492                .resources()
493                .request
494                .plan
495                .dynamic_pools()
496                .view(authority);
497        }
498        Err(invalid_resource(format!(
499            "resource `{resource_id}` is not step-shared backing"
500        )))
501    }
502
503    pub(crate) fn dynamic_descriptor(
504        &self,
505        resource_id: &ResourceId,
506    ) -> Result<&DynamicResourceDescriptor, VNextError> {
507        let pools = self.participants[0]
508            .session
509            .resources()
510            .request
511            .plan
512            .dynamic_pools();
513        let mut matches = pools.domains.iter().filter_map(|domain| {
514            domain
515                .descriptors
516                .binary_search_by(|descriptor| descriptor.base_resource_id().cmp(resource_id))
517                .ok()
518                .map(|index| &domain.descriptors[index])
519        });
520        let descriptor = matches.next().ok_or_else(|| {
521            invalid_resource(format!(
522                "resource `{resource_id}` has no dynamic plan descriptor"
523            ))
524        })?;
525        if matches.next().is_some() {
526            return Err(invalid_resource(format!(
527                "resource `{resource_id}` has duplicate dynamic plan descriptors"
528            )));
529        }
530        Ok(descriptor)
531    }
532
533    pub(crate) fn participant_backing_views(
534        &self,
535        resource_id: &ResourceId,
536    ) -> Result<Vec<(SequenceAuthorityId, LogicalBackingBufferView<'_, R::Buffer>)>, VNextError>
537    {
538        self.participants
539            .iter()
540            .map(|participant| {
541                let authority = BatchParticipantAuthority::new(
542                    participant.session.sequence_authority(),
543                    participant.session.request_authority(),
544                );
545                Ok((
546                    participant.session.sequence_authority(),
547                    self.participant_backing_view(authority, resource_id)?,
548                ))
549            })
550            .collect()
551    }
552
553    pub fn try_admit_invocation(
554        self: &Arc<Self>,
555        request: InvocationResourceAdmissionRequest,
556    ) -> Result<InvocationResourceAdmissionDecision<R>, VNextError> {
557        let _lifecycle = self.participants[0]
558            .session
559            .resources()
560            .request
561            .plan
562            .resources
563            .read_lifecycle("admit a step invocation")?;
564        if self.claimed_backing.has_shared_physical_claims() {
565            return Err(invalid_resource(
566                "shared Step activation backing requires an ordered single-fence submission wave",
567            ));
568        }
569        let deferred_node_id = request.node_id().clone();
570        let deferred_work_fingerprint = request.work_shape().fingerprint().to_owned();
571        let prepared = match self.prepare_invocation_scope(request)? {
572            PreparedInvocationScopeDecision::Prepared(prepared) => prepared,
573            PreparedInvocationScopeDecision::Deferred(deferred) => {
574                return Ok(InvocationResourceAdmissionDecision::Deferred(deferred));
575            }
576            PreparedInvocationScopeDecision::BackingDeferred(deferred) => {
577                return Ok(InvocationResourceAdmissionDecision::BackingDeferred(
578                    InvocationAdmissionBackingDeferral::new(
579                        Arc::clone(self),
580                        deferred,
581                        deferred_node_id,
582                        deferred_work_fingerprint,
583                    )?,
584                ));
585            }
586            PreparedInvocationScopeDecision::PermanentRejected(rejected) => {
587                return Ok(InvocationResourceAdmissionDecision::PermanentRejected(
588                    rejected,
589                ));
590            }
591            PreparedInvocationScopeDecision::RequestStateDeferred(deferred) => {
592                return Ok(InvocationResourceAdmissionDecision::RequestStateDeferred(
593                    deferred,
594                ));
595            }
596            PreparedInvocationScopeDecision::RequestStateSplitRequired(split) => {
597                return Ok(InvocationResourceAdmissionDecision::RequestStateSplitRequired(split));
598            }
599            PreparedInvocationScopeDecision::RequestStatePoisoned(poison) => {
600                return Ok(InvocationResourceAdmissionDecision::RequestStatePoisoned(
601                    poison,
602                ));
603            }
604        };
605        let PreparedInvocationScope {
606            participants,
607            participant_frames,
608            node_id,
609            claimed_backing,
610            flight_candidates,
611            request_state_hazards,
612        } = prepared;
613        let batch_invocation_id = issue_batch_invocation_id()?;
614        let prepared_participant_flights =
615            prepare_participant_flights(&flight_candidates, &node_id)?;
616        let topology_keys = participant_frames
617            .iter()
618            .map(|assignment| {
619                ParticipantNodeKey::new(
620                    assignment.participant(),
621                    assignment.frame_id(),
622                    node_id.clone(),
623                )
624            })
625            .collect();
626        let active_wave = self.invocation_registry.enter(
627            topology_keys,
628            batch_invocation_id,
629            claimed_backing.work_shape().fingerprint(),
630        )?;
631        Ok(InvocationResourceAdmissionDecision::Admitted(
632            InvocationResourceLease::new(
633                Arc::clone(self),
634                participants,
635                participant_frames,
636                node_id,
637                batch_invocation_id,
638                claimed_backing,
639                prepared_participant_flights,
640                active_wave,
641                request_state_hazards,
642            )?,
643        ))
644    }
645
646    pub fn try_prepare_submission_wave(
647        self: &Arc<Self>,
648        requests: Vec<InvocationResourceAdmissionRequest>,
649    ) -> Result<StepSubmissionWaveAdmissionDecision<R>, VNextError> {
650        let plan = &self.participants[0].session.resources().request.plan;
651        let plan_nodes = plan.nodes();
652        if requests.is_empty()
653            || requests.len() != plan_nodes.len()
654            || requests
655                .iter()
656                .zip(plan_nodes)
657                .any(|(request, node)| request.node_id() != node.id())
658        {
659            return Err(invalid_resource(
660                "submission wave must cover every plan node exactly once in immutable plan order",
661            ));
662        }
663        let fit_policy = requests[0].fit_policy();
664        let pressure_action = requests[0].pressure_action();
665        if requests.iter().any(|request| {
666            request.fit_policy() != fit_policy || request.pressure_action() != pressure_action
667        }) {
668            return Err(invalid_resource(
669                "submission wave nodes require one admission fit and pressure policy",
670            ));
671        }
672        let work_shape = Arc::clone(&requests[0].work_shape);
673        if requests
674            .iter()
675            .any(|request| request.work_shape.as_ref() != work_shape.as_ref())
676        {
677            return Err(invalid_resource(
678                "submission wave nodes must share one canonical work authority",
679            ));
680        }
681        self.prepare_full_plan_submission_wave(work_shape, fit_policy, pressure_action)
682    }
683
684    /// Prepares a canonical immutable-plan node subset that can only be
685    /// submitted through the exact determinism restore/readback path.
686    pub fn try_prepare_determinism_submission_wave(
687        self: &Arc<Self>,
688        requests: Vec<InvocationResourceAdmissionRequest>,
689    ) -> Result<StepSubmissionWaveAdmissionDecision<R>, VNextError> {
690        let plan = &self.participants[0].session.resources().request.plan;
691        let plan_nodes = plan.nodes();
692        if requests.is_empty() {
693            return Err(invalid_resource(
694                "determinism submission wave requires a non-empty plan node scope",
695            ));
696        }
697        let node_indices = requests
698            .iter()
699            .map(|request| {
700                plan_nodes
701                    .iter()
702                    .position(|node| node.id() == request.node_id())
703                    .ok_or_else(|| {
704                        invalid_resource(
705                            "determinism submission wave references an unknown plan node",
706                        )
707                    })
708            })
709            .collect::<Result<Vec<_>, _>>()?;
710        if node_indices.windows(2).any(|pair| pair[0] >= pair[1]) {
711            return Err(invalid_resource(
712                "determinism submission wave nodes must be unique and in immutable plan order",
713            ));
714        }
715        let fit_policy = requests[0].fit_policy();
716        let pressure_action = requests[0].pressure_action();
717        if requests.iter().any(|request| {
718            request.fit_policy() != fit_policy || request.pressure_action() != pressure_action
719        }) {
720            return Err(invalid_resource(
721                "determinism submission wave nodes require one admission fit and pressure policy",
722            ));
723        }
724        let work_shape = Arc::clone(&requests[0].work_shape);
725        if requests
726            .iter()
727            .any(|request| request.work_shape.as_ref() != work_shape.as_ref())
728        {
729            return Err(invalid_resource(
730                "determinism submission wave nodes must share one canonical work authority",
731            ));
732        }
733        self.prepare_submission_wave(
734            work_shape,
735            fit_policy,
736            pressure_action,
737            &node_indices,
738            SubmissionWavePurpose::DeterminismProbe,
739        )
740    }
741
742    /// Prepares the only production wave topology: every immutable-plan node
743    /// in order over one shared participant/work authority. Callers do not
744    /// rebuild a per-node request vector for topology already committed by the
745    /// plan hash.
746    pub fn try_prepare_full_plan_submission_wave(
747        self: &Arc<Self>,
748        work_shape: Arc<BatchWorkShape>,
749        fit_policy: AdmissionFitPolicy,
750        pressure_action: AdmissionPressureAction,
751    ) -> Result<StepSubmissionWaveAdmissionDecision<R>, VNextError> {
752        self.prepare_full_plan_submission_wave(work_shape, fit_policy, pressure_action)
753    }
754
755    fn prepare_full_plan_submission_wave(
756        self: &Arc<Self>,
757        work_shape: Arc<BatchWorkShape>,
758        fit_policy: AdmissionFitPolicy,
759        pressure_action: AdmissionPressureAction,
760    ) -> Result<StepSubmissionWaveAdmissionDecision<R>, VNextError> {
761        let node_count = self.participants[0]
762            .session
763            .resources()
764            .request
765            .plan
766            .nodes()
767            .len();
768        let node_indices = (0..node_count).collect::<Vec<_>>();
769        self.prepare_submission_wave(
770            work_shape,
771            fit_policy,
772            pressure_action,
773            &node_indices,
774            SubmissionWavePurpose::FullPlan,
775        )
776    }
777
778    fn prepare_submission_wave(
779        self: &Arc<Self>,
780        work_shape: Arc<BatchWorkShape>,
781        fit_policy: AdmissionFitPolicy,
782        pressure_action: AdmissionPressureAction,
783        node_indices: &[usize],
784        purpose: SubmissionWavePurpose,
785    ) -> Result<StepSubmissionWaveAdmissionDecision<R>, VNextError> {
786        let _lifecycle = self.participants[0]
787            .session
788            .resources()
789            .request
790            .plan
791            .resources
792            .read_lifecycle("prepare a step submission wave")?;
793        let plan = &self.participants[0].session.resources().request.plan;
794        let plan_nodes = plan.nodes();
795        if plan_nodes.is_empty()
796            || node_indices.is_empty()
797            || node_indices.windows(2).any(|pair| pair[0] >= pair[1])
798            || node_indices
799                .last()
800                .is_some_and(|node_index| *node_index >= plan_nodes.len())
801        {
802            return Err(invalid_resource(
803                "submission wave requires a non-empty canonical immutable-plan node scope",
804            ));
805        }
806        if work_shape.participants().len() != self.participants.len()
807            || work_shape
808                .participants()
809                .iter()
810                .zip(&self.participants)
811                .any(|(authority, participant)| {
812                    authority.canonical_key() != session_participant_key(&participant.session)
813                })
814        {
815            return Err(invalid_resource(
816                "submission wave must bind every step participant exactly once",
817            ));
818        }
819        let participant_authority =
820            Arc::new(self.prepare_participant_authority(Arc::clone(&work_shape), fit_policy)?);
821        let prepared_nodes = node_indices
822            .iter()
823            .copied()
824            .map(|plan_node_index| {
825                PreparedStepSubmissionNode::new(plan_node_index, Arc::clone(&participant_authority))
826            })
827            .collect::<Vec<_>>();
828        let request_participants = participant_authority
829            .participants
830            .iter()
831            .map(|participant| {
832                let request = Arc::clone(participant.request_resources());
833                RequestStateHazardParticipant::new(
834                    Arc::clone(&request.plan.dynamic_pools().request_state_hazards),
835                    request.request_authority(),
836                    request,
837                )
838            })
839            .collect::<Vec<_>>();
840        let request_state_hazards = match plan
841            .dynamic_pools()
842            .request_state_hazards
843            .try_acquire(&request_participants, node_indices)?
844        {
845            RequestStateHazardAcquireDecision::Acquired(permit) => permit,
846            RequestStateHazardAcquireDecision::Deferred(deferred) => {
847                return Ok(StepSubmissionWaveAdmissionDecision::RequestStateDeferred(
848                    deferred,
849                ));
850            }
851            RequestStateHazardAcquireDecision::SplitRequired(split) => {
852                return Ok(StepSubmissionWaveAdmissionDecision::RequestStateSplitRequired(split));
853            }
854            RequestStateHazardAcquireDecision::Poisoned(poison) => {
855                return Ok(StepSubmissionWaveAdmissionDecision::RequestStatePoisoned(
856                    poison,
857                ));
858            }
859        };
860        let immediate_shape = work_shape.immediate_shape();
861        let fit_shape = match fit_policy {
862            AdmissionFitPolicy::ImmediateOnly => immediate_shape,
863            AdmissionFitPolicy::FullInputMustFit => work_shape.fit_shape(),
864        };
865        let (demand, requested_slices) = plan.submission_wave_demand(
866            immediate_shape,
867            fit_shape,
868            self.reusable_execution_bucket.as_ref(),
869            fit_policy,
870            pressure_action,
871        )?;
872        let prepared_backing = match plan
873            .prepare_lane_stable_backing_slices(&self.execution_lane, requested_slices)?
874        {
875            LaneBackingPrepareDecision::Prepared(prepared) => prepared,
876            LaneBackingPrepareDecision::Deferred(deferred) => {
877                let deferred_node_work_fingerprints = prepared_nodes
878                    .iter()
879                    .map(|node| {
880                        (
881                            node.node_id().clone(),
882                            node.work_shape().fingerprint().to_owned(),
883                        )
884                    })
885                    .collect();
886                return Ok(StepSubmissionWaveAdmissionDecision::BackingDeferred(
887                    StepSubmissionWaveBackingDeferral::new(
888                        Arc::clone(self),
889                        deferred,
890                        deferred_node_work_fingerprints,
891                    )?,
892                ));
893            }
894        };
895        let logical_capacity = if demand.immediate_claim().is_empty() {
896            None
897        } else {
898            let parents = self
899                .participants
900                .iter()
901                .map(|participant| participant.session.resources().logical_lease())
902                .collect::<Vec<_>>();
903            match plan
904                .logical_admission()
905                .try_claim_for_sequences(&parents, &demand)?
906            {
907                BatchCapacityClaimDecision::Claimed(capacity) => {
908                    if !plan
909                        .logical_admission()
910                        .owns_batch_capacity_claim(&capacity)
911                    {
912                        return Err(invalid_resource(
913                            "submission wave admission returned foreign capacity authority",
914                        ));
915                    }
916                    Some(capacity)
917                }
918                BatchCapacityClaimDecision::Deferred(deferred) => {
919                    return Ok(StepSubmissionWaveAdmissionDecision::Deferred(deferred));
920                }
921                BatchCapacityClaimDecision::PermanentRejected(rejected) => {
922                    return Ok(StepSubmissionWaveAdmissionDecision::PermanentRejected(
923                        rejected,
924                    ));
925                }
926            }
927        };
928        let committed_backing = prepared_backing.commit();
929        let has_program_binding_nodes = prepared_nodes.iter().any(|node| {
930            plan_nodes[node.plan_node_index()]
931                .binding_resource()
932                .is_some()
933        });
934        let program_binding_layout = match (
935            self.reusable_execution_bucket.as_ref(),
936            has_program_binding_nodes,
937        ) {
938            (Some(bucket), true) => Some(
939                plan.dynamic_pools()
940                    .program_binding_layout(bucket.bucket_id())
941                    .cloned()
942                    .ok_or_else(|| {
943                        invalid_resource(
944                            "reusable submission wave has no compiled program binding layout",
945                        )
946                    })?,
947            ),
948            _ => None,
949        };
950        let claimed_backing = ClaimedSubmissionWaveBacking::new(
951            plan.plan_hash().clone(),
952            prepared_nodes.len(),
953            plan_nodes.len(),
954            Arc::clone(&work_shape),
955            demand,
956            logical_capacity,
957            committed_backing,
958            program_binding_layout,
959        )?;
960        let wave_fingerprint =
961            submission_wave_fingerprint(self, &prepared_nodes, &claimed_backing, purpose)?;
962        let batch_invocation_id = issue_batch_invocation_id()?;
963        let prepared_participant_flights =
964            prepare_submission_wave_participant_flights(&participant_authority.flight_candidates)?;
965        let covered_participant_nodes = prepared_nodes
966            .len()
967            .checked_mul(participant_authority.participants.len())
968            .ok_or_else(|| invalid_resource("submission wave topology size exceeds usize"))?;
969        let active_wave = self.invocation_registry.enter_submission_wave(
970            covered_participant_nodes,
971            batch_invocation_id,
972            &wave_fingerprint,
973        )?;
974        Ok(StepSubmissionWaveAdmissionDecision::Prepared(
975            PreparedStepSubmissionWave {
976                claimed_backing,
977                initializations: None,
978                request_state_hazards,
979                nodes: prepared_nodes,
980                prepared_participant_flights,
981                active_wave,
982                step: Arc::clone(self),
983                execution_lane_id: self.execution_lane.id(),
984                batch_invocation_id,
985                fingerprint: wave_fingerprint,
986                purpose,
987            },
988        ))
989    }
990
991    fn prepare_invocation_scope(
992        &self,
993        request: InvocationResourceAdmissionRequest,
994    ) -> Result<PreparedInvocationScopeDecision<R>, VNextError> {
995        let PreparedInvocationMetadata {
996            participants,
997            participant_frames,
998            participant_session_identities: _,
999            flight_candidates,
1000            node_id,
1001            work_shape,
1002            fit_policy,
1003            pressure_action,
1004        } = self.prepare_invocation_metadata(request)?;
1005        let plan = &participants[0].request.plan;
1006        let node_index = plan
1007            .nodes()
1008            .iter()
1009            .position(|node| node.id() == &node_id)
1010            .ok_or_else(|| invalid_resource("invocation references an unknown plan node"))?;
1011        let request_participants = participants
1012            .iter()
1013            .map(|participant| {
1014                let request = Arc::clone(participant.request_resources());
1015                RequestStateHazardParticipant::new(
1016                    Arc::clone(&request.plan.dynamic_pools().request_state_hazards),
1017                    request.request_authority(),
1018                    request,
1019                )
1020            })
1021            .collect::<Vec<_>>();
1022        let request_state_hazards = match plan
1023            .dynamic_pools()
1024            .request_state_hazards
1025            .try_acquire(&request_participants, &[node_index])?
1026        {
1027            RequestStateHazardAcquireDecision::Acquired(permit) => permit,
1028            RequestStateHazardAcquireDecision::Deferred(deferred) => {
1029                return Ok(PreparedInvocationScopeDecision::RequestStateDeferred(
1030                    deferred,
1031                ));
1032            }
1033            RequestStateHazardAcquireDecision::SplitRequired(split) => {
1034                return Ok(PreparedInvocationScopeDecision::RequestStateSplitRequired(
1035                    split,
1036                ));
1037            }
1038            RequestStateHazardAcquireDecision::Poisoned(poison) => {
1039                return Ok(PreparedInvocationScopeDecision::RequestStatePoisoned(
1040                    poison,
1041                ));
1042            }
1043        };
1044        let immediate_shape = work_shape.immediate_shape();
1045        let fit_shape = match fit_policy {
1046            AdmissionFitPolicy::ImmediateOnly => immediate_shape,
1047            AdmissionFitPolicy::FullInputMustFit => work_shape.fit_shape(),
1048        };
1049        let (demand, requested_slices) = plan.scoped_demand(
1050            AllocationLifetime::Invocation,
1051            Some(&node_id),
1052            immediate_shape,
1053            fit_shape,
1054            self.reusable_execution_bucket.as_ref(),
1055            fit_policy,
1056            pressure_action,
1057        )?;
1058        let prepared = match plan.prepare_backing_slices(requested_slices)? {
1059            BackingPrepareDecision::Prepared(prepared) => prepared,
1060            BackingPrepareDecision::Deferred(deferred) => {
1061                return Ok(PreparedInvocationScopeDecision::BackingDeferred(deferred));
1062            }
1063        };
1064        let logical_capacity = if demand.immediate_claim().is_empty() {
1065            None
1066        } else {
1067            let parents = participants
1068                .iter()
1069                .map(|participant| participant.logical_lease())
1070                .collect::<Vec<_>>();
1071            match plan
1072                .logical_admission()
1073                .try_claim_for_sequences(&parents, &demand)?
1074            {
1075                BatchCapacityClaimDecision::Claimed(capacity) => {
1076                    let parents_match = capacity
1077                        .parents()
1078                        .iter()
1079                        .map(|parent| (parent.sequence(), parent.request()))
1080                        .eq(participants.iter().map(|participant| {
1081                            (
1082                                participant.sequence_authority(),
1083                                participant.request_authority(),
1084                            )
1085                        }));
1086                    if !plan
1087                        .logical_admission()
1088                        .owns_batch_capacity_claim(&capacity)
1089                        || !parents_match
1090                    {
1091                        return Err(invalid_resource(
1092                            "invocation admission returned capacity for another participant set",
1093                        ));
1094                    }
1095                    Some(capacity)
1096                }
1097                BatchCapacityClaimDecision::Deferred(deferred) => {
1098                    return Ok(PreparedInvocationScopeDecision::Deferred(deferred));
1099                }
1100                BatchCapacityClaimDecision::PermanentRejected(rejected) => {
1101                    return Ok(PreparedInvocationScopeDecision::PermanentRejected(rejected));
1102                }
1103            }
1104        };
1105        let backing_slices = prepared.commit();
1106        let claimed_backing = ClaimedBackingTransaction::new(
1107            work_shape,
1108            demand,
1109            logical_capacity,
1110            backing_slices,
1111            None,
1112        )?;
1113        Ok(PreparedInvocationScopeDecision::Prepared(
1114            PreparedInvocationScope {
1115                participants,
1116                participant_frames,
1117                node_id,
1118                claimed_backing,
1119                flight_candidates,
1120                request_state_hazards,
1121            },
1122        ))
1123    }
1124
1125    fn prepare_invocation_metadata(
1126        &self,
1127        request: InvocationResourceAdmissionRequest,
1128    ) -> Result<PreparedInvocationMetadata<R>, VNextError> {
1129        let InvocationResourceAdmissionRequest {
1130            node_id,
1131            work_shape,
1132            fit_policy,
1133            pressure_action,
1134        } = request;
1135        let PreparedParticipantAuthority {
1136            plan_evidence: _,
1137            participants,
1138            participant_frames,
1139            participant_session_identities,
1140            flight_candidates,
1141            work_shape,
1142        } = self.prepare_participant_authority(work_shape, fit_policy)?;
1143        Ok(PreparedInvocationMetadata {
1144            participants,
1145            participant_frames,
1146            participant_session_identities,
1147            flight_candidates,
1148            node_id,
1149            work_shape,
1150            fit_policy,
1151            pressure_action,
1152        })
1153    }
1154
1155    fn prepare_participant_authority(
1156        &self,
1157        work_shape: Arc<BatchWorkShape>,
1158        fit_policy: AdmissionFitPolicy,
1159    ) -> Result<PreparedParticipantAuthority<R>, VNextError> {
1160        let immediate_shape = work_shape.immediate_shape();
1161        let fit_shape = match fit_policy {
1162            AdmissionFitPolicy::ImmediateOnly => immediate_shape,
1163            AdmissionFitPolicy::FullInputMustFit => work_shape.fit_shape(),
1164        };
1165        let participant_sessions = work_shape
1166            .participants()
1167            .iter()
1168            .map(|authority| {
1169                self.participants
1170                    .binary_search_by_key(&authority.canonical_key(), |participant| {
1171                        session_participant_key(&participant.session)
1172                    })
1173                    .map(|index| Arc::clone(&self.participants[index].session))
1174                    .map_err(|_| {
1175                        invalid_resource(
1176                            "invocation participant is not a member of its execution frame",
1177                        )
1178                    })
1179            })
1180            .collect::<Result<Vec<_>, _>>()?;
1181        let mut participant_frames = Vec::with_capacity(participant_sessions.len());
1182        let mut flight_candidates = Vec::with_capacity(participant_sessions.len());
1183        for participant in &participant_sessions {
1184            let key = session_participant_key(participant);
1185            let index = self
1186                .participants
1187                .binary_search_by_key(&key, |step_participant| {
1188                    session_participant_key(&step_participant.session)
1189                })
1190                .map_err(|_| {
1191                    invalid_resource("invocation participant lost its execution-frame assignment")
1192                })?;
1193            let step_participant = &self.participants[index];
1194            participant_frames.push(StepParticipantFrameAssignment::new(
1195                participant.sequence_authority(),
1196                participant.request_authority(),
1197                step_participant.frame.frame_id,
1198            ));
1199            flight_candidates.push(ParticipantFlightCandidate {
1200                slot: Arc::clone(&participant.slot),
1201                epoch: participant.epoch,
1202                fingerprint: participant.fingerprint.clone(),
1203                frame: ActiveSequenceFrame {
1204                    frame_id: step_participant.frame.frame_id,
1205                    batch_step_id: self.batch_step_id,
1206                },
1207                participant: BatchParticipantAuthority::new(
1208                    participant.sequence_authority(),
1209                    participant.request_authority(),
1210                ),
1211            });
1212        }
1213        let participants = participant_sessions
1214            .iter()
1215            .map(|participant| Arc::clone(participant.resources()))
1216            .collect::<Vec<_>>();
1217        let participant_count = u32::try_from(participants.len())
1218            .map_err(|_| invalid_resource("invocation participant count exceeds u32"))?;
1219        if participant_count == 0
1220            || immediate_shape.sequences() != participant_count
1221            || fit_shape.sequences() != participant_count
1222        {
1223            return Err(invalid_resource(
1224                "invocation shape sequence count differs from its exact participant set",
1225            ));
1226        }
1227        let participant_authorities = participants
1228            .iter()
1229            .map(|participant| {
1230                BatchParticipantAuthority::new(
1231                    participant.sequence_authority(),
1232                    participant.request_authority(),
1233                )
1234            })
1235            .collect::<Vec<_>>();
1236        if work_shape.participants() != participant_authorities {
1237            return Err(invalid_resource(
1238                "invocation work authority differs from selected participants",
1239            ));
1240        }
1241        let participant_session_identities = flight_candidates
1242            .iter()
1243            .map(|candidate| (candidate.epoch, candidate.fingerprint.clone()))
1244            .collect();
1245        Ok(PreparedParticipantAuthority {
1246            plan_evidence: self.plan_evidence(),
1247            participants,
1248            participant_frames,
1249            participant_session_identities,
1250            flight_candidates,
1251            work_shape,
1252        })
1253    }
1254}
1255
1256impl<R> Drop for StepResourceLease<R>
1257where
1258    R: DeviceRuntime,
1259{
1260    fn drop(&mut self) {
1261        if !self.finalized {
1262            for participant in &self.participants {
1263                poison_session_frame(&participant.frame);
1264            }
1265        }
1266    }
1267}
1268
1269pub enum InvocationResourceAdmissionDecision<R>
1270where
1271    R: DeviceRuntime,
1272{
1273    Admitted(InvocationResourceLease<R>),
1274    Deferred(AdmissionDeferred),
1275    BackingDeferred(InvocationAdmissionBackingDeferral<R>),
1276    PermanentRejected(AdmissionRejected),
1277    RequestStateDeferred(RequestStateHazardDeferral),
1278    RequestStateSplitRequired(RequestStateHazardSplitRequired),
1279    RequestStatePoisoned(RequestStateHazardPoison),
1280}
1281
1282/// Non-cloneable backing authority for one exact node invocation under one
1283/// live step.
1284#[must_use = "invocation backing deferral retains its exact step parent"]
1285pub struct InvocationAdmissionBackingDeferral<R>
1286where
1287    R: DeviceRuntime,
1288{
1289    backing: PlanBackingDeferral<R>,
1290    step: Arc<StepResourceLease<R>>,
1291    node_id: NodeId,
1292    work_fingerprint: String,
1293}
1294
1295impl<R> InvocationAdmissionBackingDeferral<R>
1296where
1297    R: DeviceRuntime,
1298{
1299    fn new(
1300        step: Arc<StepResourceLease<R>>,
1301        evidence: DynamicBackingDeferred,
1302        node_id: NodeId,
1303        work_fingerprint: String,
1304    ) -> Result<Self, VNextError> {
1305        let resources = Arc::clone(
1306            &step.participants[0]
1307                .session
1308                .resources()
1309                .request
1310                .plan
1311                .resources,
1312        );
1313        Ok(Self {
1314            backing: PlanBackingDeferral::new(resources, evidence)?,
1315            step,
1316            node_id,
1317            work_fingerprint,
1318        })
1319    }
1320
1321    pub fn evidence(&self) -> &DynamicBackingDeferred {
1322        self.backing.evidence()
1323    }
1324
1325    pub fn node_id(&self) -> &NodeId {
1326        &self.node_id
1327    }
1328
1329    pub fn work_fingerprint(&self) -> &str {
1330        &self.work_fingerprint
1331    }
1332
1333    pub fn maintain(&self) -> Result<DynamicDeferredMaintenanceOutcome, VNextError> {
1334        if self.step.finalized {
1335            return Err(invalid_resource(
1336                "finalized step cannot maintain invocation backing",
1337            ));
1338        }
1339        self.backing.maintain()
1340    }
1341
1342    pub fn register_waiter(&self) -> Result<PlanCapacityWaitRegistration<R>, VNextError> {
1343        self.backing.register_waiter()
1344    }
1345}
1346
1347enum PreparedInvocationScopeDecision<R>
1348where
1349    R: DeviceRuntime,
1350{
1351    Prepared(PreparedInvocationScope<R>),
1352    Deferred(AdmissionDeferred),
1353    BackingDeferred(DynamicBackingDeferred),
1354    PermanentRejected(AdmissionRejected),
1355    RequestStateDeferred(RequestStateHazardDeferral),
1356    RequestStateSplitRequired(RequestStateHazardSplitRequired),
1357    RequestStatePoisoned(RequestStateHazardPoison),
1358}
1359
1360struct PreparedInvocationScope<R>
1361where
1362    R: DeviceRuntime,
1363{
1364    claimed_backing: ClaimedBackingTransaction,
1365    participants: Vec<Arc<AdmittedSequenceResources<R>>>,
1366    participant_frames: Vec<StepParticipantFrameAssignment>,
1367    flight_candidates: Vec<ParticipantFlightCandidate>,
1368    node_id: NodeId,
1369    request_state_hazards: Option<RequestStateHazardPermit<Arc<AdmittedRequestResources<R>>>>,
1370}
1371
1372struct PreparedInvocationMetadata<R>
1373where
1374    R: DeviceRuntime,
1375{
1376    participants: Vec<Arc<AdmittedSequenceResources<R>>>,
1377    participant_frames: Vec<StepParticipantFrameAssignment>,
1378    participant_session_identities: Vec<(SequenceSessionEpoch, SequenceSessionFingerprint)>,
1379    flight_candidates: Vec<ParticipantFlightCandidate>,
1380    node_id: NodeId,
1381    work_shape: Arc<BatchWorkShape>,
1382    fit_policy: AdmissionFitPolicy,
1383    pressure_action: AdmissionPressureAction,
1384}
1385
1386struct PreparedParticipantAuthority<R>
1387where
1388    R: DeviceRuntime,
1389{
1390    plan_evidence: TrustedPlanRuntimeEvidence,
1391    participants: Vec<Arc<AdmittedSequenceResources<R>>>,
1392    participant_frames: Vec<StepParticipantFrameAssignment>,
1393    participant_session_identities: Vec<(SequenceSessionEpoch, SequenceSessionFingerprint)>,
1394    flight_candidates: Vec<ParticipantFlightCandidate>,
1395    work_shape: Arc<BatchWorkShape>,
1396}
1397
1398fn submission_wave_fingerprint<R>(
1399    step: &StepResourceLease<R>,
1400    nodes: &[PreparedStepSubmissionNode<R>],
1401    claimed_backing: &ClaimedSubmissionWaveBacking,
1402    purpose: SubmissionWavePurpose,
1403) -> Result<String, VNextError>
1404where
1405    R: DeviceRuntime,
1406{
1407    #[derive(Serialize)]
1408    struct WaveNodeInput<'a> {
1409        plan_node_index: usize,
1410        node_id: &'a NodeId,
1411    }
1412
1413    #[derive(Serialize)]
1414    struct WaveInput<'a> {
1415        domain: &'static str,
1416        purpose: SubmissionWavePurpose,
1417        batch_step_id: BatchStepId,
1418        plan_hash: &'a super::PlanHash,
1419        node_count: usize,
1420        nodes: &'a [WaveNodeInput<'a>],
1421        step_backing_fingerprint: &'a str,
1422        invocation_backing_fingerprint: &'a str,
1423        participant_frames: &'a [StepParticipantFrameAssignment],
1424        work_fingerprint: &'a str,
1425    }
1426
1427    let first = nodes
1428        .first()
1429        .ok_or_else(|| invalid_resource("submission wave fingerprint requires plan nodes"))?;
1430    if nodes.len() != claimed_backing.node_count()
1431        || nodes
1432            .iter()
1433            .any(|node| !Arc::ptr_eq(&node.participant_authority, &first.participant_authority))
1434        || nodes
1435            .windows(2)
1436            .any(|pair| pair[0].plan_node_index() >= pair[1].plan_node_index())
1437        || first.work_shape() != claimed_backing.work_shape()
1438    {
1439        return Err(invalid_resource(
1440            "submission wave topology differs from its shared plan/work authority",
1441        ));
1442    }
1443    let node_scope = nodes
1444        .iter()
1445        .map(|node| WaveNodeInput {
1446            plan_node_index: node.plan_node_index(),
1447            node_id: node.node_id(),
1448        })
1449        .collect::<Vec<_>>();
1450    let bytes = serde_json::to_vec(&WaveInput {
1451        domain: "ferrum.runtime-vnext.step-submission-wave.v4",
1452        purpose,
1453        batch_step_id: step.batch_step_id,
1454        plan_hash: claimed_backing.plan_hash(),
1455        node_count: claimed_backing.node_count(),
1456        nodes: &node_scope,
1457        step_backing_fingerprint: step.claimed_backing.fingerprint(),
1458        invocation_backing_fingerprint: claimed_backing.fingerprint(),
1459        participant_frames: first.participant_frames(),
1460        work_fingerprint: claimed_backing.work_shape().fingerprint(),
1461    })
1462    .map_err(|error| {
1463        invalid_resource(format!(
1464            "submission wave fingerprint encode failed: {error}"
1465        ))
1466    })?;
1467    Ok(format!("{:x}", Sha256::digest(bytes)))
1468}
1469
1470#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1471#[serde(rename_all = "snake_case")]
1472pub(crate) enum SubmissionWavePurpose {
1473    FullPlan,
1474    DeterminismProbe,
1475}
1476
1477pub enum StepSubmissionWaveAdmissionDecision<R>
1478where
1479    R: DeviceRuntime,
1480{
1481    Prepared(PreparedStepSubmissionWave<R>),
1482    Deferred(AdmissionDeferred),
1483    BackingDeferred(StepSubmissionWaveBackingDeferral<R>),
1484    PermanentRejected(AdmissionRejected),
1485    RequestStateDeferred(RequestStateHazardDeferral),
1486    RequestStateSplitRequired(RequestStateHazardSplitRequired),
1487    RequestStatePoisoned(RequestStateHazardPoison),
1488}
1489
1490/// Non-cloneable backing authority for one immutable-plan submission wave.
1491#[must_use = "submission-wave backing deferral retains its exact step parent"]
1492pub struct StepSubmissionWaveBackingDeferral<R>
1493where
1494    R: DeviceRuntime,
1495{
1496    backing: PlanBackingDeferral<R>,
1497    step: Arc<StepResourceLease<R>>,
1498    node_work_fingerprints: Vec<(NodeId, String)>,
1499}
1500
1501impl<R> StepSubmissionWaveBackingDeferral<R>
1502where
1503    R: DeviceRuntime,
1504{
1505    fn new(
1506        step: Arc<StepResourceLease<R>>,
1507        evidence: DynamicBackingDeferred,
1508        node_work_fingerprints: Vec<(NodeId, String)>,
1509    ) -> Result<Self, VNextError> {
1510        let resources = Arc::clone(
1511            &step.participants[0]
1512                .session
1513                .resources()
1514                .request
1515                .plan
1516                .resources,
1517        );
1518        Ok(Self {
1519            backing: PlanBackingDeferral::new(resources, evidence)?,
1520            step,
1521            node_work_fingerprints,
1522        })
1523    }
1524
1525    pub fn evidence(&self) -> &DynamicBackingDeferred {
1526        self.backing.evidence()
1527    }
1528
1529    pub fn node_work_fingerprints(&self) -> &[(NodeId, String)] {
1530        &self.node_work_fingerprints
1531    }
1532
1533    pub fn maintain(&self) -> Result<DynamicDeferredMaintenanceOutcome, VNextError> {
1534        if self.step.finalized {
1535            return Err(invalid_resource(
1536                "finalized step cannot maintain submission-wave backing",
1537            ));
1538        }
1539        self.backing.maintain()
1540    }
1541
1542    pub fn register_waiter(&self) -> Result<PlanCapacityWaitRegistration<R>, VNextError> {
1543        self.backing.register_waiter()
1544    }
1545}
1546
1547/// One immutable-plan node projection inside a prepared physical submission
1548/// wave. The parent wave owns device-flight and retry authority for every node
1549/// as one atomic unit.
1550pub struct PreparedStepSubmissionNode<R>
1551where
1552    R: DeviceRuntime,
1553{
1554    participant_authority: Arc<PreparedParticipantAuthority<R>>,
1555    node_index: usize,
1556}
1557
1558impl<R> PreparedStepSubmissionNode<R>
1559where
1560    R: DeviceRuntime,
1561{
1562    fn new(node_index: usize, participant_authority: Arc<PreparedParticipantAuthority<R>>) -> Self {
1563        Self {
1564            participant_authority,
1565            node_index,
1566        }
1567    }
1568
1569    pub fn node_id(&self) -> &NodeId {
1570        self.participant_authority.participants[0]
1571            .request
1572            .plan
1573            .nodes()
1574            .get(self.node_index)
1575            .expect("prepared submission node index was derived from the immutable plan")
1576            .id()
1577    }
1578
1579    pub(crate) const fn plan_node_index(&self) -> usize {
1580        self.node_index
1581    }
1582
1583    pub fn participant_count(&self) -> u32 {
1584        u32::try_from(self.participant_authority.participants.len())
1585            .expect("wave participant count was validated before admission")
1586    }
1587
1588    pub fn participants(
1589        &self,
1590    ) -> impl ExactSizeIterator<Item = &Arc<AdmittedSequenceResources<R>>> {
1591        self.participant_authority.participants.iter()
1592    }
1593
1594    pub fn participant_frames(&self) -> &[StepParticipantFrameAssignment] {
1595        &self.participant_authority.participant_frames
1596    }
1597
1598    pub fn work_shape(&self) -> &BatchWorkShape {
1599        self.participant_authority.work_shape.as_ref()
1600    }
1601
1602    pub fn plan_evidence(&self) -> TrustedPlanRuntimeEvidence {
1603        self.participant_authority.plan_evidence.clone()
1604    }
1605
1606    pub(crate) fn plan_evidence_ref(&self) -> &TrustedPlanRuntimeEvidence {
1607        &self.participant_authority.plan_evidence
1608    }
1609
1610    pub(crate) fn runtime(&self) -> &Arc<R> {
1611        &self.participant_authority.participants[0]
1612            .request
1613            .plan
1614            .runtime()
1615    }
1616
1617    pub(crate) fn participant_session_identities(
1618        &self,
1619    ) -> impl ExactSizeIterator<Item = (SequenceSessionEpoch, &SequenceSessionFingerprint)> {
1620        self.participant_authority
1621            .participant_session_identities
1622            .iter()
1623            .map(|(epoch, fingerprint)| (*epoch, fingerprint))
1624    }
1625}
1626
1627/// Exact canonical command wave for one Step. Product waves cover the complete
1628/// plan; determinism probes carry a sealed purpose for a plan-ordered subset.
1629/// Node projections and shared Step backing remain owned until the one device
1630/// fence reaches a terminal state; dropping a prepared wave rolls back.
1631#[must_use = "a prepared submission wave must be dispatched or explicitly dropped"]
1632pub struct PreparedStepSubmissionWave<R>
1633where
1634    R: DeviceRuntime,
1635{
1636    // Drop wave backing and participant flights before releasing the Step.
1637    claimed_backing: ClaimedSubmissionWaveBacking,
1638    initializations: Option<PreparedBackingInitializations>,
1639    request_state_hazards: Option<RequestStateHazardPermit<Arc<AdmittedRequestResources<R>>>>,
1640    nodes: Vec<PreparedStepSubmissionNode<R>>,
1641    prepared_participant_flights: Vec<PreparedSubmissionWaveParticipantFlightHold>,
1642    active_wave: ActiveInvocationWaveGuard,
1643    step: Arc<StepResourceLease<R>>,
1644    execution_lane_id: ExecutionLaneId,
1645    batch_invocation_id: BatchInvocationId,
1646    fingerprint: String,
1647    purpose: SubmissionWavePurpose,
1648}
1649
1650impl<R> PreparedStepSubmissionWave<R>
1651where
1652    R: DeviceRuntime,
1653{
1654    pub fn batch_step_id(&self) -> BatchStepId {
1655        self.step.batch_step_id()
1656    }
1657
1658    pub const fn batch_invocation_id(&self) -> BatchInvocationId {
1659        self.batch_invocation_id
1660    }
1661
1662    pub const fn execution_lane_id(&self) -> ExecutionLaneId {
1663        self.execution_lane_id
1664    }
1665
1666    pub fn fingerprint(&self) -> &str {
1667        &self.fingerprint
1668    }
1669
1670    pub fn nodes(&self) -> &[PreparedStepSubmissionNode<R>] {
1671        &self.nodes
1672    }
1673
1674    pub(crate) const fn purpose(&self) -> SubmissionWavePurpose {
1675        self.purpose
1676    }
1677
1678    pub fn claimed_backing(&self) -> &ClaimedSubmissionWaveBacking {
1679        &self.claimed_backing
1680    }
1681
1682    pub fn node_count(&self) -> usize {
1683        self.nodes.len()
1684    }
1685
1686    pub fn prepared_participant_flight_count(&self) -> usize {
1687        self.prepared_participant_flights.len()
1688    }
1689
1690    pub fn node_participant_projection_count(&self) -> usize {
1691        self.nodes
1692            .iter()
1693            .map(|node| node.participant_count() as usize)
1694            .sum()
1695    }
1696
1697    pub fn physical_invocation_ledger_entry_count(&self) -> usize {
1698        self.active_wave.physical_entry_count()
1699    }
1700
1701    pub fn step_resources(&self) -> &Arc<StepResourceLease<R>> {
1702        &self.step
1703    }
1704
1705    pub(crate) fn runtime(&self) -> &Arc<R> {
1706        self.nodes[0].runtime()
1707    }
1708
1709    pub(crate) fn deferred_cleanup_domain(&self) -> DeferredDeviceCleanupDomainId {
1710        self.nodes[0].participant_authority.participants[0]
1711            .request
1712            .plan
1713            .resources
1714            .deferred_cleanup_domain
1715    }
1716
1717    pub fn has_shared_step_backing(&self) -> bool {
1718        self.step.claimed_backing().has_shared_physical_claims()
1719    }
1720
1721    pub(crate) fn backing_view(
1722        &self,
1723        node_index: usize,
1724        resource_id: &ResourceId,
1725    ) -> Result<LogicalBackingBufferView<'_, R::Buffer>, VNextError> {
1726        let node = self
1727            .nodes
1728            .get(node_index)
1729            .ok_or_else(|| invalid_resource("submission wave node index is out of bounds"))?;
1730        if let Some(authority) = self
1731            .claimed_backing
1732            .backing_slices()
1733            .iter()
1734            .find(|authority| authority.resource_id() == resource_id)
1735        {
1736            return node.participant_authority.participants[0]
1737                .request
1738                .plan
1739                .dynamic_pools()
1740                .view(authority);
1741        }
1742        self.step.backing_view(resource_id)
1743    }
1744
1745    pub(crate) fn begin_dispatch(&mut self) -> Result<(), VNextError> {
1746        match &self.initializations {
1747            Some(initializations) => initializations.ensure_wave(&self.fingerprint)?,
1748            None => {
1749                self.initializations = Some(PreparedBackingInitializations::prepare(
1750                    &self.step,
1751                    &self.fingerprint,
1752                )?);
1753            }
1754        }
1755        begin_submission_wave_participant_flights_dispatch(&mut self.prepared_participant_flights)
1756    }
1757
1758    pub(crate) fn encode_backing_initializations(
1759        &self,
1760        runtime: &R,
1761        commands: &mut DeviceCommandBatch<R::Command>,
1762    ) -> Result<usize, BackingInitializationEncodeError<R::Error>> {
1763        self.initializations
1764            .as_ref()
1765            .ok_or_else(|| {
1766                BackingInitializationEncodeError::Contract(invalid_resource(
1767                    "submission wave initialization was not prepared",
1768                ))
1769            })?
1770            .encode(&self.step, runtime, commands)
1771    }
1772
1773    pub(crate) fn mark_submission_fence_installed(&mut self) -> Result<(), VNextError> {
1774        self.initializations
1775            .as_mut()
1776            .ok_or_else(|| invalid_resource("submission wave initialization was not prepared"))?
1777            .mark_in_flight()?;
1778        self.active_wave.mark_in_flight()?;
1779        if let Some(hazards) = &mut self.request_state_hazards {
1780            hazards.mark_submission_fence_installed()?;
1781        }
1782        Ok(())
1783    }
1784
1785    pub(crate) fn mark_submission_indeterminate(&mut self) {
1786        if let Some(initializations) = &mut self.initializations {
1787            initializations.mark_indeterminate();
1788        }
1789        if let Some(hazards) = &mut self.request_state_hazards {
1790            hazards.mark_submission_indeterminate();
1791        }
1792    }
1793
1794    pub(crate) fn finish_backing_initializations(
1795        &mut self,
1796        succeeded: bool,
1797    ) -> Result<(), VNextError> {
1798        self.initializations
1799            .as_mut()
1800            .ok_or_else(|| invalid_resource("submission wave initialization was not prepared"))?
1801            .finish(succeeded)
1802    }
1803
1804    pub(crate) fn finish_request_state_hazards(
1805        &mut self,
1806        disposition: RequestStateHazardTerminalDisposition,
1807    ) -> Result<(), VNextError> {
1808        match &mut self.request_state_hazards {
1809            Some(hazards) => hazards.finish(disposition),
1810            None => Ok(()),
1811        }
1812    }
1813
1814    pub(crate) fn definitely_not_submitted(
1815        mut self,
1816    ) -> Result<DefinitelyNotSubmittedWaveRetryAuthority<R>, VNextError> {
1817        self.active_wave.mark_not_submitted()?;
1818        reset_submission_wave_participant_flights_after_definitely_not_submitted(
1819            &mut self.prepared_participant_flights,
1820        )?;
1821        let topology_fingerprint = self.fingerprint.clone();
1822        let prior_attempt = self.batch_invocation_id;
1823        Ok(DefinitelyNotSubmittedWaveRetryAuthority {
1824            wave: Some(self),
1825            topology_fingerprint,
1826            prior_attempt,
1827        })
1828    }
1829
1830    fn prepare_definitely_not_submitted_retry(
1831        &mut self,
1832        fresh_attempt: BatchInvocationId,
1833        topology_fingerprint: &str,
1834    ) -> Result<(), VNextError> {
1835        if self.fingerprint != topology_fingerprint
1836            || self
1837                .prepared_participant_flights
1838                .iter()
1839                .any(|hold| hold.phase != ParticipantFlightPhase::Prepared)
1840        {
1841            return Err(invalid_resource(
1842                "definitely-not-submitted wave retry topology changed",
1843            ));
1844        }
1845        self.initializations
1846            .as_ref()
1847            .ok_or_else(|| invalid_resource("wave retry lost backing initialization authority"))?
1848            .ensure_wave(topology_fingerprint)?;
1849        self.active_wave.prepare_retry(fresh_attempt)?;
1850        self.batch_invocation_id = fresh_attempt;
1851        Ok(())
1852    }
1853}
1854
1855#[must_use = "a definitely-not-submitted wave retry must be retried or retired"]
1856pub struct DefinitelyNotSubmittedWaveRetryAuthority<R>
1857where
1858    R: DeviceRuntime,
1859{
1860    wave: Option<PreparedStepSubmissionWave<R>>,
1861    topology_fingerprint: String,
1862    prior_attempt: BatchInvocationId,
1863}
1864
1865impl<R> fmt::Debug for DefinitelyNotSubmittedWaveRetryAuthority<R>
1866where
1867    R: DeviceRuntime,
1868{
1869    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1870        formatter
1871            .debug_struct("DefinitelyNotSubmittedWaveRetryAuthority")
1872            .field("prior_attempt", &self.prior_attempt)
1873            .field("topology_fingerprint", &self.topology_fingerprint)
1874            .finish_non_exhaustive()
1875    }
1876}
1877
1878impl<R> DefinitelyNotSubmittedWaveRetryAuthority<R>
1879where
1880    R: DeviceRuntime,
1881{
1882    pub const fn prior_attempt(&self) -> BatchInvocationId {
1883        self.prior_attempt
1884    }
1885
1886    pub fn topology_fingerprint(&self) -> &str {
1887        &self.topology_fingerprint
1888    }
1889
1890    pub fn retry(mut self) -> Result<PreparedStepSubmissionWave<R>, VNextError> {
1891        let fresh_attempt = issue_batch_invocation_id()?;
1892        let wave = self
1893            .wave
1894            .as_mut()
1895            .ok_or_else(|| invalid_resource("wave retry authority no longer owns its wave"))?;
1896        wave.prepare_definitely_not_submitted_retry(fresh_attempt, &self.topology_fingerprint)?;
1897        self.wave
1898            .take()
1899            .ok_or_else(|| invalid_resource("validated wave retry authority lost its wave"))
1900    }
1901}
1902
1903/// Exact prepared batch node/provider invocation authority. No device command
1904/// has been submitted at this layer; dropping it performs the typed
1905/// definitely-not-submitted participant-flight rollback.
1906#[must_use = "prepared invocation resources must be dispatched or explicitly dropped"]
1907pub struct InvocationResourceLease<R>
1908where
1909    R: DeviceRuntime,
1910{
1911    // Claimed backing is returned before participant-flight and parent frame
1912    // authorities. It retains the immutable work fingerprint even when empty.
1913    claimed_backing: ClaimedBackingTransaction,
1914    initializations: Option<PreparedBackingInitializations>,
1915    request_state_hazards: Option<RequestStateHazardPermit<Arc<AdmittedRequestResources<R>>>>,
1916    prepared_participant_flights: Vec<PreparedParticipantFlightHold>,
1917    active_wave: ActiveInvocationWaveGuard,
1918    participants: Vec<Arc<AdmittedSequenceResources<R>>>,
1919    participant_frames: Vec<StepParticipantFrameAssignment>,
1920    step: Arc<StepResourceLease<R>>,
1921    node_id: NodeId,
1922    batch_invocation_id: BatchInvocationId,
1923}
1924
1925impl<R> InvocationResourceLease<R>
1926where
1927    R: DeviceRuntime,
1928{
1929    fn new(
1930        step: Arc<StepResourceLease<R>>,
1931        participants: Vec<Arc<AdmittedSequenceResources<R>>>,
1932        participant_frames: Vec<StepParticipantFrameAssignment>,
1933        node_id: NodeId,
1934        batch_invocation_id: BatchInvocationId,
1935        claimed_backing: ClaimedBackingTransaction,
1936        prepared_participant_flights: Vec<PreparedParticipantFlightHold>,
1937        active_wave: ActiveInvocationWaveGuard,
1938        request_state_hazards: Option<RequestStateHazardPermit<Arc<AdmittedRequestResources<R>>>>,
1939    ) -> Result<Self, VNextError> {
1940        if participants.is_empty() {
1941            return Err(invalid_resource(
1942                "invocation resources require a non-empty participant set",
1943            ));
1944        }
1945        if participant_frames.len() != participants.len()
1946            || prepared_participant_flights.len() != participants.len()
1947            || participant_frames
1948                .iter()
1949                .zip(&participants)
1950                .any(|(assignment, participant)| {
1951                    assignment.sequence_authority() != participant.sequence_authority()
1952                        || assignment.request_authority() != participant.request_authority()
1953                })
1954        {
1955            return Err(invalid_resource(
1956                "invocation frame mapping differs from its exact participant set",
1957            ));
1958        }
1959        if claimed_backing.work_shape().participants().len() != participants.len()
1960            || claimed_backing
1961                .work_shape()
1962                .participants()
1963                .iter()
1964                .zip(&participants)
1965                .any(|(authority, participant)| {
1966                    authority.sequence_authority() != participant.sequence_authority()
1967                        || authority.request_authority() != participant.request_authority()
1968                })
1969            || claimed_backing.work_shape().immediate_tokens()
1970                > step.work_shape().immediate_tokens()
1971            || claimed_backing.work_shape().immediate_pages() > step.work_shape().immediate_pages()
1972            || claimed_backing.work_shape().fit_tokens() > step.work_shape().fit_tokens()
1973            || claimed_backing.work_shape().fit_pages() > step.work_shape().fit_pages()
1974        {
1975            return Err(invalid_resource(
1976                "invocation work shape differs from participants or exceeds its step",
1977            ));
1978        }
1979        if let Some(capacity) = claimed_backing.logical_capacity() {
1980            let coordinator = participants[0].request.plan.logical_admission();
1981            let parents_match = capacity
1982                .parents()
1983                .iter()
1984                .map(|parent| (parent.sequence(), parent.request()))
1985                .eq(participants.iter().map(|participant| {
1986                    (
1987                        participant.sequence_authority(),
1988                        participant.request_authority(),
1989                    )
1990                }));
1991            if !coordinator.owns_batch_capacity_claim(capacity) || !parents_match {
1992                return Err(invalid_resource(
1993                    "invocation capacity authority differs from its exact participants",
1994                ));
1995            }
1996        }
1997        Ok(Self {
1998            claimed_backing,
1999            initializations: None,
2000            request_state_hazards,
2001            prepared_participant_flights,
2002            active_wave,
2003            participants,
2004            participant_frames,
2005            step,
2006            node_id,
2007            batch_invocation_id,
2008        })
2009    }
2010
2011    pub fn node_id(&self) -> &NodeId {
2012        &self.node_id
2013    }
2014
2015    pub const fn batch_invocation_id(&self) -> BatchInvocationId {
2016        self.batch_invocation_id
2017    }
2018
2019    pub fn batch_step_id(&self) -> BatchStepId {
2020        self.step.batch_step_id()
2021    }
2022
2023    pub fn participant_count(&self) -> u32 {
2024        u32::try_from(self.participants.len())
2025            .expect("invocation participant count was validated before admission")
2026    }
2027
2028    pub fn prepared_participant_count(&self) -> u32 {
2029        u32::try_from(self.prepared_participant_flights.len())
2030            .expect("prepared participant count was validated at construction")
2031    }
2032
2033    pub(crate) fn participant_session_identities(
2034        &self,
2035    ) -> impl ExactSizeIterator<Item = (SequenceSessionEpoch, &SequenceSessionFingerprint)> {
2036        self.prepared_participant_flights
2037            .iter()
2038            .map(|hold| (hold.epoch, &hold.fingerprint))
2039    }
2040
2041    pub(crate) fn begin_dispatch(&mut self) -> Result<(), VNextError> {
2042        let topology_fingerprint = self.retry_topology_fingerprint()?;
2043        match &self.initializations {
2044            Some(initializations) => {
2045                initializations.ensure_wave(&topology_fingerprint)?;
2046            }
2047            None => {
2048                self.initializations = Some(PreparedBackingInitializations::prepare(
2049                    &self.step,
2050                    &topology_fingerprint,
2051                )?);
2052            }
2053        }
2054        begin_participant_flights_dispatch(&mut self.prepared_participant_flights)
2055    }
2056
2057    pub(crate) fn encode_backing_initializations(
2058        &self,
2059        runtime: &R,
2060        commands: &mut DeviceCommandBatch<R::Command>,
2061    ) -> Result<usize, BackingInitializationEncodeError<R::Error>> {
2062        self.initializations
2063            .as_ref()
2064            .ok_or_else(|| {
2065                BackingInitializationEncodeError::Contract(invalid_resource(
2066                    "invocation backing initialization was not prepared",
2067                ))
2068            })?
2069            .encode(&self.step, runtime, commands)
2070    }
2071
2072    pub(crate) fn mark_submission_fence_installed(&mut self) -> Result<(), VNextError> {
2073        self.initializations
2074            .as_mut()
2075            .ok_or_else(|| invalid_resource("invocation initialization was not prepared"))?
2076            .mark_in_flight()?;
2077        self.active_wave.mark_in_flight()?;
2078        if let Some(hazards) = &mut self.request_state_hazards {
2079            hazards.mark_submission_fence_installed()?;
2080        }
2081        Ok(())
2082    }
2083
2084    pub(crate) fn mark_submission_indeterminate(&mut self) {
2085        if let Some(initializations) = &mut self.initializations {
2086            initializations.mark_indeterminate();
2087        }
2088        if let Some(hazards) = &mut self.request_state_hazards {
2089            hazards.mark_submission_indeterminate();
2090        }
2091    }
2092
2093    pub(crate) fn finish_backing_initializations(
2094        &mut self,
2095        succeeded: bool,
2096    ) -> Result<(), VNextError> {
2097        self.initializations
2098            .as_mut()
2099            .ok_or_else(|| invalid_resource("invocation initialization was not prepared"))?
2100            .finish(succeeded)
2101    }
2102
2103    pub(crate) fn finish_request_state_hazards(
2104        &mut self,
2105        disposition: RequestStateHazardTerminalDisposition,
2106    ) -> Result<(), VNextError> {
2107        match &mut self.request_state_hazards {
2108            Some(hazards) => hazards.finish(disposition),
2109            None => Ok(()),
2110        }
2111    }
2112
2113    pub(crate) fn definitely_not_submitted(
2114        mut self,
2115    ) -> Result<DefinitelyNotSubmittedRetryAuthority<R>, VNextError> {
2116        self.active_wave.mark_not_submitted()?;
2117        reset_participant_flights_after_definitely_not_submitted(
2118            &mut self.prepared_participant_flights,
2119        )?;
2120        let topology_fingerprint = self.retry_topology_fingerprint()?;
2121        let work_fingerprint = self.work_shape().fingerprint().to_owned();
2122        let prior_attempt = self.batch_invocation_id;
2123        Ok(DefinitelyNotSubmittedRetryAuthority {
2124            invocation: Some(self),
2125            topology_fingerprint,
2126            work_fingerprint,
2127            prior_attempt,
2128        })
2129    }
2130
2131    fn prepare_definitely_not_submitted_retry(
2132        &mut self,
2133        fresh_attempt: BatchInvocationId,
2134        topology_fingerprint: &str,
2135        work_fingerprint: &str,
2136    ) -> Result<(), VNextError> {
2137        if self.retry_topology_fingerprint()? != topology_fingerprint
2138            || self.work_shape().fingerprint() != work_fingerprint
2139            || self
2140                .prepared_participant_flights
2141                .iter()
2142                .any(|hold| hold.phase != ParticipantFlightPhase::Prepared)
2143        {
2144            return Err(invalid_resource(
2145                "definitely-not-submitted retry topology or work fingerprint changed",
2146            ));
2147        }
2148        self.initializations
2149            .as_ref()
2150            .ok_or_else(|| {
2151                invalid_resource("invocation retry lost backing initialization authority")
2152            })?
2153            .ensure_wave(topology_fingerprint)?;
2154        self.active_wave.prepare_retry(fresh_attempt)?;
2155        self.batch_invocation_id = fresh_attempt;
2156        Ok(())
2157    }
2158
2159    fn retry_topology_fingerprint(&self) -> Result<String, VNextError> {
2160        #[derive(Serialize)]
2161        struct FingerprintInput<'a> {
2162            domain: &'static str,
2163            node_id: &'a NodeId,
2164            participant_frames: &'a [StepParticipantFrameAssignment],
2165            work_fingerprint: &'a str,
2166        }
2167        let bytes = serde_json::to_vec(&FingerprintInput {
2168            domain: "ferrum.runtime-vnext.invocation-retry-topology.v1",
2169            node_id: &self.node_id,
2170            participant_frames: &self.participant_frames,
2171            work_fingerprint: self.work_shape().fingerprint(),
2172        })
2173        .map_err(|error| {
2174            invalid_resource(format!(
2175                "invocation retry topology fingerprint encode failed: {error}"
2176            ))
2177        })?;
2178        Ok(format!("{:x}", Sha256::digest(bytes)))
2179    }
2180
2181    pub fn coordinator_id(&self) -> LogicalAdmissionCoordinatorId {
2182        self.participants[0].coordinator_id()
2183    }
2184
2185    pub fn participants(
2186        &self,
2187    ) -> impl ExactSizeIterator<Item = &Arc<AdmittedSequenceResources<R>>> {
2188        self.participants.iter()
2189    }
2190
2191    pub fn participant_frames(&self) -> &[StepParticipantFrameAssignment] {
2192        &self.participant_frames
2193    }
2194
2195    pub fn step_resources(&self) -> &Arc<StepResourceLease<R>> {
2196        &self.step
2197    }
2198
2199    pub(crate) fn participant_backing_snapshot(
2200        &self,
2201        index: usize,
2202    ) -> Result<&Arc<SequenceBackingSnapshot<R>>, VNextError> {
2203        let participant = self.participants.get(index).ok_or_else(|| {
2204            invalid_resource("invocation backing participant index is out of range")
2205        })?;
2206        self.step
2207            .participant_backing_snapshot(BatchParticipantAuthority::new(
2208                participant.sequence_authority(),
2209                participant.request_authority(),
2210            ))
2211    }
2212
2213    pub fn backing_slices(&self) -> &[LogicalBackingSliceAuthority] {
2214        self.claimed_backing.backing_slices()
2215    }
2216
2217    pub fn logical_capacity(&self) -> Option<&LogicalBatchCapacityLease> {
2218        self.claimed_backing.logical_capacity()
2219    }
2220
2221    pub fn work_shape(&self) -> &BatchWorkShape {
2222        self.claimed_backing.work_shape()
2223    }
2224
2225    pub fn claimed_backing(&self) -> &ClaimedBackingTransaction {
2226        &self.claimed_backing
2227    }
2228
2229    pub fn plan_evidence(&self) -> TrustedPlanRuntimeEvidence {
2230        self.step.plan_evidence()
2231    }
2232
2233    pub(crate) fn runtime(&self) -> &Arc<R> {
2234        self.participants[0].request.plan.runtime()
2235    }
2236
2237    pub(crate) fn deferred_cleanup_domain(&self) -> DeferredDeviceCleanupDomainId {
2238        self.participants[0]
2239            .request
2240            .plan
2241            .resources
2242            .deferred_cleanup_domain
2243    }
2244
2245    pub(crate) fn backing_view(
2246        &self,
2247        resource_id: &ResourceId,
2248    ) -> Result<LogicalBackingBufferView<'_, R::Buffer>, VNextError> {
2249        if let Some(authority) = self
2250            .claimed_backing
2251            .backing_slices()
2252            .iter()
2253            .find(|authority| authority.resource_id() == resource_id)
2254        {
2255            return self.step.participants[0]
2256                .session
2257                .resources()
2258                .request
2259                .plan
2260                .dynamic_pools()
2261                .view(authority);
2262        }
2263        self.step.backing_view(resource_id)
2264    }
2265
2266    pub(crate) fn participant_backing_views(
2267        &self,
2268        resource_id: &ResourceId,
2269    ) -> Result<Vec<(SequenceAuthorityId, LogicalBackingBufferView<'_, R::Buffer>)>, VNextError>
2270    {
2271        self.participants
2272            .iter()
2273            .map(|participant| {
2274                let authority = BatchParticipantAuthority::new(
2275                    participant.sequence_authority(),
2276                    participant.request_authority(),
2277                );
2278                Ok((
2279                    participant.sequence_authority(),
2280                    self.step.participant_backing_view(authority, resource_id)?,
2281                ))
2282            })
2283            .collect()
2284    }
2285}
2286
2287/// The sole retry edge after a device runtime proves that submit did not
2288/// happen. It owns the exact invocation, topology and work evidence; dropping
2289/// it retires the ledger tombstone and cannot be relabeled as retryable later.
2290#[must_use = "a definitely-not-submitted retry authority must be retried or retired"]
2291pub struct DefinitelyNotSubmittedRetryAuthority<R>
2292where
2293    R: DeviceRuntime,
2294{
2295    invocation: Option<InvocationResourceLease<R>>,
2296    topology_fingerprint: String,
2297    work_fingerprint: String,
2298    prior_attempt: BatchInvocationId,
2299}
2300
2301impl<R> fmt::Debug for DefinitelyNotSubmittedRetryAuthority<R>
2302where
2303    R: DeviceRuntime,
2304{
2305    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2306        formatter
2307            .debug_struct("DefinitelyNotSubmittedRetryAuthority")
2308            .field("prior_attempt", &self.prior_attempt)
2309            .field("topology_fingerprint", &self.topology_fingerprint)
2310            .field("work_fingerprint", &self.work_fingerprint)
2311            .finish_non_exhaustive()
2312    }
2313}
2314
2315impl<R> DefinitelyNotSubmittedRetryAuthority<R>
2316where
2317    R: DeviceRuntime,
2318{
2319    pub const fn prior_attempt(&self) -> BatchInvocationId {
2320        self.prior_attempt
2321    }
2322
2323    pub fn topology_fingerprint(&self) -> &str {
2324        &self.topology_fingerprint
2325    }
2326
2327    pub fn work_fingerprint(&self) -> &str {
2328        &self.work_fingerprint
2329    }
2330
2331    pub fn retry(mut self) -> Result<InvocationResourceLease<R>, VNextError> {
2332        let fresh_attempt = issue_batch_invocation_id()?;
2333        let invocation = self
2334            .invocation
2335            .as_mut()
2336            .ok_or_else(|| invalid_resource("retry authority no longer owns its invocation"))?;
2337        invocation.prepare_definitely_not_submitted_retry(
2338            fresh_attempt,
2339            &self.topology_fingerprint,
2340            &self.work_fingerprint,
2341        )?;
2342        Ok(self
2343            .invocation
2344            .take()
2345            .expect("validated retry authority still owns its invocation"))
2346    }
2347}