canic-host 0.110.53

Host-side App build and desired-state Fleet reconciliation library for Canic workspaces
Documentation
//! Module: fleet_ensure::policy::continuation_forecast
//!
//! Responsibility: project known continuation work from one immutable review.
//! Boundary: no live reads, quote completion, persistent changes or effect admission.

#[cfg(test)]
mod tests;

use crate::fleet_ensure::{
    model::{
        CanisterDisposition, CurrentFleetProtocolAction, EnsureAction, FleetEnsurePlan,
        FleetEnsurePlanScope, FleetEnsureReport,
        capacity_import::DEFAULT_IMPORT_SOURCE_DEBIT_CYCLES,
    },
    view::continuation::{
        ContinuationAuthority, ContinuationDiscovery, ContinuationForecast, ContinuationImport,
        ContinuationImportState, DependentPoolFunding, ImportHeadroom, ImportHeadroomAssessment,
    },
};

/// Explain the reviewed ceiling separately from dependent imports and funding.
#[must_use]
pub fn forecast(report: &FleetEnsureReport) -> ContinuationForecast {
    let plan = &report.plan;
    let complete = report.terminal && plan.scope == FleetEnsurePlanScope::Full;
    let mut result = ContinuationForecast {
        authority: if complete {
            ContinuationAuthority::Complete
        } else if plan.continuation.is_some() {
            ContinuationAuthority::WithinReviewedProtocolBounds
        } else {
            ContinuationAuthority::SeparateReview
        },
        reviewed_actions: plan
            .canisters
            .iter()
            .map(|canister| canister.actions.len())
            .sum::<usize>()
            + plan.protocol_actions.len(),
        maximum_successor_actions: plan
            .continuation
            .as_ref()
            .map(|value| value.maximum_successor_actions),
        maximum_operator_debit_cycles: plan.conservation.maximum_operator_debit_cycles,
        imports: Vec::new(),
        dependent_funding: Vec::new(),
        requires_live_discovery: Vec::new(),
    };
    if complete {
        return result;
    }
    let awaiting_initialization = append_initialization_imports(plan, &mut result.imports);
    for action in &plan.protocol_actions {
        if let EnsureAction::FleetProtocol {
            name,
            principal,
            action,
            ..
        } = action
            && let CurrentFleetProtocolAction::ReconcilePoolAsset { request, .. } = action.as_ref()
        {
            result.imports.push(ContinuationImport {
                root: principal.clone(),
                canister: name.clone(),
                principal: Some(request.canister_id.to_text()),
                state: ContinuationImportState::ReviewedReconciliation,
                headroom: None,
            });
        }
    }
    if let Some(review) = &plan.recovery_review {
        result.dependent_funding = review
            .known_pool_funding
            .iter()
            .map(|funding| DependentPoolFunding {
                root: funding.root.clone(),
                principal: funding.principal.clone(),
                amount_cycles: funding.amount_cycles,
                ledger_fee_cycles: funding.ledger_fee_cycles,
            })
            .collect();
    }
    if awaiting_initialization
        || plan.scope != FleetEnsurePlanScope::Full
        || plan.continuation.is_some()
        || plan.recovery_review.is_some()
        || !result.imports.is_empty()
    {
        result.requires_live_discovery = vec![
            ContinuationDiscovery::PoolReadiness,
            ContinuationDiscovery::PoolFundingAndCapacity,
            ContinuationDiscovery::PublicationAndProvisioning,
        ];
    }
    result
}

fn append_initialization_imports(
    plan: &FleetEnsurePlan,
    imports: &mut Vec<ContinuationImport>,
) -> bool {
    let mut awaiting_initialization = false;
    if let Some(desired) = plan.reviewed_desired.as_ref().map(|value| value.desired()) {
        for root in desired
            .bootstrap
            .iter()
            .flat_map(|bootstrap| &bootstrap.roots)
        {
            let initializes = plan.canisters.iter().any(|canister| {
                canister.name == root.root
                    && matches!(
                        canister.disposition,
                        CanisterDisposition::Create
                            | CanisterDisposition::Replace
                            | CanisterDisposition::Reinstall
                    )
            });
            if !initializes && plan.scope != FleetEnsurePlanScope::InfrastructureBootstrap {
                continue;
            }
            awaiting_initialization = true;
            for name in &root.canister_pool_imports {
                let principal = match plan
                    .canisters
                    .iter()
                    .find(|canister| &canister.name == name)
                {
                    Some(canister) => canister.principal.clone(),
                    None => desired
                        .canisters
                        .iter()
                        .find(|canister| &canister.name == name)
                        .and_then(|canister| canister.principal.clone()),
                };
                imports.push(ContinuationImport {
                    root: root.root.clone(),
                    canister: name.clone(),
                    principal,
                    state: ContinuationImportState::PostInitializationObservation,
                    headroom: Some(import_headroom(
                        root.limits.canister_pool.canister_cycles.to_u128(),
                        plan.infrastructure_bootstrap
                            .as_ref()
                            .and_then(|source| source.sources.get(name))
                            .map(|source| source.sample.cycles),
                    )),
                });
            }
        }
    }
    awaiting_initialization
}

const fn import_headroom(minimum: u128, available: Option<u128>) -> ImportHeadroom {
    let required = minimum.checked_add(DEFAULT_IMPORT_SOURCE_DEBIT_CYCLES);
    let assessment = match (required, available) {
        (None, _) => ImportHeadroomAssessment::InvalidBounds,
        (Some(_), None) => ImportHeadroomAssessment::AwaitingCurrentRootObservation,
        (Some(required), Some(available)) => ImportHeadroomAssessment::Observed {
            required_cycles: required,
            available_cycles: available,
            shortfall_cycles: required.saturating_sub(available),
        },
    };
    ImportHeadroom {
        minimum_ready_cycles: minimum,
        maximum_source_debit_cycles: DEFAULT_IMPORT_SOURCE_DEBIT_CYCLES,
        assessment,
    }
}