Skip to main content

fraiseql_server/config/
pool_tuning.rs

1//! Connection pool pressure monitoring configuration.
2
3use serde::{Deserialize, Serialize};
4
5/// Configuration for connection pool pressure monitoring with scaling recommendations.
6///
7/// This monitor samples `PoolMetrics` at a configurable interval and emits
8/// scaling recommendations via `fraiseql_pool_tuning_*` Prometheus metrics and
9/// log lines. **It does not resize the pool at runtime** — the underlying
10/// `deadpool-postgres` library does not expose a `resize()` API.
11///
12/// To act on recommendations: adjust `max_connections` in `fraiseql.toml` and
13/// restart the server. Active pool resizing is tracked as future work (migration
14/// to `bb8` with `resize()` support).
15///
16/// # Recommendation mode
17///
18/// All scaling decisions are advisory. When a recommendation fires, the monitor:
19/// - Updates `fraiseql_pool_tuning_adjustments_total` (Prometheus counter)
20/// - Logs the recommendation at `WARN` level
21/// - Updates `recommended_size()` for external inspection
22///
23/// To suppress the `WARN` noise in environments that already tune the pool
24/// manually, set `enabled = false` in `[pool_tuning]`.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct PoolPressureMonitorConfig {
27    /// Enable adaptive pool sizing.  Default: `false`.
28    #[serde(default)]
29    pub enabled: bool,
30
31    /// Minimum pool size.  The tuner never shrinks below this value.  Default: 5.
32    #[serde(default = "default_min_pool_size")]
33    pub min_pool_size: u32,
34
35    /// Maximum pool size.  The tuner never grows above this value.  Default: 50.
36    #[serde(default = "default_max_pool_size")]
37    pub max_pool_size: u32,
38
39    /// Maximum acceptable queue depth before scaling up.  Default: 3.
40    #[serde(default = "default_target_queue_depth")]
41    pub target_queue_depth: u32,
42
43    /// Connections to add per scale-up step.  Default: 5.
44    #[serde(default = "default_scale_up_step")]
45    pub scale_up_step: u32,
46
47    /// Connections to remove per scale-down step.  Default: 2.
48    #[serde(default = "default_scale_down_step")]
49    pub scale_down_step: u32,
50
51    /// Minimum idle ratio (idle / total) before considering a scale-down.
52    /// Default: 0.5 (50% idle connections triggers potential shrink).
53    #[serde(default = "default_scale_down_idle_ratio")]
54    pub scale_down_idle_ratio: f64,
55
56    /// Polling interval in milliseconds.  Default: 30 000 (30 s).
57    #[serde(default = "default_tuning_interval_ms")]
58    pub tuning_interval_ms: u64,
59
60    /// Consecutive samples above threshold required before acting.  Default: 3.
61    #[serde(default = "default_samples_before_action")]
62    pub samples_before_action: u32,
63}
64
65const fn default_min_pool_size() -> u32 {
66    5
67}
68const fn default_max_pool_size() -> u32 {
69    50
70}
71const fn default_target_queue_depth() -> u32 {
72    3
73}
74const fn default_scale_up_step() -> u32 {
75    5
76}
77const fn default_scale_down_step() -> u32 {
78    2
79}
80const fn default_scale_down_idle_ratio() -> f64 {
81    0.5
82}
83const fn default_tuning_interval_ms() -> u64 {
84    30_000
85}
86const fn default_samples_before_action() -> u32 {
87    3
88}
89
90impl Default for PoolPressureMonitorConfig {
91    fn default() -> Self {
92        Self {
93            enabled:               false,
94            min_pool_size:         default_min_pool_size(),
95            max_pool_size:         default_max_pool_size(),
96            target_queue_depth:    default_target_queue_depth(),
97            scale_up_step:         default_scale_up_step(),
98            scale_down_step:       default_scale_down_step(),
99            scale_down_idle_ratio: default_scale_down_idle_ratio(),
100            tuning_interval_ms:    default_tuning_interval_ms(),
101            samples_before_action: default_samples_before_action(),
102        }
103    }
104}
105
106/// Deprecated alias for [`PoolPressureMonitorConfig`].
107///
108/// This type was renamed in v2.0.1 to clarify that pool monitoring operates in
109/// recommendation mode only — the pool is not resized at runtime.
110/// Use [`PoolPressureMonitorConfig`] in new code.
111#[deprecated(since = "2.0.1", note = "Use PoolPressureMonitorConfig")]
112pub type PoolTuningConfig = PoolPressureMonitorConfig;
113
114impl PoolPressureMonitorConfig {
115    /// Returns a builder for `PoolPressureMonitorConfig`.
116    #[must_use = "builder does nothing until .build() is called"]
117    pub fn builder() -> PoolPressureMonitorConfigBuilder {
118        PoolPressureMonitorConfigBuilder::default()
119    }
120
121    /// Validate configuration invariants.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error string if:
126    /// - `min_pool_size >= max_pool_size`
127    /// - `scale_up_step == 0` or `scale_down_step == 0`
128    /// - `scale_down_idle_ratio` is outside `[0.0, 1.0]`
129    /// - `tuning_interval_ms < 100`
130    pub fn validate(&self) -> Result<(), String> {
131        if self.min_pool_size >= self.max_pool_size {
132            return Err(format!(
133                "pool_tuning: min_pool_size ({}) must be less than max_pool_size ({})",
134                self.min_pool_size, self.max_pool_size
135            ));
136        }
137        if self.scale_up_step == 0 {
138            return Err("pool_tuning: scale_up_step must be > 0".to_string());
139        }
140        if self.scale_down_step == 0 {
141            return Err("pool_tuning: scale_down_step must be > 0".to_string());
142        }
143        if !(0.0..=1.0).contains(&self.scale_down_idle_ratio) {
144            return Err(format!(
145                "pool_tuning: scale_down_idle_ratio ({}) must be in [0.0, 1.0]",
146                self.scale_down_idle_ratio
147            ));
148        }
149        if self.tuning_interval_ms < 100 {
150            return Err(format!(
151                "pool_tuning: tuning_interval_ms ({}) must be >= 100",
152                self.tuning_interval_ms
153            ));
154        }
155        Ok(())
156    }
157}
158
159/// Builder for [`PoolPressureMonitorConfig`].
160#[derive(Debug, Default)]
161pub struct PoolPressureMonitorConfigBuilder {
162    inner: PoolPressureMonitorConfig,
163}
164
165impl PoolPressureMonitorConfigBuilder {
166    /// Enables or disables adaptive pool sizing.
167    #[must_use = "builder method returns modified builder"]
168    pub const fn enabled(mut self, enabled: bool) -> Self {
169        self.inner.enabled = enabled;
170        self
171    }
172
173    /// Sets the minimum pool size.
174    #[must_use = "builder method returns modified builder"]
175    pub const fn min_pool_size(mut self, min_pool_size: u32) -> Self {
176        self.inner.min_pool_size = min_pool_size;
177        self
178    }
179
180    /// Sets the maximum pool size.
181    #[must_use = "builder method returns modified builder"]
182    pub const fn max_pool_size(mut self, max_pool_size: u32) -> Self {
183        self.inner.max_pool_size = max_pool_size;
184        self
185    }
186
187    /// Sets the maximum queue depth before scaling up.
188    #[must_use = "builder method returns modified builder"]
189    pub const fn target_queue_depth(mut self, target_queue_depth: u32) -> Self {
190        self.inner.target_queue_depth = target_queue_depth;
191        self
192    }
193
194    /// Sets the number of connections to add per scale-up step.
195    #[must_use = "builder method returns modified builder"]
196    pub const fn scale_up_step(mut self, scale_up_step: u32) -> Self {
197        self.inner.scale_up_step = scale_up_step;
198        self
199    }
200
201    /// Sets the number of connections to remove per scale-down step.
202    #[must_use = "builder method returns modified builder"]
203    pub const fn scale_down_step(mut self, scale_down_step: u32) -> Self {
204        self.inner.scale_down_step = scale_down_step;
205        self
206    }
207
208    /// Sets the minimum idle ratio before considering a scale-down.
209    #[must_use = "builder method returns modified builder"]
210    pub const fn scale_down_idle_ratio(mut self, scale_down_idle_ratio: f64) -> Self {
211        self.inner.scale_down_idle_ratio = scale_down_idle_ratio;
212        self
213    }
214
215    /// Sets the polling interval in milliseconds.
216    #[must_use = "builder method returns modified builder"]
217    pub const fn tuning_interval_ms(mut self, tuning_interval_ms: u64) -> Self {
218        self.inner.tuning_interval_ms = tuning_interval_ms;
219        self
220    }
221
222    /// Sets the number of consecutive samples above threshold before acting.
223    #[must_use = "builder method returns modified builder"]
224    pub const fn samples_before_action(mut self, samples_before_action: u32) -> Self {
225        self.inner.samples_before_action = samples_before_action;
226        self
227    }
228
229    /// Builds the [`PoolPressureMonitorConfig`].
230    #[must_use = "building a config that is not used has no effect"]
231    pub const fn build(self) -> PoolPressureMonitorConfig {
232        self.inner
233    }
234}