use super::{
SubnetCatalogHostError, SubnetCatalogRefreshRequest, SubnetCatalogSource,
error::enforce_mainnet_network, refresh_subnet_catalog_with_source, subnet_catalog_path,
};
use crate::{
cache_file::HostCacheError,
nns::LiveNnsSource,
subnet_catalog::{
CatalogValidationContext, DEFAULT_CATALOG_MAX_FUTURE_SKEW_SECONDS,
DEFAULT_REFRESH_LOCK_STALE_SECONDS, MAINNET_REGISTRY_CANISTER_ID, ValidatedSubnetCatalog,
catalog_stale_status, parse_catalog_json,
},
};
use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SubnetCatalogCacheRequest {
pub cache_root: PathBuf,
pub network: String,
}
impl SubnetCatalogCacheRequest {
#[must_use]
pub fn new(cache_root: impl Into<PathBuf>, network: impl Into<String>) -> Self {
Self {
cache_root: cache_root.into(),
network: network.into(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CatalogReadPolicy {
CacheOnly,
RefreshMissing {
source_endpoint: String,
},
RefreshMissingOrInvalid {
source_endpoint: String,
},
RefreshMissingInvalidOrOlderThan {
source_endpoint: String,
max_age_seconds: u64,
},
ForceRefresh {
source_endpoint: String,
},
}
impl CatalogReadPolicy {
fn source_endpoint(&self) -> Option<&str> {
match self {
Self::CacheOnly => None,
Self::RefreshMissing { source_endpoint }
| Self::RefreshMissingOrInvalid { source_endpoint }
| Self::RefreshMissingInvalidOrOlderThan {
source_endpoint, ..
}
| Self::ForceRefresh { source_endpoint } => Some(source_endpoint),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SubnetCatalogLoadRequest {
pub cache: SubnetCatalogCacheRequest,
pub now_unix_secs: u64,
pub max_future_skew_seconds: u64,
pub policy: CatalogReadPolicy,
}
impl SubnetCatalogLoadRequest {
#[must_use]
pub const fn cache_only(cache: SubnetCatalogCacheRequest, now_unix_secs: u64) -> Self {
Self {
cache,
now_unix_secs,
max_future_skew_seconds: DEFAULT_CATALOG_MAX_FUTURE_SKEW_SECONDS,
policy: CatalogReadPolicy::CacheOnly,
}
}
#[must_use]
pub fn refresh_missing_or_invalid(
cache: SubnetCatalogCacheRequest,
source_endpoint: impl Into<String>,
now_unix_secs: u64,
) -> Self {
Self {
cache,
now_unix_secs,
max_future_skew_seconds: DEFAULT_CATALOG_MAX_FUTURE_SKEW_SECONDS,
policy: CatalogReadPolicy::RefreshMissingOrInvalid {
source_endpoint: source_endpoint.into(),
},
}
}
#[must_use]
pub const fn with_max_future_skew_seconds(mut self, seconds: u64) -> Self {
self.max_future_skew_seconds = seconds;
self
}
#[must_use]
pub fn with_policy(mut self, policy: CatalogReadPolicy) -> Self {
self.policy = policy;
self
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CacheDisposition {
CacheHit,
RefreshedMissing,
RefreshedInvalid,
RefreshedStale,
ForcedRefresh,
}
impl CacheDisposition {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::CacheHit => "cache_hit",
Self::RefreshedMissing => "refreshed_missing",
Self::RefreshedInvalid => "refreshed_invalid",
Self::RefreshedStale => "refreshed_stale",
Self::ForcedRefresh => "forced_refresh",
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CatalogLoadOutcome {
pub path: PathBuf,
pub catalog: ValidatedSubnetCatalog,
pub disposition: CacheDisposition,
}
pub fn load_cached_subnet_catalog(
request: &SubnetCatalogLoadRequest,
) -> Result<CatalogLoadOutcome, SubnetCatalogHostError> {
if request.policy != CatalogReadPolicy::CacheOnly {
return Err(SubnetCatalogHostError::InvalidReadPolicy {
reason: "load_cached_subnet_catalog requires CatalogReadPolicy::CacheOnly".to_string(),
});
}
load_cached_with_disposition(request, CacheDisposition::CacheHit)
}
pub fn load_subnet_catalog(
request: &SubnetCatalogLoadRequest,
) -> Result<CatalogLoadOutcome, SubnetCatalogHostError> {
load_subnet_catalog_with_source(request, &LiveNnsSource)
}
pub fn load_subnet_catalog_with_source(
request: &SubnetCatalogLoadRequest,
source: &dyn SubnetCatalogSource,
) -> Result<CatalogLoadOutcome, SubnetCatalogHostError> {
enforce_mainnet_network(&request.cache.network)?;
match &request.policy {
CatalogReadPolicy::CacheOnly => {
load_cached_with_disposition(request, CacheDisposition::CacheHit)
}
CatalogReadPolicy::ForceRefresh { .. } => {
refresh_then_load(request, source, CacheDisposition::ForcedRefresh)
}
policy => match load_cached_with_disposition(request, CacheDisposition::CacheHit) {
Ok(cached) => {
let max_age_seconds = match policy {
CatalogReadPolicy::RefreshMissingInvalidOrOlderThan {
max_age_seconds, ..
} => Some(*max_age_seconds),
_ => None,
};
if max_age_seconds.is_some_and(|max_age_seconds| {
catalog_stale_status(
cached.catalog.raw(),
request.now_unix_secs,
max_age_seconds,
)
.catalog_stale
}) {
refresh_then_load(request, source, CacheDisposition::RefreshedStale)
} else {
Ok(cached)
}
}
Err(SubnetCatalogHostError::MissingCatalog { .. }) => {
refresh_then_load(request, source, CacheDisposition::RefreshedMissing)
}
Err(SubnetCatalogHostError::Catalog(_))
if matches!(
policy,
CatalogReadPolicy::RefreshMissingOrInvalid { .. }
| CatalogReadPolicy::RefreshMissingInvalidOrOlderThan { .. }
) =>
{
refresh_then_load(request, source, CacheDisposition::RefreshedInvalid)
}
Err(error) => Err(error),
},
}
}
fn load_cached_with_disposition(
request: &SubnetCatalogLoadRequest,
disposition: CacheDisposition,
) -> Result<CatalogLoadOutcome, SubnetCatalogHostError> {
enforce_mainnet_network(&request.cache.network)?;
let path = subnet_catalog_path(&request.cache.cache_root, &request.cache.network);
if !path.is_file() {
return Err(SubnetCatalogHostError::MissingCatalog { path });
}
let data = fs::read_to_string(&path)
.map_err(|source| HostCacheError::read_cache("subnet catalog", path.clone(), source))?;
let raw = parse_catalog_json(&data)?;
let validation = CatalogValidationContext::new(
&request.cache.network,
MAINNET_REGISTRY_CANISTER_ID,
request.now_unix_secs,
request.max_future_skew_seconds,
);
let catalog = ValidatedSubnetCatalog::try_from_raw(raw, &validation)?;
Ok(CatalogLoadOutcome {
path,
catalog,
disposition,
})
}
fn refresh_then_load(
request: &SubnetCatalogLoadRequest,
source: &dyn SubnetCatalogSource,
disposition: CacheDisposition,
) -> Result<CatalogLoadOutcome, SubnetCatalogHostError> {
let source_endpoint = request.policy.source_endpoint().ok_or_else(|| {
SubnetCatalogHostError::InvalidReadPolicy {
reason: "refresh policy is missing its source endpoint".to_string(),
}
})?;
let refresh_request = SubnetCatalogRefreshRequest::new(
request.cache.clone(),
source_endpoint,
request.now_unix_secs,
DEFAULT_REFRESH_LOCK_STALE_SECONDS,
)
.with_max_future_skew_seconds(request.max_future_skew_seconds);
refresh_subnet_catalog_with_source(&refresh_request, source)?;
load_cached_with_disposition(request, disposition)
}