1use crate::{
2 config::schema::{ConfigSchemaError, Validate},
3 types::{CanisterType, Cycles, TC},
4};
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, BTreeSet};
7
8mod defaults {
9 use super::Cycles;
10
11 pub fn initial_cycles() -> Cycles {
12 Cycles::new(5_000_000_000_000)
13 }
14}
15
16#[derive(Clone, Debug, Default, Deserialize, Serialize)]
21#[serde(deny_unknown_fields)]
22pub struct SubnetConfig {
23 #[serde(default)]
24 pub canisters: BTreeMap<CanisterType, CanisterConfig>,
25
26 #[serde(default)]
27 pub auto_create: BTreeSet<CanisterType>,
28
29 #[serde(default)]
30 pub subnet_directory: BTreeSet<CanisterType>,
31
32 #[serde(default)]
33 pub reserve: CanisterReserve,
34}
35
36impl SubnetConfig {
37 #[must_use]
39 pub fn directory_canisters(&self) -> Vec<CanisterType> {
40 self.subnet_directory.iter().cloned().collect()
41 }
42
43 #[must_use]
45 pub fn get_canister(&self, ty: &CanisterType) -> Option<CanisterConfig> {
46 self.canisters.get(ty).cloned()
47 }
48}
49
50impl Validate for SubnetConfig {
51 fn validate(&self) -> Result<(), ConfigSchemaError> {
52 for canister_ty in &self.subnet_directory {
54 if !self.canisters.contains_key(canister_ty) {
55 return Err(ConfigSchemaError::ValidationError(format!(
56 "subnet directory canister '{canister_ty}' is not defined in subnet",
57 )));
58 }
59 }
60
61 for canister_ty in &self.auto_create {
63 if !self.canisters.contains_key(canister_ty) {
64 return Err(ConfigSchemaError::ValidationError(format!(
65 "auto-create canister '{canister_ty}' is not defined in subnet",
66 )));
67 }
68 }
69
70 for (parent_ty, cfg) in &self.canisters {
72 if let Some(sharding) = &cfg.sharding {
74 for (pool_name, pool) in &sharding.pools {
75 if !self.canisters.contains_key(&pool.canister_type) {
76 return Err(ConfigSchemaError::ValidationError(format!(
77 "canister '{parent_ty}' sharding pool '{pool_name}' references unknown canister type '{ty}'",
78 ty = pool.canister_type
79 )));
80 }
81
82 if pool.policy.capacity == 0 {
83 return Err(ConfigSchemaError::ValidationError(format!(
84 "canister '{parent_ty}' sharding pool '{pool_name}' has zero capacity; must be > 0",
85 )));
86 }
87
88 if pool.policy.max_shards == 0 {
89 return Err(ConfigSchemaError::ValidationError(format!(
90 "canister '{parent_ty}' sharding pool '{pool_name}' has max_shards of 0; must be > 0",
91 )));
92 }
93 }
94 }
95
96 if let Some(scaling) = &cfg.scaling {
98 for (pool_name, pool) in &scaling.pools {
99 if !self.canisters.contains_key(&pool.canister_type) {
100 return Err(ConfigSchemaError::ValidationError(format!(
101 "canister '{parent_ty}' scaling pool '{pool_name}' references unknown canister type '{ty}'",
102 ty = pool.canister_type
103 )));
104 }
105
106 if pool.policy.max_workers != 0
107 && pool.policy.max_workers < pool.policy.min_workers
108 {
109 return Err(ConfigSchemaError::ValidationError(format!(
110 "canister '{parent_ty}' scaling pool '{pool_name}' has max_workers < min_workers (min {}, max {})",
111 pool.policy.min_workers, pool.policy.max_workers
112 )));
113 }
114 }
115 }
116 }
117
118 Ok(())
119 }
120}
121
122#[derive(Clone, Debug, Default, Deserialize, Serialize)]
128#[serde(deny_unknown_fields)]
129pub struct CanisterReserve {
130 pub minimum_size: u8,
131}
132
133#[derive(Clone, Debug, Default, Deserialize, Serialize)]
138#[serde(deny_unknown_fields)]
139pub struct CanisterConfig {
140 #[serde(
141 default = "defaults::initial_cycles",
142 deserialize_with = "Cycles::from_config"
143 )]
144 pub initial_cycles: Cycles,
145
146 #[serde(default)]
147 pub topup: Option<CanisterTopup>,
148
149 #[serde(default)]
150 pub scaling: Option<ScalingConfig>,
151
152 #[serde(default)]
153 pub sharding: Option<ShardingConfig>,
154}
155
156#[derive(Clone, Debug, Deserialize, Serialize)]
161#[serde(deny_unknown_fields)]
162pub struct CanisterTopup {
163 #[serde(default, deserialize_with = "Cycles::from_config")]
164 pub threshold: Cycles,
165
166 #[serde(default, deserialize_with = "Cycles::from_config")]
167 pub amount: Cycles,
168}
169
170impl Default for CanisterTopup {
171 fn default() -> Self {
172 Self {
173 threshold: Cycles::new(10 * TC),
174 amount: Cycles::new(5 * TC),
175 }
176 }
177}
178
179#[derive(Clone, Debug, Default, Deserialize, Serialize)]
190#[serde(deny_unknown_fields)]
191pub struct ScalingConfig {
192 #[serde(default)]
193 pub pools: BTreeMap<String, ScalePool>,
194}
195
196#[derive(Clone, Debug, Deserialize, Serialize)]
202#[serde(deny_unknown_fields)]
203pub struct ScalePool {
204 pub canister_type: CanisterType,
205
206 #[serde(default)]
207 pub policy: ScalePoolPolicy,
208}
209
210#[derive(Clone, Debug, Deserialize, Serialize)]
215#[serde(deny_unknown_fields, default)]
216pub struct ScalePoolPolicy {
217 pub min_workers: u32,
219
220 pub max_workers: u32,
222}
223
224impl Default for ScalePoolPolicy {
225 fn default() -> Self {
226 Self {
227 min_workers: 1,
228 max_workers: 32,
229 }
230 }
231}
232
233#[derive(Clone, Debug, Default, Deserialize, Serialize)]
244#[serde(deny_unknown_fields)]
245pub struct ShardingConfig {
246 #[serde(default)]
247 pub pools: BTreeMap<String, ShardPool>,
248}
249
250#[derive(Clone, Debug, Deserialize, Serialize)]
255#[serde(deny_unknown_fields)]
256pub struct ShardPool {
257 pub canister_type: CanisterType,
258
259 #[serde(default)]
260 pub policy: ShardPoolPolicy,
261}
262
263#[derive(Clone, Debug, Deserialize, Serialize)]
268#[serde(deny_unknown_fields, default)]
269pub struct ShardPoolPolicy {
270 pub capacity: u32,
271 pub max_shards: u32,
272}
273
274impl Default for ShardPoolPolicy {
275 fn default() -> Self {
276 Self {
277 capacity: 1_000,
278 max_shards: 4,
279 }
280 }
281}
282
283#[cfg(test)]
288mod tests {
289 use super::*;
290 use std::collections::{BTreeMap, BTreeSet};
291
292 #[test]
293 fn auto_create_entries_must_exist_in_subnet() {
294 let mut auto_create = BTreeSet::new();
295 auto_create.insert(CanisterType::from("missing_auto_canister"));
296
297 let subnet = SubnetConfig {
298 auto_create,
299 ..Default::default()
300 };
301
302 subnet
303 .validate()
304 .expect_err("expected missing auto-create type to fail");
305 }
306
307 #[test]
308 fn sharding_pool_references_must_exist_in_subnet() {
309 let managing_ty: CanisterType = "shard_hub".into();
310 let mut canisters = BTreeMap::new();
311
312 let mut sharding = ShardingConfig::default();
313 sharding.pools.insert(
314 "primary".into(),
315 ShardPool {
316 canister_type: CanisterType::from("missing_shard_worker"),
317 policy: ShardPoolPolicy::default(),
318 },
319 );
320
321 let manager_cfg = CanisterConfig {
322 sharding: Some(sharding),
323 ..Default::default()
324 };
325
326 canisters.insert(managing_ty, manager_cfg);
327
328 let subnet = SubnetConfig {
329 canisters,
330 ..Default::default()
331 };
332
333 subnet
334 .validate()
335 .expect_err("expected missing worker type to fail");
336 }
337
338 #[test]
339 fn sharding_pool_policy_requires_positive_capacity_and_shards() {
340 let managing_ty: CanisterType = "shard_hub".into();
341 let mut canisters = BTreeMap::new();
342
343 let mut sharding = ShardingConfig::default();
344 sharding.pools.insert(
345 "primary".into(),
346 ShardPool {
347 canister_type: managing_ty.clone(),
348 policy: ShardPoolPolicy {
349 capacity: 0,
350 max_shards: 0,
351 },
352 },
353 );
354
355 canisters.insert(
356 managing_ty,
357 CanisterConfig {
358 sharding: Some(sharding),
359 ..Default::default()
360 },
361 );
362
363 let subnet = SubnetConfig {
364 canisters,
365 ..Default::default()
366 };
367
368 subnet
369 .validate()
370 .expect_err("expected invalid sharding policy to fail");
371 }
372
373 #[test]
374 fn scaling_pool_policy_requires_max_ge_min_when_bounded() {
375 let mut canisters = BTreeMap::new();
376 let mut pools = BTreeMap::new();
377 pools.insert(
378 "worker".into(),
379 ScalePool {
380 canister_type: CanisterType::from("worker"),
381 policy: ScalePoolPolicy {
382 min_workers: 5,
383 max_workers: 3,
384 },
385 },
386 );
387
388 canisters.insert(CanisterType::from("worker"), CanisterConfig::default());
389
390 let manager_cfg = CanisterConfig {
391 scaling: Some(ScalingConfig { pools }),
392 ..Default::default()
393 };
394
395 canisters.insert(CanisterType::from("manager"), manager_cfg);
396
397 let subnet = SubnetConfig {
398 canisters,
399 ..Default::default()
400 };
401
402 subnet
403 .validate()
404 .expect_err("expected invalid scaling policy to fail");
405 }
406}