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_core::{
12    bootstrap::compiled::{ComponentTopology, ConfigModel},
13    ids::{
14        ComponentSpecAdmission, ComponentSpecId, ComponentTopologyDigest, FleetRegistryAuthority,
15        FleetSubnetRootBinding, FleetSubnetRootFundingAuthority, FleetSubnetRootLimits, SubnetId,
16    },
17};
18use std::collections::BTreeSet;
19use thiserror::Error as ThisError;
20
21///
22/// RootComponentAdmissionInput
23///
24/// Operator-planned positive instance capacity for one Spec on one resolved root.
25///
26
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct RootComponentAdmissionInput {
29    pub component_spec: ComponentSpecId,
30    pub maximum_root_instances: u32,
31}
32
33///
34/// FleetSubnetRootTopologyInput
35///
36/// Exact resolved physical root placement, admissions, and immutable aggregate limits.
37///
38
39#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct FleetSubnetRootTopologyInput {
41    pub placement_subnet: SubnetId,
42    pub fleet_subnet_root: Principal,
43    pub component_admissions: Vec<RootComponentAdmissionInput>,
44    pub limits: FleetSubnetRootLimits,
45    pub funding: FleetSubnetRootFundingAuthority,
46}
47
48///
49/// PlannedFleetSubnetRootTopologyInput
50///
51/// Exact root placement, admissions, and limits resolved before Canister creation.
52///
53
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct PlannedFleetSubnetRootTopologyInput {
56    pub placement_subnet: SubnetId,
57    pub component_admissions: Vec<RootComponentAdmissionInput>,
58    pub limits: FleetSubnetRootLimits,
59}
60
61///
62/// PlannedFleetSubnetRootTopology
63///
64/// Canonical pre-creation root topology with no fabricated Canister principal.
65///
66
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct PlannedFleetSubnetRootTopology {
69    pub placement_subnet: SubnetId,
70    pub component_admissions: Vec<ComponentSpecAdmission>,
71    pub component_topology_digest: ComponentTopologyDigest,
72    pub limits: FleetSubnetRootLimits,
73}
74
75///
76/// PlannedFleetTopology
77///
78/// Canonical Component Topology and every validated pre-creation root plan.
79///
80
81#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct PlannedFleetTopology {
83    pub component_topology: ComponentTopology,
84    pub fleet_subnet_roots: Vec<PlannedFleetSubnetRootTopology>,
85}
86
87/// Root-local admitted Component demand checked against one empty-pool target.
88#[derive(Clone, Debug, Eq, PartialEq)]
89pub struct RootPoolCapacityInput {
90    pub component_admissions: Vec<ComponentSpecAdmission>,
91    pub pool_target_cycles: u128,
92    pub root: String,
93}
94
95/// Exact fail-closed mismatch between admitted Component demand and pool capacity.
96#[derive(Debug, Eq, PartialEq, ThisError)]
97pub enum RootPoolCapacityError {
98    #[error(
99        "Root {root} pool target {pool_target_cycles} cycles is below admitted Component Spec '{component_spec}' initial demand {required_cycles} cycles"
100    )]
101    Insufficient {
102        component_spec: ComponentSpecId,
103        pool_target_cycles: u128,
104        required_cycles: u128,
105        root: String,
106    },
107
108    #[error("Root {root} admits unknown Component Spec '{component_spec}'")]
109    UnknownComponentSpec {
110        component_spec: ComponentSpecId,
111        root: String,
112    },
113}
114
115///
116/// FleetTopologyPlan
117///
118/// Canonical Fleet topology plus every validated root-local binding.
119///
120
121#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct FleetTopologyPlan {
123    pub component_topology: ComponentTopology,
124    pub fleet_subnet_roots: Vec<FleetSubnetRootBinding>,
125}
126
127///
128/// FleetTopologyPlanError
129///
130/// Typed rejection while finalizing immutable root topology input.
131///
132
133#[derive(Debug, Eq, PartialEq, ThisError)]
134pub enum FleetTopologyPlanError {
135    #[error(
136        "Fleet authority App '{authority_app}' does not match configured App '{configured_app}'"
137    )]
138    AppMismatch {
139        configured_app: String,
140        authority_app: String,
141    },
142
143    #[error("root input repeats Component Spec admission '{component_spec}'")]
144    DuplicateAdmission { component_spec: ComponentSpecId },
145
146    #[error("root input repeats placement Subnet '{placement_subnet}'")]
147    DuplicatePlacementSubnet { placement_subnet: SubnetId },
148
149    #[error("root placement Subnet must not be anonymous")]
150    AnonymousPlacementSubnet,
151
152    #[error(transparent)]
153    Topology(#[from] canic_core::bootstrap::compiled::ComponentTopologyError),
154
155    #[error("root input references unknown Component Spec '{component_spec}'")]
156    UnknownComponentSpec { component_spec: ComponentSpecId },
157}
158
159/// Reject any admitted Component whose exact initial demand exceeds its Root's pool target.
160pub fn validate_root_pool_capacity(
161    config: &ConfigModel,
162    roots: &[RootPoolCapacityInput],
163) -> Result<(), RootPoolCapacityError> {
164    for root in roots {
165        for admission in &root.component_admissions {
166            let component = config
167                .component_specs
168                .get(&admission.component_spec)
169                .ok_or_else(|| RootPoolCapacityError::UnknownComponentSpec {
170                    component_spec: admission.component_spec.clone(),
171                    root: root.root.clone(),
172                })?;
173            let required_cycles = component.initial_cycles.to_u128();
174            if required_cycles > root.pool_target_cycles {
175                return Err(RootPoolCapacityError::Insufficient {
176                    component_spec: admission.component_spec.clone(),
177                    pool_target_cycles: root.pool_target_cycles,
178                    required_cycles,
179                    root: root.root.clone(),
180                });
181            }
182        }
183    }
184    Ok(())
185}
186
187/// Finalize canonical pre-creation root plans without inventing Canister principals.
188pub fn plan_initial_fleet_topology(
189    config: &ConfigModel,
190    root_inputs: Vec<PlannedFleetSubnetRootTopologyInput>,
191) -> Result<PlannedFleetTopology, FleetTopologyPlanError> {
192    let component_topology = config.compile_component_topology()?;
193    let mut fleet_subnet_roots = root_inputs
194        .into_iter()
195        .map(|input| finalize_planned_root(&component_topology, input))
196        .collect::<Result<Vec<_>, _>>()?;
197    fleet_subnet_roots.sort_by_key(|root| root.placement_subnet);
198
199    let mut placement_subnets = BTreeSet::new();
200    for root in &fleet_subnet_roots {
201        if root.placement_subnet.as_principal() == &Principal::anonymous() {
202            return Err(FleetTopologyPlanError::AnonymousPlacementSubnet);
203        }
204        if !placement_subnets.insert(root.placement_subnet) {
205            return Err(FleetTopologyPlanError::DuplicatePlacementSubnet {
206                placement_subnet: root.placement_subnet,
207            });
208        }
209        component_topology.validate_planned_root(
210            &root.component_admissions,
211            root.component_topology_digest,
212            &root.limits,
213        )?;
214    }
215    let admissions = fleet_subnet_roots
216        .iter()
217        .map(|root| root.component_admissions.as_slice())
218        .collect::<Vec<_>>();
219    component_topology.validate_fleet_admissions(&admissions)?;
220
221    Ok(PlannedFleetTopology {
222        component_topology,
223        fleet_subnet_roots,
224    })
225}
226
227/// Finalize canonical root bindings without accepting caller-supplied hashes or digests.
228pub fn plan_fleet_topology(
229    config: &ConfigModel,
230    authority: FleetRegistryAuthority,
231    root_inputs: Vec<FleetSubnetRootTopologyInput>,
232) -> Result<FleetTopologyPlan, FleetTopologyPlanError> {
233    if config.app_id() != &authority.binding.fleet.app {
234        return Err(FleetTopologyPlanError::AppMismatch {
235            configured_app: config.app_id().to_string(),
236            authority_app: authority.binding.fleet.app.to_string(),
237        });
238    }
239
240    let component_topology = config.compile_component_topology()?;
241    let mut fleet_subnet_roots = Vec::with_capacity(root_inputs.len());
242
243    for input in root_inputs {
244        fleet_subnet_roots.push(finalize_root_binding(
245            &component_topology,
246            &authority,
247            input,
248        )?);
249    }
250    fleet_subnet_roots.sort_by(|left, right| {
251        left.placement_subnet
252            .cmp(&right.placement_subnet)
253            .then_with(|| left.fleet_subnet_root.cmp(&right.fleet_subnet_root))
254    });
255
256    component_topology.validate_fleet_subnet_root_bindings(&fleet_subnet_roots)?;
257    Ok(FleetTopologyPlan {
258        component_topology,
259        fleet_subnet_roots,
260    })
261}
262
263fn finalize_root_binding(
264    component_topology: &ComponentTopology,
265    authority: &FleetRegistryAuthority,
266    mut input: FleetSubnetRootTopologyInput,
267) -> Result<FleetSubnetRootBinding, FleetTopologyPlanError> {
268    input
269        .component_admissions
270        .sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
271
272    let mut seen = BTreeSet::new();
273    let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
274    for admission in input.component_admissions {
275        if !seen.insert(admission.component_spec.clone()) {
276            return Err(FleetTopologyPlanError::DuplicateAdmission {
277                component_spec: admission.component_spec,
278            });
279        }
280        let component_spec = component_topology
281            .get(&admission.component_spec)
282            .ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
283                component_spec: admission.component_spec.clone(),
284            })?;
285        component_admissions.push(ComponentSpecAdmission {
286            component_spec: admission.component_spec,
287            spec_hash: component_spec.spec_hash,
288            maximum_root_instances: admission.maximum_root_instances,
289        });
290    }
291
292    let projection = component_topology.project_for_admissions(&component_admissions)?;
293    Ok(FleetSubnetRootBinding {
294        authority: authority.clone(),
295        placement_subnet: input.placement_subnet,
296        fleet_subnet_root: input.fleet_subnet_root,
297        component_admissions,
298        component_topology_digest: projection.digest()?,
299        limits: input.limits,
300        funding: input.funding,
301    })
302}
303
304fn finalize_planned_root(
305    component_topology: &ComponentTopology,
306    mut input: PlannedFleetSubnetRootTopologyInput,
307) -> Result<PlannedFleetSubnetRootTopology, FleetTopologyPlanError> {
308    input
309        .component_admissions
310        .sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
311
312    let mut seen = BTreeSet::new();
313    let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
314    for admission in input.component_admissions {
315        if !seen.insert(admission.component_spec.clone()) {
316            return Err(FleetTopologyPlanError::DuplicateAdmission {
317                component_spec: admission.component_spec,
318            });
319        }
320        let component_spec = component_topology
321            .get(&admission.component_spec)
322            .ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
323                component_spec: admission.component_spec.clone(),
324            })?;
325        component_admissions.push(ComponentSpecAdmission {
326            component_spec: admission.component_spec,
327            spec_hash: component_spec.spec_hash,
328            maximum_root_instances: admission.maximum_root_instances,
329        });
330    }
331
332    let projection = component_topology.project_for_admissions(&component_admissions)?;
333    Ok(PlannedFleetSubnetRootTopology {
334        placement_subnet: input.placement_subnet,
335        component_admissions,
336        component_topology_digest: projection.digest()?,
337        limits: input.limits,
338    })
339}