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(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#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct FleetTopologyPlan {
123 pub component_topology: ComponentTopology,
124 pub fleet_subnet_roots: Vec<FleetSubnetRootBinding>,
125}
126
127#[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
159pub 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
187pub 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
227pub 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}