#[cfg(test)]
mod tests;
use candid::Principal;
use canic_core::{
bootstrap::compiled::{ComponentTopology, ConfigModel},
ids::{
ComponentSpecAdmission, ComponentSpecId, ComponentTopologyDigest, FleetSubnetRootLimits,
SubnetId,
},
};
use std::collections::BTreeSet;
use thiserror::Error as ThisError;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RootComponentAdmissionInput {
pub component_spec: ComponentSpecId,
pub maximum_root_instances: u32,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlannedFleetSubnetRootTopologyInput {
pub placement_subnet: SubnetId,
pub component_admissions: Vec<RootComponentAdmissionInput>,
pub limits: FleetSubnetRootLimits,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlannedFleetSubnetRootTopology {
pub placement_subnet: SubnetId,
pub component_admissions: Vec<ComponentSpecAdmission>,
pub component_topology_digest: ComponentTopologyDigest,
pub limits: FleetSubnetRootLimits,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlannedFleetTopology {
pub component_topology: ComponentTopology,
pub fleet_subnet_roots: Vec<PlannedFleetSubnetRootTopology>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RootPoolCapacityInput {
pub component_admissions: Vec<ComponentSpecAdmission>,
pub pool_target_cycles: u128,
pub root: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RootPoolImportCapacityInput {
pub import_count: usize,
pub maximum_size: u32,
pub root: String,
}
#[derive(Debug, Eq, PartialEq, ThisError)]
pub enum RootPoolCapacityError {
#[error(
"Root {root} pool target {pool_target_cycles} cycles is below admitted Component Spec '{component_spec}' initial demand {required_cycles} cycles"
)]
Insufficient {
component_spec: ComponentSpecId,
pool_target_cycles: u128,
required_cycles: u128,
root: String,
},
#[error("Root {root} admits unknown Component Spec '{component_spec}'")]
UnknownComponentSpec {
component_spec: ComponentSpecId,
root: String,
},
}
#[derive(Debug, Eq, PartialEq, ThisError)]
#[error(
"Root {root} has {import_count} retained pool imports, above initialisation maximum {maximum_size}"
)]
pub struct RootPoolImportCapacityError {
pub import_count: usize,
pub maximum_size: u32,
pub root: String,
}
#[derive(Debug, Eq, PartialEq, ThisError)]
pub enum FleetTopologyPlanError {
#[error("root input repeats Component Spec admission '{component_spec}'")]
DuplicateAdmission { component_spec: ComponentSpecId },
#[error("root input repeats placement Subnet '{placement_subnet}'")]
DuplicatePlacementSubnet { placement_subnet: SubnetId },
#[error("root placement Subnet must not be anonymous")]
AnonymousPlacementSubnet,
#[error(transparent)]
Topology(#[from] canic_core::bootstrap::compiled::ComponentTopologyError),
#[error("root input references unknown Component Spec '{component_spec}'")]
UnknownComponentSpec { component_spec: ComponentSpecId },
}
pub fn validate_root_pool_capacity(
config: &ConfigModel,
roots: &[RootPoolCapacityInput],
) -> Result<(), RootPoolCapacityError> {
for root in roots {
for admission in &root.component_admissions {
let component = config
.component_specs
.get(&admission.component_spec)
.ok_or_else(|| RootPoolCapacityError::UnknownComponentSpec {
component_spec: admission.component_spec.clone(),
root: root.root.clone(),
})?;
let required_cycles = component.initial_cycles.to_u128();
if required_cycles > root.pool_target_cycles {
return Err(RootPoolCapacityError::Insufficient {
component_spec: admission.component_spec.clone(),
pool_target_cycles: root.pool_target_cycles,
required_cycles,
root: root.root.clone(),
});
}
}
}
Ok(())
}
pub fn validate_root_pool_import_capacity(
input: &RootPoolImportCapacityInput,
) -> Result<(), RootPoolImportCapacityError> {
if let Ok(import_count) = u32::try_from(input.import_count)
&& import_count <= input.maximum_size
{
return Ok(());
}
Err(RootPoolImportCapacityError {
import_count: input.import_count,
maximum_size: input.maximum_size,
root: input.root.clone(),
})
}
pub fn plan_initial_fleet_topology(
config: &ConfigModel,
root_inputs: Vec<PlannedFleetSubnetRootTopologyInput>,
) -> Result<PlannedFleetTopology, FleetTopologyPlanError> {
let component_topology = config.compile_component_topology()?;
let mut fleet_subnet_roots = root_inputs
.into_iter()
.map(|input| finalize_planned_root(&component_topology, input))
.collect::<Result<Vec<_>, _>>()?;
fleet_subnet_roots.sort_by_key(|root| root.placement_subnet);
let mut placement_subnets = BTreeSet::new();
for root in &fleet_subnet_roots {
if root.placement_subnet.as_principal() == &Principal::anonymous() {
return Err(FleetTopologyPlanError::AnonymousPlacementSubnet);
}
if !placement_subnets.insert(root.placement_subnet) {
return Err(FleetTopologyPlanError::DuplicatePlacementSubnet {
placement_subnet: root.placement_subnet,
});
}
component_topology.validate_planned_root(
&root.component_admissions,
root.component_topology_digest,
&root.limits,
)?;
}
let admissions = fleet_subnet_roots
.iter()
.map(|root| root.component_admissions.as_slice())
.collect::<Vec<_>>();
component_topology.validate_fleet_admissions(&admissions)?;
Ok(PlannedFleetTopology {
component_topology,
fleet_subnet_roots,
})
}
fn finalize_planned_root(
component_topology: &ComponentTopology,
mut input: PlannedFleetSubnetRootTopologyInput,
) -> Result<PlannedFleetSubnetRootTopology, FleetTopologyPlanError> {
input
.component_admissions
.sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
let mut seen = BTreeSet::new();
let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
for admission in input.component_admissions {
if !seen.insert(admission.component_spec.clone()) {
return Err(FleetTopologyPlanError::DuplicateAdmission {
component_spec: admission.component_spec,
});
}
let component_spec = component_topology
.get(&admission.component_spec)
.ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
component_spec: admission.component_spec.clone(),
})?;
component_admissions.push(ComponentSpecAdmission {
component_spec: admission.component_spec,
spec_hash: component_spec.spec_hash,
maximum_root_instances: admission.maximum_root_instances,
});
}
let projection = component_topology.project_for_admissions(&component_admissions)?;
Ok(PlannedFleetSubnetRootTopology {
placement_subnet: input.placement_subnet,
component_admissions,
component_topology_digest: projection.digest()?,
limits: input.limits,
})
}