#[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,
},
};
#[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,
}
}