ic-query 0.99.2

Internet Computer query library for CloudEngine, NNS, SNS, ICRC, system canisters, and public network metadata
Documentation
//! Module: sns::report::cache_attempt
//!
//! Responsibility: shared SNS refresh context and attempt-sidecar lifecycle.
//! Does not own: family page fetching, cache publication, or text rendering.
//! Boundary: one resolved context and attempt contract serves neuron refreshes.

use crate::cache_file::ManagedReadBudget;
use crate::{
    cache::CacheRefreshAttemptStatus,
    snapshot_cache::{
        SnapshotRefreshAttempt, SnapshotRefreshProgress, read_snapshot_refresh_attempt_strict,
        validate_snapshot_refresh_attempt,
    },
    sns::report::{
        SNS_CACHE_COMPONENT, SnsHostError, SnsRefreshAttemptStatus,
        cache_paths::sns_governance_from_cache_path, cache_storage::SnsCacheMetadata,
        neurons_cache::SnsRefreshContext,
    },
};
use std::path::Path;

pub(in crate::sns::report) type SnsRefreshAttempt = SnapshotRefreshAttempt<SnsCacheMetadata>;

pub(in crate::sns::report) fn write_starting_sns_refresh_attempt(
    context: SnsRefreshContext<'_>,
) -> Result<(), SnsHostError> {
    write_sns_refresh_attempt_status(
        context,
        CacheRefreshAttemptStatus::Running,
        SnapshotRefreshProgress::default(),
        None,
    )
}

/// Write the running attempt evidence produced by one retained SNS page.
pub(in crate::sns::report) fn write_running_sns_refresh_page(
    context: SnsRefreshContext<'_>,
    page_count: u32,
    row_count: usize,
    last_cursor: Option<String>,
) -> Result<(), SnsHostError> {
    write_sns_refresh_attempt_status(
        context,
        CacheRefreshAttemptStatus::Running,
        SnapshotRefreshProgress::new(page_count, row_count, last_cursor),
        None,
    )
}

pub(in crate::sns::report) fn write_complete_sns_refresh_attempt(
    context: SnsRefreshContext<'_>,
    progress: SnapshotRefreshProgress,
) -> Result<(), SnsHostError> {
    write_sns_refresh_attempt_status(context, CacheRefreshAttemptStatus::Complete, progress, None)
}

pub(in crate::sns::report) fn write_failed_sns_refresh_attempt(
    context: SnsRefreshContext<'_>,
    error: &SnsHostError,
) {
    let latest = read_sns_refresh_attempt(
        &context.request.cache_root,
        &context.paths.refresh_attempt_path,
        &context.request.target.network,
        None,
    )
    .ok()
    .flatten();
    let progress = latest.map_or_else(
        SnapshotRefreshProgress::default,
        SnapshotRefreshProgress::from,
    );
    let _ = write_sns_refresh_attempt_status(
        context,
        CacheRefreshAttemptStatus::Failed,
        progress,
        Some(error.to_string()),
    );
}

fn write_sns_refresh_attempt_status(
    context: SnsRefreshContext<'_>,
    status: CacheRefreshAttemptStatus,
    progress: SnapshotRefreshProgress,
    last_error: Option<String>,
) -> Result<(), SnsHostError> {
    SnsRefreshAttempt::new(
        &context.request.target.network,
        &context.request.target.source_endpoint,
        context.fetch_request.fetched_at.clone(),
        SnsCacheMetadata {
            governance_canister_id: context.governance_canister_id.to_string(),
        },
        context.request.page_size,
    )
    .write(
        &context.request.cache_root,
        &context.paths.refresh_attempt_path,
        SNS_CACHE_COMPONENT,
        status,
        progress,
        last_error,
    )
    .map_err(SnsHostError::from)
}

fn validate_sns_refresh_attempt(
    path: &Path,
    expected_network: &str,
    attempt: &SnapshotRefreshAttempt<SnsCacheMetadata>,
) -> Result<(), SnsHostError> {
    let invalid = |reason| SnsHostError::InvalidRefreshAttempt {
        path: path.to_path_buf(),
        reason,
    };
    validate_snapshot_refresh_attempt(attempt, expected_network).map_err(invalid)?;
    let expected_governance = sns_governance_from_cache_path(path)
        .and_then(|name| name.to_str())
        .ok_or_else(|| {
            invalid("attempt path does not contain a Governance identity".to_string())
        })?;
    if attempt.metadata.governance_canister_id != expected_governance {
        return Err(invalid(format!(
            "governance_canister_id is {}, expected {expected_governance}",
            attempt.metadata.governance_canister_id
        )));
    }
    super::target::parse_governance_canister_input(expected_governance)
        .map_err(|error| invalid(error.to_string()))?;
    Ok(())
}

fn read_sns_refresh_attempt(
    cache_root: &Path,
    path: &Path,
    expected_network: &str,
    budget: Option<&mut ManagedReadBudget>,
) -> Result<Option<SnsRefreshAttempt>, SnsHostError> {
    read_snapshot_refresh_attempt_strict::<SnsRefreshAttempt>(cache_root, path, budget)
        .map_err(|error| {
            error.into_error(SNS_CACHE_COMPONENT, SnsHostError::from, |path, reason| {
                SnsHostError::InvalidRefreshAttempt { path, reason }
            })
        })?
        .map(|attempt| {
            validate_sns_refresh_attempt(path, expected_network, &attempt)?;
            Ok(attempt)
        })
        .transpose()
}

pub(in crate::sns::report) fn read_sns_refresh_attempt_status_strict(
    cache_root: &Path,
    path: &Path,
    expected_network: &str,
    budget: Option<&mut ManagedReadBudget>,
) -> Result<Option<SnsRefreshAttemptStatus>, SnsHostError> {
    Ok(
        read_sns_refresh_attempt(cache_root, path, expected_network, budget)?
            .map(SnsRefreshAttemptStatus::from_validated),
    )
}