1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use sha2::{Digest, Sha256};
3use std::collections::BTreeSet;
4use std::fmt;
5
6use super::super::{
7 CapabilityId, ContractVersion, DeviceId, ExecutionIdentityEnvelope, NodeId, OperationId,
8 ProviderId, QuantizationFormatId, UnvalidatedExecutionIdentityParts, VNextError,
9 WeightFormatId,
10};
11use super::foundation::{canonical_sha256, invalid_operation};
12use super::{
13 DynamicStorageRequirement, ProfilePhase, ProviderCheckpointCapability,
14 ProviderStorageBindingRequirement, ResolvedValueRole,
15};
16
17pub const PROVIDER_EXECUTION_SEMANTICS_VERSION: ContractVersion = ContractVersion::new(1, 0);
18pub const MAX_OPERATION_FAILURE_WIRE_BYTES: usize = 16 * 1024;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub struct ProviderExecutionContractFingerprint([u8; 32]);
22
23impl ProviderExecutionContractFingerprint {
24 pub const fn as_bytes(&self) -> &[u8; 32] {
25 &self.0
26 }
27}
28
29impl fmt::Display for ProviderExecutionContractFingerprint {
30 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
31 for byte in self.0 {
32 write!(formatter, "{byte:02x}")?;
33 }
34 Ok(())
35 }
36}
37
38impl Serialize for ProviderExecutionContractFingerprint {
39 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
40 where
41 S: Serializer,
42 {
43 serializer.collect_str(self)
44 }
45}
46
47impl<'de> Deserialize<'de> for ProviderExecutionContractFingerprint {
48 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
49 where
50 D: Deserializer<'de>,
51 {
52 let value = String::deserialize(deserializer)?;
53 if !canonical_sha256(&value) {
54 return Err(serde::de::Error::custom(
55 "provider execution contract fingerprint must be a lowercase SHA256",
56 ));
57 }
58 let mut bytes = [0_u8; 32];
59 for (index, byte) in bytes.iter_mut().enumerate() {
60 *byte = u8::from_str_radix(&value[index * 2..index * 2 + 2], 16)
61 .map_err(serde::de::Error::custom)?;
62 }
63 Ok(Self(bytes))
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ProviderExecutionRepeatability {
76 BitwiseSameRuntime,
77}
78
79impl ProviderExecutionRepeatability {
80 pub const fn as_str(self) -> &'static str {
81 match self {
82 Self::BitwiseSameRuntime => "bitwise_same_runtime",
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum ProviderReplayEquivalence {
92 Ineligible,
93 BitwiseEagerEquivalent,
94}
95
96impl ProviderReplayEquivalence {
97 pub const fn as_str(self) -> &'static str {
98 match self {
99 Self::Ineligible => "ineligible",
100 Self::BitwiseEagerEquivalent => "bitwise_eager_equivalent",
101 }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
106pub struct ProviderExecutionSemantics {
107 contract_version: ContractVersion,
108 contract_fingerprint: ProviderExecutionContractFingerprint,
109 repeatability: ProviderExecutionRepeatability,
110 replay_equivalence: ProviderReplayEquivalence,
111}
112
113#[derive(Deserialize)]
114#[serde(deny_unknown_fields)]
115struct ProviderExecutionSemanticsWire {
116 contract_version: ContractVersion,
117 contract_fingerprint: ProviderExecutionContractFingerprint,
118 repeatability: ProviderExecutionRepeatability,
119 replay_equivalence: ProviderReplayEquivalence,
120}
121
122impl<'de> Deserialize<'de> for ProviderExecutionSemantics {
123 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124 where
125 D: Deserializer<'de>,
126 {
127 let wire = ProviderExecutionSemanticsWire::deserialize(deserializer)?;
128 let semantics = Self::new(
129 wire.contract_version,
130 wire.repeatability,
131 wire.replay_equivalence,
132 )
133 .map_err(serde::de::Error::custom)?;
134 if semantics.contract_fingerprint != wire.contract_fingerprint {
135 return Err(serde::de::Error::custom(format!(
136 "provider execution contract fingerprint mismatch: expected `{}`, actual `{}`",
137 semantics.contract_fingerprint, wire.contract_fingerprint
138 )));
139 }
140 Ok(semantics)
141 }
142}
143
144impl ProviderExecutionSemantics {
145 fn new(
146 contract_version: ContractVersion,
147 repeatability: ProviderExecutionRepeatability,
148 replay_equivalence: ProviderReplayEquivalence,
149 ) -> Result<Self, VNextError> {
150 if contract_version != PROVIDER_EXECUTION_SEMANTICS_VERSION {
151 return Err(invalid_operation(format!(
152 "provider execution semantics version {contract_version} is unsupported"
153 )));
154 }
155 let mut digest = Sha256::new();
156 digest.update(b"ferrum.runtime-vnext.provider-execution-semantics.v1\0");
157 digest.update(contract_version.major.to_le_bytes());
158 digest.update(contract_version.minor.to_le_bytes());
159 digest.update([match repeatability {
160 ProviderExecutionRepeatability::BitwiseSameRuntime => 1,
161 }]);
162 digest.update([match replay_equivalence {
163 ProviderReplayEquivalence::Ineligible => 0,
164 ProviderReplayEquivalence::BitwiseEagerEquivalent => 1,
165 }]);
166 Ok(Self {
167 contract_version,
168 contract_fingerprint: ProviderExecutionContractFingerprint(digest.finalize().into()),
169 repeatability,
170 replay_equivalence,
171 })
172 }
173
174 pub fn bitwise_eager_only() -> Self {
175 Self::new(
176 PROVIDER_EXECUTION_SEMANTICS_VERSION,
177 ProviderExecutionRepeatability::BitwiseSameRuntime,
178 ProviderReplayEquivalence::Ineligible,
179 )
180 .expect("built-in eager execution semantics are valid")
181 }
182
183 pub fn bitwise_eager_and_replay() -> Self {
184 Self::new(
185 PROVIDER_EXECUTION_SEMANTICS_VERSION,
186 ProviderExecutionRepeatability::BitwiseSameRuntime,
187 ProviderReplayEquivalence::BitwiseEagerEquivalent,
188 )
189 .expect("built-in reusable execution semantics are valid")
190 }
191
192 pub const fn contract_version(self) -> ContractVersion {
193 self.contract_version
194 }
195
196 pub const fn contract_fingerprint(self) -> ProviderExecutionContractFingerprint {
197 self.contract_fingerprint
198 }
199
200 pub const fn repeatability(self) -> ProviderExecutionRepeatability {
201 self.repeatability
202 }
203
204 pub const fn replay_equivalence(self) -> ProviderReplayEquivalence {
205 self.replay_equivalence
206 }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum ExecutionDeterminismRequirement {
212 BitwiseSameRuntime,
213 BitwiseSameRuntimeWithReplay,
214}
215
216impl ExecutionDeterminismRequirement {
217 pub const fn as_str(self) -> &'static str {
218 match self {
219 Self::BitwiseSameRuntime => "bitwise_same_runtime",
220 Self::BitwiseSameRuntimeWithReplay => "bitwise_same_runtime_with_replay",
221 }
222 }
223
224 pub const fn requires_replay_equivalence(self) -> bool {
225 matches!(self, Self::BitwiseSameRuntimeWithReplay)
226 }
227
228 pub fn accepts(self, semantics: ProviderExecutionSemantics) -> bool {
229 semantics.repeatability == ProviderExecutionRepeatability::BitwiseSameRuntime
230 && (!self.requires_replay_equivalence()
231 || semantics.replay_equivalence
232 == ProviderReplayEquivalence::BitwiseEagerEquivalent)
233 }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
237pub struct OperationProviderDescriptor {
238 provider_id: ProviderId,
239 operation_id: OperationId,
240 operation_fingerprint: String,
241 provider_implementation_fingerprint: String,
242 execution_semantics: ProviderExecutionSemantics,
243 #[serde(skip_serializing_if = "ProviderCheckpointCapability::is_unsupported")]
244 checkpoint: ProviderCheckpointCapability,
245 version: ContractVersion,
246 device_id: DeviceId,
247 capabilities: BTreeSet<CapabilityId>,
248 accepted_weight_formats: BTreeSet<WeightFormatId>,
249 accepted_quantization_formats: BTreeSet<QuantizationFormatId>,
250 dynamic_storage_bindings: Vec<ProviderStorageBindingRequirement>,
251 resource_estimator_id: String,
252 resource_estimator_version: ContractVersion,
253 resource_estimator_implementation_fingerprint: String,
254}
255
256#[derive(Deserialize)]
257#[serde(deny_unknown_fields)]
258struct OperationProviderDescriptorWire {
259 provider_id: ProviderId,
260 operation_id: OperationId,
261 operation_fingerprint: String,
262 provider_implementation_fingerprint: String,
263 execution_semantics: ProviderExecutionSemantics,
264 #[serde(default)]
265 checkpoint: ProviderCheckpointCapability,
266 version: ContractVersion,
267 device_id: DeviceId,
268 capabilities: BTreeSet<CapabilityId>,
269 accepted_weight_formats: BTreeSet<WeightFormatId>,
270 accepted_quantization_formats: BTreeSet<QuantizationFormatId>,
271 dynamic_storage_bindings: Vec<ProviderStorageBindingRequirement>,
272 resource_estimator_id: String,
273 resource_estimator_version: ContractVersion,
274 resource_estimator_implementation_fingerprint: String,
275}
276
277impl<'de> Deserialize<'de> for OperationProviderDescriptor {
278 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
279 where
280 D: Deserializer<'de>,
281 {
282 let wire = OperationProviderDescriptorWire::deserialize(deserializer)?;
283 let original_bindings = wire.dynamic_storage_bindings.clone();
284 let descriptor = Self::new(
285 wire.provider_id,
286 wire.operation_id,
287 wire.operation_fingerprint,
288 wire.provider_implementation_fingerprint,
289 wire.execution_semantics,
290 wire.version,
291 wire.device_id,
292 wire.capabilities,
293 wire.accepted_weight_formats,
294 wire.accepted_quantization_formats,
295 wire.dynamic_storage_bindings,
296 wire.resource_estimator_id,
297 wire.resource_estimator_version,
298 wire.resource_estimator_implementation_fingerprint,
299 )
300 .map_err(serde::de::Error::custom)?
301 .with_checkpoint_capability(wire.checkpoint);
302 if descriptor.dynamic_storage_bindings != original_bindings {
303 return Err(serde::de::Error::custom(
304 "provider storage binding requirements are not canonical",
305 ));
306 }
307 Ok(descriptor)
308 }
309}
310
311impl OperationProviderDescriptor {
312 #[allow(clippy::too_many_arguments)]
313 pub fn new(
314 provider_id: ProviderId,
315 operation_id: OperationId,
316 operation_fingerprint: impl Into<String>,
317 provider_implementation_fingerprint: impl Into<String>,
318 execution_semantics: ProviderExecutionSemantics,
319 version: ContractVersion,
320 device_id: DeviceId,
321 capabilities: BTreeSet<CapabilityId>,
322 accepted_weight_formats: BTreeSet<WeightFormatId>,
323 accepted_quantization_formats: BTreeSet<QuantizationFormatId>,
324 mut dynamic_storage_bindings: Vec<ProviderStorageBindingRequirement>,
325 resource_estimator_id: impl Into<String>,
326 resource_estimator_version: ContractVersion,
327 resource_estimator_implementation_fingerprint: impl Into<String>,
328 ) -> Result<Self, VNextError> {
329 let operation_fingerprint = operation_fingerprint.into();
330 let provider_implementation_fingerprint = provider_implementation_fingerprint.into();
331 let resource_estimator_id = resource_estimator_id.into();
332 let resource_estimator_implementation_fingerprint =
333 resource_estimator_implementation_fingerprint.into();
334 dynamic_storage_bindings.sort_by_key(|binding| (binding.role(), binding.ordinal()));
335 if version.major == 0
336 || !canonical_sha256(&operation_fingerprint)
337 || !canonical_sha256(&provider_implementation_fingerprint)
338 || resource_estimator_id.is_empty()
339 || resource_estimator_id.len() > 160
340 || !resource_estimator_id.bytes().all(|byte| {
341 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-' | b':' | b'/')
342 })
343 || resource_estimator_version.major == 0
344 || !canonical_sha256(&resource_estimator_implementation_fingerprint)
345 || dynamic_storage_bindings.is_empty()
346 || dynamic_storage_bindings.windows(2).any(|pair| {
347 (pair[0].role(), pair[0].ordinal()) == (pair[1].role(), pair[1].ordinal())
348 })
349 {
350 return Err(invalid_operation(
351 "operation provider or resource-estimator identity is invalid",
352 ));
353 }
354 Ok(Self {
355 provider_id,
356 operation_id,
357 operation_fingerprint,
358 provider_implementation_fingerprint,
359 execution_semantics,
360 checkpoint: ProviderCheckpointCapability::Unsupported,
361 version,
362 device_id,
363 capabilities,
364 accepted_weight_formats,
365 accepted_quantization_formats,
366 dynamic_storage_bindings,
367 resource_estimator_id,
368 resource_estimator_version,
369 resource_estimator_implementation_fingerprint,
370 })
371 }
372
373 pub fn provider_id(&self) -> &ProviderId {
374 &self.provider_id
375 }
376
377 pub fn operation_id(&self) -> &OperationId {
378 &self.operation_id
379 }
380
381 pub fn operation_fingerprint(&self) -> &str {
382 &self.operation_fingerprint
383 }
384
385 pub fn provider_implementation_fingerprint(&self) -> &str {
386 &self.provider_implementation_fingerprint
387 }
388
389 pub const fn execution_semantics(&self) -> ProviderExecutionSemantics {
390 self.execution_semantics
391 }
392
393 pub fn with_checkpoint_capability(mut self, capability: ProviderCheckpointCapability) -> Self {
397 self.checkpoint = capability;
398 self
399 }
400
401 pub fn checkpoint_capability(&self) -> &ProviderCheckpointCapability {
402 &self.checkpoint
403 }
404
405 pub fn version(&self) -> ContractVersion {
406 self.version
407 }
408
409 pub fn device_id(&self) -> &DeviceId {
410 &self.device_id
411 }
412
413 pub fn capabilities(&self) -> &BTreeSet<CapabilityId> {
414 &self.capabilities
415 }
416
417 pub fn accepted_weight_formats(&self) -> &BTreeSet<WeightFormatId> {
418 &self.accepted_weight_formats
419 }
420
421 pub fn accepted_quantization_formats(&self) -> &BTreeSet<QuantizationFormatId> {
422 &self.accepted_quantization_formats
423 }
424
425 pub fn dynamic_storage_bindings(&self) -> &[ProviderStorageBindingRequirement] {
426 &self.dynamic_storage_bindings
427 }
428
429 pub fn dynamic_storage_for(
430 &self,
431 role: ResolvedValueRole,
432 ordinal: u32,
433 ) -> Option<&DynamicStorageRequirement> {
434 self.dynamic_storage_bindings
435 .binary_search_by_key(&(role, ordinal), |binding| {
436 (binding.role(), binding.ordinal())
437 })
438 .ok()
439 .map(|index| self.dynamic_storage_bindings[index].storage())
440 }
441
442 pub fn resource_estimator_id(&self) -> &str {
443 &self.resource_estimator_id
444 }
445
446 pub const fn resource_estimator_version(&self) -> ContractVersion {
447 self.resource_estimator_version
448 }
449
450 pub fn resource_estimator_implementation_fingerprint(&self) -> &str {
451 &self.resource_estimator_implementation_fingerprint
452 }
453}
454
455#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
456pub struct EngineProviderDescriptor {
457 provider_id: ProviderId,
458 contract_version: ContractVersion,
459 implementation_fingerprint: String,
460 device_id: DeviceId,
461 capabilities: BTreeSet<CapabilityId>,
462}
463
464#[derive(Deserialize)]
465#[serde(deny_unknown_fields)]
466struct EngineProviderDescriptorWire {
467 provider_id: ProviderId,
468 contract_version: ContractVersion,
469 implementation_fingerprint: String,
470 device_id: DeviceId,
471 capabilities: BTreeSet<CapabilityId>,
472}
473
474impl<'de> Deserialize<'de> for EngineProviderDescriptor {
475 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
476 where
477 D: Deserializer<'de>,
478 {
479 let wire = EngineProviderDescriptorWire::deserialize(deserializer)?;
480 Self::new(
481 wire.provider_id,
482 wire.contract_version,
483 wire.implementation_fingerprint,
484 wire.device_id,
485 wire.capabilities,
486 )
487 .map_err(serde::de::Error::custom)
488 }
489}
490
491impl EngineProviderDescriptor {
492 pub fn new(
493 provider_id: ProviderId,
494 contract_version: ContractVersion,
495 implementation_fingerprint: impl Into<String>,
496 device_id: DeviceId,
497 capabilities: BTreeSet<CapabilityId>,
498 ) -> Result<Self, VNextError> {
499 let implementation_fingerprint = implementation_fingerprint.into();
500 if contract_version.major == 0 || !canonical_sha256(&implementation_fingerprint) {
501 return Err(invalid_operation(
502 "engine provider contract version or implementation fingerprint is invalid",
503 ));
504 }
505 Ok(Self {
506 provider_id,
507 contract_version,
508 implementation_fingerprint,
509 device_id,
510 capabilities,
511 })
512 }
513
514 pub fn provider_id(&self) -> &ProviderId {
515 &self.provider_id
516 }
517
518 pub const fn contract_version(&self) -> ContractVersion {
519 self.contract_version
520 }
521
522 pub fn implementation_fingerprint(&self) -> &str {
523 &self.implementation_fingerprint
524 }
525
526 pub fn device_id(&self) -> &DeviceId {
527 &self.device_id
528 }
529
530 pub fn capabilities(&self) -> &BTreeSet<CapabilityId> {
531 &self.capabilities
532 }
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
536pub struct ProviderCompatibilityRequest {
537 operation_id: OperationId,
538 required_version: ContractVersion,
539 required_capabilities: BTreeSet<CapabilityId>,
540 required_weight_formats: BTreeSet<WeightFormatId>,
541 required_quantization_formats: BTreeSet<QuantizationFormatId>,
542 execution_determinism: ExecutionDeterminismRequirement,
543}
544
545#[derive(Deserialize)]
546#[serde(deny_unknown_fields)]
547struct ProviderCompatibilityRequestWire {
548 operation_id: OperationId,
549 required_version: ContractVersion,
550 required_capabilities: BTreeSet<CapabilityId>,
551 required_weight_formats: BTreeSet<WeightFormatId>,
552 required_quantization_formats: BTreeSet<QuantizationFormatId>,
553 execution_determinism: ExecutionDeterminismRequirement,
554}
555
556impl<'de> Deserialize<'de> for ProviderCompatibilityRequest {
557 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
558 where
559 D: Deserializer<'de>,
560 {
561 let wire = ProviderCompatibilityRequestWire::deserialize(deserializer)?;
562 Self::new(
563 wire.operation_id,
564 wire.required_version,
565 wire.required_capabilities,
566 wire.required_weight_formats,
567 wire.required_quantization_formats,
568 wire.execution_determinism,
569 )
570 .map_err(serde::de::Error::custom)
571 }
572}
573
574impl ProviderCompatibilityRequest {
575 pub fn new(
576 operation_id: OperationId,
577 required_version: ContractVersion,
578 required_capabilities: BTreeSet<CapabilityId>,
579 required_weight_formats: BTreeSet<WeightFormatId>,
580 required_quantization_formats: BTreeSet<QuantizationFormatId>,
581 execution_determinism: ExecutionDeterminismRequirement,
582 ) -> Result<Self, VNextError> {
583 if required_version.major == 0 {
584 return Err(invalid_operation(
585 "provider compatibility request has a zero major version",
586 ));
587 }
588 Ok(Self {
589 operation_id,
590 required_version,
591 required_capabilities,
592 required_weight_formats,
593 required_quantization_formats,
594 execution_determinism,
595 })
596 }
597
598 pub fn operation_id(&self) -> &OperationId {
599 &self.operation_id
600 }
601
602 pub const fn required_version(&self) -> ContractVersion {
603 self.required_version
604 }
605
606 pub fn required_capabilities(&self) -> &BTreeSet<CapabilityId> {
607 &self.required_capabilities
608 }
609
610 pub fn required_weight_formats(&self) -> &BTreeSet<WeightFormatId> {
611 &self.required_weight_formats
612 }
613
614 pub fn required_quantization_formats(&self) -> &BTreeSet<QuantizationFormatId> {
615 &self.required_quantization_formats
616 }
617
618 pub const fn execution_determinism(&self) -> ExecutionDeterminismRequirement {
619 self.execution_determinism
620 }
621
622 pub(super) fn extend_required_capabilities(
623 &mut self,
624 capabilities: impl IntoIterator<Item = CapabilityId>,
625 ) {
626 self.required_capabilities.extend(capabilities);
627 }
628}
629
630#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
631#[serde(rename_all = "snake_case")]
632pub enum ProviderCompatibilityRejectReason {
633 OperationVersionMismatch {
634 required: ContractVersion,
635 available: ContractVersion,
636 },
637 ProviderVersionMismatch {
638 required: ContractVersion,
639 available: ContractVersion,
640 },
641 MissingCapabilities {
642 capabilities: BTreeSet<CapabilityId>,
643 },
644 UnsupportedWeightFormats {
645 formats: BTreeSet<WeightFormatId>,
646 },
647 UnsupportedQuantizationFormats {
648 formats: BTreeSet<QuantizationFormatId>,
649 },
650 InsufficientExecutionDeterminism {
651 required: ExecutionDeterminismRequirement,
652 available: ProviderExecutionSemantics,
653 },
654}
655
656#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
657pub struct ProviderCompatibilityRejection {
658 pub provider_id: ProviderId,
659 pub reasons: Vec<ProviderCompatibilityRejectReason>,
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
663pub struct ProviderCompatibilityReport {
664 request: ProviderCompatibilityRequest,
665 compatible_provider_ids: Vec<ProviderId>,
666 rejected: Vec<ProviderCompatibilityRejection>,
667}
668
669#[derive(Deserialize)]
670#[serde(deny_unknown_fields)]
671struct ProviderCompatibilityReportWire {
672 request: ProviderCompatibilityRequest,
673 compatible_provider_ids: Vec<ProviderId>,
674 rejected: Vec<ProviderCompatibilityRejection>,
675}
676
677impl<'de> Deserialize<'de> for ProviderCompatibilityReport {
678 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
679 where
680 D: Deserializer<'de>,
681 {
682 let wire = ProviderCompatibilityReportWire::deserialize(deserializer)?;
683 let report = Self {
684 request: wire.request,
685 compatible_provider_ids: wire.compatible_provider_ids,
686 rejected: wire.rejected,
687 };
688 report.validate_shape().map_err(serde::de::Error::custom)?;
689 Ok(report)
690 }
691}
692
693impl ProviderCompatibilityReport {
694 pub(super) fn from_classification(
695 request: ProviderCompatibilityRequest,
696 compatible_provider_ids: Vec<ProviderId>,
697 rejected: Vec<ProviderCompatibilityRejection>,
698 ) -> Result<Self, VNextError> {
699 let report = Self {
700 request,
701 compatible_provider_ids,
702 rejected,
703 };
704 report.validate_shape()?;
705 Ok(report)
706 }
707
708 fn rejection_summary(&self) -> String {
709 serde_json::to_string(&self.rejected)
710 .map(|rejected| format!("all providers were rejected: {rejected}"))
711 .unwrap_or_else(|_| "all providers were rejected".to_owned())
712 }
713
714 fn validate_shape(&self) -> Result<(), VNextError> {
715 let compatible = self.compatible_provider_ids.iter().collect::<BTreeSet<_>>();
716 let rejected = self
717 .rejected
718 .iter()
719 .map(|rejection| &rejection.provider_id)
720 .collect::<BTreeSet<_>>();
721 if compatible.len() != self.compatible_provider_ids.len()
722 || rejected.len() != self.rejected.len()
723 || (compatible.is_empty() && rejected.is_empty())
724 || !compatible.is_disjoint(&rejected)
725 || self
726 .rejected
727 .iter()
728 .any(|rejection| rejection.reasons.is_empty())
729 || self
730 .compatible_provider_ids
731 .windows(2)
732 .any(|pair| pair[0] >= pair[1])
733 || self
734 .rejected
735 .windows(2)
736 .any(|pair| pair[0].provider_id >= pair[1].provider_id)
737 {
738 return Err(invalid_operation(
739 "provider compatibility report is duplicate, overlapping, empty, or non-canonical",
740 ));
741 }
742 Ok(())
743 }
744
745 pub fn request(&self) -> &ProviderCompatibilityRequest {
746 &self.request
747 }
748
749 pub fn compatible_provider_ids(&self) -> &[ProviderId] {
750 &self.compatible_provider_ids
751 }
752
753 pub fn rejected(&self) -> &[ProviderCompatibilityRejection] {
754 &self.rejected
755 }
756
757 pub fn require_compatible(&self, device_id: &DeviceId) -> Result<(), VNextError> {
758 if self.compatible_provider_ids.is_empty() {
759 return Err(VNextError::UnsupportedOperation {
760 node_id: None,
761 operation_id: self.request.operation_id.to_string(),
762 device_id: device_id.to_string(),
763 reason: self.rejection_summary(),
764 });
765 }
766 Ok(())
767 }
768
769 pub fn require_compatible_for_node(
772 &self,
773 device_id: &DeviceId,
774 node_id: &NodeId,
775 ) -> Result<(), VNextError> {
776 if !self.compatible_provider_ids.is_empty() {
777 return Ok(());
778 }
779 let operation_version_mismatch = self
780 .rejected
781 .iter()
782 .flat_map(|rejection| &rejection.reasons)
783 .find_map(|reason| match reason {
784 ProviderCompatibilityRejectReason::OperationVersionMismatch {
785 required,
786 available,
787 } => Some((*required, *available)),
788 _ => None,
789 });
790 let provider_version_mismatch = self
791 .rejected
792 .iter()
793 .map(|rejection| {
794 rejection.reasons.iter().find_map(|reason| match reason {
795 ProviderCompatibilityRejectReason::ProviderVersionMismatch {
796 required,
797 available,
798 } => Some((*required, *available)),
799 _ => None,
800 })
801 })
802 .collect::<Option<Vec<_>>>()
803 .and_then(|versions| {
804 versions
805 .into_iter()
806 .max_by_key(|(_, available)| (available.major, available.minor))
807 });
808 if let Some((required, available)) =
809 operation_version_mismatch.or(provider_version_mismatch)
810 {
811 return Err(VNextError::IncompatibleOperationVersion {
812 node_id: Some(node_id.to_string()),
813 operation_id: self.request.operation_id.to_string(),
814 required_major: required.major,
815 required_minor: required.minor,
816 available_major: available.major,
817 available_minor: available.minor,
818 });
819 }
820 Err(VNextError::UnsupportedOperation {
821 node_id: Some(node_id.to_string()),
822 operation_id: self.request.operation_id.to_string(),
823 device_id: device_id.to_string(),
824 reason: self.rejection_summary(),
825 })
826 }
827}
828
829#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
830pub struct OperationFailure {
831 identity: ExecutionIdentityEnvelope,
832 phase: ProfilePhase,
833 code: String,
834 message: String,
835 retryable: bool,
836}
837
838#[derive(Debug, Clone, PartialEq, Eq)]
839pub struct UnvalidatedOperationFailure {
840 identity: UnvalidatedExecutionIdentityParts,
841 phase: ProfilePhase,
842 code: String,
843 message: String,
844 retryable: bool,
845}
846
847#[derive(Deserialize)]
848#[serde(deny_unknown_fields)]
849struct UnvalidatedOperationFailureWire {
850 identity: UnvalidatedExecutionIdentityParts,
851 phase: ProfilePhase,
852 code: String,
853 message: String,
854 retryable: bool,
855}
856
857impl From<UnvalidatedOperationFailureWire> for UnvalidatedOperationFailure {
858 fn from(wire: UnvalidatedOperationFailureWire) -> Self {
859 Self {
860 identity: wire.identity,
861 phase: wire.phase,
862 code: wire.code,
863 message: wire.message,
864 retryable: wire.retryable,
865 }
866 }
867}
868
869impl UnvalidatedOperationFailure {
870 pub fn revalidate(
871 self,
872 expected_identity: &ExecutionIdentityEnvelope,
873 expected_phase: ProfilePhase,
874 ) -> Result<OperationFailure, VNextError> {
875 let identity = ExecutionIdentityEnvelope::new(self.identity.into())?;
876 if &identity != expected_identity || self.phase != expected_phase {
877 return Err(invalid_operation(
878 "serialized operation failure differs from the expected execution context",
879 ));
880 }
881 OperationFailure::new(
882 identity,
883 self.phase,
884 self.code,
885 self.message,
886 self.retryable,
887 )
888 }
889}
890
891impl OperationFailure {
892 pub fn new(
893 identity: ExecutionIdentityEnvelope,
894 phase: ProfilePhase,
895 code: impl Into<String>,
896 message: impl Into<String>,
897 retryable: bool,
898 ) -> Result<Self, VNextError> {
899 let code = code.into();
900 let message = message.into();
901 let parts = identity.parts();
902 if parts.frame_id.is_none()
903 || parts.node_invocation_id.is_none()
904 || parts.node_id.is_none()
905 || parts.operation_id.is_none()
906 || parts.provider_id.is_none()
907 || parts.device_id.is_none()
908 || parts.plan_id.is_none()
909 || parts.plan_hash.is_none()
910 || parts.transaction_id.is_none()
911 || parts.resource_pool_id.is_none()
912 || parts.resource_pool_identity_fingerprint.is_none()
913 || parts.provisioning_run_id.is_none()
914 || parts.provisioning_request_id.is_none()
915 || parts.active_sequence_slot.is_none()
916 || parts.admission_generation.is_none()
917 || parts.activation_epoch.is_none()
918 || parts.runtime_implementation_fingerprint.is_none()
919 || parts.active_sequence_fingerprint.is_none()
920 || parts.completed_sequence_fingerprint.is_some()
921 || parts.aborted_sequence_fingerprint.is_some()
922 || parts.resource_id.is_some()
923 || parts.resource_generation.is_some()
924 || parts.resource_batch_fingerprint.is_some()
925 || code.trim().is_empty()
926 || message.trim().is_empty()
927 || code.len() > 64
928 || message
929 .bytes()
930 .any(|byte| byte.is_ascii_control() && !matches!(byte, b'\n' | b'\t'))
931 || message.len() > 4096
932 || !code
933 .bytes()
934 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
935 {
936 return Err(invalid_operation(
937 "operation failure requires complete execution identity, code, and message",
938 ));
939 }
940 Ok(Self {
941 identity,
942 phase,
943 code,
944 message,
945 retryable,
946 })
947 }
948
949 pub fn identity(&self) -> &ExecutionIdentityEnvelope {
950 &self.identity
951 }
952
953 pub const fn phase(&self) -> ProfilePhase {
954 self.phase
955 }
956
957 pub fn code(&self) -> &str {
958 &self.code
959 }
960
961 pub fn message(&self) -> &str {
962 &self.message
963 }
964
965 pub const fn retryable(&self) -> bool {
966 self.retryable
967 }
968
969 pub fn decode_untrusted(bytes: &[u8]) -> Result<UnvalidatedOperationFailure, VNextError> {
970 if bytes.len() > MAX_OPERATION_FAILURE_WIRE_BYTES {
971 return Err(VNextError::Serialization {
972 context: "decode untrusted operation failure",
973 message: format!(
974 "operation failure wire size {} exceeds limit {}",
975 bytes.len(),
976 MAX_OPERATION_FAILURE_WIRE_BYTES
977 ),
978 });
979 }
980 serde_json::from_slice::<UnvalidatedOperationFailureWire>(bytes)
981 .map(UnvalidatedOperationFailure::from)
982 .map_err(|error| VNextError::Serialization {
983 context: "decode untrusted operation failure",
984 message: error.to_string(),
985 })
986 }
987}