Skip to main content

grpc_quic_server/
server.rs

1//! QuicServer — builder and main serve loop.
2
3use grpc_quic_metrics::record_connection;
4use grpc_quic_transport::{QuicConnection, QuicEndpoint, TlsConfig};
5use std::net::SocketAddr;
6use std::sync::Arc;
7use tokio::sync::Semaphore;
8use tracing::{error, info};
9
10use crate::acceptor::handle_request;
11use crate::error::ServerError;
12
13/// Builder for [`QuicServer`].
14#[derive(Debug)]
15pub struct QuicServerBuilder {
16    tls: Option<TlsConfig>,
17    max_concurrent_streams: Option<u32>,
18    graceful_timeout: std::time::Duration,
19}
20
21impl Default for QuicServerBuilder {
22    fn default() -> Self {
23        Self {
24            tls: None,
25            max_concurrent_streams: None,
26            graceful_timeout: std::time::Duration::from_secs(30),
27        }
28    }
29}
30
31impl QuicServerBuilder {
32    /// Set the TLS configuration (required for production; test helpers available).
33    pub fn tls(mut self, tls: TlsConfig) -> Self {
34        self.tls = Some(tls);
35        self
36    }
37
38    /// Limit the number of concurrent streams per connection.
39    pub fn max_concurrent_streams(mut self, limit: u32) -> Self {
40        self.max_concurrent_streams = Some(limit);
41        self
42    }
43
44    /// Set the timeout for graceful shutdown to drain existing streams (default 30s).
45    pub fn graceful_timeout(mut self, timeout: std::time::Duration) -> Self {
46        self.graceful_timeout = timeout;
47        self
48    }
49
50    /// Return a configured [`QuicServer`]. The actual socket bind happens in
51    /// [`serve`](QuicServer::serve) or [`serve_with_incoming`](QuicServer::serve_with_incoming).
52    pub fn build(self) -> QuicServer {
53        QuicServer {
54            tls: self.tls,
55            max_concurrent_streams: self.max_concurrent_streams.unwrap_or(256),
56            graceful_timeout: self.graceful_timeout,
57        }
58    }
59}
60
61/// A QUIC server that delegates incoming gRPC requests to a tonic service.
62///
63/// ```text
64/// QuicServer
65///   └── quinn::Endpoint  (accepts QUIC connections)
66///         └── per connection: accept bi-streams
67///               └── each bi-stream: read path + gRPC bytes → tonic handler
68/// ```
69///
70/// ```ignore
71/// // Build and start the server:
72/// let server = QuicServer::builder()
73///     .tls(tls_config)
74///     .build();
75///
76/// // Pass any tonic-generated Router or service_fn:
77/// server.serve(addr, MyServiceServer::new(my_service)).await?;
78/// ```
79#[derive(Debug)]
80pub struct QuicServer {
81    pub(crate) tls: Option<TlsConfig>,
82    pub(crate) max_concurrent_streams: u32,
83    pub(crate) graceful_timeout: std::time::Duration,
84}
85
86impl QuicServer {
87    /// Return a builder to configure the server.
88    pub fn builder() -> QuicServerBuilder {
89        QuicServerBuilder::default()
90    }
91
92    /// Bind to `addr` and serve requests until a shutdown signal is received.
93    pub async fn serve<S>(self, addr: SocketAddr, service: S) -> Result<(), ServerError>
94    where
95        S: tower::Service<
96                http::Request<tonic::body::BoxBody>,
97                Response = http::Response<tonic::body::BoxBody>,
98            > + Clone
99            + Send
100            + Sync
101            + 'static,
102        S::Future: Send + 'static,
103        S::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send,
104    {
105        self.serve_with_shutdown(addr, service, std::future::pending())
106            .await
107    }
108
109    /// Bind to `addr` and serve requests until the `signal` future completes.
110    pub async fn serve_with_shutdown<S, F>(
111        self,
112        addr: SocketAddr,
113        service: S,
114        signal: F,
115    ) -> Result<(), ServerError>
116    where
117        S: tower::Service<
118                http::Request<tonic::body::BoxBody>,
119                Response = http::Response<tonic::body::BoxBody>,
120            > + Clone
121            + Send
122            + Sync
123            + 'static,
124        S::Future: Send + 'static,
125        S::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send,
126        F: std::future::Future<Output = ()> + Send + 'static,
127    {
128        let tls = self.tls.clone().ok_or_else(|| {
129            ServerError::Transport(grpc_quic_transport::TransportError::Tls(
130                "TLS config is required".into(),
131            ))
132        })?;
133
134        let endpoint = grpc_quic_transport::QuicEndpoint::server(addr, tls)?;
135        self.serve_with_incoming_shutdown(endpoint, service, signal)
136            .await
137    }
138
139    /// Serve requests over an already-bound `QuicEndpoint`.
140    pub async fn serve_with_incoming<S>(
141        self,
142        endpoint: QuicEndpoint,
143        service: S,
144    ) -> Result<(), ServerError>
145    where
146        S: tower::Service<
147                http::Request<tonic::body::BoxBody>,
148                Response = http::Response<tonic::body::BoxBody>,
149            > + Clone
150            + Send
151            + Sync
152            + 'static,
153        S::Future: Send + 'static,
154        S::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send,
155    {
156        self.serve_with_incoming_shutdown(endpoint, service, std::future::pending())
157            .await
158    }
159
160    /// Serve requests over an already-bound `QuicEndpoint` until the `signal` future completes.
161    #[tracing::instrument(skip(self, endpoint, service, signal))]
162    pub async fn serve_with_incoming_shutdown<S, F>(
163        self,
164        endpoint: QuicEndpoint,
165        service: S,
166        signal: F,
167    ) -> Result<(), ServerError>
168    where
169        S: tower::Service<
170                http::Request<tonic::body::BoxBody>,
171                Response = http::Response<tonic::body::BoxBody>,
172            > + Clone
173            + Send
174            + Sync
175            + 'static,
176        S::Future: Send + 'static,
177        S::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send,
178        F: std::future::Future<Output = ()> + Send + 'static,
179    {
180        info!(
181            local_addr = ?endpoint.local_addr(),
182            max_concurrent_streams = self.max_concurrent_streams,
183            "QuicServer listening"
184        );
185
186        let mut signal = Box::pin(signal);
187
188        // Global semaphore that bounds the total number of concurrent stream
189        // handler tasks across all connections.  When exhausted, new streams
190        // are dropped (try_acquire_owned fails), providing backpressure.
191        let stream_limit = (self.max_concurrent_streams as usize).max(64) * 4;
192        let stream_semaphore = Arc::new(Semaphore::new(stream_limit));
193
194        let mut join_set = tokio::task::JoinSet::new();
195        let (cancel_tx, _) = tokio::sync::broadcast::channel::<()>(1);
196
197        loop {
198            tokio::select! {
199                _ = &mut signal => {
200                    info!("shutdown signal received, rejecting new connections");
201                    endpoint.reject_new_connections();
202                    let _ = cancel_tx.send(());
203                    break;
204                }
205                conn_res = endpoint.accept() => {
206                    let conn_res = match conn_res {
207                        Some(res) => res,
208                        None => break,
209                    };
210                    let conn = match conn_res {
211                        Ok(c) => {
212                            record_connection("server");
213                            c
214                        }
215                        Err(e) => {
216                            error!(error = %e, "failed to accept connection");
217                            continue;
218                        }
219                    };
220
221                    let service = service.clone();
222                    let sem = stream_semaphore.clone();
223                    let cancel_rx = cancel_tx.subscribe();
224                    join_set.spawn(async move {
225                        if let Err(e) = handle_connection(conn, service, sem, cancel_rx).await {
226                            error!(error = %e, "connection handling error");
227                        }
228                    });
229                }
230            }
231        }
232
233        // Wait for all in-flight connections to complete with a 30s timeout
234        let wait_for_connections = async {
235            while let Some(result) = join_set.join_next().await {
236                if let Err(e) = result {
237                    error!("connection task failed: {e}");
238                }
239            }
240        };
241
242        if tokio::time::timeout(self.graceful_timeout, wait_for_connections)
243            .await
244            .is_err()
245        {
246            error!("graceful shutdown timed out, closing endpoint forcefully");
247            endpoint.close(0, b"shutdown timeout");
248        }
249
250        Ok(())
251    }
252}
253
254#[tracing::instrument(skip(conn, service, semaphore, cancel_rx))]
255async fn handle_connection<S>(
256    conn: QuicConnection,
257    service: S,
258    semaphore: Arc<Semaphore>,
259    mut cancel_rx: tokio::sync::broadcast::Receiver<()>,
260) -> Result<(), ServerError>
261where
262    S: tower::Service<
263            http::Request<tonic::body::BoxBody>,
264            Response = http::Response<tonic::body::BoxBody>,
265        > + Clone
266        + Send
267        + Sync
268        + 'static,
269    S::Future: Send + 'static,
270    S::Error: Into<Box<dyn std::error::Error + Send + Sync>> + Send,
271{
272    use grpc_quic_core::server::build_server_conn;
273
274    let mut h3_conn = match build_server_conn(conn.get_ref().clone()).await {
275        Ok(c) => c,
276        Err(e) => {
277            error!("failed to build h3 server connection: {e}");
278            return Ok(());
279        }
280    };
281
282    let mut request_join_set = tokio::task::JoinSet::new();
283
284    loop {
285        tokio::select! {
286            _ = cancel_rx.recv() => {
287                let _ = h3_conn.shutdown(0).await;
288                // As requested: do not poll accept() anymore, but we must
289                // keep h3_conn alive so that active streams aren't abruptly destroyed.
290                break;
291            }
292            accept_res = h3_conn.accept() => {
293                let resolver = match accept_res {
294                    Ok(Some(r)) => r,
295                    Ok(None) => break,
296                    Err(e) => {
297                        error!("h3 accept error: {e}");
298                        break;
299                    }
300                };
301
302                let (req, stream) = match resolver.resolve_request().await {
303                    Ok(pair) => pair,
304                    Err(e) => {
305                        error!("resolve request error: {e}");
306                        continue;
307                    }
308                };
309
310                let permit = match semaphore.clone().try_acquire_owned() {
311                    Ok(p) => p,
312                    Err(_) => {
313                        error!("server overloaded — dropping request");
314                        continue;
315                    }
316                };
317
318                let service = service.clone();
319                request_join_set.spawn(async move {
320                    let _permit = permit;
321                    if let Err(e) = handle_request(req, stream, service).await {
322                        error!(error = %e, "request handling error");
323                    }
324                });
325            }
326        }
327    }
328
329    // Wait for all active requests on this connection to finish
330    while let Some(res) = request_join_set.join_next().await {
331        if let Err(e) = res {
332            error!("request task failed: {e}");
333        }
334    }
335
336    drop(h3_conn);
337
338    Ok(())
339}