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