Skip to main content

xds_server/
builder.rs

1//! Server builder for configuring and creating the xDS server.
2
3use std::sync::Arc;
4use std::time::Duration;
5
6use xds_cache::ShardedCache;
7use xds_core::{ResourceRegistry, XdsError, XdsResult};
8
9use crate::config::{CompressionConfig, ServerConfig};
10use crate::connections::{ConnectionLimits, ConnectionTracker};
11use crate::metrics::XdsMetrics;
12use crate::shutdown::ShutdownController;
13use crate::XdsServer;
14
15/// Builder for creating an [`XdsServer`].
16///
17/// # Example
18///
19/// ```rust
20/// use xds_server::XdsServerBuilder;
21/// use xds_cache::ShardedCache;
22/// use std::sync::Arc;
23/// use std::time::Duration;
24///
25/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
26/// let cache = Arc::new(ShardedCache::new());
27/// let _server = XdsServerBuilder::new()
28///     .cache(cache)
29///     .enable_sotw()
30///     .enable_delta()
31///     .enable_health_check()
32///     .enable_metrics()
33///     .graceful_shutdown(Duration::from_secs(30))
34///     .max_concurrent_streams(200)
35///     .build()?;
36/// # Ok(()) }
37/// ```
38#[must_use = "builder is unused unless `.build()` is called"]
39#[derive(Debug, Default)]
40pub struct XdsServerBuilder {
41    cache: Option<Arc<ShardedCache>>,
42    registry: Option<Arc<ResourceRegistry>>,
43    enable_sotw: bool,
44    enable_delta: bool,
45    max_concurrent_streams: Option<u32>,
46    keepalive_interval: Option<Duration>,
47    keepalive_timeout: Option<Duration>,
48    max_request_size: Option<usize>,
49    compression: Option<CompressionConfig>,
50    // Production features
51    enable_health: bool,
52    enable_metrics: bool,
53    enable_connection_tracking: bool,
54    grace_period: Option<Duration>,
55    connection_limits: Option<ConnectionLimits>,
56    // TLS
57    #[cfg(feature = "tls")]
58    tls_config: Option<tonic::transport::ServerTlsConfig>,
59}
60
61impl XdsServerBuilder {
62    /// Create a new server builder.
63    pub fn new() -> Self {
64        Self {
65            enable_sotw: true,    // Enable SotW by default
66            enable_health: true,  // Enable health by default
67            enable_metrics: true, // Enable metrics by default
68            enable_connection_tracking: true, // Enable connection tracking by default
69            ..Default::default()
70        }
71    }
72
73    /// Set the cache to use.
74    ///
75    /// This is required.
76    pub fn cache(mut self, cache: Arc<ShardedCache>) -> Self {
77        self.cache = Some(cache);
78        self
79    }
80
81    /// Set the resource registry.
82    ///
83    /// If not set, a default registry will be created.
84    pub fn registry(mut self, registry: Arc<ResourceRegistry>) -> Self {
85        self.registry = Some(registry);
86        self
87    }
88
89    /// Enable State-of-the-World protocol (enabled by default).
90    pub fn enable_sotw(mut self) -> Self {
91        self.enable_sotw = true;
92        self
93    }
94
95    /// Disable State-of-the-World protocol.
96    pub fn disable_sotw(mut self) -> Self {
97        self.enable_sotw = false;
98        self
99    }
100
101    /// Enable Delta xDS protocol.
102    pub fn enable_delta(mut self) -> Self {
103        self.enable_delta = true;
104        self
105    }
106
107    /// Disable Delta xDS protocol.
108    pub fn disable_delta(mut self) -> Self {
109        self.enable_delta = false;
110        self
111    }
112
113    /// Set maximum concurrent streams per connection.
114    pub fn max_concurrent_streams(mut self, max: u32) -> Self {
115        self.max_concurrent_streams = Some(max);
116        self
117    }
118
119    /// Set keepalive interval.
120    pub fn keepalive_interval(mut self, interval: Duration) -> Self {
121        self.keepalive_interval = Some(interval);
122        self
123    }
124
125    /// Set keepalive timeout.
126    pub fn keepalive_timeout(mut self, timeout: Duration) -> Self {
127        self.keepalive_timeout = Some(timeout);
128        self
129    }
130
131    /// Set maximum request size in bytes.
132    pub fn max_request_size(mut self, size: usize) -> Self {
133        self.max_request_size = Some(size);
134        self
135    }
136
137    /// Enable gzip compression for responses.
138    pub fn enable_compression(mut self) -> Self {
139        self.compression = Some(CompressionConfig::gzip());
140        self
141    }
142
143    /// Set custom compression configuration.
144    pub fn compression(mut self, config: CompressionConfig) -> Self {
145        self.compression = Some(config);
146        self
147    }
148
149    // Production feature builders
150
151    /// Enable gRPC health checking (enabled by default).
152    pub fn enable_health_check(mut self) -> Self {
153        self.enable_health = true;
154        self
155    }
156
157    /// Disable gRPC health checking.
158    pub fn disable_health_check(mut self) -> Self {
159        self.enable_health = false;
160        self
161    }
162
163    /// Enable Prometheus metrics (enabled by default).
164    pub fn enable_metrics(mut self) -> Self {
165        self.enable_metrics = true;
166        self
167    }
168
169    /// Disable Prometheus metrics.
170    pub fn disable_metrics(mut self) -> Self {
171        self.enable_metrics = false;
172        self
173    }
174
175    /// Enable connection tracking (enabled by default).
176    pub fn enable_connection_tracking(mut self) -> Self {
177        self.enable_connection_tracking = true;
178        self
179    }
180
181    /// Disable connection tracking.
182    pub fn disable_connection_tracking(mut self) -> Self {
183        self.enable_connection_tracking = false;
184        self
185    }
186
187    /// Set the graceful shutdown grace period.
188    ///
189    /// During shutdown, the server will:
190    /// 1. Stop accepting new connections
191    /// 2. Mark health as not serving
192    /// 3. Wait for existing connections to drain (up to grace period)
193    /// 4. Force close remaining connections
194    pub fn graceful_shutdown(mut self, grace_period: Duration) -> Self {
195        self.grace_period = Some(grace_period);
196        self
197    }
198
199    /// Set connection limits.
200    ///
201    /// Controls maximum total connections and per-IP limits.
202    pub fn connection_limits(mut self, limits: ConnectionLimits) -> Self {
203        self.connection_limits = Some(limits);
204        self
205    }
206
207    /// Set maximum total connections.
208    pub fn max_connections(mut self, max: u64) -> Self {
209        let limits = self.connection_limits.get_or_insert_with(ConnectionLimits::default);
210        limits.max_connections = max;
211        self
212    }
213
214    /// Set maximum connections per IP address.
215    pub fn max_connections_per_ip(mut self, max: u64) -> Self {
216        let limits = self.connection_limits.get_or_insert_with(ConnectionLimits::default);
217        limits.max_per_ip = max;
218        self
219    }
220
221    /// Configure TLS (and optionally mTLS) for the gRPC server.
222    ///
223    /// Pass a [`tonic::transport::ServerTlsConfig`] populated with the server
224    /// identity (`identity()`) and, for mTLS, the client CA
225    /// (`client_ca_root()` / `client_auth_optional()`).
226    ///
227    /// # Example
228    ///
229    /// ```rust,no_run
230    /// # use xds_server::XdsServerBuilder;
231    /// # use xds_cache::ShardedCache;
232    /// # use std::sync::Arc;
233    /// use tonic::transport::{Identity, ServerTlsConfig};
234    ///
235    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
236    /// # let cache = Arc::new(ShardedCache::new());
237    /// let cert = std::fs::read("server.pem")?;
238    /// let key = std::fs::read("server.key")?;
239    /// let tls = ServerTlsConfig::new().identity(Identity::from_pem(cert, key));
240    ///
241    /// let _server = XdsServerBuilder::new()
242    ///     .cache(cache)
243    ///     .tls_config(tls)
244    ///     .build()?;
245    /// # Ok(()) }
246    /// ```
247    ///
248    /// Available only when the `tls` feature is enabled.
249    #[cfg(feature = "tls")]
250    #[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
251    pub fn tls_config(mut self, config: tonic::transport::ServerTlsConfig) -> Self {
252        self.tls_config = Some(config);
253        self
254    }
255
256    /// Build the server.
257    ///
258    /// # Errors
259    ///
260    /// Returns an error if:
261    /// - No cache was provided
262    /// - Neither SotW nor Delta is enabled
263    pub fn build(self) -> XdsResult<XdsServer> {
264        let cache = self
265            .cache
266            .ok_or_else(|| XdsError::Configuration("cache is required".into()))?;
267
268        if !self.enable_sotw && !self.enable_delta {
269            return Err(XdsError::Configuration(
270                "at least one protocol (SotW or Delta) must be enabled".into(),
271            ));
272        }
273
274        let registry = self
275            .registry
276            .unwrap_or_else(|| Arc::new(ResourceRegistry::new()));
277
278        let config = ServerConfig {
279            enable_sotw: self.enable_sotw,
280            enable_delta: self.enable_delta,
281            max_concurrent_streams: self.max_concurrent_streams.or(Some(100)),
282            keepalive_interval: self.keepalive_interval.or(Some(Duration::from_secs(30))),
283            keepalive_timeout: self.keepalive_timeout.or(Some(Duration::from_secs(10))),
284            max_request_size: self.max_request_size.unwrap_or(4 * 1024 * 1024),
285            compression: self.compression.unwrap_or_default(),
286            grace_period: self.grace_period.unwrap_or(Duration::from_secs(30)),
287            enable_health: self.enable_health,
288            enable_metrics: self.enable_metrics,
289            enable_connection_tracking: self.enable_connection_tracking,
290        };
291
292        // Create optional production components
293        let metrics = if self.enable_metrics {
294            Some(XdsMetrics::new())
295        } else {
296            None
297        };
298
299        let connections = if self.enable_connection_tracking {
300            let limits = self.connection_limits.unwrap_or_default();
301            Some(ConnectionTracker::new(limits))
302        } else {
303            None
304        };
305
306        let shutdown = ShutdownController::new();
307
308        Ok(XdsServer {
309            cache,
310            registry,
311            config,
312            metrics,
313            shutdown,
314            connections,
315            #[cfg(feature = "tls")]
316            tls_config: self.tls_config,
317        })
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn builder_requires_cache() {
327        let result = XdsServerBuilder::new().build();
328        assert!(result.is_err());
329    }
330
331    #[test]
332    fn builder_requires_protocol() {
333        let cache = Arc::new(ShardedCache::new());
334        let result = XdsServerBuilder::new()
335            .cache(cache)
336            .disable_sotw()
337            .disable_delta()
338            .build();
339        assert!(result.is_err());
340    }
341
342    #[test]
343    fn builder_success() {
344        let cache = Arc::new(ShardedCache::new());
345        let server = XdsServerBuilder::new()
346            .cache(cache)
347            .enable_sotw()
348            .enable_delta()
349            .max_concurrent_streams(200)
350            .build()
351            .expect("server should build successfully");
352
353        assert!(server.config().enable_sotw);
354        assert!(server.config().enable_delta);
355        assert_eq!(server.config().max_concurrent_streams, Some(200));
356    }
357
358    #[cfg(feature = "tls")]
359    #[test]
360    fn builder_accepts_tls_config() {
361        use tonic::transport::ServerTlsConfig;
362
363        let cache = Arc::new(ShardedCache::new());
364        let tls = ServerTlsConfig::new();
365
366        let server = XdsServerBuilder::new()
367            .cache(cache)
368            .tls_config(tls)
369            .build()
370            .expect("server should build with TLS config");
371
372        // No public accessor for tls_config (kept private to defer the
373        // tonic re-export decision); confirming `build()` succeeds is enough
374        // proof that the builder field is wired through.
375        assert!(server.config().enable_sotw);
376    }
377}