canic_host/component_topology/
mod.rs1#[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#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct RootComponentAdmissionInput {
29 pub component_spec: ComponentSpecId,
30 pub maximum_root_instances: u32,
31}
32
33#[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#[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#[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#[derive(Clone, Debug, Eq, PartialEq)]
82pub struct PlannedFleetTopology {
83 pub component_topology: ComponentTopology,
84 pub fleet_subnet_roots: Vec<PlannedFleetSubnetRootTopology>,
85}
86
87#[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#[derive(Clone, Debug, Eq, PartialEq)]
97pub struct RootPoolImportCapacityInput {
98 pub import_count: usize,
99 pub maximum_size: u32,
100 pub root: String,
101}
102
103#[derive(Debug, Eq, PartialEq, ThisError)]
105pub enum RootPoolCapacityError {
106 #[error(
107 "Root {root} pool target {pool_target_cycles} cycles is below admitted Component Spec '{component_spec}' initial demand {required_cycles} cycles"
108 )]
109 Insufficient {
110 component_spec: ComponentSpecId,
111 pool_target_cycles: u128,
112 required_cycles: u128,
113 root: String,
114 },
115
116 #[error("Root {root} admits unknown Component Spec '{component_spec}'")]
117 UnknownComponentSpec {
118 component_spec: ComponentSpecId,
119 root: String,
120 },
121}
122
123#[derive(Debug, Eq, PartialEq, ThisError)]
125#[error(
126 "Root {root} has {import_count} retained pool imports, above initialisation maximum {maximum_size}"
127)]
128pub struct RootPoolImportCapacityError {
129 pub import_count: usize,
130 pub maximum_size: u32,
131 pub root: String,
132}
133
134#[derive(Clone, Debug, Eq, PartialEq)]
141pub struct FleetTopologyPlan {
142 pub component_topology: ComponentTopology,
143 pub fleet_subnet_roots: Vec<FleetSubnetRootBinding>,
144}
145
146#[derive(Debug, Eq, PartialEq, ThisError)]
153pub enum FleetTopologyPlanError {
154 #[error(
155 "Fleet authority App '{authority_app}' does not match configured App '{configured_app}'"
156 )]
157 AppMismatch {
158 configured_app: String,
159 authority_app: String,
160 },
161
162 #[error("root input repeats Component Spec admission '{component_spec}'")]
163 DuplicateAdmission { component_spec: ComponentSpecId },
164
165 #[error("root input repeats placement Subnet '{placement_subnet}'")]
166 DuplicatePlacementSubnet { placement_subnet: SubnetId },
167
168 #[error("root placement Subnet must not be anonymous")]
169 AnonymousPlacementSubnet,
170
171 #[error(transparent)]
172 Topology(#[from] canic_core::bootstrap::compiled::ComponentTopologyError),
173
174 #[error("root input references unknown Component Spec '{component_spec}'")]
175 UnknownComponentSpec { component_spec: ComponentSpecId },
176}
177
178pub fn validate_root_pool_capacity(
180 config: &ConfigModel,
181 roots: &[RootPoolCapacityInput],
182) -> Result<(), RootPoolCapacityError> {
183 for root in roots {
184 for admission in &root.component_admissions {
185 let component = config
186 .component_specs
187 .get(&admission.component_spec)
188 .ok_or_else(|| RootPoolCapacityError::UnknownComponentSpec {
189 component_spec: admission.component_spec.clone(),
190 root: root.root.clone(),
191 })?;
192 let required_cycles = component.initial_cycles.to_u128();
193 if required_cycles > root.pool_target_cycles {
194 return Err(RootPoolCapacityError::Insufficient {
195 component_spec: admission.component_spec.clone(),
196 pool_target_cycles: root.pool_target_cycles,
197 required_cycles,
198 root: root.root.clone(),
199 });
200 }
201 }
202 }
203 Ok(())
204}
205
206pub fn validate_root_pool_import_capacity(
208 input: &RootPoolImportCapacityInput,
209) -> Result<(), RootPoolImportCapacityError> {
210 if let Ok(import_count) = u32::try_from(input.import_count)
211 && import_count <= input.maximum_size
212 {
213 return Ok(());
214 }
215 Err(RootPoolImportCapacityError {
216 import_count: input.import_count,
217 maximum_size: input.maximum_size,
218 root: input.root.clone(),
219 })
220}
221
222pub fn plan_initial_fleet_topology(
224 config: &ConfigModel,
225 root_inputs: Vec<PlannedFleetSubnetRootTopologyInput>,
226) -> Result<PlannedFleetTopology, FleetTopologyPlanError> {
227 let component_topology = config.compile_component_topology()?;
228 let mut fleet_subnet_roots = root_inputs
229 .into_iter()
230 .map(|input| finalize_planned_root(&component_topology, input))
231 .collect::<Result<Vec<_>, _>>()?;
232 fleet_subnet_roots.sort_by_key(|root| root.placement_subnet);
233
234 let mut placement_subnets = BTreeSet::new();
235 for root in &fleet_subnet_roots {
236 if root.placement_subnet.as_principal() == &Principal::anonymous() {
237 return Err(FleetTopologyPlanError::AnonymousPlacementSubnet);
238 }
239 if !placement_subnets.insert(root.placement_subnet) {
240 return Err(FleetTopologyPlanError::DuplicatePlacementSubnet {
241 placement_subnet: root.placement_subnet,
242 });
243 }
244 component_topology.validate_planned_root(
245 &root.component_admissions,
246 root.component_topology_digest,
247 &root.limits,
248 )?;
249 }
250 let admissions = fleet_subnet_roots
251 .iter()
252 .map(|root| root.component_admissions.as_slice())
253 .collect::<Vec<_>>();
254 component_topology.validate_fleet_admissions(&admissions)?;
255
256 Ok(PlannedFleetTopology {
257 component_topology,
258 fleet_subnet_roots,
259 })
260}
261
262pub fn plan_fleet_topology(
264 config: &ConfigModel,
265 authority: FleetRegistryAuthority,
266 root_inputs: Vec<FleetSubnetRootTopologyInput>,
267) -> Result<FleetTopologyPlan, FleetTopologyPlanError> {
268 if config.app_id() != &authority.binding.fleet.app {
269 return Err(FleetTopologyPlanError::AppMismatch {
270 configured_app: config.app_id().to_string(),
271 authority_app: authority.binding.fleet.app.to_string(),
272 });
273 }
274
275 let component_topology = config.compile_component_topology()?;
276 let mut fleet_subnet_roots = Vec::with_capacity(root_inputs.len());
277
278 for input in root_inputs {
279 fleet_subnet_roots.push(finalize_root_binding(
280 &component_topology,
281 &authority,
282 input,
283 )?);
284 }
285 fleet_subnet_roots.sort_by(|left, right| {
286 left.placement_subnet
287 .cmp(&right.placement_subnet)
288 .then_with(|| left.fleet_subnet_root.cmp(&right.fleet_subnet_root))
289 });
290
291 component_topology.validate_fleet_subnet_root_bindings(&fleet_subnet_roots)?;
292 Ok(FleetTopologyPlan {
293 component_topology,
294 fleet_subnet_roots,
295 })
296}
297
298fn finalize_root_binding(
299 component_topology: &ComponentTopology,
300 authority: &FleetRegistryAuthority,
301 mut input: FleetSubnetRootTopologyInput,
302) -> Result<FleetSubnetRootBinding, FleetTopologyPlanError> {
303 input
304 .component_admissions
305 .sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
306
307 let mut seen = BTreeSet::new();
308 let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
309 for admission in input.component_admissions {
310 if !seen.insert(admission.component_spec.clone()) {
311 return Err(FleetTopologyPlanError::DuplicateAdmission {
312 component_spec: admission.component_spec,
313 });
314 }
315 let component_spec = component_topology
316 .get(&admission.component_spec)
317 .ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
318 component_spec: admission.component_spec.clone(),
319 })?;
320 component_admissions.push(ComponentSpecAdmission {
321 component_spec: admission.component_spec,
322 spec_hash: component_spec.spec_hash,
323 maximum_root_instances: admission.maximum_root_instances,
324 });
325 }
326
327 let projection = component_topology.project_for_admissions(&component_admissions)?;
328 Ok(FleetSubnetRootBinding {
329 authority: authority.clone(),
330 placement_subnet: input.placement_subnet,
331 fleet_subnet_root: input.fleet_subnet_root,
332 component_admissions,
333 component_topology_digest: projection.digest()?,
334 limits: input.limits,
335 funding: input.funding,
336 })
337}
338
339fn finalize_planned_root(
340 component_topology: &ComponentTopology,
341 mut input: PlannedFleetSubnetRootTopologyInput,
342) -> Result<PlannedFleetSubnetRootTopology, FleetTopologyPlanError> {
343 input
344 .component_admissions
345 .sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
346
347 let mut seen = BTreeSet::new();
348 let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
349 for admission in input.component_admissions {
350 if !seen.insert(admission.component_spec.clone()) {
351 return Err(FleetTopologyPlanError::DuplicateAdmission {
352 component_spec: admission.component_spec,
353 });
354 }
355 let component_spec = component_topology
356 .get(&admission.component_spec)
357 .ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
358 component_spec: admission.component_spec.clone(),
359 })?;
360 component_admissions.push(ComponentSpecAdmission {
361 component_spec: admission.component_spec,
362 spec_hash: component_spec.spec_hash,
363 maximum_root_instances: admission.maximum_root_instances,
364 });
365 }
366
367 let projection = component_topology.project_for_admissions(&component_admissions)?;
368 Ok(PlannedFleetSubnetRootTopology {
369 placement_subnet: input.placement_subnet,
370 component_admissions,
371 component_topology_digest: projection.digest()?,
372 limits: input.limits,
373 })
374}