canic_host/component_topology/
mod.rs1#[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#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct RootComponentAdmissionInput {
27 pub component_spec: ComponentSpecId,
28 pub maximum_root_instances: u32,
29}
30
31#[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#[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#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct PlannedFleetTopology {
66 pub component_topology: ComponentTopology,
67 pub fleet_subnet_roots: Vec<PlannedFleetSubnetRootTopology>,
68}
69
70#[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#[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#[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#[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#[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
141pub 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
169pub 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
185pub 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}