use super::{
CachedNnsSubnetTopologyReport, LiveNnsSubnetTopologySource,
NNS_SUBNET_TOPOLOGY_REPORT_SCHEMA_VERSION, NnsSubnetTopologyCacheRequest,
NnsSubnetTopologyFreshness, NnsSubnetTopologyHostError, NnsSubnetTopologyRefreshRequest,
NnsSubnetTopologyReport, NnsSubnetTopologySource, error::enforce_mainnet_network,
source::source_request,
};
use crate::{
cache_file::{
HostCacheError, LoadJsonCacheErrorMapper, LoadJsonCacheRequest, RefreshLockRequest,
create_parent_directory, load_json_cache, with_refresh_lock, write_text_atomically,
},
freshness::freshness_facts,
subnet_catalog::{MAINNET_REGISTRY_CANISTER_ID, parse_utc_timestamp_secs},
};
use std::{
io,
path::{Path, PathBuf},
};
const CACHE_DIR: &str = "subnet-topology";
const CACHE_FILE: &str = "report.json";
const CACHE_COMPONENT: &str = "Subnet topology";
#[must_use]
pub fn nns_subnet_topology_cache_path(icp_root: &Path, network: &str) -> PathBuf {
cache_dir(icp_root, network).join(CACHE_FILE)
}
#[must_use]
pub fn nns_subnet_topology_refresh_lock_path(icp_root: &Path, network: &str) -> PathBuf {
cache_dir(icp_root, network).join("refresh.lock")
}
fn cache_dir(icp_root: &Path, network: &str) -> PathBuf {
icp_root.join(".icq").join(CACHE_DIR).join(network)
}
pub fn load_cached_nns_subnet_topology(
request: &NnsSubnetTopologyCacheRequest,
) -> Result<CachedNnsSubnetTopologyReport, NnsSubnetTopologyHostError> {
enforce_mainnet_network(&request.network)?;
let cached = load_json_cache(
LoadJsonCacheRequest {
path: nns_subnet_topology_cache_path(&request.icp_root, &request.network),
network: &request.network,
expected_schema_version: NNS_SUBNET_TOPOLOGY_REPORT_SCHEMA_VERSION,
},
SubnetTopologyLoadErrors,
)?;
validate_report_identity(&cached.report, &request.network, None)?;
Ok(CachedNnsSubnetTopologyReport {
path: cached.path,
report: cached.report,
})
}
pub fn refresh_nns_subnet_topology(
request: &NnsSubnetTopologyRefreshRequest,
) -> Result<CachedNnsSubnetTopologyReport, NnsSubnetTopologyHostError> {
refresh_nns_subnet_topology_with_source(request, &LiveNnsSubnetTopologySource)
}
pub fn refresh_nns_subnet_topology_with_source(
request: &NnsSubnetTopologyRefreshRequest,
source: &dyn NnsSubnetTopologySource,
) -> Result<CachedNnsSubnetTopologyReport, NnsSubnetTopologyHostError> {
enforce_mainnet_network(&request.cache.network)?;
let cache_path =
nns_subnet_topology_cache_path(&request.cache.icp_root, &request.cache.network);
let lock_path =
nns_subnet_topology_refresh_lock_path(&request.cache.icp_root, &request.cache.network);
create_parent_directory(&cache_path)
.map_err(|error| HostCacheError::operation(CACHE_COMPONENT, error))?;
with_refresh_lock(
RefreshLockRequest {
lock_path: &lock_path,
target_path: &cache_path,
network: &request.cache.network,
now_unix_secs: request.now_unix_secs,
lock_stale_after_seconds: request.lock_stale_after_seconds,
},
|error| HostCacheError::operation(CACHE_COMPONENT, error).into(),
|| {
let source_request = source_request(
&request.cache.network,
&request.source_endpoint,
request.now_unix_secs,
);
let report = source.fetch_subnet_topology_report(&source_request)?;
validate_report_identity(
&report,
&request.cache.network,
Some(&request.source_endpoint),
)?;
let report_json = serde_json::to_string_pretty(&report).map_err(|source| {
HostCacheError::serialize_cache(CACHE_COMPONENT, cache_path.clone(), source)
})?;
write_text_atomically(&cache_path, &report_json)
.map_err(|error| HostCacheError::operation(CACHE_COMPONENT, error))?;
Ok(CachedNnsSubnetTopologyReport {
path: cache_path.clone(),
report,
})
},
)
}
pub fn load_or_refresh_missing_nns_subnet_topology(
request: &NnsSubnetTopologyRefreshRequest,
) -> Result<CachedNnsSubnetTopologyReport, NnsSubnetTopologyHostError> {
load_or_refresh_missing_nns_subnet_topology_with_source(request, &LiveNnsSubnetTopologySource)
}
pub fn load_or_refresh_missing_nns_subnet_topology_with_source(
request: &NnsSubnetTopologyRefreshRequest,
source: &dyn NnsSubnetTopologySource,
) -> Result<CachedNnsSubnetTopologyReport, NnsSubnetTopologyHostError> {
match load_cached_nns_subnet_topology(&request.cache) {
Ok(cached) => Ok(cached),
Err(NnsSubnetTopologyHostError::Cache(HostCacheError::MissingCache { .. })) => {
refresh_nns_subnet_topology_with_source(request, source)
}
Err(error) => Err(error),
}
}
pub fn load_or_refresh_stale_nns_subnet_topology(
request: &NnsSubnetTopologyRefreshRequest,
stale_after_seconds: u64,
) -> Result<CachedNnsSubnetTopologyReport, NnsSubnetTopologyHostError> {
load_or_refresh_stale_nns_subnet_topology_with_source(
request,
stale_after_seconds,
&LiveNnsSubnetTopologySource,
)
}
pub fn load_or_refresh_stale_nns_subnet_topology_with_source(
request: &NnsSubnetTopologyRefreshRequest,
stale_after_seconds: u64,
source: &dyn NnsSubnetTopologySource,
) -> Result<CachedNnsSubnetTopologyReport, NnsSubnetTopologyHostError> {
match load_cached_nns_subnet_topology(&request.cache) {
Ok(cached)
if !nns_subnet_topology_freshness(
&cached.report,
request.now_unix_secs,
stale_after_seconds,
)
.stale =>
{
Ok(cached)
}
Ok(_) | Err(NnsSubnetTopologyHostError::Cache(HostCacheError::MissingCache { .. })) => {
refresh_nns_subnet_topology_with_source(request, source)
}
Err(error) => Err(error),
}
}
#[must_use]
pub fn nns_subnet_topology_freshness(
report: &NnsSubnetTopologyReport,
now_unix_secs: u64,
stale_after_seconds: u64,
) -> NnsSubnetTopologyFreshness {
let freshness = freshness_facts(
parse_utc_timestamp_secs(&report.fetched_at),
now_unix_secs,
stale_after_seconds,
);
NnsSubnetTopologyFreshness {
stale: freshness.stale,
reason: freshness.reason.to_string(),
stale_after_seconds: freshness.stale_after_seconds,
fetched_at_unix_secs: freshness.fetched_at_unix_secs,
age_seconds: freshness.age_seconds,
}
}
fn validate_report_identity(
report: &NnsSubnetTopologyReport,
network: &str,
endpoint: Option<&str>,
) -> Result<(), NnsSubnetTopologyHostError> {
if report.network != network {
return Err(NnsSubnetTopologyHostError::RefreshNetworkMismatch {
requested: network.to_string(),
actual: report.network.clone(),
});
}
if report.registry_canister_id != MAINNET_REGISTRY_CANISTER_ID {
return Err(NnsSubnetTopologyHostError::RegistryCanisterMismatch {
expected: MAINNET_REGISTRY_CANISTER_ID.to_string(),
actual: report.registry_canister_id.clone(),
});
}
if let Some(endpoint) = endpoint
&& report.source_endpoint != endpoint
{
return Err(NnsSubnetTopologyHostError::SourceEndpointMismatch {
requested: endpoint.to_string(),
actual: report.source_endpoint.clone(),
});
}
report.validate()?;
Ok(())
}
struct SubnetTopologyLoadErrors;
impl LoadJsonCacheErrorMapper for SubnetTopologyLoadErrors {
type Error = NnsSubnetTopologyHostError;
fn missing_cache(&self, path: PathBuf) -> Self::Error {
HostCacheError::missing_cache(CACHE_COMPONENT, path).into()
}
fn read_cache(&self, path: PathBuf, source: io::Error) -> Self::Error {
HostCacheError::read_cache(CACHE_COMPONENT, path, source).into()
}
fn parse_cache(&self, path: PathBuf, source: serde_json::Error) -> Self::Error {
HostCacheError::parse_cache(CACHE_COMPONENT, path, source).into()
}
fn unsupported_schema(&self, version: u32, expected: u32) -> Self::Error {
HostCacheError::unsupported_cache_schema_version(CACHE_COMPONENT, version, expected).into()
}
fn network_mismatch(&self, requested: String, actual: String) -> Self::Error {
HostCacheError::network_mismatch(CACHE_COMPONENT, requested, actual).into()
}
}