use super::{
CatalogSourceSelection, SubnetCatalogErrorCategory, SubnetCatalogErrorCode,
SubnetCatalogHostError, SubnetCatalogLoadRequest, SubnetCatalogRetryability,
};
use crate::subnet_catalog::{
CatalogAssurance, CatalogError, RoutingRange, SubnetCatalogRegistryRecordEvidence,
SubnetCatalogRegistryRecordSubject,
};
use candid::Principal;
use std::{error::Error, fmt, path::PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SubnetCatalogLoadFailureRequest {
pub network: String,
pub source: Option<CatalogSourceSelection>,
pub minimum_assurance: CatalogAssurance,
}
impl SubnetCatalogLoadFailureRequest {
pub(super) fn from_load_request(request: &SubnetCatalogLoadRequest) -> Self {
Self {
network: request.cache.network.clone(),
source: request.policy.source().cloned(),
minimum_assurance: request.minimum_assurance,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SubnetCatalogLoadStage {
RequestValidation,
CacheOnlyLoad,
CacheLookup,
CacheAbsence,
CacheRejection,
CacheBypass,
RefreshAttempted,
RefreshFailed,
PostRefreshCacheLoadFailed,
RuntimeAdapter,
}
impl SubnetCatalogLoadStage {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::RequestValidation => "request_validation",
Self::CacheOnlyLoad => "cache_only_load",
Self::CacheLookup => "cache_lookup",
Self::CacheAbsence => "cache_absence",
Self::CacheRejection => "cache_rejection",
Self::CacheBypass => "cache_bypass",
Self::RefreshAttempted => "refresh_attempted",
Self::RefreshFailed => "refresh_failed",
Self::PostRefreshCacheLoadFailed => "post_refresh_cache_load_failed",
Self::RuntimeAdapter => "runtime_adapter",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SubnetCatalogRefreshTrigger {
Missing,
Rejected,
Stale,
Forced,
}
impl SubnetCatalogRefreshTrigger {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Missing => "missing",
Self::Rejected => "rejected",
Self::Stale => "stale",
Self::Forced => "forced",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SubnetCatalogFailureCacheDisposition {
NotExamined,
CacheOnly,
CacheBypassed,
CacheMissing,
CacheRejected,
CacheReadFailed,
RefreshAttempted(SubnetCatalogRefreshTrigger),
RefreshFailed(SubnetCatalogRefreshTrigger),
PostRefreshLoadFailed(SubnetCatalogRefreshTrigger),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SubnetCatalogField {
SubnetListEntry,
RoutingTableRange,
RoutingTableSubnetId,
RoutingRangeStart,
RoutingRangeEnd,
Network,
RegistryCanister,
RegistryVersion,
SourceEndpoint,
SubnetPrincipal,
CollectionTimestamp,
Classification,
AgreementDigest,
CatalogDigest,
Provenance,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum SubnetCatalogSubject {
Network(String),
Endpoint(String),
CachePath(PathBuf),
RegistryLatestVersion,
RegistryRecord(SubnetCatalogRegistryRecordSubject),
Subnet {
subnet: Principal,
field: Option<SubnetCatalogField>,
},
RegistryRoutingTableEntry {
index: usize,
field: Option<SubnetCatalogField>,
},
RoutingRange {
range: RoutingRange,
field: Option<SubnetCatalogField>,
},
Field(SubnetCatalogField),
}
#[derive(Debug)]
pub struct SubnetCatalogSourceFailure {
pub registry_version: Option<u64>,
pub returned_registry_value_version: Option<u64>,
pub source_endpoint: Option<String>,
pub assurance: Option<CatalogAssurance>,
pub registry_records: Vec<SubnetCatalogRegistryRecordEvidence>,
pub subject: Option<SubnetCatalogSubject>,
pub source: SubnetCatalogHostError,
}
impl SubnetCatalogSourceFailure {
#[must_use]
pub fn new(
registry_version: Option<u64>,
subject: Option<SubnetCatalogSubject>,
source: SubnetCatalogHostError,
) -> Box<Self> {
Box::new(Self {
registry_version,
returned_registry_value_version: None,
source_endpoint: None,
assurance: None,
registry_records: Vec::new(),
subject,
source,
})
}
#[must_use]
pub fn with_registry_evidence(
mut self: Box<Self>,
returned_registry_value_version: Option<u64>,
source_endpoint: Option<String>,
assurance: Option<CatalogAssurance>,
registry_records: Vec<SubnetCatalogRegistryRecordEvidence>,
) -> Box<Self> {
self.returned_registry_value_version = returned_registry_value_version;
self.source_endpoint = source_endpoint;
self.assurance = assurance;
self.registry_records = registry_records;
self
}
#[must_use]
pub fn from_source(source: SubnetCatalogHostError) -> Box<Self> {
Self::new(None, None, source)
}
#[must_use]
pub fn into_source(self: Box<Self>) -> SubnetCatalogHostError {
self.source
}
}
#[derive(Debug)]
pub struct SubnetCatalogLoadFailure {
pub request: SubnetCatalogLoadFailureRequest,
pub stage: SubnetCatalogLoadStage,
pub registry_version: Option<u64>,
pub returned_registry_value_version: Option<u64>,
pub source_endpoint: Option<String>,
pub assurance: Option<CatalogAssurance>,
pub registry_records: Vec<SubnetCatalogRegistryRecordEvidence>,
pub cache_disposition: SubnetCatalogFailureCacheDisposition,
pub subject: Option<SubnetCatalogSubject>,
pub code: SubnetCatalogErrorCode,
pub category: SubnetCatalogErrorCategory,
pub retryability: SubnetCatalogRetryability,
pub source: SubnetCatalogHostError,
}
impl SubnetCatalogLoadFailure {
pub(super) fn from_source_failure(
request: &SubnetCatalogLoadRequest,
stage: SubnetCatalogLoadStage,
cache_disposition: SubnetCatalogFailureCacheDisposition,
failure: Box<SubnetCatalogSourceFailure>,
) -> Box<Self> {
let code = failure.source.code();
let category = failure.source.category();
let retryability = failure.source.retryability();
Box::new(Self {
request: SubnetCatalogLoadFailureRequest::from_load_request(request),
stage,
registry_version: failure.registry_version,
returned_registry_value_version: failure.returned_registry_value_version,
source_endpoint: failure.source_endpoint,
assurance: failure.assurance,
registry_records: failure.registry_records,
cache_disposition,
subject: failure.subject,
code,
category,
retryability,
source: failure.source,
})
}
#[must_use]
pub fn into_source(self: Box<Self>) -> SubnetCatalogHostError {
self.source
}
}
impl fmt::Display for SubnetCatalogLoadFailure {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"Subnet Catalog load failed at {}: {}",
self.stage.as_str(),
self.source
)
}
}
impl Error for SubnetCatalogLoadFailure {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
pub fn subject_from_catalog_error(error: &CatalogError) -> Option<SubnetCatalogSubject> {
match error {
CatalogError::NetworkMismatch { actual, .. } => {
Some(SubnetCatalogSubject::Network(actual.clone()))
}
CatalogError::RegistryCanisterMismatch { .. } => Some(SubnetCatalogSubject::Field(
SubnetCatalogField::RegistryCanister,
)),
CatalogError::InvalidRegistryVersion => Some(SubnetCatalogSubject::Field(
SubnetCatalogField::RegistryVersion,
)),
CatalogError::InvalidSourceEndpoint { endpoint, .. } => {
Some(SubnetCatalogSubject::Endpoint(endpoint.clone()))
}
CatalogError::InvalidTimestamp { .. } | CatalogError::FutureTimestamp { .. } => Some(
SubnetCatalogSubject::Field(SubnetCatalogField::CollectionTimestamp),
),
CatalogError::InvalidPrincipal { field, .. } => subject_from_principal_field(field),
CatalogError::InvalidProvenance { .. } => {
Some(SubnetCatalogSubject::Field(SubnetCatalogField::Provenance))
}
CatalogError::InvalidAgreementDigest { .. }
| CatalogError::AgreementDigestMismatch { .. } => Some(SubnetCatalogSubject::Field(
SubnetCatalogField::AgreementDigest,
)),
CatalogError::InvalidCatalogDigest { .. } | CatalogError::CatalogDigestMismatch { .. } => {
Some(SubnetCatalogSubject::Field(
SubnetCatalogField::CatalogDigest,
))
}
CatalogError::DuplicateSubnet { subnet_principal } => {
subnet_subject(subnet_principal, Some(SubnetCatalogField::SubnetListEntry))
}
CatalogError::UnknownRoutingSubnet { subnet_principal } => subnet_subject(
subnet_principal,
Some(SubnetCatalogField::RoutingTableSubnetId),
),
CatalogError::SubnetKindMismatch {
subnet_principal, ..
}
| CatalogError::ChargingPolicyMismatch {
subnet_principal, ..
} => subnet_subject(subnet_principal, None),
CatalogError::ClassificationMismatch {
subnet_principal, ..
} => subnet_subject(subnet_principal, Some(SubnetCatalogField::Classification)),
CatalogError::InvalidRoutingRange {
start_canister_id,
end_canister_id,
subnet_principal,
} => Some(SubnetCatalogSubject::RoutingRange {
range: RoutingRange {
start_canister_id: start_canister_id.clone(),
end_canister_id: end_canister_id.clone(),
subnet_principal: subnet_principal.clone(),
},
field: None,
}),
CatalogError::OverlappingRoutingRanges { first, .. }
| CatalogError::NonCanonicalRoutingOrder {
previous: first, ..
} => Some(SubnetCatalogSubject::RoutingRange {
range: first.as_ref().clone(),
field: None,
}),
CatalogError::Json(_)
| CatalogError::UnsupportedSchemaVersion { .. }
| CatalogError::EmptySubnets
| CatalogError::EmptyRoutingRanges
| CatalogError::NonCanonicalSubnetOrder { .. }
| CatalogError::UnsupportedAssurance { .. }
| CatalogError::ClassificationPolicyVersionMismatch { .. }
| CatalogError::ClassificationPolicyDigestMismatch { .. }
| CatalogError::ResolverPolicyMismatch { .. }
| CatalogError::UnknownSubnet { .. }
| CatalogError::PrincipalPrefixNotFound { .. }
| CatalogError::AmbiguousPrincipalPrefix { .. }
| CatalogError::RouteNotFound { .. } => None,
}
}
fn subnet_subject(
subnet_principal: &str,
field: Option<SubnetCatalogField>,
) -> Option<SubnetCatalogSubject> {
Principal::from_text(subnet_principal)
.ok()
.map(|subnet| SubnetCatalogSubject::Subnet { subnet, field })
}
fn subject_from_principal_field(field: &str) -> Option<SubnetCatalogSubject> {
let field = match field {
"provenance.registry_canister_id" => SubnetCatalogField::RegistryCanister,
"subnet_principal" => SubnetCatalogField::SubnetPrincipal,
"routing_range.subnet_principal" => SubnetCatalogField::RoutingTableSubnetId,
"start_canister_id" => SubnetCatalogField::RoutingRangeStart,
"end_canister_id" => SubnetCatalogField::RoutingRangeEnd,
_ => return None,
};
Some(SubnetCatalogSubject::Field(field))
}