1use ic_query::subnet_catalog::{
8 CatalogSourceSelection, SubnetCatalogFailureCacheDisposition, SubnetCatalogField,
9 SubnetCatalogLoadFailure, SubnetCatalogLoadStage, SubnetCatalogRefreshTrigger,
10 SubnetCatalogRegistryRecordEvidence, SubnetCatalogRegistryRecordKind,
11 SubnetCatalogRegistryValueEncoding, SubnetCatalogRetryability, SubnetCatalogSubject,
12 SubnetCatalogUnknownRetryReason,
13};
14use serde::Serialize;
15
16#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
18pub struct SubnetCatalogLoadFailureEvidenceV1 {
19 pub schema_version: u16,
20 pub network: String,
21 pub source_kind: Option<SubnetCatalogSourceKindV1>,
22 pub source_endpoints: Vec<String>,
23 pub source_assurance: Option<String>,
24 pub minimum_assurance: String,
25 pub stage: SubnetCatalogLoadStageV1,
26 pub registry_version: Option<u64>,
27 pub returned_registry_value_version: Option<u64>,
28 pub source_endpoint: Option<String>,
29 pub assurance: Option<String>,
30 pub registry_records: Vec<SubnetCatalogRegistryRecordEvidenceV1>,
31 pub cache_disposition: SubnetCatalogFailureCacheDispositionV1,
32 pub subject: Option<SubnetCatalogSubjectV1>,
33 pub code: String,
34 pub category: String,
35 pub retryability: SubnetCatalogRetryabilityV1,
36 pub source_message: String,
37 pub effects: SubnetCatalogFailureEffectsV1,
38}
39
40impl SubnetCatalogLoadFailureEvidenceV1 {
41 #[must_use]
43 pub fn from_preflight_failure(failure: &SubnetCatalogLoadFailure) -> Self {
44 let (source_kind, source_endpoints, source_assurance) = failure
45 .request
46 .source
47 .as_ref()
48 .map_or_else(|| (None, Vec::new(), None), source_evidence);
49 Self {
50 schema_version: 1,
51 network: failure.request.network.clone(),
52 source_kind,
53 source_endpoints,
54 source_assurance,
55 minimum_assurance: failure.request.minimum_assurance.as_str().to_string(),
56 stage: failure.stage.into(),
57 registry_version: failure.registry_version,
58 returned_registry_value_version: failure.returned_registry_value_version,
59 source_endpoint: failure.source_endpoint.clone(),
60 assurance: failure
61 .assurance
62 .map(|assurance| assurance.as_str().to_string()),
63 registry_records: failure.registry_records.iter().map(Into::into).collect(),
64 cache_disposition: failure.cache_disposition.into(),
65 subject: failure.subject.as_ref().map(Into::into),
66 code: failure.code.as_str().to_string(),
67 category: failure.category.as_str().to_string(),
68 retryability: failure.retryability.into(),
69 source_message: failure.source.to_string(),
70 effects: SubnetCatalogFailureEffectsV1::none_started(),
71 }
72 }
73}
74
75fn source_evidence(
76 source: &CatalogSourceSelection,
77) -> (
78 Option<SubnetCatalogSourceKindV1>,
79 Vec<String>,
80 Option<String>,
81) {
82 let (kind, endpoints) = match source {
83 CatalogSourceSelection::UncertifiedQuery { endpoint } => (
84 SubnetCatalogSourceKindV1::UncertifiedQuery,
85 vec![endpoint.clone()],
86 ),
87 CatalogSourceSelection::MultiEndpointAgreement { endpoints } => (
88 SubnetCatalogSourceKindV1::MultiEndpointAgreement,
89 endpoints.clone(),
90 ),
91 };
92 (
93 Some(kind),
94 endpoints,
95 Some(source.assurance().as_str().to_string()),
96 )
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
101#[serde(rename_all = "snake_case")]
102pub enum SubnetCatalogSourceKindV1 {
103 UncertifiedQuery,
104 MultiEndpointAgreement,
105}
106
107#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
109#[serde(rename_all = "snake_case")]
110pub enum SubnetCatalogLoadStageV1 {
111 RequestValidation,
112 CacheOnlyLoad,
113 CacheLookup,
114 CacheAbsence,
115 CacheRejection,
116 CacheBypass,
117 RefreshAttempted,
118 RefreshFailed,
119 PostRefreshCacheLoadFailed,
120 RuntimeAdapter,
121}
122
123impl From<SubnetCatalogLoadStage> for SubnetCatalogLoadStageV1 {
124 fn from(value: SubnetCatalogLoadStage) -> Self {
125 match value {
126 SubnetCatalogLoadStage::RequestValidation => Self::RequestValidation,
127 SubnetCatalogLoadStage::CacheOnlyLoad => Self::CacheOnlyLoad,
128 SubnetCatalogLoadStage::CacheLookup => Self::CacheLookup,
129 SubnetCatalogLoadStage::CacheAbsence => Self::CacheAbsence,
130 SubnetCatalogLoadStage::CacheRejection => Self::CacheRejection,
131 SubnetCatalogLoadStage::CacheBypass => Self::CacheBypass,
132 SubnetCatalogLoadStage::RefreshAttempted => Self::RefreshAttempted,
133 SubnetCatalogLoadStage::RefreshFailed => Self::RefreshFailed,
134 SubnetCatalogLoadStage::PostRefreshCacheLoadFailed => Self::PostRefreshCacheLoadFailed,
135 SubnetCatalogLoadStage::RuntimeAdapter => Self::RuntimeAdapter,
136 }
137 }
138}
139
140#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
142#[serde(tag = "kind", rename_all = "snake_case")]
143pub enum SubnetCatalogFailureCacheDispositionV1 {
144 NotExamined,
145 CacheOnly,
146 CacheBypassed,
147 CacheMissing,
148 CacheRejected,
149 CacheReadFailed,
150 RefreshAttempted {
151 trigger: SubnetCatalogRefreshTriggerV1,
152 },
153 RefreshFailed {
154 trigger: SubnetCatalogRefreshTriggerV1,
155 },
156 PostRefreshLoadFailed {
157 trigger: SubnetCatalogRefreshTriggerV1,
158 },
159}
160
161impl From<SubnetCatalogFailureCacheDisposition> for SubnetCatalogFailureCacheDispositionV1 {
162 fn from(value: SubnetCatalogFailureCacheDisposition) -> Self {
163 match value {
164 SubnetCatalogFailureCacheDisposition::NotExamined => Self::NotExamined,
165 SubnetCatalogFailureCacheDisposition::CacheOnly => Self::CacheOnly,
166 SubnetCatalogFailureCacheDisposition::CacheBypassed => Self::CacheBypassed,
167 SubnetCatalogFailureCacheDisposition::CacheMissing => Self::CacheMissing,
168 SubnetCatalogFailureCacheDisposition::CacheRejected => Self::CacheRejected,
169 SubnetCatalogFailureCacheDisposition::CacheReadFailed => Self::CacheReadFailed,
170 SubnetCatalogFailureCacheDisposition::RefreshAttempted(trigger) => {
171 Self::RefreshAttempted {
172 trigger: trigger.into(),
173 }
174 }
175 SubnetCatalogFailureCacheDisposition::RefreshFailed(trigger) => Self::RefreshFailed {
176 trigger: trigger.into(),
177 },
178 SubnetCatalogFailureCacheDisposition::PostRefreshLoadFailed(trigger) => {
179 Self::PostRefreshLoadFailed {
180 trigger: trigger.into(),
181 }
182 }
183 }
184 }
185}
186
187#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
189#[serde(rename_all = "snake_case")]
190pub enum SubnetCatalogRefreshTriggerV1 {
191 Missing,
192 Rejected,
193 Stale,
194 Forced,
195}
196
197impl From<SubnetCatalogRefreshTrigger> for SubnetCatalogRefreshTriggerV1 {
198 fn from(value: SubnetCatalogRefreshTrigger) -> Self {
199 match value {
200 SubnetCatalogRefreshTrigger::Missing => Self::Missing,
201 SubnetCatalogRefreshTrigger::Rejected => Self::Rejected,
202 SubnetCatalogRefreshTrigger::Stale => Self::Stale,
203 SubnetCatalogRefreshTrigger::Forced => Self::Forced,
204 }
205 }
206}
207
208#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
210#[serde(tag = "kind", rename_all = "snake_case")]
211pub enum SubnetCatalogSubjectV1 {
212 Network {
213 network: String,
214 },
215 Endpoint {
216 endpoint: String,
217 },
218 CachePath {
219 path: String,
220 },
221 RegistryLatestVersion,
222 RegistryRecord {
223 record_kind: SubnetCatalogRegistryRecordKindV1,
224 key: String,
225 subnet: Option<String>,
226 canister_range_start: Option<String>,
227 },
228 Subnet {
229 subnet: String,
230 field: Option<SubnetCatalogFieldV1>,
231 },
232 RegistryRoutingTableEntry {
233 index: usize,
234 field: Option<SubnetCatalogFieldV1>,
235 },
236 RoutingRange {
237 start_canister_id: String,
238 end_canister_id: String,
239 subnet_principal: String,
240 field: Option<SubnetCatalogFieldV1>,
241 },
242 Field {
243 field: SubnetCatalogFieldV1,
244 },
245}
246
247impl From<&SubnetCatalogSubject> for SubnetCatalogSubjectV1 {
248 fn from(value: &SubnetCatalogSubject) -> Self {
249 match value {
250 SubnetCatalogSubject::Network(network) => Self::Network {
251 network: network.clone(),
252 },
253 SubnetCatalogSubject::Endpoint(endpoint) => Self::Endpoint {
254 endpoint: endpoint.clone(),
255 },
256 SubnetCatalogSubject::CachePath(path) => Self::CachePath {
257 path: path.display().to_string(),
258 },
259 SubnetCatalogSubject::RegistryLatestVersion => Self::RegistryLatestVersion,
260 SubnetCatalogSubject::RegistryRecord(record) => Self::RegistryRecord {
261 record_kind: record.kind.into(),
262 key: record.key.clone(),
263 subnet: record.subnet.map(|subnet| subnet.to_text()),
264 canister_range_start: record
265 .canister_range_start
266 .map(|canister| canister.to_text()),
267 },
268 SubnetCatalogSubject::Subnet { subnet, field } => Self::Subnet {
269 subnet: subnet.to_text(),
270 field: field.map(Into::into),
271 },
272 SubnetCatalogSubject::RegistryRoutingTableEntry { index, field } => {
273 Self::RegistryRoutingTableEntry {
274 index: *index,
275 field: field.map(Into::into),
276 }
277 }
278 SubnetCatalogSubject::RoutingRange { range, field } => Self::RoutingRange {
279 start_canister_id: range.start_canister_id.clone(),
280 end_canister_id: range.end_canister_id.clone(),
281 subnet_principal: range.subnet_principal.clone(),
282 field: field.map(Into::into),
283 },
284 SubnetCatalogSubject::Field(field) => Self::Field {
285 field: (*field).into(),
286 },
287 }
288 }
289}
290
291#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
293#[serde(rename_all = "snake_case")]
294pub enum SubnetCatalogRegistryRecordKindV1 {
295 SubnetList,
296 RoutingTable,
297 SubnetRecord,
298}
299
300impl From<SubnetCatalogRegistryRecordKind> for SubnetCatalogRegistryRecordKindV1 {
301 fn from(value: SubnetCatalogRegistryRecordKind) -> Self {
302 match value {
303 SubnetCatalogRegistryRecordKind::SubnetList => Self::SubnetList,
304 SubnetCatalogRegistryRecordKind::RoutingTable => Self::RoutingTable,
305 SubnetCatalogRegistryRecordKind::SubnetRecord => Self::SubnetRecord,
306 }
307 }
308}
309
310#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
312pub struct SubnetCatalogRegistryRecordEvidenceV1 {
313 pub record_kind: SubnetCatalogRegistryRecordKindV1,
314 pub key: String,
315 pub subnet: Option<String>,
316 pub canister_range_start: Option<String>,
317 pub requested_registry_version: u64,
318 pub returned_registry_version: u64,
319 pub timestamp_nanoseconds: u64,
320 pub source_endpoint: String,
321 pub assurance: String,
322 pub value_encoding: SubnetCatalogRegistryValueEncodingV1,
323}
324
325impl From<&SubnetCatalogRegistryRecordEvidence> for SubnetCatalogRegistryRecordEvidenceV1 {
326 fn from(value: &SubnetCatalogRegistryRecordEvidence) -> Self {
327 Self {
328 record_kind: value.record.kind.into(),
329 key: value.record.key.clone(),
330 subnet: value.record.subnet.map(|subnet| subnet.to_text()),
331 canister_range_start: value
332 .record
333 .canister_range_start
334 .map(|canister| canister.to_text()),
335 requested_registry_version: value.requested_registry_version,
336 returned_registry_version: value.returned_registry_version,
337 timestamp_nanoseconds: value.timestamp_nanoseconds,
338 source_endpoint: value.source_endpoint.clone(),
339 assurance: value.assurance.as_str().to_string(),
340 value_encoding: value.value_encoding.into(),
341 }
342 }
343}
344
345#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
347#[serde(rename_all = "snake_case")]
348pub enum SubnetCatalogRegistryValueEncodingV1 {
349 Inline,
350 Chunked,
351}
352
353impl From<SubnetCatalogRegistryValueEncoding> for SubnetCatalogRegistryValueEncodingV1 {
354 fn from(value: SubnetCatalogRegistryValueEncoding) -> Self {
355 match value {
356 SubnetCatalogRegistryValueEncoding::Inline => Self::Inline,
357 SubnetCatalogRegistryValueEncoding::Chunked => Self::Chunked,
358 }
359 }
360}
361
362#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
364#[serde(rename_all = "snake_case")]
365pub enum SubnetCatalogFieldV1 {
366 SubnetListEntry,
367 RoutingTableRange,
368 RoutingTableSubnetId,
369 RoutingRangeStart,
370 RoutingRangeEnd,
371 Network,
372 RegistryCanister,
373 RegistryVersion,
374 SourceEndpoint,
375 SubnetPrincipal,
376 CollectionTimestamp,
377 Classification,
378 AgreementDigest,
379 CatalogDigest,
380 Provenance,
381}
382
383impl From<SubnetCatalogField> for SubnetCatalogFieldV1 {
384 fn from(value: SubnetCatalogField) -> Self {
385 match value {
386 SubnetCatalogField::SubnetListEntry => Self::SubnetListEntry,
387 SubnetCatalogField::RoutingTableRange => Self::RoutingTableRange,
388 SubnetCatalogField::RoutingTableSubnetId => Self::RoutingTableSubnetId,
389 SubnetCatalogField::RoutingRangeStart => Self::RoutingRangeStart,
390 SubnetCatalogField::RoutingRangeEnd => Self::RoutingRangeEnd,
391 SubnetCatalogField::Network => Self::Network,
392 SubnetCatalogField::RegistryCanister => Self::RegistryCanister,
393 SubnetCatalogField::RegistryVersion => Self::RegistryVersion,
394 SubnetCatalogField::SourceEndpoint => Self::SourceEndpoint,
395 SubnetCatalogField::SubnetPrincipal => Self::SubnetPrincipal,
396 SubnetCatalogField::CollectionTimestamp => Self::CollectionTimestamp,
397 SubnetCatalogField::Classification => Self::Classification,
398 SubnetCatalogField::AgreementDigest => Self::AgreementDigest,
399 SubnetCatalogField::CatalogDigest => Self::CatalogDigest,
400 SubnetCatalogField::Provenance => Self::Provenance,
401 }
402 }
403}
404
405#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
407#[serde(tag = "kind", rename_all = "snake_case")]
408pub enum SubnetCatalogRetryabilityV1 {
409 Retryable,
410 NotRetryable,
411 Unknown {
412 reason: SubnetCatalogUnknownRetryReasonV1,
413 },
414}
415
416impl From<SubnetCatalogRetryability> for SubnetCatalogRetryabilityV1 {
417 fn from(value: SubnetCatalogRetryability) -> Self {
418 match value {
419 SubnetCatalogRetryability::Retryable => Self::Retryable,
420 SubnetCatalogRetryability::NotRetryable => Self::NotRetryable,
421 SubnetCatalogRetryability::Unknown(reason) => Self::Unknown {
422 reason: reason.into(),
423 },
424 }
425 }
426}
427
428#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
430#[serde(rename_all = "snake_case")]
431pub enum SubnetCatalogUnknownRetryReasonV1 {
432 CacheOperation,
433 RegistryResponse,
434 RegistryTransport,
435 RuntimeAdapter,
436}
437
438impl From<SubnetCatalogUnknownRetryReason> for SubnetCatalogUnknownRetryReasonV1 {
439 fn from(value: SubnetCatalogUnknownRetryReason) -> Self {
440 match value {
441 SubnetCatalogUnknownRetryReason::CacheOperation => Self::CacheOperation,
442 SubnetCatalogUnknownRetryReason::RegistryResponse => Self::RegistryResponse,
443 SubnetCatalogUnknownRetryReason::RegistryTransport => Self::RegistryTransport,
444 SubnetCatalogUnknownRetryReason::RuntimeAdapter => Self::RuntimeAdapter,
445 }
446 }
447}
448
449#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
451pub struct SubnetCatalogFailureEffectsV1 {
452 pub build_started: bool,
453 pub workspace_mutation_started: bool,
454 pub ic_mutation_started: bool,
455}
456
457impl SubnetCatalogFailureEffectsV1 {
458 const fn none_started() -> Self {
459 Self {
460 build_started: false,
461 workspace_mutation_started: false,
462 ic_mutation_started: false,
463 }
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use candid::Principal;
471 use ic_query::subnet_catalog::{
472 CatalogAssurance, SubnetCatalogErrorCategory, SubnetCatalogErrorCode,
473 SubnetCatalogHostError, SubnetCatalogLoadFailureRequest,
474 SubnetCatalogRegistryRecordSubject, SubnetCatalogRegistryValueEncoding,
475 };
476
477 #[test]
478 fn projection_preserves_every_typed_failure_field_without_reclassification() {
479 let subnet = Principal::from_slice(&[9; 29]);
480 let canister_range_start = Principal::from_slice(&[7; 29]);
481 let failure = detailed_failure_fixture();
482
483 let evidence = SubnetCatalogLoadFailureEvidenceV1::from_preflight_failure(&failure);
484
485 assert_eq!(evidence.network, "ic");
486 assert_eq!(
487 evidence.source_kind,
488 Some(SubnetCatalogSourceKindV1::UncertifiedQuery)
489 );
490 assert_eq!(evidence.source_endpoints, ["https://ic0.app"]);
491 assert_eq!(
492 evidence.source_assurance.as_deref(),
493 Some("uncertified_query")
494 );
495 assert_eq!(evidence.minimum_assurance, "uncertified_query");
496 assert_eq!(evidence.stage, SubnetCatalogLoadStageV1::RefreshFailed);
497 assert_eq!(evidence.registry_version, Some(881_337));
498 assert_eq!(evidence.returned_registry_value_version, Some(881_336));
499 assert_eq!(evidence.source_endpoint.as_deref(), Some("https://ic0.app"));
500 assert_eq!(evidence.assurance.as_deref(), Some("uncertified_query"));
501 assert_eq!(
502 evidence.registry_records,
503 [SubnetCatalogRegistryRecordEvidenceV1 {
504 record_kind: SubnetCatalogRegistryRecordKindV1::RoutingTable,
505 key: "canister_ranges_test".to_string(),
506 subnet: None,
507 canister_range_start: Some(canister_range_start.to_text()),
508 requested_registry_version: 881_337,
509 returned_registry_version: 881_330,
510 timestamp_nanoseconds: 42,
511 source_endpoint: "https://ic0.app".to_string(),
512 assurance: "uncertified_query".to_string(),
513 value_encoding: SubnetCatalogRegistryValueEncodingV1::Chunked,
514 }]
515 );
516 assert_eq!(
517 evidence.cache_disposition,
518 SubnetCatalogFailureCacheDispositionV1::RefreshFailed {
519 trigger: SubnetCatalogRefreshTriggerV1::Missing,
520 }
521 );
522 assert_eq!(
523 evidence.subject,
524 Some(SubnetCatalogSubjectV1::RegistryRecord {
525 record_kind: SubnetCatalogRegistryRecordKindV1::SubnetRecord,
526 key: "subnet_record_test".to_string(),
527 subnet: Some(subnet.to_text()),
528 canister_range_start: None,
529 })
530 );
531 assert_eq!(evidence.code, "invalid_read_policy");
532 assert_eq!(evidence.category, "input");
533 assert_eq!(
534 evidence.retryability,
535 SubnetCatalogRetryabilityV1::Unknown {
536 reason: SubnetCatalogUnknownRetryReasonV1::RegistryResponse,
537 }
538 );
539 assert!(!evidence.effects.build_started);
540 assert!(!evidence.effects.workspace_mutation_started);
541 assert!(!evidence.effects.ic_mutation_started);
542
543 let json = serde_json::to_value(evidence).expect("serialize typed projection");
544 assert_eq!(json["retryability"]["kind"], "unknown");
545 assert_eq!(json["retryability"]["reason"], "registry_response");
546 assert_eq!(json["registry_records"][0]["value_encoding"], "chunked");
547 }
548
549 fn detailed_failure_fixture() -> SubnetCatalogLoadFailure {
550 let subnet = Principal::from_slice(&[9; 29]);
551 let canister_range_start = Principal::from_slice(&[7; 29]);
552 SubnetCatalogLoadFailure {
553 request: SubnetCatalogLoadFailureRequest {
554 network: "ic".to_string(),
555 source: Some(CatalogSourceSelection::uncertified_query("https://ic0.app")),
556 minimum_assurance: CatalogAssurance::UncertifiedQuery,
557 },
558 stage: SubnetCatalogLoadStage::RefreshFailed,
559 registry_version: Some(881_337),
560 returned_registry_value_version: Some(881_336),
561 source_endpoint: Some("https://ic0.app".to_string()),
562 assurance: Some(CatalogAssurance::UncertifiedQuery),
563 registry_records: vec![SubnetCatalogRegistryRecordEvidence {
564 record: SubnetCatalogRegistryRecordSubject {
565 kind: SubnetCatalogRegistryRecordKind::RoutingTable,
566 key: "canister_ranges_test".to_string(),
567 subnet: None,
568 canister_range_start: Some(canister_range_start),
569 },
570 requested_registry_version: 881_337,
571 returned_registry_version: 881_330,
572 timestamp_nanoseconds: 42,
573 source_endpoint: "https://ic0.app".to_string(),
574 assurance: CatalogAssurance::UncertifiedQuery,
575 value_encoding: SubnetCatalogRegistryValueEncoding::Chunked,
576 }],
577 cache_disposition: SubnetCatalogFailureCacheDisposition::RefreshFailed(
578 SubnetCatalogRefreshTrigger::Missing,
579 ),
580 subject: Some(SubnetCatalogSubject::RegistryRecord(
581 SubnetCatalogRegistryRecordSubject {
582 kind: SubnetCatalogRegistryRecordKind::SubnetRecord,
583 key: "subnet_record_test".to_string(),
584 subnet: Some(subnet),
585 canister_range_start: None,
586 },
587 )),
588 code: SubnetCatalogErrorCode::InvalidReadPolicy,
589 category: SubnetCatalogErrorCategory::Input,
590 retryability: SubnetCatalogRetryability::Unknown(
591 SubnetCatalogUnknownRetryReason::RegistryResponse,
592 ),
593 source: SubnetCatalogHostError::InvalidReadPolicy {
594 reason: "typed fixture".to_string(),
595 },
596 }
597 }
598}