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, 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}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
54pub struct PlannedFleetSubnetRootTopologyInput {
55 pub placement_subnet: SubnetId,
56 pub component_admissions: Vec<RootComponentAdmissionInput>,
57 pub limits: FleetSubnetRootLimits,
58}
59
60#[derive(Clone, Debug, Eq, PartialEq)]
67pub struct PlannedFleetSubnetRootTopology {
68 pub placement_subnet: SubnetId,
69 pub component_admissions: Vec<ComponentSpecAdmission>,
70 pub component_topology_digest: ComponentTopologyDigest,
71 pub limits: FleetSubnetRootLimits,
72}
73
74#[derive(Clone, Debug, Eq, PartialEq)]
81pub struct PlannedFleetTopology {
82 pub component_topology: ComponentTopology,
83 pub fleet_subnet_roots: Vec<PlannedFleetSubnetRootTopology>,
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
93pub struct FleetTopologyPlan {
94 pub component_topology: ComponentTopology,
95 pub fleet_subnet_roots: Vec<FleetSubnetRootBinding>,
96}
97
98#[derive(Debug, Eq, PartialEq, ThisError)]
105pub enum FleetTopologyPlanError {
106 #[error(
107 "Fleet authority App '{authority_app}' does not match configured App '{configured_app}'"
108 )]
109 AppMismatch {
110 configured_app: String,
111 authority_app: String,
112 },
113
114 #[error("root input repeats Component Spec admission '{component_spec}'")]
115 DuplicateAdmission { component_spec: ComponentSpecId },
116
117 #[error("root input repeats placement Subnet '{placement_subnet}'")]
118 DuplicatePlacementSubnet { placement_subnet: SubnetId },
119
120 #[error("root placement Subnet must not be anonymous")]
121 AnonymousPlacementSubnet,
122
123 #[error(transparent)]
124 Topology(#[from] canic_core::bootstrap::compiled::ComponentTopologyError),
125
126 #[error("root input references unknown Component Spec '{component_spec}'")]
127 UnknownComponentSpec { component_spec: ComponentSpecId },
128}
129
130pub fn plan_initial_fleet_topology(
132 config: &ConfigModel,
133 root_inputs: Vec<PlannedFleetSubnetRootTopologyInput>,
134) -> Result<PlannedFleetTopology, FleetTopologyPlanError> {
135 let component_topology = config.compile_component_topology()?;
136 let mut fleet_subnet_roots = root_inputs
137 .into_iter()
138 .map(|input| finalize_planned_root(&component_topology, input))
139 .collect::<Result<Vec<_>, _>>()?;
140 fleet_subnet_roots.sort_by_key(|root| root.placement_subnet);
141
142 let mut placement_subnets = BTreeSet::new();
143 for root in &fleet_subnet_roots {
144 if root.placement_subnet.as_principal() == &Principal::anonymous() {
145 return Err(FleetTopologyPlanError::AnonymousPlacementSubnet);
146 }
147 if !placement_subnets.insert(root.placement_subnet) {
148 return Err(FleetTopologyPlanError::DuplicatePlacementSubnet {
149 placement_subnet: root.placement_subnet,
150 });
151 }
152 component_topology.validate_planned_root(
153 &root.component_admissions,
154 root.component_topology_digest,
155 &root.limits,
156 )?;
157 }
158 let admissions = fleet_subnet_roots
159 .iter()
160 .map(|root| root.component_admissions.as_slice())
161 .collect::<Vec<_>>();
162 component_topology.validate_fleet_admissions(&admissions)?;
163
164 Ok(PlannedFleetTopology {
165 component_topology,
166 fleet_subnet_roots,
167 })
168}
169
170pub fn plan_fleet_topology(
172 config: &ConfigModel,
173 authority: FleetRegistryAuthority,
174 root_inputs: Vec<FleetSubnetRootTopologyInput>,
175) -> Result<FleetTopologyPlan, FleetTopologyPlanError> {
176 if config.app_id() != &authority.binding.fleet.app {
177 return Err(FleetTopologyPlanError::AppMismatch {
178 configured_app: config.app_id().to_string(),
179 authority_app: authority.binding.fleet.app.to_string(),
180 });
181 }
182
183 let component_topology = config.compile_component_topology()?;
184 let mut fleet_subnet_roots = Vec::with_capacity(root_inputs.len());
185
186 for input in root_inputs {
187 fleet_subnet_roots.push(finalize_root_binding(
188 &component_topology,
189 &authority,
190 input,
191 )?);
192 }
193 fleet_subnet_roots.sort_by(|left, right| {
194 left.placement_subnet
195 .cmp(&right.placement_subnet)
196 .then_with(|| left.fleet_subnet_root.cmp(&right.fleet_subnet_root))
197 });
198
199 component_topology.validate_fleet_root_bindings(&fleet_subnet_roots)?;
200 Ok(FleetTopologyPlan {
201 component_topology,
202 fleet_subnet_roots,
203 })
204}
205
206fn finalize_root_binding(
207 component_topology: &ComponentTopology,
208 authority: &FleetRegistryAuthority,
209 mut input: FleetSubnetRootTopologyInput,
210) -> Result<FleetSubnetRootBinding, FleetTopologyPlanError> {
211 input
212 .component_admissions
213 .sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
214
215 let mut seen = BTreeSet::new();
216 let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
217 for admission in input.component_admissions {
218 if !seen.insert(admission.component_spec.clone()) {
219 return Err(FleetTopologyPlanError::DuplicateAdmission {
220 component_spec: admission.component_spec,
221 });
222 }
223 let component_spec = component_topology
224 .get(&admission.component_spec)
225 .ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
226 component_spec: admission.component_spec.clone(),
227 })?;
228 component_admissions.push(ComponentSpecAdmission {
229 component_spec: admission.component_spec,
230 spec_hash: component_spec.spec_hash,
231 maximum_root_instances: admission.maximum_root_instances,
232 });
233 }
234
235 let projection = component_topology.project_for_admissions(&component_admissions)?;
236 Ok(FleetSubnetRootBinding {
237 authority: authority.clone(),
238 placement_subnet: input.placement_subnet,
239 fleet_subnet_root: input.fleet_subnet_root,
240 component_admissions,
241 component_topology_digest: projection.digest()?,
242 limits: input.limits,
243 })
244}
245
246fn finalize_planned_root(
247 component_topology: &ComponentTopology,
248 mut input: PlannedFleetSubnetRootTopologyInput,
249) -> Result<PlannedFleetSubnetRootTopology, FleetTopologyPlanError> {
250 input
251 .component_admissions
252 .sort_by(|left, right| left.component_spec.cmp(&right.component_spec));
253
254 let mut seen = BTreeSet::new();
255 let mut component_admissions = Vec::with_capacity(input.component_admissions.len());
256 for admission in input.component_admissions {
257 if !seen.insert(admission.component_spec.clone()) {
258 return Err(FleetTopologyPlanError::DuplicateAdmission {
259 component_spec: admission.component_spec,
260 });
261 }
262 let component_spec = component_topology
263 .get(&admission.component_spec)
264 .ok_or_else(|| FleetTopologyPlanError::UnknownComponentSpec {
265 component_spec: admission.component_spec.clone(),
266 })?;
267 component_admissions.push(ComponentSpecAdmission {
268 component_spec: admission.component_spec,
269 spec_hash: component_spec.spec_hash,
270 maximum_root_instances: admission.maximum_root_instances,
271 });
272 }
273
274 let projection = component_topology.project_for_admissions(&component_admissions)?;
275 Ok(PlannedFleetSubnetRootTopology {
276 placement_subnet: input.placement_subnet,
277 component_admissions,
278 component_topology_digest: projection.digest()?,
279 limits: input.limits,
280 })
281}