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