Skip to main content

canic_host/component_topology/
mod.rs

1//! Module: component_topology
2//!
3//! Responsibility: finalize Fleet Subnet Root topology bindings from config and explicit host input.
4//! Does not own: Subnet selection, Canister creation, release sets, Registry commit, or runtime.
5//! Boundary: accepts resolved authority/placement facts and emits validated immutable bindings.
6
7#[cfg(test)]
8mod tests;
9
10use candid::Principal;
11use canic_contracts::ids::{
12    ComponentSpecAdmission, ComponentSpecId, ComponentTopologyDigest, FleetSubnetRootLimits,
13    SubnetId,
14};
15use canic_core::bootstrap::compiled::{ComponentTopology, ConfigModel};
16use std::collections::BTreeSet;
17use thiserror::Error as ThisError;
18
19///
20/// RootComponentAdmissionInput
21///
22/// Operator-planned positive instance capacity for one Spec on one resolved root.
23///
24
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct RootComponentAdmissionInput {
27    pub component_spec: ComponentSpecId,
28    pub maximum_root_instances: u32,
29}
30
31///
32/// PlannedFleetSubnetRootTopologyInput
33///
34/// Exact root placement, admissions, and limits resolved before Canister creation.
35///
36
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct PlannedFleetSubnetRootTopologyInput {
39    pub placement_subnet: SubnetId,
40    pub component_admissions: Vec<RootComponentAdmissionInput>,
41    pub limits: FleetSubnetRootLimits,
42}
43
44///
45/// PlannedFleetSubnetRootTopology
46///
47/// Canonical pre-creation root topology with no fabricated Canister principal.
48///
49
50#[derive(Clone, Debug, Eq, PartialEq)]
51pub struct PlannedFleetSubnetRootTopology {
52    pub placement_subnet: SubnetId,
53    pub component_admissions: Vec<ComponentSpecAdmission>,
54    pub component_topology_digest: ComponentTopologyDigest,
55    pub limits: FleetSubnetRootLimits,
56}
57
58///
59/// PlannedFleetTopology
60///
61/// Canonical Component Topology and every validated pre-creation root plan.
62///
63
64#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct PlannedFleetTopology {
66    pub component_topology: ComponentTopology,
67    pub fleet_subnet_roots: Vec<PlannedFleetSubnetRootTopology>,
68}
69
70/// Root-local admitted Component demand checked against one empty-pool target.
71#[derive(Clone, Debug, Eq, PartialEq)]
72pub struct RootPoolCapacityInput {
73    pub component_admissions: Vec<ComponentSpecAdmission>,
74    pub pool_target_cycles: u128,
75    pub root: String,
76}
77
78/// Exact retained import count checked against one Root's initial pool capacity.
79#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct RootPoolImportCapacityInput {
81    pub import_count: usize,
82    pub maximum_size: u32,
83    pub root: String,
84}
85
86/// Exact fail-closed mismatch between admitted Component demand and pool capacity.
87#[derive(Debug, Eq, PartialEq, ThisError)]
88pub enum RootPoolCapacityError {
89    #[error(
90        "Root {root} pool target {pool_target_cycles} cycles is below admitted Component Spec '{component_spec}' initial demand {required_cycles} cycles"
91    )]
92    Insufficient {
93        component_spec: ComponentSpecId,
94        pool_target_cycles: u128,
95        required_cycles: u128,
96        root: String,
97    },
98
99    #[error("Root {root} admits unknown Component Spec '{component_spec}'")]
100    UnknownComponentSpec {
101        component_spec: ComponentSpecId,
102        root: String,
103    },
104}
105
106/// Exact fail-closed mismatch between retained imports and Root initialisation capacity.
107#[derive(Debug, Eq, PartialEq, ThisError)]
108#[error(
109    "Root {root} has {import_count} retained pool imports, above initialisation maximum {maximum_size}"
110)]
111pub struct RootPoolImportCapacityError {
112    pub import_count: usize,
113    pub maximum_size: u32,
114    pub root: String,
115}
116
117///
118/// FleetTopologyPlanError
119///
120/// Typed rejection while finalizing immutable root topology input.
121///
122
123#[derive(Debug, Eq, PartialEq, ThisError)]
124pub enum FleetTopologyPlanError {
125    #[error("root input repeats Component Spec admission '{component_spec}'")]
126    DuplicateAdmission { component_spec: ComponentSpecId },
127
128    #[error("root input repeats placement Subnet '{placement_subnet}'")]
129    DuplicatePlacementSubnet { placement_subnet: SubnetId },
130
131    #[error("root placement Subnet must not be anonymous")]
132    AnonymousPlacementSubnet,
133
134    #[error(transparent)]
135    Topology(#[from] canic_core::bootstrap::compiled::ComponentTopologyError),
136
137    #[error("root input references unknown Component Spec '{component_spec}'")]
138    UnknownComponentSpec { component_spec: ComponentSpecId },
139}
140
141/// Reject any admitted Component whose exact initial demand exceeds its Root's pool target.
142pub fn validate_root_pool_capacity(
143    config: &ConfigModel,
144    roots: &[RootPoolCapacityInput],
145) -> Result<(), RootPoolCapacityError> {
146    for root in roots {
147        for admission in &root.component_admissions {
148            let component = config
149                .component_specs
150                .get(&admission.component_spec)
151                .ok_or_else(|| RootPoolCapacityError::UnknownComponentSpec {
152                    component_spec: admission.component_spec.clone(),
153                    root: root.root.clone(),
154                })?;
155            let required_cycles = component.initial_cycles.to_u128();
156            if required_cycles > root.pool_target_cycles {
157                return Err(RootPoolCapacityError::Insufficient {
158                    component_spec: admission.component_spec.clone(),
159                    pool_target_cycles: root.pool_target_cycles,
160                    required_cycles,
161                    root: root.root.clone(),
162                });
163            }
164        }
165    }
166    Ok(())
167}
168
169/// Reject a Root bootstrap whose complete retained import set cannot be initialised.
170pub fn validate_root_pool_import_capacity(
171    input: &RootPoolImportCapacityInput,
172) -> Result<(), RootPoolImportCapacityError> {
173    if let Ok(import_count) = u32::try_from(input.import_count)
174        && import_count <= input.maximum_size
175    {
176        return Ok(());
177    }
178    Err(RootPoolImportCapacityError {
179        import_count: input.import_count,
180        maximum_size: input.maximum_size,
181        root: input.root.clone(),
182    })
183}
184
185/// Finalize canonical pre-creation root plans without inventing Canister principals.
186pub fn plan_initial_fleet_topology(
187    config: &ConfigModel,
188    root_inputs: Vec<PlannedFleetSubnetRootTopologyInput>,
189) -> Result<PlannedFleetTopology, FleetTopologyPlanError> {
190    let component_topology = config.compile_component_topology()?;
191    let mut fleet_subnet_roots = root_inputs
192        .into_iter()
193        .map(|input| finalize_planned_root(&component_topology, input))
194        .collect::<Result<Vec<_>, _>>()?;
195    fleet_subnet_roots.sort_by_key(|root| root.placement_subnet);
196
197    let mut placement_subnets = BTreeSet::new();
198    for root in &fleet_subnet_roots {
199        if root.placement_subnet.as_principal() == &Principal::anonymous() {
200            return Err(FleetTopologyPlanError::AnonymousPlacementSubnet);
201        }
202        if !placement_subnets.insert(root.placement_subnet) {
203            return Err(FleetTopologyPlanError::DuplicatePlacementSubnet {
204                placement_subnet: root.placement_subnet,
205            });
206        }
207        component_topology.validate_planned_root(
208            &root.component_admissions,
209            root.component_topology_digest,
210            &root.limits,
211        )?;
212    }
213    let admissions = fleet_subnet_roots
214        .iter()
215        .map(|root| root.component_admissions.as_slice())
216        .collect::<Vec<_>>();
217    component_topology.validate_fleet_admissions(&admissions)?;
218
219    Ok(PlannedFleetTopology {
220        component_topology,
221        fleet_subnet_roots,
222    })
223}
224
225fn finalize_planned_root(
226    component_topology: &ComponentTopology,
227    mut input: PlannedFleetSubnetRootTopologyInput,
228) -> Result<PlannedFleetSubnetRootTopology, FleetTopologyPlanError> {
229    input
230        .component_admissions
231        .sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
232
233    let mut seen = BTreeSet::new();
234    let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
235    for admission in input.component_admissions {
236        if !seen.insert(admission.component_spec.clone()) {
237            return Err(FleetTopologyPlanError::DuplicateAdmission {
238                component_spec: admission.component_spec,
239            });
240        }
241        let component_spec = component_topology
242            .get(&admission.component_spec)
243            .ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
244                component_spec: admission.component_spec.clone(),
245            })?;
246        component_admissions.push(ComponentSpecAdmission {
247            component_spec: admission.component_spec,
248            spec_hash: component_spec.spec_hash,
249            maximum_root_instances: admission.maximum_root_instances,
250        });
251    }
252
253    let projection = component_topology.project_for_admissions(&component_admissions)?;
254    Ok(PlannedFleetSubnetRootTopology {
255        placement_subnet: input.placement_subnet,
256        component_admissions,
257        component_topology_digest: projection.digest()?,
258        limits: input.limits,
259    })
260}