hyperi_rustlib/scaling/config.rs
1// Project: hyperi-rustlib
2// File: src/scaling/config.rs
3// Purpose: Scaling pressure configuration types
4// Language: Rust
5//
6// License: BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! Configuration for the scaling pressure calculator.
10//!
11//! [`ScalingPressureConfig`] provides the base gate thresholds shared across
12//! all apps. Per-component weights and saturation points are defined in each
13//! app's own config struct and passed as [`ScalingComponent`] at construction.
14
15use serde::{Deserialize, Serialize};
16
17/// Base configuration for scaling pressure calculation.
18///
19/// Lives in the app's config cascade so thresholds are env-var overridable
20/// (e.g., `DFE_LOADER__SCALING__MEMORY_GATE_THRESHOLD=0.9`).
21///
22/// Component weights and saturation points are app-specific -- defined in
23/// each app's config and passed to [`super::ScalingPressure::new`] via
24/// [`ScalingComponent`].
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(default)]
27pub struct ScalingPressureConfig {
28 /// Enable scaling pressure calculation.
29 /// When disabled, `calculate()` always returns 0.0.
30 pub enabled: bool,
31
32 /// Memory usage ratio that triggers the memory gate (0.0-1.0).
33 ///
34 /// When `memory_used / memory_limit >= threshold`, scaling pressure
35 /// is forced to 100.0 to trigger immediate scale-up before OOM.
36 pub memory_gate_threshold: f64,
37}
38
39impl Default for ScalingPressureConfig {
40 fn default() -> Self {
41 Self {
42 enabled: true,
43 memory_gate_threshold: 0.8,
44 }
45 }
46}
47
48impl ScalingPressureConfig {
49 /// Load from the config cascade under the `scaling` key.
50 ///
51 /// Falls back to [`ScalingPressureConfig::default()`] if config is not
52 /// initialised or the key is absent.
53 #[must_use]
54 pub fn from_cascade() -> Self {
55 #[cfg(feature = "config")]
56 {
57 if let Some(cfg) = crate::config::try_get()
58 && let Ok(scaling) = cfg.unmarshal_key_registered::<Self>("scaling")
59 {
60 return scaling;
61 }
62 }
63 Self::default()
64 }
65}
66
67/// Named scaling component with weight and saturation point.
68///
69/// Apps define their components with service-specific signals:
70///
71/// ```rust
72/// use hyperi_rustlib::scaling::ScalingComponent;
73///
74/// let components = vec![
75/// ScalingComponent::new("kafka_lag", 0.35, 100_000.0),
76/// ScalingComponent::new("buffer_depth", 0.25, 10_000.0),
77/// ScalingComponent::new("insert_latency", 0.15, 5.0),
78/// ScalingComponent::new("memory", 0.15, 1.0),
79/// ScalingComponent::new("errors", 0.10, 100.0),
80/// ];
81/// ```
82#[derive(Debug, Clone)]
83pub struct ScalingComponent {
84 /// Component name (e.g., "kafka_lag", "buffer_depth").
85 pub name: String,
86 /// Relative weight (0.0-1.0). All weights should sum to ~1.0.
87 pub weight: f64,
88 /// Value at which this component contributes its full weight.
89 /// Score = `(value / saturation).min(1.0) * weight * 100.0`.
90 pub saturation: f64,
91}
92
93impl ScalingComponent {
94 /// Create a new scaling component.
95 #[must_use]
96 pub fn new(name: impl Into<String>, weight: f64, saturation: f64) -> Self {
97 Self {
98 name: name.into(),
99 weight,
100 saturation,
101 }
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_config_defaults() {
111 let config = ScalingPressureConfig::default();
112 assert!(config.enabled);
113 assert!((config.memory_gate_threshold - 0.8).abs() < f64::EPSILON);
114 }
115
116 #[test]
117 fn test_config_serde_roundtrip() {
118 let config = ScalingPressureConfig {
119 enabled: false,
120 memory_gate_threshold: 0.9,
121 };
122 let json = serde_json::to_string(&config).unwrap();
123 let parsed: ScalingPressureConfig = serde_json::from_str(&json).unwrap();
124 assert!(!parsed.enabled);
125 assert!((parsed.memory_gate_threshold - 0.9).abs() < f64::EPSILON);
126 }
127
128 #[test]
129 fn test_component_new() {
130 let c = ScalingComponent::new("kafka_lag", 0.35, 100_000.0);
131 assert_eq!(c.name, "kafka_lag");
132 assert!((c.weight - 0.35).abs() < f64::EPSILON);
133 assert!((c.saturation - 100_000.0).abs() < f64::EPSILON);
134 }
135}