use super::super::{
MAINNET_GOVERNANCE_CANISTER_ID, NnsProposalHostError, enforce_mainnet_network,
model::NnsProposalRow,
source::{
LiveNnsProposalSource, NnsProposalFetchRequest, NnsProposalSource,
nns_proposal_row_from_info,
},
};
use super::{
NNS_PROPOSAL_CACHE_SCHEMA_VERSION, NNS_PROPOSAL_REFRESH_REPORT_SCHEMA_VERSION,
attempt::{
NnsProposalAttemptProgress, write_complete_attempt, write_failed_attempt,
write_running_attempt, write_starting_attempt,
},
cache_file_error,
model::{
CompleteNnsProposalCollection, NnsProposalCache, NnsProposalCacheMetadata,
NnsProposalCacheRows, NnsProposalRefreshReport, NnsProposalRefreshRequest,
},
paths::nns_proposal_cache_paths,
};
use crate::{
snapshot_cache::{
LockedSnapshotRefreshRequest, PagedCollectionPage, PagedCollectionState,
PagedSnapshotRefresh, SnapshotCompleteness, run_paged_snapshot_refresh,
run_snapshot_refresh_with_attempts, with_locked_snapshot_refresh, write_snapshot_json,
},
subnet_catalog::{MAINNET_NETWORK, format_utc_timestamp_secs},
};
use std::{cmp::Reverse, path::Path};
const NNS_PROPOSAL_REFRESH_LOCK_STALE_AFTER_SECONDS: u64 = 30 * 60;
pub(in crate::nns::proposals) fn refresh_nns_proposal_cache(
request: &NnsProposalRefreshRequest,
) -> Result<NnsProposalRefreshReport, NnsProposalHostError> {
refresh_nns_proposal_cache_with_source(request, &LiveNnsProposalSource)
}
pub(in crate::nns::proposals::report) fn refresh_nns_proposal_cache_with_source(
request: &NnsProposalRefreshRequest,
source: &dyn NnsProposalSource,
) -> Result<NnsProposalRefreshReport, NnsProposalHostError> {
enforce_mainnet_network(&request.network)?;
let paths = nns_proposal_cache_paths(&request.icp_root, &request.network);
with_locked_snapshot_refresh(
LockedSnapshotRefreshRequest {
snapshot_path: &paths.snapshot_path,
refresh_lock_path: &paths.refresh_lock_path,
network: &request.network,
now_unix_secs: request.now_unix_secs,
lock_stale_after_seconds: NNS_PROPOSAL_REFRESH_LOCK_STALE_AFTER_SECONDS,
},
cache_file_error,
|refresh_state| {
run_snapshot_refresh_with_attempts(
|| write_starting_attempt(&paths.refresh_attempt_path, request),
|| {
let complete = fetch_complete_nns_proposal_collection(
request,
source,
&paths.refresh_attempt_path,
)?;
publish_complete_nns_proposal_cache(
request,
&paths,
refresh_state.replaced_existing_snapshot,
complete,
)
},
|err| write_failed_attempt(&paths.refresh_attempt_path, request, err),
)
},
)
}
fn fetch_complete_nns_proposal_collection(
request: &NnsProposalRefreshRequest,
source: &dyn NnsProposalSource,
attempt_path: &Path,
) -> Result<CompleteNnsProposalCollection, NnsProposalHostError> {
let fetched_at = format_utc_timestamp_secs(request.now_unix_secs);
run_paged_snapshot_refresh(NnsProposalRefreshPages {
request,
fetch_request: NnsProposalFetchRequest {
endpoint: request.source_endpoint.clone(),
fetched_at,
fetched_by: "ic-query".to_string(),
},
source,
attempt_path,
state: NnsProposalCollectionState::new(),
})
}
struct NnsProposalRefreshPages<'a> {
request: &'a NnsProposalRefreshRequest,
fetch_request: NnsProposalFetchRequest,
source: &'a dyn NnsProposalSource,
attempt_path: &'a Path,
state: NnsProposalCollectionState,
}
impl PagedSnapshotRefresh for NnsProposalRefreshPages<'_> {
type Complete = CompleteNnsProposalCollection;
type Error = NnsProposalHostError;
fn progress_text(&self) -> String {
format!(
"refreshing NNS proposals: pages={} rows={}",
self.state.page_count(),
self.state.row_count()
)
}
fn max_pages_reached(&self) -> bool {
self.request
.max_pages
.is_some_and(|max_pages| self.state.page_count() >= max_pages)
}
fn incomplete_refresh_error(&self) -> Self::Error {
NnsProposalHostError::IncompleteRefresh {
pages_fetched: self.state.page_count(),
rows_fetched: self.state.row_count(),
reason: "max pages reached before API exhaustion".to_string(),
}
}
fn fetch_next_page(&mut self) -> Result<PagedCollectionPage, Self::Error> {
let proposal_infos = self.source.fetch_proposals(
&self.fetch_request,
self.request.page_size,
self.state.before_proposal_id(),
&[],
&[],
)?;
self.state.ingest_page(
proposal_infos
.into_iter()
.map(nns_proposal_row_from_info)
.collect(),
)
}
fn write_running_attempt(&self, page: &PagedCollectionPage) -> Result<(), Self::Error> {
write_running_attempt(
self.attempt_path,
self.request,
NnsProposalAttemptProgress::new(
self.state.page_count(),
self.state.row_count(),
page.last_cursor_text.clone(),
),
)
}
fn page_exhausts_collection(&self, page: &PagedCollectionPage) -> bool {
page.exhausts_collection(self.request.page_size, self.state.has_next_cursor())
}
fn into_complete(self) -> Self::Complete {
self.state.into_complete()
}
}
struct NnsProposalCollectionState {
state: PagedCollectionState<NnsProposalRow, u64>,
}
impl NnsProposalCollectionState {
fn new() -> Self {
Self {
state: PagedCollectionState::new(),
}
}
const fn page_count(&self) -> u32 {
self.state.page_count()
}
const fn row_count(&self) -> usize {
self.state.row_count()
}
const fn before_proposal_id(&self) -> Option<u64> {
self.state.next_cursor().copied()
}
const fn has_next_cursor(&self) -> bool {
self.state.has_next_cursor()
}
fn ingest_page(
&mut self,
rows: Vec<NnsProposalRow>,
) -> Result<PagedCollectionPage, NnsProposalHostError> {
if rows.iter().any(|row| row.proposal_id.is_none()) {
return Err(NnsProposalHostError::MissingProposalIdInPage);
}
let next_cursor = rows
.iter()
.filter_map(|row| row.proposal_id)
.min()
.filter(|proposal_id| *proposal_id > 1);
Ok(self
.state
.ingest_page(rows, next_cursor, ToString::to_string, row_id))
}
fn into_complete(self) -> CompleteNnsProposalCollection {
let complete = self.state.into_complete(ToString::to_string);
let mut proposals = complete.rows;
proposals.sort_by_key(|proposal| Reverse(proposal.proposal_id));
CompleteNnsProposalCollection {
proposals,
page_count: complete.page_count,
last_cursor: complete.last_cursor,
}
}
}
fn row_id(row: &NnsProposalRow) -> String {
row.proposal_id
.map_or_else(|| "missing-proposal-id".to_string(), |id| id.to_string())
}
fn publish_complete_nns_proposal_cache(
request: &NnsProposalRefreshRequest,
paths: &crate::snapshot_cache::SnapshotJsonPaths,
replaced_existing_cache: bool,
complete: CompleteNnsProposalCollection,
) -> Result<NnsProposalRefreshReport, NnsProposalHostError> {
let CompleteNnsProposalCollection {
proposals,
page_count,
last_cursor,
} = complete;
let fetched_at = format_utc_timestamp_secs(request.now_unix_secs);
let cache = NnsProposalCache {
schema_version: NNS_PROPOSAL_CACHE_SCHEMA_VERSION,
network: MAINNET_NETWORK.to_string(),
source_endpoint: request.source_endpoint.clone(),
fetched_at: fetched_at.clone(),
fetched_by: "ic-query".to_string(),
metadata: NnsProposalCacheMetadata {
governance_canister_id: MAINNET_GOVERNANCE_CANISTER_ID.to_string(),
},
completeness: SnapshotCompleteness::api_exhausted(
request.page_size,
page_count,
proposals.len(),
false,
),
data: NnsProposalCacheRows { proposals },
};
let proposal_count = cache.data.proposals.len();
write_snapshot_json(
&paths.snapshot_path,
&cache,
|path, source| NnsProposalHostError::SerializeCache { path, source },
cache_file_error,
)?;
write_complete_attempt(
&paths.refresh_attempt_path,
request,
NnsProposalAttemptProgress::new(page_count, proposal_count, last_cursor),
)?;
Ok(NnsProposalRefreshReport {
schema_version: NNS_PROPOSAL_REFRESH_REPORT_SCHEMA_VERSION,
network: MAINNET_NETWORK.to_string(),
governance_canister_id: MAINNET_GOVERNANCE_CANISTER_ID.to_string(),
proposal_count,
page_size: request.page_size,
page_count,
complete: true,
replaced_existing_cache,
wrote_cache: true,
fetched_at,
source_endpoint: request.source_endpoint.clone(),
fetched_by: "ic-query".to_string(),
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(),
})
}