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#[must_use = "step resources must live through every child invocation"]
22pub struct StepResourceLease<R>
23where
24 R: DeviceRuntime,
25{
26 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#[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
215pub(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
403pub(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 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 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 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#[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#[must_use = "claimed backing must remain owned through its device fence"]
1562pub struct ClaimedBackingTransaction {
1563 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 _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#[must_use = "submission wave backing must remain owned through its device fence"]
1738pub struct ClaimedSubmissionWaveBacking {
1739 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 _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}