use ic_host_text::timestamp::format_utc_timestamp_secs;
mod cache;
pub use cache::{
NnsGovernanceCacheAdmission, NnsGovernanceCacheStatusReport, NnsGovernanceCacheSummary,
NnsGovernanceValidCacheSummary,
};
pub(in crate::nns) use cache::{
build_governance_cache_status_report, validate_governance_cache_completeness,
};
mod refresh;
pub(in crate::nns) use refresh::run_governance_snapshot_refresh;
use crate::cache_file::write_managed_json_pretty_atomically;
use crate::{
HostCacheError,
cache::CacheRefreshAttemptStatus,
nns::MAINNET_GOVERNANCE_CANISTER_ID,
snapshot_cache::{
SNAPSHOT_REFRESH_ATTEMPT_SCHEMA_VERSION, SnapshotEnvelope, SnapshotJsonPaths,
SnapshotRefreshAttempt, SnapshotRefreshProgress, current_attempt_timestamp,
read_snapshot_refresh_attempt_strict, validate_snapshot_refresh_attempt,
},
};
use serde::{Deserialize as SerdeDeserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsGovernanceRefreshRequest {
pub cache_root: PathBuf,
pub network: String,
pub source_endpoint: String,
pub now_unix_secs: u64,
pub page_size: u32,
pub max_pages: Option<u32>,
}
impl NnsGovernanceRefreshRequest {
#[must_use]
pub fn new(
cache_root: impl Into<PathBuf>,
network: impl Into<String>,
source_endpoint: impl Into<String>,
now_unix_secs: u64,
page_size: u32,
) -> Self {
Self {
cache_root: cache_root.into(),
network: network.into(),
source_endpoint: source_endpoint.into(),
now_unix_secs,
page_size,
max_pages: None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NnsGovernanceCacheRequest {
pub cache_root: PathBuf,
pub network: String,
}
impl NnsGovernanceCacheRequest {
#[must_use]
pub fn new(cache_root: impl Into<PathBuf>, network: impl Into<String>) -> Self {
Self {
cache_root: cache_root.into(),
network: network.into(),
}
}
#[must_use]
pub fn cache_root(&self) -> &Path {
&self.cache_root
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[expect(
clippy::struct_excessive_bools,
reason = "Independent snapshot assurance and publication observations remain explicit in raw JSON."
)]
pub struct NnsGovernanceRefreshReport {
pub schema_version: u32,
pub network: String,
pub governance_canister_id: String,
pub row_count: usize,
pub collection: crate::nns::governance::NnsGovernanceCollection,
pub wrote_cache: bool,
pub page_size: u32,
pub page_count: u32,
pub complete: bool,
pub point_in_time_guaranteed: bool,
pub replaced_existing_cache: bool,
pub attempt_finalization_error: Option<String>,
pub fetched_at: String,
pub source_endpoint: String,
pub fetched_by: String,
pub cache_path: String,
pub refresh_attempt_path: String,
pub refresh_lock_path: String,
}
impl NnsGovernanceRefreshReport {
pub(in crate::nns) fn from_snapshot<Rows>(
cache: &SnapshotEnvelope<NnsGovernanceCacheMetadata, Rows>,
collection: crate::nns::governance::NnsGovernanceCollection,
paths: &SnapshotJsonPaths,
replaced_existing_cache: bool,
attempt_finalization_error: Option<String>,
) -> Self {
Self {
schema_version: 1,
network: cache.network.clone(),
governance_canister_id: cache.metadata.governance_canister_id.clone(),
row_count: cache.completeness.row_count,
collection,
page_size: cache.completeness.page_size,
page_count: cache.completeness.page_count,
complete: cache.completeness.is_api_exhausted(),
point_in_time_guaranteed: cache.completeness.point_in_time_guaranteed,
wrote_cache: true,
replaced_existing_cache,
attempt_finalization_error,
fetched_at: cache.fetched_at.clone(),
source_endpoint: cache.source_endpoint.clone(),
fetched_by: cache.fetched_by.clone(),
cache_path: paths.snapshot_path.display().to_string(),
refresh_attempt_path: paths.refresh_attempt_path.display().to_string(),
refresh_lock_path: paths.refresh_lock_path.display().to_string(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub(in crate::nns) struct NnsGovernanceCollectionEvidence {
requested_source: super::NnsGovernanceSourceSelection,
max_pages: u32,
started_at: String,
}
impl NnsGovernanceCollectionEvidence {
pub(in crate::nns) fn from_state(state: &super::GovernanceCollectionState) -> Self {
Self {
requested_source: state.requested_source.clone(),
max_pages: state.max_pages,
started_at: state.started_at.clone(),
}
}
pub(in crate::nns) fn restore_state<Rows>(
&self,
cache: &SnapshotEnvelope<NnsGovernanceCacheMetadata, Rows>,
) -> super::GovernanceCollectionState {
super::GovernanceCollectionState {
schema_version: 1,
network: cache.network.clone(),
governance_canister_id: cache.metadata.governance_canister_id.clone(),
requested_source: self.requested_source.clone(),
source: Some(super::NnsGovernanceSourceProvenance::ReplicaQuery {
endpoint: cache.source_endpoint.clone(),
fetched_by: cache.fetched_by.clone(),
}),
page_size: cache.completeness.page_size,
max_pages: self.max_pages,
pages_fetched: cache.completeness.page_count,
started_at: self.started_at.clone(),
updated_at: cache.fetched_at.clone(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, SerdeDeserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub(in crate::nns) struct NnsGovernanceCacheMetadata {
pub(in crate::nns) governance_canister_id: String,
}
#[derive(Debug)]
pub(in crate::nns) enum NnsGovernanceAttemptReadError {
Cache(HostCacheError),
Invalid {
path: PathBuf,
reason: String,
},
}
#[must_use]
pub(in crate::nns) fn mainnet_governance_cache_metadata() -> NnsGovernanceCacheMetadata {
NnsGovernanceCacheMetadata {
governance_canister_id: MAINNET_GOVERNANCE_CANISTER_ID.to_string(),
}
}
pub(in crate::nns) fn validate_governance_cache_metadata(
metadata: &NnsGovernanceCacheMetadata,
) -> Result<(), String> {
if metadata.governance_canister_id == MAINNET_GOVERNANCE_CANISTER_ID {
return Ok(());
}
Err(format!(
"governance_canister_id is {}, expected {MAINNET_GOVERNANCE_CANISTER_ID}",
metadata.governance_canister_id
))
}
pub(in crate::nns) fn read_governance_refresh_attempt(
cache_root: &Path,
path: &Path,
expected_network: &str,
cache_component: &'static str,
) -> Result<Option<SnapshotRefreshAttempt<NnsGovernanceCacheMetadata>>, NnsGovernanceAttemptReadError>
{
let attempt = read_snapshot_refresh_attempt_strict::<
SnapshotRefreshAttempt<NnsGovernanceCacheMetadata>,
>(cache_root, path, None)
.map_err(|error| {
error.into_error(
cache_component,
NnsGovernanceAttemptReadError::Cache,
|path, reason| NnsGovernanceAttemptReadError::Invalid { path, reason },
)
})?;
attempt
.map(|attempt| {
let invalid = |reason| NnsGovernanceAttemptReadError::Invalid {
path: path.to_path_buf(),
reason,
};
validate_snapshot_refresh_attempt(&attempt, expected_network).map_err(invalid)?;
validate_governance_cache_metadata(&attempt.metadata).map_err(invalid)?;
Ok(attempt)
})
.transpose()
}
pub(in crate::nns) fn read_governance_refresh_attempt_status(
cache_root: &Path,
path: &Path,
expected_network: &str,
cache_component: &'static str,
) -> Result<Option<crate::cache::SnapshotRefreshAttemptStatus>, NnsGovernanceAttemptReadError> {
read_governance_refresh_attempt(cache_root, path, expected_network, cache_component)
.map(|attempt| attempt.map(Into::into))
}
fn write_governance_refresh_attempt(
path: &Path,
request: &NnsGovernanceRefreshRequest,
cache_component: &'static str,
status: CacheRefreshAttemptStatus,
progress: SnapshotRefreshProgress,
last_error: Option<String>,
) -> Result<(), HostCacheError> {
let started_at = format_utc_timestamp_secs(request.now_unix_secs);
let attempt = SnapshotRefreshAttempt {
schema_version: SNAPSHOT_REFRESH_ATTEMPT_SCHEMA_VERSION,
network: request.network.clone(),
source_endpoint: request.source_endpoint.clone(),
started_at: started_at.clone(),
updated_at: current_attempt_timestamp(&started_at),
metadata: mainnet_governance_cache_metadata(),
status,
page_size: request.page_size,
pages_fetched: progress.pages_fetched,
rows_fetched: progress.rows_fetched,
last_cursor: progress.last_cursor,
last_error,
};
write_managed_json_pretty_atomically(
&request.cache_root,
path,
&attempt,
crate::cache_file::MAX_REFRESH_ATTEMPT_BYTES,
|path, source| HostCacheError::serialize_cache(cache_component, path, source),
|error| HostCacheError::operation(cache_component, error),
)
}
pub(in crate::nns) fn write_starting_governance_refresh_attempt(
path: &Path,
request: &NnsGovernanceRefreshRequest,
cache_component: &'static str,
) -> Result<(), HostCacheError> {
write_governance_refresh_attempt(
path,
request,
cache_component,
CacheRefreshAttemptStatus::Running,
SnapshotRefreshProgress::default(),
None,
)
}
pub(in crate::nns) fn write_running_governance_refresh_attempt(
path: &Path,
request: &NnsGovernanceRefreshRequest,
cache_component: &'static str,
progress: SnapshotRefreshProgress,
) -> Result<(), HostCacheError> {
write_governance_refresh_attempt(
path,
request,
cache_component,
CacheRefreshAttemptStatus::Running,
progress,
None,
)
}
pub(in crate::nns) fn write_complete_governance_refresh_attempt(
path: &Path,
request: &NnsGovernanceRefreshRequest,
cache_component: &'static str,
progress: SnapshotRefreshProgress,
) -> Result<(), HostCacheError> {
write_governance_refresh_attempt(
path,
request,
cache_component,
CacheRefreshAttemptStatus::Complete,
progress,
None,
)
}
pub(in crate::nns) fn write_failed_governance_refresh_attempt(
path: &Path,
request: &NnsGovernanceRefreshRequest,
cache_component: &'static str,
last_error: String,
) -> Result<(), HostCacheError> {
let progress = read_governance_refresh_attempt(
&request.cache_root,
path,
&request.network,
cache_component,
)
.ok()
.flatten()
.map(SnapshotRefreshProgress::from)
.unwrap_or_default();
write_governance_refresh_attempt(
path,
request,
cache_component,
CacheRefreshAttemptStatus::Failed,
progress,
Some(last_error),
)
}