Skip to main content

hyperi_rustlib/deployment/
keda.rs

1// Project:   hyperi-rustlib
2// File:      src/deployment/keda.rs
3// Purpose:   KEDA autoscaling configuration and contract types
4// Language:  Rust
5//
6// License:   BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! KEDA autoscaling configuration.
10//!
11//! [`KedaConfig`] lives in the app's config cascade so thresholds are
12//! overridable via env vars (e.g., `DFE_LOADER__KEDA__KAFKA_LAG_THRESHOLD=5000`).
13//!
14//! [`KedaContract`] is the subset validated against Helm `values.yaml`.
15
16use serde::{Deserialize, Serialize};
17
18/// KEDA autoscaling configuration for the app config cascade.
19///
20/// Include this in your app's `Config` struct so KEDA thresholds
21/// participate in the figment cascade and are env-var overridable.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(default)]
24pub struct KedaConfig {
25    /// Whether KEDA scaling is enabled.
26    pub enabled: bool,
27    /// Minimum replica count (0 = scale-to-zero).
28    pub min_replicas: u32,
29    /// Maximum replica count.
30    pub max_replicas: u32,
31    /// Seconds between KEDA polling the scaler.
32    pub polling_interval: u32,
33    /// Seconds before scale-down after load drops.
34    pub cooldown_period: u32,
35    /// Scale when consumer group lag exceeds this per partition.
36    pub kafka_lag_threshold: u64,
37    /// Wake from zero replicas when lag exceeds this.
38    pub activation_lag_threshold: u64,
39    /// Enable CPU-based scaling trigger.
40    pub cpu_enabled: bool,
41    /// CPU utilisation percentage threshold.
42    pub cpu_threshold: u32,
43}
44
45impl Default for KedaConfig {
46    fn default() -> Self {
47        Self {
48            enabled: true,
49            min_replicas: 1,
50            max_replicas: 10,
51            polling_interval: 15,
52            cooldown_period: 300,
53            kafka_lag_threshold: 1000,
54            activation_lag_threshold: 0,
55            cpu_enabled: true,
56            cpu_threshold: 80,
57        }
58    }
59}
60
61/// KEDA contract points validated against Helm `values.yaml`.
62///
63/// Built from [`KedaConfig`] defaults. Use [`KedaContract::from_config`]
64/// to convert.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct KedaContract {
67    pub min_replicas: u32,
68    pub max_replicas: u32,
69    pub polling_interval: u32,
70    pub cooldown_period: u32,
71    pub kafka_lag_threshold: u64,
72    pub activation_lag_threshold: u64,
73    pub cpu_enabled: bool,
74    pub cpu_threshold: u32,
75}
76
77impl KedaContract {
78    /// Build a contract from a [`KedaConfig`].
79    #[must_use]
80    pub fn from_config(config: &KedaConfig) -> Self {
81        Self {
82            min_replicas: config.min_replicas,
83            max_replicas: config.max_replicas,
84            polling_interval: config.polling_interval,
85            cooldown_period: config.cooldown_period,
86            kafka_lag_threshold: config.kafka_lag_threshold,
87            activation_lag_threshold: config.activation_lag_threshold,
88            cpu_enabled: config.cpu_enabled,
89            cpu_threshold: config.cpu_threshold,
90        }
91    }
92}
93
94impl Default for KedaContract {
95    fn default() -> Self {
96        Self::from_config(&KedaConfig::default())
97    }
98}
99
100impl From<&KedaConfig> for KedaContract {
101    fn from(config: &KedaConfig) -> Self {
102        Self::from_config(config)
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_keda_config_defaults() {
112        let cfg = KedaConfig::default();
113        assert!(cfg.enabled);
114        assert_eq!(cfg.min_replicas, 1);
115        assert_eq!(cfg.max_replicas, 10);
116        assert_eq!(cfg.polling_interval, 15);
117        assert_eq!(cfg.cooldown_period, 300);
118        assert_eq!(cfg.kafka_lag_threshold, 1000);
119        assert_eq!(cfg.activation_lag_threshold, 0);
120        assert!(cfg.cpu_enabled);
121        assert_eq!(cfg.cpu_threshold, 80);
122    }
123
124    #[test]
125    fn test_keda_contract_from_config() {
126        let cfg = KedaConfig {
127            kafka_lag_threshold: 5000,
128            cpu_threshold: 90,
129            ..Default::default()
130        };
131        let contract = KedaContract::from_config(&cfg);
132        assert_eq!(contract.kafka_lag_threshold, 5000);
133        assert_eq!(contract.cpu_threshold, 90);
134    }
135
136    #[test]
137    fn test_keda_config_serde_roundtrip() {
138        let cfg = KedaConfig::default();
139        let yaml = serde_yaml_ng::to_string(&cfg).unwrap();
140        let parsed: KedaConfig = serde_yaml_ng::from_str(&yaml).unwrap();
141        assert_eq!(parsed.kafka_lag_threshold, cfg.kafka_lag_threshold);
142    }
143}