use ic_query::subnet_catalog::{
CatalogSourceSelection, SubnetCatalogFailureCacheDisposition, SubnetCatalogField,
SubnetCatalogLoadFailure, SubnetCatalogLoadStage, SubnetCatalogRefreshTrigger,
SubnetCatalogRegistryRecordKind, SubnetCatalogRetryability, SubnetCatalogSubject,
SubnetCatalogUnknownRetryReason,
};
use serde::Serialize;
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct SubnetCatalogLoadFailureEvidenceV1 {
pub schema_version: u16,
pub network: String,
pub source_kind: Option<SubnetCatalogSourceKindV1>,
pub source_endpoints: Vec<String>,
pub source_assurance: Option<String>,
pub minimum_assurance: String,
pub stage: SubnetCatalogLoadStageV1,
pub registry_version: Option<u64>,
pub cache_disposition: SubnetCatalogFailureCacheDispositionV1,
pub subject: Option<SubnetCatalogSubjectV1>,
pub code: String,
pub category: String,
pub retryability: SubnetCatalogRetryabilityV1,
pub source_message: String,
pub effects: SubnetCatalogFailureEffectsV1,
}
impl SubnetCatalogLoadFailureEvidenceV1 {
#[must_use]
pub fn from_preflight_failure(failure: &SubnetCatalogLoadFailure) -> Self {
let (source_kind, source_endpoints, source_assurance) = failure
.request
.source
.as_ref()
.map_or_else(|| (None, Vec::new(), None), source_evidence);
Self {
schema_version: 1,
network: failure.request.network.clone(),
source_kind,
source_endpoints,
source_assurance,
minimum_assurance: failure.request.minimum_assurance.as_str().to_string(),
stage: failure.stage.into(),
registry_version: failure.registry_version,
cache_disposition: failure.cache_disposition.into(),
subject: failure.subject.as_ref().map(Into::into),
code: failure.code.as_str().to_string(),
category: failure.category.as_str().to_string(),
retryability: failure.retryability.into(),
source_message: failure.source.to_string(),
effects: SubnetCatalogFailureEffectsV1::none_started(),
}
}
}
fn source_evidence(
source: &CatalogSourceSelection,
) -> (
Option<SubnetCatalogSourceKindV1>,
Vec<String>,
Option<String>,
) {
let (kind, endpoints) = match source {
CatalogSourceSelection::UncertifiedQuery { endpoint } => (
SubnetCatalogSourceKindV1::UncertifiedQuery,
vec![endpoint.clone()],
),
CatalogSourceSelection::MultiEndpointAgreement { endpoints } => (
SubnetCatalogSourceKindV1::MultiEndpointAgreement,
endpoints.clone(),
),
};
(
Some(kind),
endpoints,
Some(source.assurance().as_str().to_string()),
)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SubnetCatalogSourceKindV1 {
UncertifiedQuery,
MultiEndpointAgreement,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SubnetCatalogLoadStageV1 {
RequestValidation,
CacheOnlyLoad,
CacheLookup,
CacheAbsence,
CacheRejection,
CacheBypass,
RefreshAttempted,
RefreshFailed,
PostRefreshCacheLoadFailed,
RuntimeAdapter,
}
impl From<SubnetCatalogLoadStage> for SubnetCatalogLoadStageV1 {
fn from(value: SubnetCatalogLoadStage) -> Self {
match value {
SubnetCatalogLoadStage::RequestValidation => Self::RequestValidation,
SubnetCatalogLoadStage::CacheOnlyLoad => Self::CacheOnlyLoad,
SubnetCatalogLoadStage::CacheLookup => Self::CacheLookup,
SubnetCatalogLoadStage::CacheAbsence => Self::CacheAbsence,
SubnetCatalogLoadStage::CacheRejection => Self::CacheRejection,
SubnetCatalogLoadStage::CacheBypass => Self::CacheBypass,
SubnetCatalogLoadStage::RefreshAttempted => Self::RefreshAttempted,
SubnetCatalogLoadStage::RefreshFailed => Self::RefreshFailed,
SubnetCatalogLoadStage::PostRefreshCacheLoadFailed => Self::PostRefreshCacheLoadFailed,
SubnetCatalogLoadStage::RuntimeAdapter => Self::RuntimeAdapter,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SubnetCatalogFailureCacheDispositionV1 {
NotExamined,
CacheOnly,
CacheBypassed,
CacheMissing,
CacheRejected,
CacheReadFailed,
RefreshAttempted {
trigger: SubnetCatalogRefreshTriggerV1,
},
RefreshFailed {
trigger: SubnetCatalogRefreshTriggerV1,
},
PostRefreshLoadFailed {
trigger: SubnetCatalogRefreshTriggerV1,
},
}
impl From<SubnetCatalogFailureCacheDisposition> for SubnetCatalogFailureCacheDispositionV1 {
fn from(value: SubnetCatalogFailureCacheDisposition) -> Self {
match value {
SubnetCatalogFailureCacheDisposition::NotExamined => Self::NotExamined,
SubnetCatalogFailureCacheDisposition::CacheOnly => Self::CacheOnly,
SubnetCatalogFailureCacheDisposition::CacheBypassed => Self::CacheBypassed,
SubnetCatalogFailureCacheDisposition::CacheMissing => Self::CacheMissing,
SubnetCatalogFailureCacheDisposition::CacheRejected => Self::CacheRejected,
SubnetCatalogFailureCacheDisposition::CacheReadFailed => Self::CacheReadFailed,
SubnetCatalogFailureCacheDisposition::RefreshAttempted(trigger) => {
Self::RefreshAttempted {
trigger: trigger.into(),
}
}
SubnetCatalogFailureCacheDisposition::RefreshFailed(trigger) => Self::RefreshFailed {
trigger: trigger.into(),
},
SubnetCatalogFailureCacheDisposition::PostRefreshLoadFailed(trigger) => {
Self::PostRefreshLoadFailed {
trigger: trigger.into(),
}
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SubnetCatalogRefreshTriggerV1 {
Missing,
Rejected,
Stale,
Forced,
}
impl From<SubnetCatalogRefreshTrigger> for SubnetCatalogRefreshTriggerV1 {
fn from(value: SubnetCatalogRefreshTrigger) -> Self {
match value {
SubnetCatalogRefreshTrigger::Missing => Self::Missing,
SubnetCatalogRefreshTrigger::Rejected => Self::Rejected,
SubnetCatalogRefreshTrigger::Stale => Self::Stale,
SubnetCatalogRefreshTrigger::Forced => Self::Forced,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SubnetCatalogSubjectV1 {
Network {
network: String,
},
Endpoint {
endpoint: String,
},
CachePath {
path: String,
},
RegistryLatestVersion,
RegistryRecord {
record_kind: SubnetCatalogRegistryRecordKindV1,
key: String,
subnet: Option<String>,
},
Subnet {
subnet: String,
field: Option<SubnetCatalogFieldV1>,
},
RegistryRoutingTableEntry {
index: usize,
field: Option<SubnetCatalogFieldV1>,
},
RoutingRange {
start_canister_id: String,
end_canister_id: String,
subnet_principal: String,
field: Option<SubnetCatalogFieldV1>,
},
Field {
field: SubnetCatalogFieldV1,
},
}
impl From<&SubnetCatalogSubject> for SubnetCatalogSubjectV1 {
fn from(value: &SubnetCatalogSubject) -> Self {
match value {
SubnetCatalogSubject::Network(network) => Self::Network {
network: network.clone(),
},
SubnetCatalogSubject::Endpoint(endpoint) => Self::Endpoint {
endpoint: endpoint.clone(),
},
SubnetCatalogSubject::CachePath(path) => Self::CachePath {
path: path.display().to_string(),
},
SubnetCatalogSubject::RegistryLatestVersion => Self::RegistryLatestVersion,
SubnetCatalogSubject::RegistryRecord(record) => Self::RegistryRecord {
record_kind: record.kind.into(),
key: record.key.clone(),
subnet: record.subnet.map(|subnet| subnet.to_text()),
},
SubnetCatalogSubject::Subnet { subnet, field } => Self::Subnet {
subnet: subnet.to_text(),
field: field.map(Into::into),
},
SubnetCatalogSubject::RegistryRoutingTableEntry { index, field } => {
Self::RegistryRoutingTableEntry {
index: *index,
field: field.map(Into::into),
}
}
SubnetCatalogSubject::RoutingRange { range, field } => Self::RoutingRange {
start_canister_id: range.start_canister_id.clone(),
end_canister_id: range.end_canister_id.clone(),
subnet_principal: range.subnet_principal.clone(),
field: field.map(Into::into),
},
SubnetCatalogSubject::Field(field) => Self::Field {
field: (*field).into(),
},
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SubnetCatalogRegistryRecordKindV1 {
SubnetList,
RoutingTable,
SubnetRecord,
}
impl From<SubnetCatalogRegistryRecordKind> for SubnetCatalogRegistryRecordKindV1 {
fn from(value: SubnetCatalogRegistryRecordKind) -> Self {
match value {
SubnetCatalogRegistryRecordKind::SubnetList => Self::SubnetList,
SubnetCatalogRegistryRecordKind::RoutingTable => Self::RoutingTable,
SubnetCatalogRegistryRecordKind::SubnetRecord => Self::SubnetRecord,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SubnetCatalogFieldV1 {
SubnetListEntry,
RoutingTableRange,
RoutingTableSubnetId,
RoutingRangeStart,
RoutingRangeEnd,
Network,
RegistryCanister,
RegistryVersion,
SourceEndpoint,
SubnetPrincipal,
CollectionTimestamp,
Classification,
AgreementDigest,
CatalogDigest,
Provenance,
}
impl From<SubnetCatalogField> for SubnetCatalogFieldV1 {
fn from(value: SubnetCatalogField) -> Self {
match value {
SubnetCatalogField::SubnetListEntry => Self::SubnetListEntry,
SubnetCatalogField::RoutingTableRange => Self::RoutingTableRange,
SubnetCatalogField::RoutingTableSubnetId => Self::RoutingTableSubnetId,
SubnetCatalogField::RoutingRangeStart => Self::RoutingRangeStart,
SubnetCatalogField::RoutingRangeEnd => Self::RoutingRangeEnd,
SubnetCatalogField::Network => Self::Network,
SubnetCatalogField::RegistryCanister => Self::RegistryCanister,
SubnetCatalogField::RegistryVersion => Self::RegistryVersion,
SubnetCatalogField::SourceEndpoint => Self::SourceEndpoint,
SubnetCatalogField::SubnetPrincipal => Self::SubnetPrincipal,
SubnetCatalogField::CollectionTimestamp => Self::CollectionTimestamp,
SubnetCatalogField::Classification => Self::Classification,
SubnetCatalogField::AgreementDigest => Self::AgreementDigest,
SubnetCatalogField::CatalogDigest => Self::CatalogDigest,
SubnetCatalogField::Provenance => Self::Provenance,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum SubnetCatalogRetryabilityV1 {
Retryable,
NotRetryable,
Unknown {
reason: SubnetCatalogUnknownRetryReasonV1,
},
}
impl From<SubnetCatalogRetryability> for SubnetCatalogRetryabilityV1 {
fn from(value: SubnetCatalogRetryability) -> Self {
match value {
SubnetCatalogRetryability::Retryable => Self::Retryable,
SubnetCatalogRetryability::NotRetryable => Self::NotRetryable,
SubnetCatalogRetryability::Unknown(reason) => Self::Unknown {
reason: reason.into(),
},
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SubnetCatalogUnknownRetryReasonV1 {
CacheOperation,
RegistryResponse,
RegistryTransport,
RuntimeAdapter,
}
impl From<SubnetCatalogUnknownRetryReason> for SubnetCatalogUnknownRetryReasonV1 {
fn from(value: SubnetCatalogUnknownRetryReason) -> Self {
match value {
SubnetCatalogUnknownRetryReason::CacheOperation => Self::CacheOperation,
SubnetCatalogUnknownRetryReason::RegistryResponse => Self::RegistryResponse,
SubnetCatalogUnknownRetryReason::RegistryTransport => Self::RegistryTransport,
SubnetCatalogUnknownRetryReason::RuntimeAdapter => Self::RuntimeAdapter,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
pub struct SubnetCatalogFailureEffectsV1 {
pub build_started: bool,
pub workspace_mutation_started: bool,
pub ic_mutation_started: bool,
}
impl SubnetCatalogFailureEffectsV1 {
const fn none_started() -> Self {
Self {
build_started: false,
workspace_mutation_started: false,
ic_mutation_started: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use candid::Principal;
use ic_query::subnet_catalog::{
CatalogAssurance, SubnetCatalogErrorCategory, SubnetCatalogErrorCode,
SubnetCatalogHostError, SubnetCatalogLoadFailureRequest,
SubnetCatalogRegistryRecordSubject,
};
#[test]
fn projection_preserves_every_typed_failure_field_without_reclassification() {
let subnet = Principal::from_slice(&[9; 29]);
let failure = SubnetCatalogLoadFailure {
request: SubnetCatalogLoadFailureRequest {
network: "ic".to_string(),
source: Some(CatalogSourceSelection::uncertified_query("https://ic0.app")),
minimum_assurance: CatalogAssurance::UncertifiedQuery,
},
stage: SubnetCatalogLoadStage::RefreshFailed,
registry_version: Some(881_337),
cache_disposition: SubnetCatalogFailureCacheDisposition::RefreshFailed(
SubnetCatalogRefreshTrigger::Missing,
),
subject: Some(SubnetCatalogSubject::RegistryRecord(
SubnetCatalogRegistryRecordSubject {
kind: SubnetCatalogRegistryRecordKind::SubnetRecord,
key: "subnet_record_test".to_string(),
subnet: Some(subnet),
},
)),
code: SubnetCatalogErrorCode::InvalidReadPolicy,
category: SubnetCatalogErrorCategory::Input,
retryability: SubnetCatalogRetryability::Unknown(
SubnetCatalogUnknownRetryReason::RegistryResponse,
),
source: SubnetCatalogHostError::InvalidReadPolicy {
reason: "typed fixture".to_string(),
},
};
let evidence = SubnetCatalogLoadFailureEvidenceV1::from_preflight_failure(&failure);
assert_eq!(evidence.network, "ic");
assert_eq!(
evidence.source_kind,
Some(SubnetCatalogSourceKindV1::UncertifiedQuery)
);
assert_eq!(evidence.source_endpoints, ["https://ic0.app"]);
assert_eq!(
evidence.source_assurance.as_deref(),
Some("uncertified_query")
);
assert_eq!(evidence.minimum_assurance, "uncertified_query");
assert_eq!(evidence.stage, SubnetCatalogLoadStageV1::RefreshFailed);
assert_eq!(evidence.registry_version, Some(881_337));
assert_eq!(
evidence.cache_disposition,
SubnetCatalogFailureCacheDispositionV1::RefreshFailed {
trigger: SubnetCatalogRefreshTriggerV1::Missing,
}
);
assert_eq!(
evidence.subject,
Some(SubnetCatalogSubjectV1::RegistryRecord {
record_kind: SubnetCatalogRegistryRecordKindV1::SubnetRecord,
key: "subnet_record_test".to_string(),
subnet: Some(subnet.to_text()),
})
);
assert_eq!(evidence.code, "invalid_read_policy");
assert_eq!(evidence.category, "input");
assert_eq!(
evidence.retryability,
SubnetCatalogRetryabilityV1::Unknown {
reason: SubnetCatalogUnknownRetryReasonV1::RegistryResponse,
}
);
assert!(!evidence.effects.build_started);
assert!(!evidence.effects.workspace_mutation_started);
assert!(!evidence.effects.ic_mutation_started);
let json = serde_json::to_value(evidence).expect("serialize typed projection");
assert_eq!(json["retryability"]["kind"], "unknown");
assert_eq!(json["retryability"]["reason"], "registry_response");
}
}