Skip to main content

fraiseql_server/server_config/
observers.rs

1//! Observer runtime and admission control configuration.
2
3use serde::{Deserialize, Serialize};
4
5#[cfg(feature = "observers")]
6const fn default_observers_enabled() -> bool {
7    true
8}
9
10#[cfg(feature = "observers")]
11const fn default_poll_interval_ms() -> u64 {
12    100
13}
14
15#[cfg(feature = "observers")]
16const fn default_batch_size() -> usize {
17    100
18}
19
20#[cfg(feature = "observers")]
21const fn default_channel_capacity() -> usize {
22    1000
23}
24
25#[cfg(feature = "observers")]
26const fn default_auto_reload() -> bool {
27    true
28}
29
30#[cfg(feature = "observers")]
31const fn default_reload_interval_secs() -> u64 {
32    60
33}
34
35/// Pool configuration for the observer's dedicated PostgreSQL connection pool.
36///
37/// The observer pool is separate from the application pool because the
38/// LISTEN/NOTIFY connection occupies a persistent slot. Smaller defaults
39/// are appropriate since observers need far fewer connections than the app.
40///
41/// Configure via `[observers.pool]` in `fraiseql.toml`:
42///
43/// ```toml
44/// [observers.pool]
45/// min_connections = 2
46/// max_connections = 5
47/// acquire_timeout_secs = 10
48/// ```
49#[cfg(feature = "observers")]
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(default)]
52pub struct ObserverPoolConfig {
53    /// Minimum number of connections to keep open (default: 2).
54    #[serde(default = "default_observer_pool_min")]
55    pub min_connections: u32,
56
57    /// Maximum number of connections in the observer pool (default: 5).
58    #[serde(default = "default_observer_pool_max")]
59    pub max_connections: u32,
60
61    /// Timeout in seconds for acquiring a connection from the pool (default: 10).
62    #[serde(default = "default_observer_acquire_timeout")]
63    pub acquire_timeout_secs: u64,
64}
65
66#[cfg(feature = "observers")]
67const fn default_observer_pool_min() -> u32 {
68    2
69}
70
71#[cfg(feature = "observers")]
72const fn default_observer_pool_max() -> u32 {
73    5
74}
75
76#[cfg(feature = "observers")]
77const fn default_observer_acquire_timeout() -> u64 {
78    10
79}
80
81#[cfg(feature = "observers")]
82impl Default for ObserverPoolConfig {
83    fn default() -> Self {
84        Self {
85            min_connections:      default_observer_pool_min(),
86            max_connections:      default_observer_pool_max(),
87            acquire_timeout_secs: default_observer_acquire_timeout(),
88        }
89    }
90}
91
92/// Observer runtime configuration.
93#[cfg(feature = "observers")]
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct ObserverConfig {
96    /// Enable observer runtime (default: true).
97    #[serde(default = "default_observers_enabled")]
98    pub enabled: bool,
99
100    /// Poll interval for change log in milliseconds (default: 100).
101    #[serde(default = "default_poll_interval_ms")]
102    pub poll_interval_ms: u64,
103
104    /// Batch size for fetching change log entries (default: 100).
105    #[serde(default = "default_batch_size")]
106    pub batch_size: usize,
107
108    /// Channel capacity for event buffering (default: 1000).
109    #[serde(default = "default_channel_capacity")]
110    pub channel_capacity: usize,
111
112    /// Auto-reload observers on changes (default: true).
113    #[serde(default = "default_auto_reload")]
114    pub auto_reload: bool,
115
116    /// Reload interval in seconds (default: 60).
117    #[serde(default = "default_reload_interval_secs")]
118    pub reload_interval_secs: u64,
119
120    /// Dedicated connection pool configuration for the observer runtime.
121    ///
122    /// When absent, sensible observer-specific defaults are used (smaller
123    /// than the application pool). Operators can set `[observers.pool]` in
124    /// `fraiseql.toml` to tune independently of the main pool.
125    #[serde(default)]
126    pub pool: ObserverPoolConfig,
127}
128
129/// Admission control configuration for backpressure limiting.
130///
131/// Pairs with `crate::resilience::backpressure::AdmissionController`.
132/// See [`super::ServerConfig::admission_control`] for wiring instructions.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct AdmissionConfig {
135    /// Maximum number of in-flight concurrent requests (semaphore permits).
136    ///
137    /// Defaults to 500.
138    #[serde(default = "default_admission_max_concurrent")]
139    pub max_concurrent: usize,
140
141    /// Maximum number of requests waiting for a permit (queue depth).
142    ///
143    /// When the queue is full, new requests are rejected with 503.
144    /// Defaults to 1000.
145    #[serde(default = "default_admission_max_queue_depth")]
146    pub max_queue_depth: u64,
147}
148
149pub(crate) const fn default_admission_max_concurrent() -> usize {
150    500
151}
152
153pub(crate) const fn default_admission_max_queue_depth() -> u64 {
154    1000
155}
156
157#[cfg(all(test, feature = "observers"))]
158mod tests {
159    #![allow(clippy::unwrap_used)] // Reason: test code, panics acceptable
160
161    use super::*;
162
163    #[test]
164    fn observer_pool_config_defaults_are_sensible() {
165        let cfg = ObserverPoolConfig::default();
166        assert!(cfg.min_connections >= 1, "observer pool needs at least 1 connection");
167        assert!(
168            cfg.max_connections >= cfg.min_connections,
169            "max_connections ({}) must be >= min_connections ({})",
170            cfg.max_connections,
171            cfg.min_connections,
172        );
173        assert!(cfg.acquire_timeout_secs > 0, "acquire_timeout_secs should be > 0");
174        // Observer pool should be smaller than a typical app pool.
175        assert!(
176            cfg.max_connections <= 10,
177            "observer pool defaults should be small (<=10), got {}",
178            cfg.max_connections,
179        );
180    }
181
182    #[test]
183    fn observer_config_with_pool_section_deserializes() {
184        let toml = r"
185            enabled = true
186
187            [pool]
188            min_connections = 3
189            max_connections = 8
190            acquire_timeout_secs = 15
191        ";
192        let cfg: ObserverConfig = toml::from_str(toml).unwrap();
193        assert_eq!(cfg.pool.min_connections, 3);
194        assert_eq!(cfg.pool.max_connections, 8);
195        assert_eq!(cfg.pool.acquire_timeout_secs, 15);
196    }
197
198    #[test]
199    fn observer_config_pool_defaults_when_section_absent() {
200        let toml = r"enabled = true";
201        let cfg: ObserverConfig = toml::from_str(toml).unwrap();
202        assert_eq!(cfg.pool.min_connections, 2, "default min_connections should be 2");
203        assert_eq!(cfg.pool.max_connections, 5, "default max_connections should be 5");
204        assert_eq!(cfg.pool.acquire_timeout_secs, 10, "default acquire_timeout_secs should be 10");
205    }
206}