Skip to main content

xds_server/
lib.rs

1//! # xds-server
2//!
3//! xDS gRPC server implementation for control planes.
4//!
5//! This crate provides the gRPC server layer for xDS:
6//!
7//! - [`XdsServer`] - Main server type wrapping all xDS services
8//! - [`XdsServerBuilder`] - Builder for configuring the server
9//! - State-of-the-World (SotW) protocol support
10//! - Delta xDS protocol support (incremental updates)
11//! - Health checking via gRPC health protocol
12//! - Prometheus metrics for observability
13//! - Graceful shutdown with connection draining
14//! - Connection tracking and limits
15//!
16//! ## Example
17//!
18//! ```rust,no_run
19//! use xds_server::XdsServerBuilder;
20//! use xds_cache::ShardedCache;
21//! use std::sync::Arc;
22//!
23//! # async fn run() -> Result<(), Box<dyn std::error::Error>> {
24//! let cache = Arc::new(ShardedCache::new());
25//! let server = XdsServerBuilder::new()
26//!     .cache(cache)
27//!     .enable_sotw()
28//!     .enable_delta()
29//!     .enable_health_check()
30//!     .enable_metrics()
31//!     .build()?;
32//!
33//! // Use with tonic - includes health and metrics
34//! server.serve("[::]:18000".parse()?).await?;
35//! # Ok(())
36//! # }
37//! ```
38//!
39//! ## Production Features
40//!
41//! ### Health Checking
42//!
43//! The server implements the gRPC health checking protocol:
44//!
45//! ```rust,no_run
46//! # use xds_server::XdsServerBuilder;
47//! # use xds_cache::ShardedCache;
48//! # use std::sync::Arc;
49//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
50//! # let cache = Arc::new(ShardedCache::new());
51//! let _server = XdsServerBuilder::new()
52//!     .cache(cache)
53//!     .enable_health_check()
54//!     .build()?;
55//!
56//! // Health status is managed automatically
57//! // Available at grpc.health.v1.Health/Check
58//! # Ok(()) }
59//! ```
60//!
61//! ### Metrics
62//!
63//! Prometheus metrics are exposed for monitoring:
64//!
65//! ```rust,no_run
66//! # use xds_server::XdsServerBuilder;
67//! # use xds_cache::ShardedCache;
68//! # use std::sync::Arc;
69//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
70//! # let cache = Arc::new(ShardedCache::new());
71//! let _server = XdsServerBuilder::new()
72//!     .cache(cache)
73//!     .enable_metrics()
74//!     .build()?;
75//! # Ok(()) }
76//! ```
77//!
78//! ### Graceful Shutdown
79//!
80//! The server supports graceful shutdown with connection draining:
81//!
82//! ```rust,no_run
83//! # use xds_server::XdsServerBuilder;
84//! # use xds_cache::ShardedCache;
85//! # use std::sync::Arc;
86//! use std::time::Duration;
87//!
88//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
89//! # let cache = Arc::new(ShardedCache::new());
90//! let _server = XdsServerBuilder::new()
91//!     .cache(cache)
92//!     .graceful_shutdown(Duration::from_secs(30))
93//!     .build()?;
94//!
95//! // Server will drain connections on SIGTERM/SIGINT
96//! # Ok(()) }
97//! ```
98
99#![cfg_attr(docsrs, feature(doc_cfg))]
100#![deny(unsafe_code)]
101#![warn(missing_docs)]
102// Tests are allowed to use .unwrap() / .expect() / panic! freely; the
103// production-only warnings still apply.
104#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
105
106mod builder;
107mod config;
108pub mod connections;
109mod delta;
110pub mod health;
111pub mod metrics;
112pub mod reflection;
113pub mod shutdown;
114mod sotw;
115mod stream;
116pub mod streaming;
117pub mod utils;
118
119#[cfg(test)]
120mod protocol_tests;
121
122// Re-export service modules
123pub mod services;
124
125pub use builder::XdsServerBuilder;
126pub use config::ServerConfig;
127pub use connections::{ConnectionGuard, ConnectionLimits, ConnectionTracker};
128pub use health::{HealthConfig, HealthService};
129pub use metrics::XdsMetrics;
130#[cfg(feature = "reflection")]
131pub use reflection::{ReflectionConfig, ReflectionService};
132pub use shutdown::{ShutdownConfig, ShutdownController};
133pub use stream::{StreamContext, StreamId};
134
135#[cfg(feature = "tls")]
136#[cfg_attr(docsrs, doc(cfg(feature = "tls")))]
137pub use tonic::transport::{Identity as TlsIdentity, ServerTlsConfig};
138
139use std::net::SocketAddr;
140use std::sync::Arc;
141
142use tokio::sync::oneshot;
143use tonic::transport::Server;
144use tracing::info;
145use xds_cache::ShardedCache;
146use xds_core::ResourceRegistry;
147
148use crate::health::HealthService as HealthSvc;
149use crate::services::ServiceState;
150
151/// The main xDS server.
152///
153/// This server wraps the cache and provides gRPC services for xDS.
154/// It includes production-ready features like health checking,
155/// metrics, graceful shutdown, and connection tracking.
156#[derive(Debug)]
157pub struct XdsServer {
158    /// Shared cache.
159    cache: Arc<ShardedCache>,
160    /// Resource registry.
161    registry: Arc<ResourceRegistry>,
162    /// Server configuration.
163    config: ServerConfig,
164    /// Metrics collector.
165    metrics: Option<XdsMetrics>,
166    /// Shutdown controller.
167    shutdown: ShutdownController,
168    /// Connection tracker.
169    connections: Option<ConnectionTracker>,
170    /// Optional TLS configuration applied to the gRPC transport.
171    #[cfg(feature = "tls")]
172    tls_config: Option<tonic::transport::ServerTlsConfig>,
173}
174
175impl XdsServer {
176    /// Create a new builder for configuring the server.
177    #[must_use = "builder is unused unless `.build()` is called"]
178    pub fn builder() -> XdsServerBuilder {
179        XdsServerBuilder::new()
180    }
181
182    /// Get a reference to the cache.
183    #[inline]
184    pub fn cache(&self) -> &Arc<ShardedCache> {
185        &self.cache
186    }
187
188    /// Get a reference to the resource registry.
189    #[inline]
190    pub fn registry(&self) -> &Arc<ResourceRegistry> {
191        &self.registry
192    }
193
194    /// Get the server configuration.
195    #[inline]
196    pub fn config(&self) -> &ServerConfig {
197        &self.config
198    }
199
200    /// Get the metrics instance, if enabled.
201    #[inline]
202    pub fn metrics(&self) -> Option<&XdsMetrics> {
203        self.metrics.as_ref()
204    }
205
206    /// Get the shutdown controller.
207    #[inline]
208    pub fn shutdown_controller(&self) -> &ShutdownController {
209        &self.shutdown
210    }
211
212    /// Get the connection tracker, if enabled.
213    #[inline]
214    pub fn connections(&self) -> Option<&ConnectionTracker> {
215        self.connections.as_ref()
216    }
217
218    /// Create the service state for all discovery services.
219    pub fn service_state(&self) -> ServiceState {
220        ServiceState::new(
221            Arc::clone(&self.cache),
222            Arc::clone(&self.registry),
223            self.config.clone(),
224        )
225    }
226
227    /// Build the router with all xDS services configured.
228    ///
229    /// This is an internal helper to reduce duplication between serve methods.
230    async fn build_router(
231        &self,
232    ) -> Result<(tonic::transport::server::Router, Option<HealthSvc>), tonic::transport::Error>
233    {
234        let state = self.service_state();
235        let (ads, cds, lds, rds, eds, sds) = state.create_services();
236
237        // Build the server with configured options
238        let mut builder = Server::builder();
239
240        if let Some(interval) = self.config.keepalive_interval {
241            builder = builder.http2_keepalive_interval(Some(interval));
242        }
243        if let Some(timeout) = self.config.keepalive_timeout {
244            builder = builder.http2_keepalive_timeout(Some(timeout));
245        }
246        if let Some(max_streams) = self.config.max_concurrent_streams {
247            builder = builder.concurrency_limit_per_connection(max_streams as usize);
248        }
249
250        #[cfg(feature = "tls")]
251        if let Some(ref tls) = self.tls_config {
252            builder = builder.tls_config(tls.clone())?;
253        }
254
255        // Add all xDS services
256        let mut router = builder
257            .add_service(ads.into_service())
258            .add_service(cds.into_service())
259            .add_service(lds.into_service())
260            .add_service(rds.into_service())
261            .add_service(eds.into_service())
262            .add_service(sds.into_service());
263
264        // Add health service if enabled
265        let health = if self.config.enable_health {
266            let (health, health_svc) = HealthSvc::new();
267            router = router.add_service(health_svc);
268            health.set_all_serving().await;
269            Some(health)
270        } else {
271            None
272        };
273
274        Ok((router, health))
275    }
276
277    /// Start the server and listen on the given address.
278    ///
279    /// This method will:
280    /// 1. Set up health checking (if enabled)
281    /// 2. Register all xDS services
282    /// 3. Handle graceful shutdown on SIGTERM/SIGINT
283    ///
284    /// # Example
285    ///
286    /// ```rust,no_run
287    /// # use xds_server::XdsServerBuilder;
288    /// # use xds_cache::ShardedCache;
289    /// # use std::sync::Arc;
290    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
291    /// # let cache = Arc::new(ShardedCache::new());
292    /// let server = XdsServerBuilder::new()
293    ///     .cache(cache)
294    ///     .build()?;
295    ///
296    /// server.serve("[::]:18000".parse()?).await?;
297    /// # Ok(()) }
298    /// ```
299    pub async fn serve(self, addr: SocketAddr) -> Result<(), tonic::transport::Error> {
300        info!(addr = %addr, "starting xDS server");
301
302        let (router, health) = self.build_router().await?;
303        let grace_period = self.config.grace_period;
304
305        let serve_future = router.serve_with_shutdown(addr, async move {
306            // Wait for shutdown signal
307            shutdown::wait_for_signal().await;
308
309            // Mark services as not serving (for load balancer drain)
310            if let Some(ref health) = health {
311                health.set_all_not_serving().await;
312            }
313
314            // Wait for grace period
315            info!(grace_period = ?grace_period, "draining connections");
316            tokio::time::sleep(grace_period).await;
317        });
318
319        info!(addr = %addr, "xDS server listening");
320        serve_future.await
321    }
322
323    /// Start the server with a custom shutdown signal.
324    ///
325    /// This allows you to control shutdown programmatically.
326    pub async fn serve_with_shutdown(
327        self,
328        addr: SocketAddr,
329        shutdown_rx: oneshot::Receiver<()>,
330    ) -> Result<(), tonic::transport::Error> {
331        info!(addr = %addr, "starting xDS server with custom shutdown");
332
333        let (router, health) = self.build_router().await?;
334        let grace_period = self.config.grace_period;
335
336        let serve_future = router.serve_with_shutdown(addr, async move {
337            let _ = shutdown_rx.await;
338
339            if let Some(ref health) = health {
340                health.set_all_not_serving().await;
341            }
342
343            info!(grace_period = ?grace_period, "draining connections");
344            tokio::time::sleep(grace_period).await;
345        });
346
347        info!(addr = %addr, "xDS server listening");
348        serve_future.await
349    }
350}