Skip to main content

vil_sidecar/
config.rs

1// =============================================================================
2// Sidecar Configuration — YAML-driven sidecar definitions
3// =============================================================================
4
5use crate::reconnect::ReconnectPolicy;
6use serde::{Deserialize, Serialize};
7
8/// Configuration for a single sidecar.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct SidecarConfig {
11    /// Sidecar name (used for registry lookup and SHM naming).
12    pub name: String,
13
14    /// Optional command to auto-spawn the sidecar process.
15    /// If None, sidecar must self-register via UDS.
16    #[serde(default)]
17    pub command: Option<String>,
18
19    /// Unix domain socket path. Defaults to `/tmp/vil_sidecar_{name}.sock`.
20    #[serde(default)]
21    pub socket: Option<String>,
22
23    /// SHM region size in bytes. Default: 64 MB.
24    #[serde(default = "default_shm_size")]
25    pub shm_size: u64,
26
27    /// Health check interval in milliseconds. Default: 5000.
28    #[serde(default = "default_health_interval")]
29    pub health_interval_ms: u64,
30
31    /// Invocation timeout in milliseconds. Default: 30000.
32    #[serde(default = "default_timeout")]
33    pub timeout_ms: u64,
34
35    /// Number of retries on invocation failure. Default: 0.
36    #[serde(default)]
37    pub retry: u8,
38
39    /// Number of concurrent connections to the sidecar. Default: 4.
40    #[serde(default = "default_pool_size")]
41    pub pool_size: usize,
42
43    /// Maximum number of in-flight requests (0 = unlimited). Default: 1000.
44    #[serde(default = "default_max_in_flight")]
45    pub max_in_flight: u64,
46
47    /// Optional authentication token for handshake.
48    #[serde(default)]
49    pub auth_token: Option<String>,
50
51    /// Failover configuration.
52    #[serde(default)]
53    pub failover: Option<FailoverConfig>,
54
55    /// Reconnect policy for dropped connections.
56    #[serde(default = "default_reconnect_policy")]
57    pub reconnect_policy: ReconnectPolicy,
58}
59
60impl SidecarConfig {
61    /// Create a minimal config with just a name.
62    pub fn new(name: impl Into<String>) -> Self {
63        Self {
64            name: name.into(),
65            command: None,
66            socket: None,
67            shm_size: default_shm_size(),
68            health_interval_ms: default_health_interval(),
69            timeout_ms: default_timeout(),
70            retry: 0,
71            pool_size: default_pool_size(),
72            max_in_flight: default_max_in_flight(),
73            auth_token: None,
74            failover: None,
75            reconnect_policy: default_reconnect_policy(),
76        }
77    }
78
79    /// Set the spawn command.
80    pub fn command(mut self, cmd: impl Into<String>) -> Self {
81        self.command = Some(cmd.into());
82        self
83    }
84
85    /// Set the timeout in milliseconds.
86    pub fn timeout(mut self, ms: u64) -> Self {
87        self.timeout_ms = ms;
88        self
89    }
90
91    /// Set the SHM size.
92    pub fn shm_size(mut self, bytes: u64) -> Self {
93        self.shm_size = bytes;
94        self
95    }
96
97    /// Set the connection pool size.
98    pub fn pool_size(mut self, n: usize) -> Self {
99        self.pool_size = n;
100        self
101    }
102
103    /// Set the maximum number of in-flight requests (0 = unlimited).
104    pub fn max_in_flight(mut self, n: u64) -> Self {
105        self.max_in_flight = n;
106        self
107    }
108
109    /// Set the maximum reconnect retries.
110    pub fn reconnect_max_retries(mut self, n: u32) -> Self {
111        self.reconnect_policy.max_retries = n;
112        self
113    }
114
115    /// Set the reconnect backoff parameters (base and max in milliseconds).
116    pub fn reconnect_backoff_ms(mut self, base: u64, max: u64) -> Self {
117        self.reconnect_policy.base_backoff_ms = base;
118        self.reconnect_policy.max_backoff_ms = max;
119        self
120    }
121
122    /// Resolve the socket path (use explicit or generate from name).
123    pub fn socket_path(&self) -> String {
124        self.socket
125            .clone()
126            .unwrap_or_else(|| crate::transport::socket_path(&self.name))
127    }
128
129    /// Resolve the SHM path.
130    pub fn shm_path(&self) -> String {
131        crate::transport::shm_path(&self.name)
132    }
133}
134
135/// Failover configuration for a sidecar.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct FailoverConfig {
138    /// Name of backup sidecar to failover to.
139    #[serde(default)]
140    pub backup: Option<String>,
141
142    /// WASM module to fall back to if all sidecars are down.
143    #[serde(default)]
144    pub fallback_wasm: Option<String>,
145
146    /// Circuit breaker failure threshold.
147    #[serde(default = "default_cb_threshold")]
148    pub failure_threshold: u32,
149
150    /// Circuit breaker cooldown in seconds.
151    #[serde(default = "default_cb_cooldown")]
152    pub cooldown_secs: u32,
153}
154
155fn default_shm_size() -> u64 {
156    64 * 1024 * 1024 // 64 MB
157}
158fn default_health_interval() -> u64 {
159    5000
160}
161fn default_timeout() -> u64 {
162    30000
163}
164fn default_pool_size() -> usize {
165    4
166}
167fn default_max_in_flight() -> u64 {
168    1000
169}
170fn default_reconnect_policy() -> ReconnectPolicy {
171    ReconnectPolicy::default()
172}
173fn default_cb_threshold() -> u32 {
174    5
175}
176fn default_cb_cooldown() -> u32 {
177    30
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_config_defaults() {
186        let cfg = SidecarConfig::new("fraud-checker");
187        assert_eq!(cfg.name, "fraud-checker");
188        assert_eq!(cfg.shm_size, 64 * 1024 * 1024);
189        assert_eq!(cfg.timeout_ms, 30000);
190        assert_eq!(cfg.pool_size, 4);
191        assert_eq!(cfg.max_in_flight, 1000);
192        assert_eq!(cfg.reconnect_policy.max_retries, 10);
193        assert_eq!(cfg.reconnect_policy.base_backoff_ms, 100);
194        assert_eq!(cfg.reconnect_policy.max_backoff_ms, 30000);
195        assert!(cfg.command.is_none());
196    }
197
198    #[test]
199    fn test_config_builder() {
200        let cfg = SidecarConfig::new("ml-engine")
201            .command("python -m ml_service")
202            .timeout(60000)
203            .shm_size(256 * 1024 * 1024);
204
205        assert_eq!(cfg.command.as_deref(), Some("python -m ml_service"));
206        assert_eq!(cfg.timeout_ms, 60000);
207        assert_eq!(cfg.shm_size, 256 * 1024 * 1024);
208    }
209
210    #[test]
211    fn test_pool_and_reconnect_builder() {
212        let cfg = SidecarConfig::new("ml-engine")
213            .pool_size(8)
214            .max_in_flight(5000)
215            .reconnect_max_retries(20)
216            .reconnect_backoff_ms(200, 60000);
217
218        assert_eq!(cfg.pool_size, 8);
219        assert_eq!(cfg.max_in_flight, 5000);
220        assert_eq!(cfg.reconnect_policy.max_retries, 20);
221        assert_eq!(cfg.reconnect_policy.base_backoff_ms, 200);
222        assert_eq!(cfg.reconnect_policy.max_backoff_ms, 60000);
223    }
224
225    #[test]
226    fn test_socket_path_resolution() {
227        let cfg = SidecarConfig::new("fraud");
228        assert_eq!(cfg.socket_path(), "/tmp/vil_sidecar_fraud.sock");
229
230        let cfg2 = SidecarConfig {
231            socket: Some("/custom/path.sock".into()),
232            ..SidecarConfig::new("fraud")
233        };
234        assert_eq!(cfg2.socket_path(), "/custom/path.sock");
235    }
236
237    #[test]
238    fn test_yaml_deserialize() {
239        let yaml = r#"
240name: fraud-checker
241command: "python -m fraud_service"
242shm_size: 67108864
243timeout_ms: 30000
244retry: 3
245pool_size: 4
246failover:
247  backup: fraud-checker-2
248  fallback_wasm: fraud_basic.wasm
249  failure_threshold: 5
250  cooldown_secs: 30
251"#;
252        let cfg: SidecarConfig = serde_yaml::from_str(yaml).unwrap();
253        assert_eq!(cfg.name, "fraud-checker");
254        assert_eq!(cfg.retry, 3);
255        assert_eq!(cfg.pool_size, 4);
256        assert!(cfg.failover.is_some());
257        let fo = cfg.failover.unwrap();
258        assert_eq!(fo.backup.as_deref(), Some("fraud-checker-2"));
259        assert_eq!(fo.fallback_wasm.as_deref(), Some("fraud_basic.wasm"));
260    }
261}