Skip to main content

ferrum_interfaces/vnext/resource/
batch.rs

1use super::{
2    fmt, invalid_resource, ActiveSequenceFrame, AdmissionDeferred, AdmissionDemand,
3    AdmissionRejected, AdmittedSequenceResources, Arc, BTreeMap, BackingClaimCertificate,
4    BatchInvocationId, BatchParticipantAuthority, BatchParticipantTokenSpan, BatchStepId,
5    BatchWorkShape, CommittedLaneBackingClaim, DeviceRuntime, Digest, DynamicBackingDeferred,
6    DynamicDeferredMaintenanceOutcome, ExecutionFrameId, ExecutionLane,
7    LaneStableArenaSlotIdentity, LaneStableArenaSlotLease, LogicalBackingSliceAuthority,
8    LogicalBatchCapacityLease, Mutex, NodeId, ParticipantFlightPhase, ParticipantNodeKey,
9    PlanBackingDeferral, PlanCapacityWaitRegistration, PlanHash, ProgramBindingExecutionBinding,
10    ProgramBindingLayout, ProgramBindingNodeBinding, RequestAuthorityId, SequenceAuthorityId,
11    SequenceBackingSnapshot, SequenceSession, SequenceSessionEpoch, SequenceSessionFingerprint,
12    SequenceSessionPhase, SequenceSessionSlot, SequenceSessionSlotState, Serialize, Sha256,
13    StepParticipantFrameAssignment, TokenSpanWork, TrustedPlanRuntimeEvidence, VNextError,
14};
15use crate::vnext::DeviceReusableExecutionProgramId;
16use crate::vnext::{ReusableExecutionBucketId, ReusableExecutionBucketSpec};
17
18/// Resources whose lifetime is one exact continuous-batch execution frame.
19/// Child invocation leases retain this scope through `Arc`, so shared frame
20/// capacity and every participant authority outlive asynchronous device work.
21#[must_use = "step resources must live through every child invocation"]
22pub struct StepResourceLease<R>
23where
24    R: DeviceRuntime,
25{
26    // The transaction releases physical extents before its logical claim,
27    // then per-sequence frame guards release before their parent sessions.
28    pub(super) claimed_backing: ClaimedBackingTransaction,
29    pub(super) participants: Vec<AdmittedStepParticipant<R>>,
30    pub(super) invocation_registry: Arc<InvocationRegistry>,
31    pub(super) execution_lane: Arc<ExecutionLane<R>>,
32    pub(super) reusable_execution_bucket: Option<ReusableExecutionBucketSpec>,
33    pub(super) batch_step_id: BatchStepId,
34    pub(super) finalized: bool,
35}
36
37/// Canonical non-empty set selected by the scheduler for one continuous
38/// batch. Membership is exact; capacity shapes may not claim a different
39/// sequence count and no global concurrency ceiling is embedded here.
40#[must_use = "a batch participant set is required to admit one execution frame"]
41pub struct ExecutionBatchParticipants<R>
42where
43    R: DeviceRuntime,
44{
45    pub(super) sessions: Vec<Arc<SequenceSession<R>>>,
46    plan_evidence: TrustedPlanRuntimeEvidence,
47}
48
49fn sequence_participant_key<R: DeviceRuntime>(
50    sequence: &AdmittedSequenceResources<R>,
51) -> (u32, u64, u32, u64) {
52    let sequence_authority = sequence.sequence_authority();
53    let request_authority = sequence.request_authority();
54    (
55        sequence_authority.sparse_id(),
56        sequence_authority.generation(),
57        request_authority.sparse_id(),
58        request_authority.generation(),
59    )
60}
61
62pub(super) fn session_participant_key<R: DeviceRuntime>(
63    session: &SequenceSession<R>,
64) -> (u32, u64, u32, u64) {
65    sequence_participant_key(session.resources())
66}
67
68impl<R> ExecutionBatchParticipants<R>
69where
70    R: DeviceRuntime,
71{
72    pub fn new(mut sessions: Vec<Arc<SequenceSession<R>>>) -> Result<Self, VNextError> {
73        sessions.sort_by_key(|session| session_participant_key(session));
74        if sessions.is_empty()
75            || sessions
76                .windows(2)
77                .any(|pair| session_participant_key(&pair[0]) == session_participant_key(&pair[1]))
78        {
79            return Err(invalid_resource(
80                "execution batch participants must be non-empty and unique",
81            ));
82        }
83        u32::try_from(sessions.len())
84            .map_err(|_| invalid_resource("execution batch participant count exceeds u32"))?;
85        let plan_evidence = sessions[0].resources().plan_evidence();
86        if sessions.iter().any(|session| {
87            session.resources().is_poisoned()
88                || session.resources().plan_evidence() != plan_evidence
89        }) {
90            return Err(invalid_resource(
91                "execution batch participants differ in plan, runtime, pool, coordinator, or health",
92            ));
93        }
94        let resources = Arc::clone(&sessions[0].resources().request.plan.resources);
95        if sessions
96            .iter()
97            .any(|session| !Arc::ptr_eq(&resources, &session.resources().request.plan.resources))
98        {
99            return Err(invalid_resource(
100                "execution batch participants belong to distinct plan runtime roots",
101            ));
102        }
103        let _lifecycle = resources.read_lifecycle("create execution batch participants")?;
104        Ok(Self {
105            sessions,
106            plan_evidence,
107        })
108    }
109
110    pub fn len(&self) -> u32 {
111        u32::try_from(self.sessions.len())
112            .expect("execution batch participant count is validated at construction")
113    }
114
115    pub fn is_empty(&self) -> bool {
116        false
117    }
118
119    pub fn sessions(&self) -> &[Arc<SequenceSession<R>>] {
120        &self.sessions
121    }
122
123    pub fn plan_evidence(&self) -> &TrustedPlanRuntimeEvidence {
124        &self.plan_evidence
125    }
126
127    pub fn bind_work_shape(
128        &self,
129        token_spans: Vec<TokenSpanWork>,
130    ) -> Result<BatchWorkShape, VNextError> {
131        if token_spans.len() != self.sessions.len() {
132            return Err(invalid_resource(
133                "batch token work count differs from its exact participant set",
134            ));
135        }
136        BatchWorkShape::new(
137            self.sessions
138                .iter()
139                .zip(token_spans)
140                .map(|(session, token_span)| {
141                    BatchParticipantTokenSpan::new(
142                        BatchParticipantAuthority::new(
143                            session.sequence_authority(),
144                            session.request_authority(),
145                        ),
146                        token_span,
147                    )
148                })
149                .collect(),
150        )
151    }
152}
153
154#[derive(Clone)]
155pub(super) struct SequenceFrameCandidate {
156    pub(super) slot: Arc<SequenceSessionSlot>,
157    pub(super) epoch: SequenceSessionEpoch,
158    pub(super) fingerprint: SequenceSessionFingerprint,
159}
160
161pub(super) struct SequenceFrameCaptureCandidate<R>
162where
163    R: DeviceRuntime,
164{
165    frame: SequenceFrameCandidate,
166    resources: Arc<AdmittedSequenceResources<R>>,
167}
168
169pub(super) struct SessionFrameHold {
170    pub(super) slot: Arc<SequenceSessionSlot>,
171    pub(super) epoch: SequenceSessionEpoch,
172    pub(super) fingerprint: SequenceSessionFingerprint,
173    pub(super) frame_id: ExecutionFrameId,
174    pub(super) batch_step_id: BatchStepId,
175    pub(super) finalized: bool,
176}
177
178pub(super) struct CapturedSessionFrame<R>
179where
180    R: DeviceRuntime,
181{
182    pub(super) hold: SessionFrameHold,
183    pub(super) backing_snapshot: Arc<SequenceBackingSnapshot<R>>,
184}
185
186#[derive(Clone)]
187pub(super) struct ParticipantFlightCandidate {
188    pub(super) slot: Arc<SequenceSessionSlot>,
189    pub(super) epoch: SequenceSessionEpoch,
190    pub(super) fingerprint: SequenceSessionFingerprint,
191    pub(super) frame: ActiveSequenceFrame,
192    pub(super) participant: BatchParticipantAuthority,
193}
194
195#[derive(Clone)]
196struct ParticipantNodeFlightCandidate {
197    candidate: ParticipantFlightCandidate,
198    node_id: NodeId,
199}
200
201impl ParticipantNodeFlightCandidate {
202    fn new(candidate: ParticipantFlightCandidate, node_id: NodeId) -> Self {
203        Self { candidate, node_id }
204    }
205
206    fn key(&self) -> ParticipantNodeKey {
207        ParticipantNodeKey::new(
208            self.candidate.participant,
209            self.candidate.frame.frame_id,
210            self.node_id.clone(),
211        )
212    }
213}
214
215/// One participant-local flight owned by an exact invocation. Dropping this
216/// hold removes the sequence-flight count, while the physical ledger guard
217/// independently retires its topology. Only the sealed device DNF path may
218/// reset an in-flight hold to Prepared for retry.
219pub(super) struct PreparedParticipantFlightHold {
220    slot: Arc<SequenceSessionSlot>,
221    pub(super) epoch: SequenceSessionEpoch,
222    pub(super) fingerprint: SequenceSessionFingerprint,
223    key: ParticipantNodeKey,
224    batch_step_id: BatchStepId,
225    pub(super) phase: ParticipantFlightPhase,
226}
227
228impl Drop for PreparedParticipantFlightHold {
229    fn drop(&mut self) {
230        let mut state = match self.slot.state.lock() {
231            Ok(state) => state,
232            Err(poisoned) => {
233                let mut state = poisoned.into_inner();
234                *state = SequenceSessionSlotState::FailClosed;
235                return;
236            }
237        };
238        match &mut *state {
239            SequenceSessionSlotState::Active(active)
240                if active.epoch == self.epoch && active.fingerprint == self.fingerprint =>
241            {
242                if active.participant_flights.remove(&self.key) != Some(self.phase) {
243                    active.phase = SequenceSessionPhase::Poisoned;
244                }
245            }
246            _ => *state = SequenceSessionSlotState::FailClosed,
247        }
248    }
249}
250
251pub(super) fn prepare_participant_flights(
252    candidates: &[ParticipantFlightCandidate],
253    node_id: &NodeId,
254) -> Result<Vec<PreparedParticipantFlightHold>, VNextError> {
255    prepare_participant_node_flights(
256        candidates
257            .iter()
258            .cloned()
259            .map(|candidate| ParticipantNodeFlightCandidate::new(candidate, node_id.clone()))
260            .collect(),
261    )
262}
263
264fn prepare_participant_node_flights(
265    candidates: Vec<ParticipantNodeFlightCandidate>,
266) -> Result<Vec<PreparedParticipantFlightHold>, VNextError> {
267    let mut candidates = candidates
268        .into_iter()
269        .map(|candidate| {
270            let key = candidate.key();
271            (candidate.candidate, key)
272        })
273        .collect::<Vec<_>>();
274    candidates.sort_by(|left, right| left.1.cmp(&right.1));
275    if candidates.is_empty() || candidates.windows(2).any(|pair| pair[0].1 >= pair[1].1) {
276        return Err(invalid_resource(
277            "prepared submission wave requires canonical non-empty unique participant-node keys",
278        ));
279    }
280
281    type ParticipantFrameKey = (u32, u64, u32, u64, u64);
282    let participant_frame_key = |key: &ParticipantNodeKey| -> ParticipantFrameKey {
283        (
284            key.sequence_authority().sparse_id(),
285            key.sequence_authority().generation(),
286            key.request_authority().sparse_id(),
287            key.request_authority().generation(),
288            key.frame_id().get(),
289        )
290    };
291    let mut slot_by_participant = BTreeMap::<ParticipantFrameKey, usize>::new();
292    let mut participant_by_slot = BTreeMap::<usize, ParticipantFrameKey>::new();
293    let mut unique_slots = Vec::<Arc<SequenceSessionSlot>>::new();
294    let mut slot_indices = Vec::with_capacity(candidates.len());
295    for (candidate, key) in &candidates {
296        let participant = participant_frame_key(key);
297        let slot_identity = Arc::as_ptr(&candidate.slot) as usize;
298        if slot_by_participant
299            .get(&participant)
300            .is_some_and(|known| *known != slot_identity)
301            || participant_by_slot
302                .get(&slot_identity)
303                .is_some_and(|known| *known != participant)
304        {
305            return Err(invalid_resource(
306                "submission wave participant/frame authority maps to inconsistent session slots",
307            ));
308        }
309        slot_by_participant.insert(participant, slot_identity);
310        participant_by_slot.insert(slot_identity, participant);
311        let slot_index = if let Some(index) = unique_slots
312            .iter()
313            .position(|slot| Arc::ptr_eq(slot, &candidate.slot))
314        {
315            index
316        } else {
317            unique_slots.push(Arc::clone(&candidate.slot));
318            unique_slots.len() - 1
319        };
320        slot_indices.push(slot_index);
321    }
322
323    let mut states = Vec::with_capacity(unique_slots.len());
324    for slot in &unique_slots {
325        states.push(
326            slot.state
327                .lock()
328                .map_err(|_| invalid_resource("sequence session state mutex is poisoned"))?,
329        );
330    }
331    for ((candidate, key), &slot_index) in candidates.iter().zip(&slot_indices) {
332        match &*states[slot_index] {
333            SequenceSessionSlotState::Active(active)
334                if active.epoch == candidate.epoch
335                    && active.fingerprint == candidate.fingerprint
336                    && active.phase == SequenceSessionPhase::Open
337                    && active.active_frame == Some(candidate.frame)
338                    && active.submission_wave_flight.is_none()
339                    && !active.participant_flights.contains_key(key) => {}
340            SequenceSessionSlotState::Active(active)
341                if active.epoch != candidate.epoch
342                    || active.fingerprint != candidate.fingerprint =>
343            {
344                return Err(invalid_resource(
345                    "stale prepared submission wave participant authority",
346                ));
347            }
348            SequenceSessionSlotState::Active(_) => {
349                return Err(invalid_resource(
350                    "cancelled, poisoned, duplicate, or cross-frame participant cannot enter a submission wave",
351                ));
352            }
353            _ => {
354                return Err(invalid_resource(
355                    "inactive or terminal participant cannot enter a submission wave",
356                ));
357            }
358        }
359    }
360
361    let mut inserted = Vec::<(usize, ParticipantNodeKey)>::new();
362    for ((_, key), &slot_index) in candidates.iter().zip(&slot_indices) {
363        let previous = {
364            let SequenceSessionSlotState::Active(active) = &mut *states[slot_index] else {
365                unreachable!("all prepared wave participants were validated");
366            };
367            active
368                .participant_flights
369                .insert(key.clone(), ParticipantFlightPhase::Prepared)
370        };
371        if let Some(previous) = previous {
372            for (rollback_slot, rollback_key) in inserted.into_iter().rev() {
373                if let SequenceSessionSlotState::Active(rollback) = &mut *states[rollback_slot] {
374                    rollback.participant_flights.remove(&rollback_key);
375                    rollback.phase = SequenceSessionPhase::Poisoned;
376                }
377            }
378            if let SequenceSessionSlotState::Active(active) = &mut *states[slot_index] {
379                active.participant_flights.insert(key.clone(), previous);
380                active.phase = SequenceSessionPhase::Poisoned;
381            }
382            return Err(invalid_resource(
383                "prepared participant wave changed during atomic insertion",
384            ));
385        }
386        inserted.push((slot_index, key.clone()));
387    }
388    drop(states);
389
390    Ok(candidates
391        .into_iter()
392        .map(|(candidate, key)| PreparedParticipantFlightHold {
393            slot: candidate.slot,
394            epoch: candidate.epoch,
395            fingerprint: candidate.fingerprint,
396            key,
397            batch_step_id: candidate.frame.batch_step_id,
398            phase: ParticipantFlightPhase::Prepared,
399        })
400        .collect())
401}
402
403/// One participant-local owner for a physical all-node submission wave. Node
404/// coverage remains in the immutable plan topology and invocation registry;
405/// the sequence session only tracks whether this participant has device work.
406pub(super) struct PreparedSubmissionWaveParticipantFlightHold {
407    slot: Arc<SequenceSessionSlot>,
408    pub(super) epoch: SequenceSessionEpoch,
409    pub(super) fingerprint: SequenceSessionFingerprint,
410    participant: BatchParticipantAuthority,
411    frame: ActiveSequenceFrame,
412    pub(super) phase: ParticipantFlightPhase,
413}
414
415impl PreparedSubmissionWaveParticipantFlightHold {
416    fn canonical_key(&self) -> (u32, u64, u32, u64, u64) {
417        let (sequence, sequence_generation, request, request_generation) =
418            self.participant.canonical_key();
419        (
420            sequence,
421            sequence_generation,
422            request,
423            request_generation,
424            self.frame.frame_id.get(),
425        )
426    }
427}
428
429impl Drop for PreparedSubmissionWaveParticipantFlightHold {
430    fn drop(&mut self) {
431        let mut state = match self.slot.state.lock() {
432            Ok(state) => state,
433            Err(poisoned) => {
434                let mut state = poisoned.into_inner();
435                *state = SequenceSessionSlotState::FailClosed;
436                return;
437            }
438        };
439        match &mut *state {
440            SequenceSessionSlotState::Active(active)
441                if active.epoch == self.epoch && active.fingerprint == self.fingerprint =>
442            {
443                if active.submission_wave_flight.take() != Some(self.phase) {
444                    active.phase = SequenceSessionPhase::Poisoned;
445                }
446            }
447            _ => *state = SequenceSessionSlotState::FailClosed,
448        }
449    }
450}
451
452pub(super) fn prepare_submission_wave_participant_flights(
453    candidates: &[ParticipantFlightCandidate],
454) -> Result<Vec<PreparedSubmissionWaveParticipantFlightHold>, VNextError> {
455    type ParticipantFrameKey = (u32, u64, u32, u64, u64);
456
457    let mut candidates = candidates
458        .iter()
459        .cloned()
460        .map(|candidate| {
461            let (sequence, sequence_generation, request, request_generation) =
462                candidate.participant.canonical_key();
463            let key = (
464                sequence,
465                sequence_generation,
466                request,
467                request_generation,
468                candidate.frame.frame_id.get(),
469            );
470            (candidate, key)
471        })
472        .collect::<Vec<_>>();
473    candidates.sort_by_key(|(_, key)| *key);
474    if candidates.is_empty() || candidates.windows(2).any(|pair| pair[0].1 >= pair[1].1) {
475        return Err(invalid_resource(
476            "submission wave requires canonical non-empty unique participant/frame authorities",
477        ));
478    }
479
480    let mut slot_by_participant = BTreeMap::<ParticipantFrameKey, usize>::new();
481    let mut participant_by_slot = BTreeMap::<usize, ParticipantFrameKey>::new();
482    let mut unique_slots = Vec::<Arc<SequenceSessionSlot>>::new();
483    let mut unique_slot_indices = BTreeMap::<usize, usize>::new();
484    let mut slot_indices = Vec::with_capacity(candidates.len());
485    for (candidate, participant) in &candidates {
486        let slot_identity = Arc::as_ptr(&candidate.slot) as usize;
487        if slot_by_participant
488            .get(participant)
489            .is_some_and(|known| *known != slot_identity)
490            || participant_by_slot
491                .get(&slot_identity)
492                .is_some_and(|known| known != participant)
493        {
494            return Err(invalid_resource(
495                "submission wave participant/frame authority maps to inconsistent session slots",
496            ));
497        }
498        slot_by_participant.insert(*participant, slot_identity);
499        participant_by_slot.insert(slot_identity, *participant);
500        let slot_index = match unique_slot_indices.get(&slot_identity).copied() {
501            Some(index) => index,
502            None => {
503                let index = unique_slots.len();
504                unique_slots.push(Arc::clone(&candidate.slot));
505                unique_slot_indices.insert(slot_identity, index);
506                index
507            }
508        };
509        slot_indices.push(slot_index);
510    }
511
512    let mut states = Vec::with_capacity(unique_slots.len());
513    for slot in &unique_slots {
514        states.push(
515            slot.state
516                .lock()
517                .map_err(|_| invalid_resource("sequence session state mutex is poisoned"))?,
518        );
519    }
520    for ((candidate, _), &slot_index) in candidates.iter().zip(&slot_indices) {
521        match &*states[slot_index] {
522            SequenceSessionSlotState::Active(active)
523                if active.epoch == candidate.epoch
524                    && active.fingerprint == candidate.fingerprint
525                    && active.phase == SequenceSessionPhase::Open
526                    && active.active_frame == Some(candidate.frame)
527                    && active.participant_flights.is_empty()
528                    && active.submission_wave_flight.is_none() => {}
529            SequenceSessionSlotState::Active(active)
530                if active.epoch != candidate.epoch
531                    || active.fingerprint != candidate.fingerprint =>
532            {
533                return Err(invalid_resource(
534                    "stale submission wave participant authority",
535                ));
536            }
537            SequenceSessionSlotState::Active(_) => {
538                return Err(invalid_resource(
539                    "cancelled, poisoned, duplicate, cross-frame, or node-busy participant cannot enter a submission wave",
540                ));
541            }
542            _ => {
543                return Err(invalid_resource(
544                    "inactive or terminal participant cannot enter a submission wave",
545                ));
546            }
547        }
548    }
549
550    let mut inserted_slots = Vec::<usize>::with_capacity(candidates.len());
551    for &slot_index in &slot_indices {
552        let previous = {
553            let SequenceSessionSlotState::Active(active) = &mut *states[slot_index] else {
554                unreachable!("all submission wave participants were validated");
555            };
556            active
557                .submission_wave_flight
558                .replace(ParticipantFlightPhase::Prepared)
559        };
560        if previous.is_some() {
561            for rollback_slot in inserted_slots.into_iter().rev() {
562                if let SequenceSessionSlotState::Active(rollback) = &mut *states[rollback_slot] {
563                    rollback.submission_wave_flight = None;
564                    rollback.phase = SequenceSessionPhase::Poisoned;
565                }
566            }
567            if let SequenceSessionSlotState::Active(active) = &mut *states[slot_index] {
568                active.submission_wave_flight = previous;
569                active.phase = SequenceSessionPhase::Poisoned;
570            }
571            return Err(invalid_resource(
572                "submission wave participant flights changed during atomic insertion",
573            ));
574        }
575        inserted_slots.push(slot_index);
576    }
577    drop(states);
578
579    Ok(candidates
580        .into_iter()
581        .map(
582            |(candidate, _)| PreparedSubmissionWaveParticipantFlightHold {
583                slot: candidate.slot,
584                epoch: candidate.epoch,
585                fingerprint: candidate.fingerprint,
586                participant: candidate.participant,
587                frame: candidate.frame,
588                phase: ParticipantFlightPhase::Prepared,
589            },
590        )
591        .collect())
592}
593
594pub(super) fn begin_submission_wave_participant_flights_dispatch(
595    holds: &mut [PreparedSubmissionWaveParticipantFlightHold],
596) -> Result<(), VNextError> {
597    transition_submission_wave_participant_flights(
598        holds,
599        ParticipantFlightPhase::Prepared,
600        ParticipantFlightPhase::InFlight,
601        "begin submission wave dispatch",
602    )
603}
604
605pub(super) fn reset_submission_wave_participant_flights_after_definitely_not_submitted(
606    holds: &mut [PreparedSubmissionWaveParticipantFlightHold],
607) -> Result<(), VNextError> {
608    transition_submission_wave_participant_flights(
609        holds,
610        ParticipantFlightPhase::InFlight,
611        ParticipantFlightPhase::Prepared,
612        "reset definitely-not-submitted submission wave dispatch",
613    )
614}
615
616fn transition_submission_wave_participant_flights(
617    holds: &mut [PreparedSubmissionWaveParticipantFlightHold],
618    expected: ParticipantFlightPhase,
619    next: ParticipantFlightPhase,
620    context: &'static str,
621) -> Result<(), VNextError> {
622    if holds.is_empty()
623        || holds.iter().any(|hold| hold.phase != expected)
624        || holds
625            .windows(2)
626            .any(|pair| pair[0].canonical_key() >= pair[1].canonical_key())
627    {
628        return Err(invalid_resource(format!(
629            "{context} requires canonical non-empty unique participant flights in the expected phase"
630        )));
631    }
632
633    let slots = holds
634        .iter()
635        .map(|hold| Arc::clone(&hold.slot))
636        .collect::<Vec<_>>();
637    let mut states = Vec::with_capacity(slots.len());
638    for slot in &slots {
639        states.push(
640            slot.state
641                .lock()
642                .map_err(|_| invalid_resource("sequence session state mutex is poisoned"))?,
643        );
644    }
645    for (hold, state) in holds.iter().zip(&states) {
646        match &**state {
647            SequenceSessionSlotState::Active(active)
648                if active.epoch == hold.epoch
649                    && active.fingerprint == hold.fingerprint
650                    && active.phase == SequenceSessionPhase::Open
651                    && active.active_frame == Some(hold.frame)
652                    && active.participant_flights.is_empty()
653                    && active.submission_wave_flight == Some(expected) => {}
654            SequenceSessionSlotState::Active(active)
655                if active.epoch != hold.epoch || active.fingerprint != hold.fingerprint =>
656            {
657                return Err(invalid_resource(
658                    "stale submission wave participant authority during phase transition",
659                ));
660            }
661            SequenceSessionSlotState::Active(_) => {
662                return Err(invalid_resource(format!(
663                    "cancelled, poisoned, wrong-phase, cross-frame, or node-busy participant cannot {context}"
664                )));
665            }
666            _ => {
667                return Err(invalid_resource(format!(
668                    "inactive or terminal participant cannot {context}"
669                )));
670            }
671        }
672    }
673    for state in &mut states {
674        let SequenceSessionSlotState::Active(active) = &mut **state else {
675            unreachable!("all submission wave participants were validated while locked");
676        };
677        active.submission_wave_flight = Some(next);
678    }
679    drop(states);
680    for hold in holds {
681        hold.phase = next;
682    }
683    Ok(())
684}
685
686pub(super) fn begin_participant_flights_dispatch(
687    holds: &mut [PreparedParticipantFlightHold],
688) -> Result<(), VNextError> {
689    transition_participant_flights(
690        holds,
691        ParticipantFlightPhase::Prepared,
692        ParticipantFlightPhase::InFlight,
693        "begin dispatch",
694    )
695}
696
697pub(super) fn reset_participant_flights_after_definitely_not_submitted(
698    holds: &mut [PreparedParticipantFlightHold],
699) -> Result<(), VNextError> {
700    transition_participant_flights(
701        holds,
702        ParticipantFlightPhase::InFlight,
703        ParticipantFlightPhase::Prepared,
704        "reset definitely-not-submitted dispatch",
705    )
706}
707
708fn transition_participant_flights(
709    holds: &mut [PreparedParticipantFlightHold],
710    expected: ParticipantFlightPhase,
711    next: ParticipantFlightPhase,
712    context: &'static str,
713) -> Result<(), VNextError> {
714    if holds.is_empty()
715        || holds.iter().any(|hold| hold.phase != expected)
716        || holds.windows(2).any(|pair| pair[0].key >= pair[1].key)
717    {
718        return Err(invalid_resource(format!(
719            "{context} requires canonical non-empty unique participant-node flights in the expected phase"
720        )));
721    }
722
723    let mut unique_slots = Vec::<Arc<SequenceSessionSlot>>::new();
724    let mut slot_indices = Vec::with_capacity(holds.len());
725    for hold in holds.iter() {
726        let slot_index = if let Some(index) = unique_slots
727            .iter()
728            .position(|slot| Arc::ptr_eq(slot, &hold.slot))
729        {
730            index
731        } else {
732            unique_slots.push(Arc::clone(&hold.slot));
733            unique_slots.len() - 1
734        };
735        slot_indices.push(slot_index);
736    }
737    let mut states = Vec::with_capacity(unique_slots.len());
738    for slot in &unique_slots {
739        states.push(
740            slot.state
741                .lock()
742                .map_err(|_| invalid_resource("sequence session state mutex is poisoned"))?,
743        );
744    }
745    for (hold, &slot_index) in holds.iter().zip(&slot_indices) {
746        match &*states[slot_index] {
747            SequenceSessionSlotState::Active(active)
748                if active.epoch == hold.epoch
749                    && active.fingerprint == hold.fingerprint
750                    && active.phase == SequenceSessionPhase::Open
751                    && active.active_frame
752                        == Some(ActiveSequenceFrame {
753                            frame_id: hold.key.frame_id(),
754                            batch_step_id: hold.batch_step_id,
755                        })
756                    && active.submission_wave_flight.is_none()
757                    && active.participant_flights.get(&hold.key) == Some(&expected) => {}
758            SequenceSessionSlotState::Active(active)
759                if active.epoch != hold.epoch || active.fingerprint != hold.fingerprint =>
760            {
761                return Err(invalid_resource(
762                    "stale invocation participant authority during phase transition",
763                ));
764            }
765            SequenceSessionSlotState::Active(_) => {
766                return Err(invalid_resource(format!(
767                    "cancelled, poisoned, wrong-phase, or cross-frame participant cannot {context}"
768                )));
769            }
770            _ => {
771                return Err(invalid_resource(format!(
772                    "inactive or terminal participant cannot {context}"
773                )));
774            }
775        }
776    }
777    for (hold, &slot_index) in holds.iter().zip(&slot_indices) {
778        let SequenceSessionSlotState::Active(active) = &mut *states[slot_index] else {
779            unreachable!("all dispatch participants were validated while locked");
780        };
781        let phase = active
782            .participant_flights
783            .get_mut(&hold.key)
784            .expect("validated participant flight remains present while locked");
785        *phase = next;
786    }
787    for hold in holds {
788        hold.phase = next;
789    }
790    Ok(())
791}
792
793impl Drop for SessionFrameHold {
794    fn drop(&mut self) {
795        if self.finalized {
796            return;
797        }
798        let mut state = match self.slot.state.lock() {
799            Ok(state) => state,
800            Err(poisoned) => poisoned.into_inner(),
801        };
802        if let SequenceSessionSlotState::Active(active) = &mut *state {
803            if active.epoch == self.epoch
804                && active.fingerprint == self.fingerprint
805                && active.active_frame
806                    == Some(ActiveSequenceFrame {
807                        frame_id: self.frame_id,
808                        batch_step_id: self.batch_step_id,
809                    })
810            {
811                active.phase = SequenceSessionPhase::Poisoned;
812            }
813        }
814    }
815}
816
817pub(super) struct AdmittedStepParticipant<R>
818where
819    R: DeviceRuntime,
820{
821    pub(super) frame: SessionFrameHold,
822    // Drop the independently retained backing before the session parent. The
823    // snapshot also owns a runtime keepalive for fence/reaper handoff.
824    pub(super) backing_snapshot: Arc<SequenceBackingSnapshot<R>>,
825    pub(super) session: Arc<SequenceSession<R>>,
826}
827
828fn execution_frame_successor(frame_id: ExecutionFrameId) -> Option<ExecutionFrameId> {
829    frame_id
830        .get()
831        .checked_add(1)
832        .and_then(|next| ExecutionFrameId::try_from(next).ok())
833}
834
835pub(super) fn acquire_session_frames(
836    candidates: &[SequenceFrameCandidate],
837    batch_step_id: BatchStepId,
838) -> Result<Vec<SessionFrameHold>, VNextError> {
839    if candidates.is_empty()
840        || candidates.iter().enumerate().any(|(index, candidate)| {
841            candidates[..index]
842                .iter()
843                .any(|prior| Arc::ptr_eq(&prior.slot, &candidate.slot))
844        })
845    {
846        return Err(invalid_resource(
847            "step frame acquisition requires non-empty unique session slots",
848        ));
849    }
850    let mut holds = Vec::with_capacity(candidates.len());
851    let mut states = Vec::with_capacity(candidates.len());
852    for candidate in candidates {
853        states.push(
854            candidate
855                .slot
856                .state
857                .lock()
858                .map_err(|_| invalid_resource("sequence session state mutex is poisoned"))?,
859        );
860    }
861    for (candidate, state) in candidates.iter().zip(&states) {
862        match &**state {
863            SequenceSessionSlotState::Active(active)
864                if active.epoch == candidate.epoch
865                    && active.fingerprint == candidate.fingerprint
866                    && active.phase == SequenceSessionPhase::Open
867                    && active.active_frame.is_none()
868                    && active.next_frame.is_some() => {}
869            SequenceSessionSlotState::Active(active)
870                if active.epoch != candidate.epoch
871                    || active.fingerprint != candidate.fingerprint =>
872            {
873                return Err(invalid_resource("stale sequence session frame authority"));
874            }
875            SequenceSessionSlotState::Active(_) => {
876                return Err(invalid_resource(
877                    "sequence session cannot acquire a frame in its current phase",
878                ));
879            }
880            _ => {
881                return Err(invalid_resource(
882                    "inactive or terminal sequence session cannot acquire a frame",
883                ));
884            }
885        }
886    }
887    for (candidate, state) in candidates.iter().zip(&mut states) {
888        let SequenceSessionSlotState::Active(active) = &mut **state else {
889            unreachable!("all session frame candidates were validated");
890        };
891        let frame_id = active
892            .next_frame
893            .take()
894            .expect("validated session has a next execution frame");
895        active.next_frame = execution_frame_successor(frame_id);
896        active.active_frame = Some(ActiveSequenceFrame {
897            frame_id,
898            batch_step_id,
899        });
900        holds.push(SessionFrameHold {
901            slot: Arc::clone(&candidate.slot),
902            epoch: candidate.epoch,
903            fingerprint: candidate.fingerprint.clone(),
904            frame_id,
905            batch_step_id,
906            finalized: false,
907        });
908    }
909    Ok(holds)
910}
911
912pub(super) fn session_frame_candidates<R: DeviceRuntime>(
913    sessions: &[Arc<SequenceSession<R>>],
914) -> Vec<SequenceFrameCandidate> {
915    sessions
916        .iter()
917        .map(|session| SequenceFrameCandidate {
918            slot: Arc::clone(&session.slot),
919            epoch: session.epoch,
920            fingerprint: session.fingerprint.clone(),
921        })
922        .collect()
923}
924
925pub(super) fn acquire_session_frames_with_backing<R>(
926    candidates: &[SequenceFrameCaptureCandidate<R>],
927    batch_step_id: BatchStepId,
928) -> Result<Vec<CapturedSessionFrame<R>>, VNextError>
929where
930    R: DeviceRuntime,
931{
932    if candidates.is_empty()
933        || candidates.iter().enumerate().any(|(index, candidate)| {
934            candidates[..index]
935                .iter()
936                .any(|prior| Arc::ptr_eq(&prior.frame.slot, &candidate.frame.slot))
937        })
938    {
939        return Err(invalid_resource(
940            "step frame acquisition requires non-empty unique session slots",
941        ));
942    }
943    let mut states = candidates
944        .iter()
945        .map(|candidate| {
946            candidate
947                .frame
948                .slot
949                .state
950                .lock()
951                .map_err(|_| invalid_resource("sequence session state mutex is poisoned"))
952        })
953        .collect::<Result<Vec<_>, _>>()?;
954    for (candidate, state) in candidates.iter().zip(&states) {
955        match &**state {
956            SequenceSessionSlotState::Active(active)
957                if active.epoch == candidate.frame.epoch
958                    && active.fingerprint == candidate.frame.fingerprint
959                    && active.phase == SequenceSessionPhase::Open
960                    && active.active_frame.is_none()
961                    && active.next_frame.is_some() => {}
962            SequenceSessionSlotState::Active(active)
963                if active.epoch != candidate.frame.epoch
964                    || active.fingerprint != candidate.frame.fingerprint =>
965            {
966                return Err(invalid_resource("stale sequence session frame authority"));
967            }
968            SequenceSessionSlotState::Active(_) => {
969                return Err(invalid_resource(
970                    "sequence session cannot acquire a frame in its current phase",
971                ));
972            }
973            _ => {
974                return Err(invalid_resource(
975                    "inactive or terminal sequence session cannot acquire a frame",
976                ));
977            }
978        }
979    }
980    // Frame capture and extension publication use the same slot -> backing
981    // lock order. Allocator, device, copy, and fence work stay outside it.
982    let backing_states = candidates
983        .iter()
984        .map(|candidate| candidate.resources.lock_backing_state())
985        .collect::<Result<Vec<_>, _>>()?;
986    let mut captured = Vec::with_capacity(candidates.len());
987    for ((candidate, state), backing_state) in
988        candidates.iter().zip(&mut states).zip(&backing_states)
989    {
990        let SequenceSessionSlotState::Active(active) = &mut **state else {
991            unreachable!("all session frame candidates were validated");
992        };
993        let frame_id = active
994            .next_frame
995            .take()
996            .expect("validated session has a next execution frame");
997        active.next_frame = execution_frame_successor(frame_id);
998        active.active_frame = Some(ActiveSequenceFrame {
999            frame_id,
1000            batch_step_id,
1001        });
1002        captured.push(CapturedSessionFrame {
1003            hold: SessionFrameHold {
1004                slot: Arc::clone(&candidate.frame.slot),
1005                epoch: candidate.frame.epoch,
1006                fingerprint: candidate.frame.fingerprint.clone(),
1007                frame_id,
1008                batch_step_id,
1009                finalized: false,
1010            },
1011            backing_snapshot: Arc::clone(&backing_state.current),
1012        });
1013    }
1014    Ok(captured)
1015}
1016
1017pub(super) fn session_frame_capture_candidates<R: DeviceRuntime>(
1018    sessions: &[Arc<SequenceSession<R>>],
1019) -> Vec<SequenceFrameCaptureCandidate<R>> {
1020    sessions
1021        .iter()
1022        .map(|session| SequenceFrameCaptureCandidate {
1023            frame: SequenceFrameCandidate {
1024                slot: Arc::clone(&session.slot),
1025                epoch: session.epoch,
1026                fingerprint: session.fingerprint.clone(),
1027            },
1028            resources: Arc::clone(session.resources()),
1029        })
1030        .collect()
1031}
1032
1033pub(super) fn poison_session_frame(hold: &SessionFrameHold) {
1034    let mut state = match hold.slot.state.lock() {
1035        Ok(state) => state,
1036        Err(poisoned) => poisoned.into_inner(),
1037    };
1038    if let SequenceSessionSlotState::Active(active) = &mut *state {
1039        if active.epoch == hold.epoch
1040            && active.fingerprint == hold.fingerprint
1041            && active.active_frame
1042                == Some(ActiveSequenceFrame {
1043                    frame_id: hold.frame_id,
1044                    batch_step_id: hold.batch_step_id,
1045                })
1046        {
1047            active.phase = SequenceSessionPhase::Poisoned;
1048        }
1049    }
1050}
1051
1052#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1053pub(super) enum StepFrameFinalization {
1054    Commit,
1055    Abort,
1056    RollbackUnsubmitted,
1057}
1058
1059pub(super) fn finalize_session_frames(
1060    holds: &mut [&mut SessionFrameHold],
1061    finalization: StepFrameFinalization,
1062) -> Result<Vec<StepParticipantRetirementDisposition>, VNextError> {
1063    let slots = holds
1064        .iter()
1065        .map(|hold| Arc::clone(&hold.slot))
1066        .collect::<Vec<_>>();
1067    let mut states = Vec::with_capacity(slots.len());
1068    for slot in &slots {
1069        states.push(
1070            slot.state
1071                .lock()
1072                .map_err(|_| invalid_resource("sequence session state mutex is poisoned"))?,
1073        );
1074    }
1075    let mut dispositions = Vec::with_capacity(holds.len());
1076    for (hold, state) in holds.iter().zip(&states) {
1077        let hold = &**hold;
1078        let SequenceSessionSlotState::Active(active) = &**state else {
1079            return Err(invalid_resource(
1080                "step participant session is no longer active",
1081            ));
1082        };
1083        if active.epoch != hold.epoch
1084            || active.fingerprint != hold.fingerprint
1085            || active.active_frame
1086                != Some(ActiveSequenceFrame {
1087                    frame_id: hold.frame_id,
1088                    batch_step_id: hold.batch_step_id,
1089                })
1090            || active.next_frame != execution_frame_successor(hold.frame_id)
1091            || active.has_participant_flights()
1092            || (finalization == StepFrameFinalization::Commit
1093                && active.phase == SequenceSessionPhase::Poisoned)
1094            || (finalization == StepFrameFinalization::Commit && active.retired_frames == u64::MAX)
1095            || (finalization == StepFrameFinalization::RollbackUnsubmitted
1096                && active.phase != SequenceSessionPhase::Open)
1097        {
1098            return Err(invalid_resource(
1099                "step finalization differs from its exact session frame or has live participant work",
1100            ));
1101        }
1102        dispositions.push(match finalization {
1103            StepFrameFinalization::Abort => StepParticipantRetirementDisposition::Aborted,
1104            StepFrameFinalization::RollbackUnsubmitted => {
1105                StepParticipantRetirementDisposition::RolledBackUnsubmitted
1106            }
1107            StepFrameFinalization::Commit
1108                if active.phase == SequenceSessionPhase::CancelRequested =>
1109            {
1110                StepParticipantRetirementDisposition::DiscardedCancelled
1111            }
1112            StepFrameFinalization::Commit => StepParticipantRetirementDisposition::Committed,
1113        });
1114    }
1115    for (hold, state) in holds.iter().zip(&mut states) {
1116        let hold = &**hold;
1117        let SequenceSessionSlotState::Active(active) = &mut **state else {
1118            unreachable!("all step participant sessions were validated");
1119        };
1120        active.active_frame = None;
1121        match finalization {
1122            StepFrameFinalization::Abort => active.phase = SequenceSessionPhase::Poisoned,
1123            StepFrameFinalization::Commit => active.retired_frames += 1,
1124            StepFrameFinalization::RollbackUnsubmitted => active.next_frame = Some(hold.frame_id),
1125        }
1126    }
1127    drop(states);
1128    for hold in holds {
1129        hold.finalized = true;
1130    }
1131    Ok(dispositions)
1132}
1133
1134#[derive(Default)]
1135pub(super) struct InvocationRegistryState {
1136    pub(super) entries: BTreeMap<ParticipantNodeKey, ParticipantNodeLedgerEntry>,
1137    pub(super) submission_wave: Option<SubmissionWaveLedgerEntry>,
1138    pub(super) poisoned: bool,
1139}
1140
1141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1142pub(super) enum PhysicalInvocationPhase {
1143    Prepared,
1144    NotSubmitted,
1145    InFlight,
1146    Retired,
1147}
1148
1149#[derive(Clone, PartialEq, Eq)]
1150pub(super) struct ParticipantNodeLedgerEntry {
1151    pub(super) batch_invocation_id: BatchInvocationId,
1152    pub(super) work_fingerprint: String,
1153    pub(super) phase: PhysicalInvocationPhase,
1154}
1155
1156#[derive(Clone, PartialEq, Eq)]
1157pub(super) struct SubmissionWaveLedgerEntry {
1158    // Full-plan preparation has already proved the exact node set. This compact entry is
1159    // therefore the unexpanded participant x plan-node Cartesian tombstone for the step.
1160    pub(super) ledger: ParticipantNodeLedgerEntry,
1161    pub(super) covered_participant_nodes: usize,
1162}
1163
1164#[derive(Default)]
1165pub(super) struct InvocationRegistry {
1166    pub(super) state: Mutex<InvocationRegistryState>,
1167}
1168
1169impl InvocationRegistry {
1170    pub(super) fn ensure_pristine_for_step_rollback(&self) -> Result<(), VNextError> {
1171        let state = self
1172            .state
1173            .lock()
1174            .map_err(|_| invalid_resource("invocation registry is poisoned"))?;
1175        if state.poisoned || !state.entries.is_empty() || state.submission_wave.is_some() {
1176            return Err(invalid_resource(
1177                "unsubmitted step rollback requires a pristine invocation registry",
1178            ));
1179        }
1180        Ok(())
1181    }
1182
1183    pub(super) fn enter(
1184        self: &Arc<Self>,
1185        keys: Vec<ParticipantNodeKey>,
1186        batch_invocation_id: BatchInvocationId,
1187        work_fingerprint: &str,
1188    ) -> Result<ActiveInvocationWaveGuard, VNextError> {
1189        if keys.is_empty() || keys.windows(2).any(|pair| pair[0] >= pair[1]) {
1190            return Err(invalid_resource(
1191                "physical invocation ledger requires canonical non-empty unique participant-node keys",
1192            ));
1193        }
1194        let mut state = self
1195            .state
1196            .lock()
1197            .map_err(|_| invalid_resource("invocation registry is poisoned"))?;
1198        if state.poisoned {
1199            return Err(invalid_resource("invocation registry is fail-closed"));
1200        }
1201        if state.submission_wave.is_some() || keys.iter().any(|key| state.entries.contains_key(key))
1202        {
1203            return Err(invalid_resource(
1204                "participant/frame/node topology is already prepared, in flight, or retired in this step",
1205            ));
1206        }
1207        let entry = ParticipantNodeLedgerEntry {
1208            batch_invocation_id,
1209            work_fingerprint: work_fingerprint.to_owned(),
1210            phase: PhysicalInvocationPhase::Prepared,
1211        };
1212        for key in &keys {
1213            if state.entries.insert(key.clone(), entry.clone()).is_some() {
1214                state.poisoned = true;
1215                return Err(invalid_resource(
1216                    "physical invocation ledger changed during atomic prepare",
1217                ));
1218            }
1219        }
1220        Ok(ActiveInvocationWaveGuard {
1221            registry: Arc::clone(self),
1222            topology: ActiveInvocationLedgerTopology::ParticipantNodes(keys),
1223            work_fingerprint: work_fingerprint.to_owned(),
1224            batch_invocation_id,
1225            phase: PhysicalInvocationPhase::Prepared,
1226        })
1227    }
1228
1229    pub(super) fn enter_submission_wave(
1230        self: &Arc<Self>,
1231        covered_participant_nodes: usize,
1232        batch_invocation_id: BatchInvocationId,
1233        topology_fingerprint: &str,
1234    ) -> Result<ActiveInvocationWaveGuard, VNextError> {
1235        if covered_participant_nodes == 0 {
1236            return Err(invalid_resource(
1237                "submission wave ledger requires non-zero participant-node coverage",
1238            ));
1239        }
1240        let mut state = self
1241            .state
1242            .lock()
1243            .map_err(|_| invalid_resource("invocation registry is poisoned"))?;
1244        if state.poisoned {
1245            return Err(invalid_resource("invocation registry is fail-closed"));
1246        }
1247        if state.submission_wave.is_some() || !state.entries.is_empty() {
1248            return Err(invalid_resource(
1249                "submission wave overlaps prepared, in-flight, or retired participant-node topology in this step",
1250            ));
1251        }
1252        let ledger = ParticipantNodeLedgerEntry {
1253            batch_invocation_id,
1254            work_fingerprint: topology_fingerprint.to_owned(),
1255            phase: PhysicalInvocationPhase::Prepared,
1256        };
1257        state.submission_wave = Some(SubmissionWaveLedgerEntry {
1258            ledger,
1259            covered_participant_nodes,
1260        });
1261        Ok(ActiveInvocationWaveGuard {
1262            registry: Arc::clone(self),
1263            topology: ActiveInvocationLedgerTopology::SubmissionWave {
1264                covered_participant_nodes,
1265            },
1266            work_fingerprint: topology_fingerprint.to_owned(),
1267            batch_invocation_id,
1268            phase: PhysicalInvocationPhase::Prepared,
1269        })
1270    }
1271}
1272
1273enum ActiveInvocationLedgerTopology {
1274    ParticipantNodes(Vec<ParticipantNodeKey>),
1275    SubmissionWave { covered_participant_nodes: usize },
1276}
1277
1278pub(super) struct ActiveInvocationWaveGuard {
1279    registry: Arc<InvocationRegistry>,
1280    topology: ActiveInvocationLedgerTopology,
1281    work_fingerprint: String,
1282    batch_invocation_id: BatchInvocationId,
1283    phase: PhysicalInvocationPhase,
1284}
1285
1286impl ActiveInvocationWaveGuard {
1287    pub(super) const fn physical_entry_count(&self) -> usize {
1288        match &self.topology {
1289            ActiveInvocationLedgerTopology::ParticipantNodes(keys) => keys.len(),
1290            ActiveInvocationLedgerTopology::SubmissionWave { .. } => 1,
1291        }
1292    }
1293
1294    fn transition(
1295        &mut self,
1296        expected: PhysicalInvocationPhase,
1297        next: PhysicalInvocationPhase,
1298        next_attempt: BatchInvocationId,
1299    ) -> Result<(), VNextError> {
1300        if self.phase != expected {
1301            return Err(invalid_resource(
1302                "physical invocation guard is not in the expected phase",
1303            ));
1304        }
1305        let mut state = self
1306            .registry
1307            .state
1308            .lock()
1309            .map_err(|_| invalid_resource("invocation registry is poisoned"))?;
1310        let expected_entry = ParticipantNodeLedgerEntry {
1311            batch_invocation_id: self.batch_invocation_id,
1312            work_fingerprint: self.work_fingerprint.clone(),
1313            phase: expected,
1314        };
1315        let authority_matches = match &self.topology {
1316            ActiveInvocationLedgerTopology::ParticipantNodes(keys) => {
1317                state.submission_wave.is_none()
1318                    && keys
1319                        .iter()
1320                        .all(|key| state.entries.get(key) == Some(&expected_entry))
1321            }
1322            ActiveInvocationLedgerTopology::SubmissionWave {
1323                covered_participant_nodes,
1324            } => {
1325                state.entries.is_empty()
1326                    && state.submission_wave
1327                        == Some(SubmissionWaveLedgerEntry {
1328                            ledger: expected_entry.clone(),
1329                            covered_participant_nodes: *covered_participant_nodes,
1330                        })
1331            }
1332        };
1333        if state.poisoned || !authority_matches {
1334            state.poisoned = true;
1335            return Err(invalid_resource(
1336                "physical invocation ledger differs from its exact transition authority",
1337            ));
1338        }
1339        match &self.topology {
1340            ActiveInvocationLedgerTopology::ParticipantNodes(keys) => {
1341                for key in keys {
1342                    let entry = state
1343                        .entries
1344                        .get_mut(key)
1345                        .expect("validated physical invocation key remains present");
1346                    entry.batch_invocation_id = next_attempt;
1347                    entry.phase = next;
1348                }
1349            }
1350            ActiveInvocationLedgerTopology::SubmissionWave { .. } => {
1351                let entry = &mut state
1352                    .submission_wave
1353                    .as_mut()
1354                    .expect("validated submission wave remains present")
1355                    .ledger;
1356                entry.batch_invocation_id = next_attempt;
1357                entry.phase = next;
1358            }
1359        }
1360        self.batch_invocation_id = next_attempt;
1361        self.phase = next;
1362        Ok(())
1363    }
1364
1365    pub(super) fn mark_not_submitted(&mut self) -> Result<(), VNextError> {
1366        self.transition(
1367            PhysicalInvocationPhase::Prepared,
1368            PhysicalInvocationPhase::NotSubmitted,
1369            self.batch_invocation_id,
1370        )
1371    }
1372
1373    pub(super) fn prepare_retry(
1374        &mut self,
1375        fresh_attempt: BatchInvocationId,
1376    ) -> Result<(), VNextError> {
1377        if fresh_attempt == self.batch_invocation_id {
1378            return Err(invalid_resource(
1379                "definitely-not-submitted retry requires a fresh physical attempt id",
1380            ));
1381        }
1382        self.transition(
1383            PhysicalInvocationPhase::NotSubmitted,
1384            PhysicalInvocationPhase::Prepared,
1385            fresh_attempt,
1386        )
1387    }
1388
1389    pub(super) fn mark_in_flight(&mut self) -> Result<(), VNextError> {
1390        self.transition(
1391            PhysicalInvocationPhase::Prepared,
1392            PhysicalInvocationPhase::InFlight,
1393            self.batch_invocation_id,
1394        )
1395    }
1396}
1397
1398impl Drop for ActiveInvocationWaveGuard {
1399    fn drop(&mut self) {
1400        if self.phase == PhysicalInvocationPhase::Retired {
1401            return;
1402        }
1403        let mut state = match self.registry.state.lock() {
1404            Ok(state) => state,
1405            Err(poisoned) => {
1406                let mut state = poisoned.into_inner();
1407                state.poisoned = true;
1408                state
1409            }
1410        };
1411        let expected = ParticipantNodeLedgerEntry {
1412            batch_invocation_id: self.batch_invocation_id,
1413            work_fingerprint: self.work_fingerprint.clone(),
1414            phase: self.phase,
1415        };
1416        let authority_matches = match &self.topology {
1417            ActiveInvocationLedgerTopology::ParticipantNodes(keys) => {
1418                state.submission_wave.is_none()
1419                    && keys
1420                        .iter()
1421                        .all(|key| state.entries.get(key) == Some(&expected))
1422            }
1423            ActiveInvocationLedgerTopology::SubmissionWave {
1424                covered_participant_nodes,
1425            } => {
1426                state.entries.is_empty()
1427                    && state.submission_wave
1428                        == Some(SubmissionWaveLedgerEntry {
1429                            ledger: expected,
1430                            covered_participant_nodes: *covered_participant_nodes,
1431                        })
1432            }
1433        };
1434        if !authority_matches {
1435            state.poisoned = true;
1436            return;
1437        }
1438        match &self.topology {
1439            ActiveInvocationLedgerTopology::ParticipantNodes(keys) => {
1440                for key in keys {
1441                    state
1442                        .entries
1443                        .get_mut(key)
1444                        .expect("validated physical invocation key remains present")
1445                        .phase = PhysicalInvocationPhase::Retired;
1446                }
1447            }
1448            ActiveInvocationLedgerTopology::SubmissionWave { .. } => {
1449                state
1450                    .submission_wave
1451                    .as_mut()
1452                    .expect("validated submission wave remains present")
1453                    .ledger
1454                    .phase = PhysicalInvocationPhase::Retired;
1455            }
1456        }
1457        self.phase = PhysicalInvocationPhase::Retired;
1458    }
1459}
1460
1461pub enum StepResourceAdmissionDecision<R>
1462where
1463    R: DeviceRuntime,
1464{
1465    Admitted(Arc<StepResourceLease<R>>),
1466    Deferred(AdmissionDeferred),
1467    BackingDeferred(StepAdmissionBackingDeferral<R>),
1468    PermanentRejected(AdmissionRejected),
1469}
1470
1471/// Non-cloneable physical-backing authority for one exact batch participant
1472/// set and immutable step work shape.
1473#[must_use = "step backing deferral retains its exact participant parents"]
1474pub struct StepAdmissionBackingDeferral<R>
1475where
1476    R: DeviceRuntime,
1477{
1478    backing: PlanBackingDeferral<R>,
1479    participants: Vec<Arc<SequenceSession<R>>>,
1480    work_fingerprint: String,
1481}
1482
1483impl<R> StepAdmissionBackingDeferral<R>
1484where
1485    R: DeviceRuntime,
1486{
1487    pub(super) fn new(
1488        evidence: DynamicBackingDeferred,
1489        participants: Vec<Arc<SequenceSession<R>>>,
1490        work_fingerprint: String,
1491    ) -> Result<Self, VNextError> {
1492        let first = participants
1493            .first()
1494            .ok_or_else(|| invalid_resource("step backing deferral requires participants"))?;
1495        let resources = Arc::clone(&first.resources().request.plan.resources);
1496        if participants.iter().any(|participant| {
1497            !Arc::ptr_eq(&resources, &participant.resources().request.plan.resources)
1498        }) {
1499            return Err(invalid_resource(
1500                "step backing deferral participants belong to different plans",
1501            ));
1502        }
1503        Ok(Self {
1504            backing: PlanBackingDeferral::new(resources, evidence)?,
1505            participants,
1506            work_fingerprint,
1507        })
1508    }
1509
1510    pub fn evidence(&self) -> &DynamicBackingDeferred {
1511        self.backing.evidence()
1512    }
1513
1514    pub fn participant_count(&self) -> usize {
1515        self.participants.len()
1516    }
1517
1518    pub fn work_fingerprint(&self) -> &str {
1519        &self.work_fingerprint
1520    }
1521
1522    pub fn maintain(&self) -> Result<DynamicDeferredMaintenanceOutcome, VNextError> {
1523        for participant in &self.participants {
1524            participant.ensure_open_identity()?;
1525        }
1526        self.backing.maintain()
1527    }
1528
1529    pub fn register_waiter(&self) -> Result<PlanCapacityWaitRegistration<R>, VNextError> {
1530        self.backing.register_waiter()
1531    }
1532}
1533
1534#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
1535pub enum StepParticipantRetirementDisposition {
1536    Committed,
1537    DiscardedCancelled,
1538    RolledBackUnsubmitted,
1539    Aborted,
1540}
1541
1542#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1543pub struct StepParticipantRetirement {
1544    pub(super) assignment: StepParticipantFrameAssignment,
1545    pub(super) disposition: StepParticipantRetirementDisposition,
1546}
1547
1548impl StepParticipantRetirement {
1549    pub const fn assignment(&self) -> StepParticipantFrameAssignment {
1550        self.assignment
1551    }
1552
1553    pub const fn disposition(&self) -> StepParticipantRetirementDisposition {
1554        self.disposition
1555    }
1556}
1557
1558/// One atomic physical/logical backing claim bound to an immutable batch work
1559/// authority. Even an empty resource demand retains the work shape and claim
1560/// fingerprint through dispatch and fence ownership.
1561#[must_use = "claimed backing must remain owned through its device fence"]
1562pub struct ClaimedBackingTransaction {
1563    // Physical extents release before the logical capacity claim.
1564    backing_slices: Vec<LogicalBackingSliceAuthority>,
1565    logical_capacity: Option<LogicalBatchCapacityLease>,
1566    work_shape: Arc<BatchWorkShape>,
1567    demand: AdmissionDemand,
1568    fingerprint: String,
1569    physical_claim_count: usize,
1570    has_shared_physical_claims: bool,
1571    // Occupancy releases after every physical/logical claim field above.
1572    _lane_slot_lease: Option<LaneStableArenaSlotLease>,
1573}
1574
1575fn logical_capacity_matches(
1576    logical_capacity: &Option<LogicalBatchCapacityLease>,
1577    demand: &AdmissionDemand,
1578    participants: &[BatchParticipantAuthority],
1579    backing_slices: &[LogicalBackingSliceAuthority],
1580) -> bool {
1581    match logical_capacity {
1582        Some(capacity) => {
1583            capacity.claims() == demand.immediate_claim()
1584                && capacity.parents().len() == participants.len()
1585                && capacity
1586                    .parents()
1587                    .iter()
1588                    .zip(participants)
1589                    .all(|(parent, participant)| {
1590                        parent.sequence() == participant.sequence_authority()
1591                            && parent.request() == participant.request_authority()
1592                    })
1593        }
1594        None => demand.immediate_claim().is_empty() && backing_slices.is_empty(),
1595    }
1596}
1597
1598impl ClaimedBackingTransaction {
1599    pub(super) fn new(
1600        work_shape: Arc<BatchWorkShape>,
1601        demand: AdmissionDemand,
1602        logical_capacity: Option<LogicalBatchCapacityLease>,
1603        backing_slices: Vec<LogicalBackingSliceAuthority>,
1604        lane_slot_lease: Option<LaneStableArenaSlotLease>,
1605    ) -> Result<Self, VNextError> {
1606        let certificate = Arc::new(BackingClaimCertificate::from_slices(&backing_slices)?);
1607        Self::new_certified(
1608            work_shape,
1609            demand,
1610            logical_capacity,
1611            backing_slices,
1612            certificate,
1613            lane_slot_lease,
1614        )
1615    }
1616
1617    pub(super) fn new_lane_stable(
1618        work_shape: Arc<BatchWorkShape>,
1619        demand: AdmissionDemand,
1620        logical_capacity: Option<LogicalBatchCapacityLease>,
1621        committed_backing: CommittedLaneBackingClaim,
1622    ) -> Result<Self, VNextError> {
1623        let (backing_slices, certificate, lane_slot_lease) =
1624            committed_backing.into_certified_parts();
1625        Self::new_certified(
1626            work_shape,
1627            demand,
1628            logical_capacity,
1629            backing_slices,
1630            certificate,
1631            lane_slot_lease,
1632        )
1633    }
1634
1635    fn new_certified(
1636        work_shape: Arc<BatchWorkShape>,
1637        demand: AdmissionDemand,
1638        logical_capacity: Option<LogicalBatchCapacityLease>,
1639        backing_slices: Vec<LogicalBackingSliceAuthority>,
1640        certificate: Arc<BackingClaimCertificate>,
1641        lane_slot_lease: Option<LaneStableArenaSlotLease>,
1642    ) -> Result<Self, VNextError> {
1643        let bound_certificate = certificate.bind(&backing_slices, &demand)?;
1644        if !logical_capacity_matches(
1645            &logical_capacity,
1646            &demand,
1647            work_shape.participants(),
1648            &backing_slices,
1649        ) {
1650            return Err(invalid_resource(
1651                "logical backing claim differs from work participants or evaluated demand",
1652            ));
1653        }
1654        #[derive(Serialize)]
1655        struct FingerprintInput<'a> {
1656            domain: &'static str,
1657            work_fingerprint: &'a str,
1658            demand: &'a AdmissionDemand,
1659            backing_certificate_fingerprint: &'a str,
1660            capacity_parents: Vec<(SequenceAuthorityId, RequestAuthorityId)>,
1661        }
1662        let input = FingerprintInput {
1663            domain: "ferrum.runtime-vnext.claimed-backing.v5",
1664            work_fingerprint: work_shape.fingerprint(),
1665            demand: &demand,
1666            backing_certificate_fingerprint: bound_certificate.fingerprint(),
1667            capacity_parents: logical_capacity
1668                .as_ref()
1669                .map(|capacity| {
1670                    capacity
1671                        .parents()
1672                        .iter()
1673                        .map(|parent| (parent.sequence(), parent.request()))
1674                        .collect()
1675                })
1676                .unwrap_or_default(),
1677        };
1678        let bytes = serde_json::to_vec(&input).map_err(|error| {
1679            invalid_resource(format!(
1680                "claimed backing fingerprint encode failed: {error}"
1681            ))
1682        })?;
1683        Ok(Self {
1684            backing_slices,
1685            logical_capacity,
1686            work_shape,
1687            demand,
1688            fingerprint: format!("{:x}", Sha256::digest(bytes)),
1689            physical_claim_count: bound_certificate.physical_claim_count(),
1690            has_shared_physical_claims: bound_certificate.has_shared_physical_claims(),
1691            _lane_slot_lease: lane_slot_lease,
1692        })
1693    }
1694
1695    pub fn work_shape(&self) -> &BatchWorkShape {
1696        self.work_shape.as_ref()
1697    }
1698
1699    pub(super) fn work_shape_arc(&self) -> &Arc<BatchWorkShape> {
1700        &self.work_shape
1701    }
1702
1703    pub fn backing_slices(&self) -> &[LogicalBackingSliceAuthority] {
1704        &self.backing_slices
1705    }
1706
1707    pub fn logical_capacity(&self) -> Option<&LogicalBatchCapacityLease> {
1708        self.logical_capacity.as_ref()
1709    }
1710
1711    pub fn fingerprint(&self) -> &str {
1712        &self.fingerprint
1713    }
1714
1715    pub fn demand(&self) -> &AdmissionDemand {
1716        &self.demand
1717    }
1718
1719    pub fn physical_claim_count(&self) -> usize {
1720        self.physical_claim_count
1721    }
1722
1723    pub fn has_shared_physical_claims(&self) -> bool {
1724        self.has_shared_physical_claims
1725    }
1726
1727    pub fn lane_stable_slot_identity(&self) -> Option<LaneStableArenaSlotIdentity> {
1728        self._lane_slot_lease
1729            .as_ref()
1730            .map(LaneStableArenaSlotLease::identity)
1731    }
1732}
1733
1734/// One physical/logical Invocation backing transaction shared by every node
1735/// in an immutable-plan submission wave. Physical demand is charged once for
1736/// the liveness-derived peak and retained until the wave's terminal fence.
1737#[must_use = "submission wave backing must remain owned through its device fence"]
1738pub struct ClaimedSubmissionWaveBacking {
1739    // Physical extents release before the logical capacity claim.
1740    backing_slices: Vec<LogicalBackingSliceAuthority>,
1741    logical_capacity: Option<LogicalBatchCapacityLease>,
1742    plan_hash: PlanHash,
1743    node_count: usize,
1744    plan_node_count: usize,
1745    work_shape: Arc<BatchWorkShape>,
1746    demand: AdmissionDemand,
1747    fingerprint: String,
1748    physical_claim_count: usize,
1749    program_binding: Option<Arc<ProgramBindingExecutionBinding>>,
1750    // Occupancy releases only after terminal completion readback drops this claim.
1751    _lane_slot_lease: Option<LaneStableArenaSlotLease>,
1752}
1753
1754impl ClaimedSubmissionWaveBacking {
1755    pub(super) fn new(
1756        plan_hash: PlanHash,
1757        node_count: usize,
1758        plan_node_count: usize,
1759        work_shape: Arc<BatchWorkShape>,
1760        demand: AdmissionDemand,
1761        logical_capacity: Option<LogicalBatchCapacityLease>,
1762        committed_backing: CommittedLaneBackingClaim,
1763        program_binding_layout: Option<Arc<ProgramBindingLayout>>,
1764    ) -> Result<Self, VNextError> {
1765        let (backing_slices, certificate, lane_slot_lease) =
1766            committed_backing.into_certified_parts();
1767        let participants = work_shape.participants();
1768        if node_count == 0
1769            || node_count > plan_node_count
1770            || participants.is_empty()
1771            || participants
1772                .windows(2)
1773                .any(|pair| pair[0].canonical_key() >= pair[1].canonical_key())
1774        {
1775            return Err(invalid_resource(
1776                "submission wave backing requires ordered unique nodes and participants",
1777            ));
1778        }
1779        let bound_certificate = certificate.bind(&backing_slices, &demand)?;
1780        if !logical_capacity_matches(&logical_capacity, &demand, participants, &backing_slices) {
1781            return Err(invalid_resource(
1782                "submission wave capacity differs from its participants or evaluated demand",
1783            ));
1784        }
1785        let program_binding = program_binding_layout
1786            .map(|layout| {
1787                let lane_slot_identity = lane_slot_lease
1788                    .as_ref()
1789                    .map(LaneStableArenaSlotLease::identity)
1790                    .ok_or_else(|| {
1791                        invalid_resource(
1792                            "compiled program binding layout has no lane-stable slot authority",
1793                        )
1794                    })?;
1795                ProgramBindingExecutionBinding::bind(
1796                    plan_hash.clone(),
1797                    plan_node_count,
1798                    layout,
1799                    lane_slot_identity,
1800                    &backing_slices,
1801                )
1802            })
1803            .transpose()?;
1804
1805        #[derive(Serialize)]
1806        struct FingerprintInput<'a> {
1807            domain: &'static str,
1808            plan_hash: &'a PlanHash,
1809            node_count: usize,
1810            plan_node_count: usize,
1811            work_fingerprint: &'a str,
1812            demand: &'a AdmissionDemand,
1813            backing_certificate_fingerprint: &'a str,
1814            capacity_parents: Vec<(SequenceAuthorityId, RequestAuthorityId)>,
1815        }
1816        let input = FingerprintInput {
1817            domain: "ferrum.runtime-vnext.claimed-submission-wave-backing.v5",
1818            plan_hash: &plan_hash,
1819            node_count,
1820            plan_node_count,
1821            work_fingerprint: work_shape.fingerprint(),
1822            demand: &demand,
1823            backing_certificate_fingerprint: bound_certificate.fingerprint(),
1824            capacity_parents: logical_capacity
1825                .as_ref()
1826                .map(|capacity| {
1827                    capacity
1828                        .parents()
1829                        .iter()
1830                        .map(|parent| (parent.sequence(), parent.request()))
1831                        .collect()
1832                })
1833                .unwrap_or_default(),
1834        };
1835        let bytes = serde_json::to_vec(&input).map_err(|error| {
1836            invalid_resource(format!(
1837                "submission wave backing fingerprint encode failed: {error}"
1838            ))
1839        })?;
1840        Ok(Self {
1841            backing_slices,
1842            logical_capacity,
1843            plan_hash,
1844            node_count,
1845            plan_node_count,
1846            work_shape,
1847            demand,
1848            fingerprint: format!("{:x}", Sha256::digest(bytes)),
1849            physical_claim_count: bound_certificate.physical_claim_count(),
1850            program_binding,
1851            _lane_slot_lease: lane_slot_lease,
1852        })
1853    }
1854
1855    pub fn backing_slices(&self) -> &[LogicalBackingSliceAuthority] {
1856        &self.backing_slices
1857    }
1858
1859    pub fn logical_capacity(&self) -> Option<&LogicalBatchCapacityLease> {
1860        self.logical_capacity.as_ref()
1861    }
1862
1863    pub fn plan_hash(&self) -> &PlanHash {
1864        &self.plan_hash
1865    }
1866
1867    pub const fn node_count(&self) -> usize {
1868        self.node_count
1869    }
1870
1871    pub const fn plan_node_count(&self) -> usize {
1872        self.plan_node_count
1873    }
1874
1875    pub fn work_shape(&self) -> &BatchWorkShape {
1876        self.work_shape.as_ref()
1877    }
1878
1879    pub fn participants(&self) -> &[BatchParticipantAuthority] {
1880        self.work_shape.participants()
1881    }
1882
1883    pub fn demand(&self) -> &AdmissionDemand {
1884        &self.demand
1885    }
1886
1887    pub fn program_binding_node(&self, node_index: usize) -> Option<ProgramBindingNodeBinding> {
1888        self.program_binding
1889            .as_ref()
1890            .and_then(|binding| binding.node(node_index))
1891    }
1892
1893    pub fn program_binding_layout(&self) -> Option<&ProgramBindingLayout> {
1894        self.program_binding
1895            .as_ref()
1896            .map(|binding| binding.layout())
1897    }
1898
1899    pub fn program_binding_lane_slot_identity(&self) -> Option<&LaneStableArenaSlotIdentity> {
1900        self.program_binding
1901            .as_ref()
1902            .map(|binding| binding.lane_slot_identity())
1903    }
1904
1905    pub fn reusable_execution_bucket_id(&self) -> Option<&ReusableExecutionBucketId> {
1906        self.program_binding_layout()
1907            .map(ProgramBindingLayout::reusable_execution_bucket_id)
1908            .or_else(|| {
1909                self.backing_slices
1910                    .iter()
1911                    .find_map(|slice| slice.evidence().reusable_execution_bucket_id())
1912            })
1913    }
1914
1915    pub fn reusable_execution_program_id(
1916        &self,
1917        runtime_implementation_fingerprint: &str,
1918        lane_id: super::ExecutionLaneId,
1919    ) -> Result<Option<DeviceReusableExecutionProgramId>, VNextError> {
1920        let Some(layout) = self.program_binding_layout() else {
1921            if self.program_binding_lane_slot_identity().is_some() {
1922                return Err(invalid_resource(
1923                    "reusable execution lane slot has no compiled program binding layout",
1924                ));
1925            }
1926            return Ok(None);
1927        };
1928        let lane_slot = self.program_binding_lane_slot_identity().ok_or_else(|| {
1929            invalid_resource("compiled program binding layout has no lane-stable slot identity")
1930        })?;
1931        if lane_slot.lane_id() != lane_id
1932            || lane_slot.reusable_execution_bucket_id() != layout.reusable_execution_bucket_id()
1933        {
1934            return Err(invalid_resource(
1935                "reusable execution program layout differs from its lane-stable slot",
1936            ));
1937        }
1938        DeviceReusableExecutionProgramId::new(
1939            self.plan_hash.clone(),
1940            runtime_implementation_fingerprint.to_owned(),
1941            lane_id,
1942            layout.reusable_execution_bucket_id().clone(),
1943            layout.fingerprint().to_owned(),
1944            lane_slot.layout_fingerprint().to_owned(),
1945            lane_slot.slot_id(),
1946            self.work_shape.immediate_sequences(),
1947            self.work_shape.immediate_tokens(),
1948            self.work_shape.immediate_pages(),
1949        )
1950        .map(Some)
1951    }
1952
1953    pub fn fingerprint(&self) -> &str {
1954        &self.fingerprint
1955    }
1956
1957    pub fn physical_claim_count(&self) -> usize {
1958        self.physical_claim_count
1959    }
1960}
1961
1962#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1963pub struct StepRetirementReceipt {
1964    pub(super) batch_step_id: BatchStepId,
1965    pub(super) participants: Vec<StepParticipantRetirement>,
1966}
1967
1968impl StepRetirementReceipt {
1969    pub const fn batch_step_id(&self) -> BatchStepId {
1970        self.batch_step_id
1971    }
1972
1973    pub fn participants(&self) -> &[StepParticipantRetirement] {
1974        &self.participants
1975    }
1976}
1977
1978#[must_use = "failed step finalization retains the exact step authority"]
1979pub struct StepFinalizationFailure<R>
1980where
1981    R: DeviceRuntime,
1982{
1983    pub(super) step: Arc<StepResourceLease<R>>,
1984    pub(super) error: VNextError,
1985}
1986
1987impl<R> fmt::Debug for StepFinalizationFailure<R>
1988where
1989    R: DeviceRuntime,
1990{
1991    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1992        formatter
1993            .debug_struct("StepFinalizationFailure")
1994            .field("error", &self.error)
1995            .finish_non_exhaustive()
1996    }
1997}
1998
1999impl<R> StepFinalizationFailure<R>
2000where
2001    R: DeviceRuntime,
2002{
2003    pub fn error(&self) -> &VNextError {
2004        &self.error
2005    }
2006
2007    pub fn into_step(self) -> Arc<StepResourceLease<R>> {
2008        self.step
2009    }
2010}