1use serde::{ser::SerializeSeq, Serialize, Serializer};
2use std::collections::BTreeSet;
3use std::sync::{Arc, OnceLock};
4
5use super::super::{
6 BatchInvocationId, BatchStepId, DeviceId, ExecutionIdentityEnvelope, ExecutionLaneId, NodeId,
7 OperationId, ParticipantNodeKey, PlanHash, PlanId, ProviderId, VNextError,
8};
9use super::compiled_identity::{
10 CompiledSubmissionWaveIdentity, SubmissionWaveParticipantIdentitySeed,
11};
12use super::foundation::{canonical_operation_fingerprint, canonical_sha256, invalid_operation};
13use super::ProviderExecutionSemantics;
14
15#[derive(Debug, PartialEq, Eq, Serialize)]
16struct BatchOperationParticipantIdentityData {
17 participant_index: u32,
18 node_key: ParticipantNodeKey,
19 identity: ExecutionIdentityEnvelope,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct BatchOperationParticipantIdentity {
24 data: Arc<BatchOperationParticipantIdentityData>,
25}
26
27impl Serialize for BatchOperationParticipantIdentity {
28 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
29 where
30 S: Serializer,
31 {
32 self.data.as_ref().serialize(serializer)
33 }
34}
35
36impl BatchOperationParticipantIdentity {
37 pub(super) fn new(
38 participant_index: u32,
39 node_key: ParticipantNodeKey,
40 identity: ExecutionIdentityEnvelope,
41 ) -> Self {
42 Self {
43 data: Arc::new(BatchOperationParticipantIdentityData {
44 participant_index,
45 node_key,
46 identity,
47 }),
48 }
49 }
50
51 pub fn participant_index(&self) -> u32 {
52 self.data.participant_index
53 }
54
55 pub fn node_key(&self) -> &ParticipantNodeKey {
56 &self.data.node_key
57 }
58
59 pub fn identity(&self) -> &ExecutionIdentityEnvelope {
60 &self.data.identity
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
67pub struct BatchOperationNodeIdentity {
68 node_index: u32,
69 node_id: NodeId,
70 operation_id: OperationId,
71 provider_id: ProviderId,
72 provider_implementation_fingerprint: String,
73 provider_execution_semantics: ProviderExecutionSemantics,
74 work_shape_fingerprint: String,
75 participants: Vec<BatchOperationParticipantIdentity>,
76 fingerprint: String,
77}
78
79impl BatchOperationNodeIdentity {
80 pub(super) fn from_validated(
81 node_index: u32,
82 node_id: NodeId,
83 operation_id: OperationId,
84 provider_id: ProviderId,
85 provider_implementation_fingerprint: String,
86 provider_execution_semantics: ProviderExecutionSemantics,
87 work_shape_fingerprint: String,
88 participants: Vec<BatchOperationParticipantIdentity>,
89 ) -> Result<Self, VNextError> {
90 let participant_start = participants
91 .first()
92 .map(BatchOperationParticipantIdentity::participant_index);
93 if participants.is_empty()
94 || participants.iter().enumerate().any(|(index, participant)| {
95 participant_start.and_then(|start| start.checked_add(index as u32))
96 != Some(participant.participant_index())
97 || participant.node_key().node_id() != &node_id
98 || participant.identity().parts().frame_id
99 != Some(participant.node_key().frame_id())
100 || participant.identity().parts().node_id.as_ref() != Some(&node_id)
101 || participant.identity().parts().operation_id.as_ref() != Some(&operation_id)
102 || participant.identity().parts().provider_id.as_ref() != Some(&provider_id)
103 })
104 || participants
105 .windows(2)
106 .any(|pair| pair[0].node_key() >= pair[1].node_key())
107 || !canonical_sha256(&provider_implementation_fingerprint)
108 || !canonical_sha256(&work_shape_fingerprint)
109 {
110 return Err(invalid_operation(
111 "batch node identity is empty, non-canonical, or differs from its participant projections",
112 ));
113 }
114 #[derive(Serialize)]
115 struct FingerprintInput<'a> {
116 domain: &'static str,
117 node_index: u32,
118 node_id: &'a NodeId,
119 operation_id: &'a OperationId,
120 provider_id: &'a ProviderId,
121 provider_implementation_fingerprint: &'a str,
122 provider_execution_semantics: ProviderExecutionSemantics,
123 work_shape_fingerprint: &'a str,
124 participants: &'a [BatchOperationParticipantIdentity],
125 }
126 let fingerprint = canonical_operation_fingerprint(
127 &FingerprintInput {
128 domain: "ferrum.runtime-vnext.batch-operation-node-identity.v2",
129 node_index,
130 node_id: &node_id,
131 operation_id: &operation_id,
132 provider_id: &provider_id,
133 provider_implementation_fingerprint: &provider_implementation_fingerprint,
134 provider_execution_semantics,
135 work_shape_fingerprint: &work_shape_fingerprint,
136 participants: &participants,
137 },
138 "batch node identity encode failed",
139 )?;
140 Ok(Self {
141 node_index,
142 node_id,
143 operation_id,
144 provider_id,
145 provider_implementation_fingerprint,
146 provider_execution_semantics,
147 work_shape_fingerprint,
148 participants,
149 fingerprint,
150 })
151 }
152
153 pub const fn node_index(&self) -> u32 {
154 self.node_index
155 }
156
157 pub fn node_id(&self) -> &NodeId {
158 &self.node_id
159 }
160
161 pub fn operation_id(&self) -> &OperationId {
162 &self.operation_id
163 }
164
165 pub fn provider_id(&self) -> &ProviderId {
166 &self.provider_id
167 }
168
169 pub fn provider_implementation_fingerprint(&self) -> &str {
170 &self.provider_implementation_fingerprint
171 }
172
173 pub const fn provider_execution_semantics(&self) -> ProviderExecutionSemantics {
174 self.provider_execution_semantics
175 }
176
177 pub fn work_shape_fingerprint(&self) -> &str {
178 &self.work_shape_fingerprint
179 }
180
181 pub fn participants(&self) -> &[BatchOperationParticipantIdentity] {
182 &self.participants
183 }
184
185 pub fn fingerprint(&self) -> &str {
186 &self.fingerprint
187 }
188
189 pub(super) fn contains_identity(&self, identity: &ExecutionIdentityEnvelope) -> bool {
190 self.participants
191 .iter()
192 .any(|participant| participant.identity() == identity)
193 }
194}
195
196#[derive(Debug)]
199struct BatchOperationIdentityData {
200 batch_step_id: BatchStepId,
201 batch_invocation_id: BatchInvocationId,
202 plan_id: PlanId,
203 plan_hash: PlanHash,
204 device_id: DeviceId,
205 runtime_implementation_fingerprint: String,
206 lane_id: ExecutionLaneId,
207 claimed_backing_fingerprint: String,
208 nodes: OnceLock<Vec<BatchOperationNodeIdentity>>,
209 participants: OnceLock<Vec<BatchOperationParticipantIdentity>>,
210 deferred_recipe: Option<DeferredBatchOperationIdentityRecipe>,
211 fingerprint: String,
212}
213
214#[derive(Debug, Clone)]
215pub struct BatchOperationIdentity {
216 data: Arc<BatchOperationIdentityData>,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
220pub struct BatchOperationIdentityMaterializationSnapshot {
221 logical_nodes: u32,
222 materialized_nodes: u32,
223 full_participant_projection: bool,
224}
225
226impl BatchOperationIdentityMaterializationSnapshot {
227 pub const fn logical_nodes(self) -> u32 {
228 self.logical_nodes
229 }
230
231 pub const fn materialized_nodes(self) -> u32 {
232 self.materialized_nodes
233 }
234
235 pub const fn full_participant_projection(self) -> bool {
236 self.full_participant_projection
237 }
238}
239
240impl PartialEq for BatchOperationIdentity {
241 fn eq(&self, other: &Self) -> bool {
242 self.data.batch_step_id == other.data.batch_step_id
243 && self.data.batch_invocation_id == other.data.batch_invocation_id
244 && self.data.plan_id == other.data.plan_id
245 && self.data.plan_hash == other.data.plan_hash
246 && self.data.device_id == other.data.device_id
247 && self.data.runtime_implementation_fingerprint
248 == other.data.runtime_implementation_fingerprint
249 && self.data.lane_id == other.data.lane_id
250 && self.data.claimed_backing_fingerprint == other.data.claimed_backing_fingerprint
251 && self.data.fingerprint == other.data.fingerprint
252 }
253}
254
255impl Eq for BatchOperationIdentity {}
256
257impl Serialize for BatchOperationIdentity {
258 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
259 where
260 S: Serializer,
261 {
262 #[derive(Serialize)]
263 struct Wire<'a> {
264 batch_step_id: BatchStepId,
265 batch_invocation_id: BatchInvocationId,
266 plan_id: &'a PlanId,
267 plan_hash: &'a PlanHash,
268 device_id: &'a DeviceId,
269 runtime_implementation_fingerprint: &'a str,
270 lane_id: ExecutionLaneId,
271 claimed_backing_fingerprint: &'a str,
272 nodes: &'a [BatchOperationNodeIdentity],
273 participants: &'a [BatchOperationParticipantIdentity],
274 fingerprint: &'a str,
275 }
276
277 Wire {
278 batch_step_id: self.data.batch_step_id,
279 batch_invocation_id: self.data.batch_invocation_id,
280 plan_id: &self.data.plan_id,
281 plan_hash: &self.data.plan_hash,
282 device_id: &self.data.device_id,
283 runtime_implementation_fingerprint: &self.data.runtime_implementation_fingerprint,
284 lane_id: self.data.lane_id,
285 claimed_backing_fingerprint: &self.data.claimed_backing_fingerprint,
286 nodes: self.nodes(),
287 participants: self.participants(),
288 fingerprint: &self.data.fingerprint,
289 }
290 .serialize(serializer)
291 }
292}
293
294struct BatchOperationNodeFingerprints<'a>(&'a [BatchOperationNodeIdentity]);
295
296impl Serialize for BatchOperationNodeFingerprints<'_> {
297 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
298 where
299 S: Serializer,
300 {
301 let mut sequence = serializer.serialize_seq(Some(self.0.len()))?;
302 for node in self.0 {
303 sequence.serialize_element(node.fingerprint())?;
304 }
305 sequence.end()
306 }
307}
308
309impl BatchOperationIdentity {
310 #[allow(clippy::too_many_arguments)]
311 fn from_deferred_validated(
312 batch_step_id: BatchStepId,
313 batch_invocation_id: BatchInvocationId,
314 plan_id: PlanId,
315 plan_hash: PlanHash,
316 device_id: DeviceId,
317 runtime_implementation_fingerprint: String,
318 lane_id: ExecutionLaneId,
319 claimed_backing_fingerprint: String,
320 deferred_recipe: DeferredBatchOperationIdentityRecipe,
321 fingerprint: String,
322 ) -> Self {
323 Self {
324 data: Arc::new(BatchOperationIdentityData {
325 batch_step_id,
326 batch_invocation_id,
327 plan_id,
328 plan_hash,
329 device_id,
330 runtime_implementation_fingerprint,
331 lane_id,
332 claimed_backing_fingerprint,
333 nodes: OnceLock::new(),
334 participants: OnceLock::new(),
335 deferred_recipe: Some(deferred_recipe),
336 fingerprint,
337 }),
338 }
339 }
340
341 #[allow(clippy::too_many_arguments)]
342 pub(super) fn from_validated(
343 batch_step_id: BatchStepId,
344 batch_invocation_id: BatchInvocationId,
345 plan_id: PlanId,
346 plan_hash: PlanHash,
347 device_id: DeviceId,
348 runtime_implementation_fingerprint: String,
349 lane_id: ExecutionLaneId,
350 claimed_backing_fingerprint: String,
351 nodes: Vec<BatchOperationNodeIdentity>,
352 ) -> Result<Self, VNextError> {
353 if nodes.is_empty()
354 || nodes.iter().enumerate().any(|(index, node)| {
355 node.node_index as usize != index
356 || node.participants.iter().any(|participant| {
357 participant.identity().parts().plan_id.as_ref() != Some(&plan_id)
358 || participant.identity().parts().plan_hash.as_ref() != Some(&plan_hash)
359 || participant.identity().parts().device_id.as_ref() != Some(&device_id)
360 || participant
361 .identity()
362 .parts()
363 .runtime_implementation_fingerprint
364 .as_deref()
365 != Some(runtime_implementation_fingerprint.as_str())
366 })
367 })
368 || nodes
369 .iter()
370 .map(BatchOperationNodeIdentity::node_id)
371 .collect::<BTreeSet<_>>()
372 .len()
373 != nodes.len()
374 || !canonical_sha256(&runtime_implementation_fingerprint)
375 || !canonical_sha256(&claimed_backing_fingerprint)
376 {
377 return Err(invalid_operation(
378 "physical batch identity is empty, non-canonical, or differs from its plan/runtime projections",
379 ));
380 }
381 let participants = nodes
382 .iter()
383 .flat_map(|node| node.participants.iter().cloned())
384 .collect::<Vec<_>>();
385 if participants
386 .iter()
387 .enumerate()
388 .any(|(index, participant)| participant.participant_index() as usize != index)
389 {
390 return Err(invalid_operation(
391 "physical batch participant indices are not globally contiguous",
392 ));
393 }
394 #[derive(Serialize)]
395 struct FingerprintInput<'a> {
396 domain: &'static str,
397 batch_step_id: BatchStepId,
398 batch_invocation_id: BatchInvocationId,
399 plan_id: &'a PlanId,
400 plan_hash: &'a PlanHash,
401 device_id: &'a DeviceId,
402 runtime_implementation_fingerprint: &'a str,
403 lane_id: ExecutionLaneId,
404 claimed_backing_fingerprint: &'a str,
405 node_fingerprints: BatchOperationNodeFingerprints<'a>,
406 }
407 let fingerprint = canonical_operation_fingerprint(
408 &FingerprintInput {
409 domain: "ferrum.runtime-vnext.physical-command-batch-identity.v2",
410 batch_step_id,
411 batch_invocation_id,
412 plan_id: &plan_id,
413 plan_hash: &plan_hash,
414 device_id: &device_id,
415 runtime_implementation_fingerprint: &runtime_implementation_fingerprint,
416 lane_id,
417 claimed_backing_fingerprint: &claimed_backing_fingerprint,
418 node_fingerprints: BatchOperationNodeFingerprints(&nodes),
419 },
420 "physical batch identity encode failed",
421 )?;
422 Ok(Self {
423 data: Arc::new(BatchOperationIdentityData {
424 batch_step_id,
425 batch_invocation_id,
426 plan_id,
427 plan_hash,
428 device_id,
429 runtime_implementation_fingerprint,
430 lane_id,
431 claimed_backing_fingerprint,
432 nodes: OnceLock::from(nodes),
433 participants: OnceLock::from(participants),
434 deferred_recipe: None,
435 fingerprint,
436 }),
437 })
438 }
439
440 pub fn batch_step_id(&self) -> BatchStepId {
441 self.data.batch_step_id
442 }
443
444 pub fn batch_invocation_id(&self) -> BatchInvocationId {
445 self.data.batch_invocation_id
446 }
447
448 pub fn plan_id(&self) -> &PlanId {
449 &self.data.plan_id
450 }
451
452 pub fn plan_hash(&self) -> &PlanHash {
453 &self.data.plan_hash
454 }
455
456 pub fn device_id(&self) -> &DeviceId {
457 &self.data.device_id
458 }
459
460 pub fn runtime_implementation_fingerprint(&self) -> &str {
461 &self.data.runtime_implementation_fingerprint
462 }
463
464 pub fn lane_id(&self) -> ExecutionLaneId {
465 self.data.lane_id
466 }
467
468 pub fn claimed_backing_fingerprint(&self) -> &str {
469 &self.data.claimed_backing_fingerprint
470 }
471
472 pub fn node_count(&self) -> usize {
473 self.data.nodes.get().map_or_else(
474 || {
475 self.data
476 .deferred_recipe
477 .as_ref()
478 .map_or(0, |recipe| recipe.topology.node_count())
479 },
480 Vec::len,
481 )
482 }
483
484 fn materialized_node_count(&self) -> usize {
485 self.data.nodes.get().map_or_else(
486 || {
487 self.data.deferred_recipe.as_ref().map_or(0, |recipe| {
488 recipe
489 .node_identities
490 .iter()
491 .filter(|identity| identity.get().is_some())
492 .count()
493 })
494 },
495 Vec::len,
496 )
497 }
498
499 pub fn materialization_snapshot(&self) -> BatchOperationIdentityMaterializationSnapshot {
500 BatchOperationIdentityMaterializationSnapshot {
501 logical_nodes: u32::try_from(self.node_count())
502 .expect("validated physical batch node count fits u32"),
503 materialized_nodes: u32::try_from(self.materialized_node_count())
504 .expect("materialized physical batch node count fits u32"),
505 full_participant_projection: self.data.participants.get().is_some(),
506 }
507 }
508
509 pub fn node_participant_count(&self, node_index: usize) -> Option<usize> {
510 if let Some(nodes) = self.data.nodes.get() {
511 return nodes.get(node_index).map(|node| node.participants().len());
512 }
513 let recipe = self.data.deferred_recipe.as_ref()?;
514 (node_index < recipe.topology.node_count()).then_some(recipe.participant_seeds.len())
515 }
516
517 pub fn node_id_at(&self, node_index: usize) -> Option<&NodeId> {
518 if let Some(nodes) = self.data.nodes.get() {
519 return nodes
520 .get(node_index)
521 .map(BatchOperationNodeIdentity::node_id);
522 }
523 self.data
524 .deferred_recipe
525 .as_ref()?
526 .topology
527 .node_id_at(node_index)
528 }
529
530 pub fn operation_id_at(&self, node_index: usize) -> Option<&OperationId> {
531 if let Some(nodes) = self.data.nodes.get() {
532 return nodes
533 .get(node_index)
534 .map(BatchOperationNodeIdentity::operation_id);
535 }
536 self.data
537 .deferred_recipe
538 .as_ref()?
539 .topology
540 .operation_id_at(node_index)
541 }
542
543 pub fn provider_id_at(&self, node_index: usize) -> Option<&ProviderId> {
544 if let Some(nodes) = self.data.nodes.get() {
545 return nodes
546 .get(node_index)
547 .map(BatchOperationNodeIdentity::provider_id);
548 }
549 self.data
550 .deferred_recipe
551 .as_ref()?
552 .topology
553 .provider_id_at(node_index)
554 }
555
556 pub fn work_shape_fingerprint_at(&self, node_index: usize) -> Option<&str> {
557 if let Some(nodes) = self.data.nodes.get() {
558 return nodes
559 .get(node_index)
560 .map(BatchOperationNodeIdentity::work_shape_fingerprint);
561 }
562 let recipe = self.data.deferred_recipe.as_ref()?;
563 (node_index < recipe.topology.node_count()).then_some(recipe.work_shape_fingerprint())
564 }
565
566 pub fn node_index(&self, node_id: &NodeId) -> Option<usize> {
567 if let Some(nodes) = self.data.nodes.get() {
568 return nodes.iter().position(|node| node.node_id() == node_id);
569 }
570 self.data
571 .deferred_recipe
572 .as_ref()?
573 .topology
574 .node_index(node_id)
575 }
576
577 pub(crate) fn materialize_node(
578 &self,
579 node_index: usize,
580 ) -> Result<&BatchOperationNodeIdentity, VNextError> {
581 if let Some(nodes) = self.data.nodes.get() {
582 return nodes
583 .get(node_index)
584 .ok_or_else(|| invalid_operation("physical batch node index is out of bounds"));
585 }
586 let recipe = self.data.deferred_recipe.as_ref().ok_or_else(|| {
587 invalid_operation("physical batch has neither materialized nodes nor a compiled recipe")
588 })?;
589 let slot = recipe.node_identities.get(node_index).ok_or_else(|| {
590 invalid_operation("compiled physical batch node index is out of bounds")
591 })?;
592 if let Some(identity) = slot.get() {
593 return Ok(identity);
594 }
595 let identity = recipe.materialize_node(node_index)?;
596 let _ = slot.set(identity);
597 slot.get().ok_or_else(|| {
598 invalid_operation("compiled physical batch node identity publication failed")
599 })
600 }
601
602 pub fn nodes(&self) -> &[BatchOperationNodeIdentity] {
603 self.data.nodes.get_or_init(|| {
604 (0..self.node_count())
605 .map(|node_index| {
606 self.materialize_node(node_index)
607 .expect("validated compiled physical batch node must materialize")
608 .clone()
609 })
610 .collect()
611 })
612 }
613
614 pub fn single_node(&self) -> Option<&BatchOperationNodeIdentity> {
615 (self.node_count() == 1).then(|| {
616 self.materialize_node(0)
617 .expect("validated single-node physical batch must materialize")
618 })
619 }
620
621 pub fn participants(&self) -> &[BatchOperationParticipantIdentity] {
622 self.data.participants.get_or_init(|| {
623 self.nodes()
624 .iter()
625 .flat_map(|node| node.participants().iter().cloned())
626 .collect()
627 })
628 }
629
630 pub fn fingerprint(&self) -> &str {
631 &self.data.fingerprint
632 }
633
634 pub(super) fn contains_identity(&self, identity: &ExecutionIdentityEnvelope) -> bool {
635 self.participants()
636 .iter()
637 .any(|participant| participant.identity() == identity)
638 }
639}
640
641#[derive(Debug)]
642struct DeferredBatchOperationIdentityRecipe {
643 topology: CompiledSubmissionWaveIdentity,
644 work_shape_fingerprint: String,
645 participant_seeds: Vec<SubmissionWaveParticipantIdentitySeed>,
646 node_identities: Box<[OnceLock<BatchOperationNodeIdentity>]>,
647}
648
649impl DeferredBatchOperationIdentityRecipe {
650 fn work_shape_fingerprint(&self) -> &str {
651 &self.work_shape_fingerprint
652 }
653
654 fn materialize_node(
655 &self,
656 node_index: usize,
657 ) -> Result<BatchOperationNodeIdentity, VNextError> {
658 let node = self
659 .topology
660 .node_at(node_index)
661 .ok_or_else(|| invalid_operation("compiled wave node index is out of bounds"))?;
662 let participant_start = node_index
663 .checked_mul(self.participant_seeds.len())
664 .and_then(|value| u32::try_from(value).ok())
665 .ok_or_else(|| {
666 invalid_operation("compiled wave participant index space exceeds u32")
667 })?;
668 let participants = self
669 .participant_seeds
670 .iter()
671 .enumerate()
672 .map(|(local_index, seed)| {
673 let local_index = u32::try_from(local_index)
674 .expect("compiled wave participant count was validated");
675 let frame = seed.frame();
676 let identity = seed
677 .operation_identity(&self.topology, node_index)
678 .ok_or_else(|| invalid_operation("compiled wave node identity disappeared"))?;
679 Ok(BatchOperationParticipantIdentity::new(
680 participant_start
681 .checked_add(local_index)
682 .expect("compiled wave participant index was validated"),
683 ParticipantNodeKey::new(
684 frame.participant(),
685 frame.frame_id(),
686 node.node_id().clone(),
687 ),
688 identity,
689 ))
690 })
691 .collect::<Result<Vec<_>, VNextError>>()?;
692 BatchOperationNodeIdentity::from_validated(
693 node.node_index(),
694 node.node_id().clone(),
695 node.operation_id().clone(),
696 node.provider_id().clone(),
697 node.provider_implementation_fingerprint().to_owned(),
698 node.provider_execution_semantics(),
699 self.work_shape_fingerprint.clone(),
700 participants,
701 )
702 }
703}
704
705impl BatchOperationIdentity {
706 #[allow(clippy::too_many_arguments)]
707 pub(super) fn from_compiled_wave(
708 topology: CompiledSubmissionWaveIdentity,
709 batch_step_id: BatchStepId,
710 batch_invocation_id: BatchInvocationId,
711 claimed_backing_fingerprint: String,
712 work_shape_fingerprint: String,
713 participant_seeds: Vec<SubmissionWaveParticipantIdentitySeed>,
714 ) -> Result<Self, VNextError> {
715 let participant_count = participant_seeds.len();
716 let participant_projection_count = topology
717 .node_count()
718 .checked_mul(participant_count)
719 .and_then(|count| u32::try_from(count).ok());
720 if topology.node_count() == 0
721 || participant_count == 0
722 || participant_projection_count.is_none()
723 || participant_seeds.windows(2).any(|pair| {
724 let left = pair[0].frame().participant();
725 let right = pair[1].frame().participant();
726 (
727 left.sequence_authority().sparse_id(),
728 left.sequence_authority().generation(),
729 left.request_authority().sparse_id(),
730 left.request_authority().generation(),
731 ) >= (
732 right.sequence_authority().sparse_id(),
733 right.sequence_authority().generation(),
734 right.request_authority().sparse_id(),
735 right.request_authority().generation(),
736 )
737 })
738 || participant_seeds.iter().any(|seed| {
739 seed.runtime_implementation_fingerprint()
740 != topology.runtime_implementation_fingerprint()
741 })
742 || !canonical_sha256(&claimed_backing_fingerprint)
743 || !canonical_sha256(&work_shape_fingerprint)
744 {
745 return Err(invalid_operation(
746 "compiled physical batch identity is empty, non-canonical, or exceeds its participant index space",
747 ));
748 }
749 #[derive(Serialize)]
750 struct FingerprintInput<'a> {
751 domain: &'static str,
752 batch_step_id: BatchStepId,
753 batch_invocation_id: BatchInvocationId,
754 topology_fingerprint: &'a str,
755 claimed_backing_fingerprint: &'a str,
756 work_shape_fingerprint: &'a str,
757 participant_seeds: &'a [SubmissionWaveParticipantIdentitySeed],
758 }
759 let fingerprint = canonical_operation_fingerprint(
760 &FingerprintInput {
761 domain: "ferrum.runtime-vnext.compiled-physical-command-batch-identity.v1",
762 batch_step_id,
763 batch_invocation_id,
764 topology_fingerprint: topology.fingerprint(),
765 claimed_backing_fingerprint: &claimed_backing_fingerprint,
766 work_shape_fingerprint: &work_shape_fingerprint,
767 participant_seeds: &participant_seeds,
768 },
769 "compiled physical batch identity encode failed",
770 )?;
771 let node_identities = std::iter::repeat_with(OnceLock::new)
772 .take(topology.node_count())
773 .collect::<Vec<_>>()
774 .into_boxed_slice();
775 let plan_id = topology.plan_id().clone();
776 let plan_hash = topology.plan_hash().clone();
777 let device_id = topology.device_id().clone();
778 let runtime_implementation_fingerprint =
779 topology.runtime_implementation_fingerprint().to_owned();
780 let lane_id = topology.lane_id();
781 Ok(Self::from_deferred_validated(
782 batch_step_id,
783 batch_invocation_id,
784 plan_id,
785 plan_hash,
786 device_id,
787 runtime_implementation_fingerprint,
788 lane_id,
789 claimed_backing_fingerprint,
790 DeferredBatchOperationIdentityRecipe {
791 topology,
792 work_shape_fingerprint,
793 participant_seeds,
794 node_identities,
795 },
796 fingerprint,
797 ))
798 }
799}
800
801#[cfg(test)]
802impl BatchOperationIdentity {
803 pub(crate) fn test_only_for_wave<R: crate::vnext::DeviceRuntime>(
807 wave: &crate::vnext::PreparedStepSubmissionWave<R>,
808 plan_nodes: &[crate::vnext::PlanNode],
809 active: &[crate::vnext::TrustedActiveSequenceBinding],
810 ) -> Result<Self, VNextError> {
811 let resources = wave
812 .nodes()
813 .iter()
814 .enumerate()
815 .map(|(node_index, prepared)| {
816 let node = plan_nodes
817 .get(prepared.plan_node_index())
818 .ok_or_else(|| invalid_operation("test wave plan node is absent"))?;
819 Ok((
820 super::invocation::OperationInvocationResources::Wave { wave, node_index },
821 node,
822 ))
823 })
824 .collect::<Result<Vec<_>, VNextError>>()?;
825 Self::test_only_for_resources(&resources, active)
826 }
827
828 pub(crate) fn test_only_for_invocation<R: crate::vnext::DeviceRuntime>(
829 invocation: &crate::vnext::InvocationResourceLease<R>,
830 node: &crate::vnext::PlanNode,
831 active: &[crate::vnext::TrustedActiveSequenceBinding],
832 ) -> Result<Self, VNextError> {
833 Self::test_only_for_resources(
834 &[(
835 super::invocation::OperationInvocationResources::Invocation(invocation),
836 node,
837 )],
838 active,
839 )
840 }
841
842 fn test_only_for_resources<R: crate::vnext::DeviceRuntime>(
843 resources: &[(
844 super::invocation::OperationInvocationResources<'_, R>,
845 &crate::vnext::PlanNode,
846 )],
847 active: &[crate::vnext::TrustedActiveSequenceBinding],
848 ) -> Result<Self, VNextError> {
849 use crate::vnext::{
850 ExecutionIdentityParts, NodeInvocationId, SpanId, EXECUTION_IDENTITY_VERSION,
851 };
852 let first = resources
853 .first()
854 .ok_or_else(|| invalid_operation("test operation resources are empty"))?
855 .0;
856 let plan = first.step_resources().plan_evidence();
857 let mut nodes = Vec::new();
858 let mut participant_index = 0_u32;
859 for (index, (prepared, node)) in resources.iter().enumerate() {
860 if prepared.node_id()? != node.id()
861 || active.len() != prepared.participant_frames()?.len()
862 {
863 return Err(invalid_operation("test wave nodes or participants differ"));
864 }
865 let mut participants = Vec::new();
866 for (binding, frame) in active.iter().zip(prepared.participant_frames()?) {
867 if binding.sequence_authority() != frame.participant().sequence_authority()
868 || binding.plan().plan_hash() != plan.plan_hash()
869 {
870 return Err(invalid_operation("test wave active binding differs"));
871 }
872 let invocation = NodeInvocationId::try_from(index as u64 + 1)?;
873 let identity = ExecutionIdentityEnvelope::new(ExecutionIdentityParts {
874 version: EXECUTION_IDENTITY_VERSION,
875 run_id: binding.run_id().clone(),
876 request_id: binding.request_id().clone(),
877 sequence: u64::from(participant_index) + 1,
878 plan_id: Some(plan.plan_id().clone()),
879 plan_hash: Some(plan.plan_hash().clone()),
880 frame_id: Some(frame.frame_id()),
881 node_invocation_id: Some(invocation),
882 node_id: Some(node.id().clone()),
883 operation_id: Some(node.operation_id().clone()),
884 provider_id: Some(node.selection().selected_provider().clone()),
885 device_id: Some(plan.device_id().clone()),
886 resource_pool_id: None,
887 resource_pool_identity_fingerprint: None,
888 provisioning_run_id: None,
889 provisioning_request_id: None,
890 transaction_id: None,
891 active_sequence_slot: Some(binding.sequence_authority().sparse_id()),
892 admission_generation: Some(binding.sequence_authority().generation()),
893 activation_epoch: Some(binding.activation_epoch()),
894 runtime_implementation_fingerprint: Some(
895 plan.runtime_implementation_fingerprint().to_owned(),
896 ),
897 active_sequence_fingerprint: Some(binding.fingerprint().to_owned()),
898 completed_sequence_fingerprint: None,
899 aborted_sequence_fingerprint: None,
900 resource_id: None,
901 resource_generation: None,
902 resource_batch_fingerprint: None,
903 span_id: SpanId::new(format!(
904 "test/completion/{}/{participant_index}",
905 first.batch_invocation_id().get()
906 ))?,
907 parent_span_id: None,
908 async_links: Vec::new(),
909 })?;
910 participants.push(BatchOperationParticipantIdentity::new(
911 participant_index,
912 ParticipantNodeKey::new(
913 frame.participant(),
914 frame.frame_id(),
915 node.id().clone(),
916 ),
917 identity,
918 ));
919 participant_index += 1;
920 }
921 nodes.push(BatchOperationNodeIdentity::from_validated(
922 index as u32,
923 node.id().clone(),
924 node.operation_id().clone(),
925 node.selection().selected_provider().clone(),
926 node.provider_implementation_fingerprint().to_owned(),
927 node.provider_execution_semantics(),
928 prepared.work_shape()?.fingerprint().to_owned(),
929 participants,
930 )?);
931 }
932 Self::from_validated(
933 first.batch_step_id(),
934 first.batch_invocation_id(),
935 plan.plan_id().clone(),
936 plan.plan_hash().clone(),
937 plan.device_id().clone(),
938 plan.runtime_implementation_fingerprint().to_owned(),
939 first.step_resources().execution_lane().id(),
940 first.backing_fingerprint().to_owned(),
941 nodes,
942 )
943 }
944}
945
946#[cfg(test)]
947mod batch_operation_identity_fingerprint_tests {
948 use super::{
949 canonical_operation_fingerprint, BatchOperationNodeFingerprints,
950 BatchOperationNodeIdentity, NodeId, OperationId, ProviderExecutionSemantics, ProviderId,
951 Serialize,
952 };
953 use sha2::{Digest, Sha256};
954
955 fn fingerprint_node(index: u32, marker: char) -> BatchOperationNodeIdentity {
956 BatchOperationNodeIdentity {
957 node_index: index,
958 node_id: NodeId::new(format!("node.{index}")).unwrap(),
959 operation_id: OperationId::new(format!("operation.{index}")).unwrap(),
960 provider_id: ProviderId::new(format!("provider.{index}")).unwrap(),
961 provider_implementation_fingerprint: std::iter::repeat_n(marker, 64).collect(),
962 provider_execution_semantics: ProviderExecutionSemantics::bitwise_eager_and_replay(),
963 work_shape_fingerprint: std::iter::repeat_n(marker, 64).collect(),
964 participants: Vec::new(),
965 fingerprint: std::iter::repeat_n(marker, 64).collect(),
966 }
967 }
968
969 #[test]
970 fn streaming_fingerprint_matches_canonical_json_digest() {
971 #[derive(Serialize)]
972 struct Input<'a> {
973 domain: &'static str,
974 value: &'a str,
975 }
976
977 let input = Input {
978 domain: "ferrum.runtime-vnext.test",
979 value: "evidence",
980 };
981 let expected = format!("{:x}", Sha256::digest(serde_json::to_vec(&input).unwrap()));
982
983 assert_eq!(
984 canonical_operation_fingerprint(&input, "test fingerprint").unwrap(),
985 expected
986 );
987 }
988
989 #[test]
990 fn batch_fingerprint_projection_contains_only_validated_node_digests() {
991 let first = std::iter::repeat_n('a', 64).collect::<String>();
992 let second = std::iter::repeat_n('b', 64).collect::<String>();
993 let nodes = [fingerprint_node(0, 'a'), fingerprint_node(1, 'b')];
994
995 let encoded = serde_json::to_string(&BatchOperationNodeFingerprints(&nodes)).unwrap();
996
997 assert_eq!(encoded, format!("[\"{first}\",\"{second}\"]"));
998 assert!(!encoded.contains("node.0"));
999 assert!(!encoded.contains("operation.0"));
1000 assert!(!encoded.contains("participants"));
1001 }
1002}