canic_core/domain/policy/placement/
scaling.rs1use crate::{
9 InternalError,
10 cdk::types::BoundedString64,
11 config::schema::{ScalePool, ScalingConfig},
12 domain::policy::PolicyError,
13};
14use thiserror::Error as ThisError;
15
16pub use crate::view::placement::scaling::ScalingWorkerPlanEntry;
17
18#[derive(Debug, ThisError)]
24pub enum ScalingPolicyError {
25 #[error("scaling capability disabled for this canister")]
26 ScalingDisabled,
27
28 #[error("scaling pool '{0}' not found")]
29 PoolNotFound(String),
30}
31
32impl From<ScalingPolicyError> for InternalError {
33 fn from(err: ScalingPolicyError) -> Self {
34 PolicyError::from(err).into()
35 }
36}
37
38#[derive(Clone, Debug)]
45pub struct ScalingPlan {
46 pub should_spawn: bool,
47 pub reason: String,
48 pub worker_entry: Option<ScalingWorkerPlanEntry>,
49}
50
51pub struct ScalingPolicy;
56
57impl ScalingPolicy {
58 pub(crate) fn plan_create_worker(
59 pool: &str,
60 worker_count: u32,
61 scaling: Option<ScalingConfig>,
62 ) -> Result<ScalingPlan, InternalError> {
63 let pool_cfg = Self::get_scaling_pool_cfg(pool, scaling)?;
64 let policy = pool_cfg.policy;
65
66 if policy.max_workers > 0 && worker_count >= policy.max_workers {
68 return Ok(ScalingPlan {
69 should_spawn: false,
70 reason: format!(
71 "pool '{pool}' at max_workers ({}/{})",
72 worker_count, policy.max_workers
73 ),
74 worker_entry: None,
75 });
76 }
77
78 if worker_count < policy.min_workers {
80 let entry = ScalingWorkerPlanEntry {
81 pool: BoundedString64::new(pool),
82 canister_role: pool_cfg.canister_role,
83 };
84
85 return Ok(ScalingPlan {
86 should_spawn: true,
87 reason: format!(
88 "pool '{pool}' below min_workers (current {worker_count}, min {})",
89 policy.min_workers
90 ),
91 worker_entry: Some(entry),
92 });
93 }
94
95 Ok(ScalingPlan {
96 should_spawn: false,
97 reason: format!(
98 "pool '{pool}' within policy bounds (current {worker_count}, min {}, max {})",
99 policy.min_workers, policy.max_workers
100 ),
101 worker_entry: None,
102 })
103 }
104
105 fn get_scaling_pool_cfg(
106 pool: &str,
107 scaling: Option<ScalingConfig>,
108 ) -> Result<ScalePool, InternalError> {
109 let Some(scaling) = scaling else {
110 return Err(ScalingPolicyError::ScalingDisabled.into());
111 };
112
113 let Some(pool_cfg) = scaling.pools.get(pool) else {
114 return Err(ScalingPolicyError::PoolNotFound(pool.to_string()).into());
115 };
116
117 Ok(pool_cfg.clone())
118 }
119}