Skip to main content

eggserve_core/server/
mod.rs

1//! Reusable HTTP runtime and service boundary.
2//!
3//! This module provides a transport-owning HTTP runtime that downstream Rust
4//! projects can embed without importing internal modules or depending directly
5//! on Hyper.
6//!
7//! # Architecture
8//!
9//! ```text
10//! let server = Server::builder()
11//!     .runtime(RuntimeConfig::default())
12//!     .build()?;
13//! let handle = server.start_with_service(my_service).await?;
14//!
15//! handle.ready().await?;
16//! // server is accepting connections
17//!
18//! handle.shutdown();
19//! // server drains and stops
20//! handle.wait().await?;
21//! ```
22//!
23//! The runtime owns:
24//! - Listener acceptance
25//! - HTTP/1 parsing
26//! - Request conversion to canonical types
27//! - Response normalization
28//! - Timeout enforcement
29//! - Connection and file-stream permits
30//! - Connection/task tracking
31//! - Graceful shutdown with drain deadline
32//! - Forced shutdown with task cancellation
33//!
34//! Services own:
35//! - Request handling logic
36//! - Response construction
37//!
38//! # Public types
39//!
40//! - [`Server`] — the main entry point for embedding
41//! - [`ServerBuilder`] — configured builder for the server
42//! - [`ServerHandle`] — control handle for a running server
43//! - [`RuntimeConfig`] — transport-level configuration
44//! - [`Service`] — the service trait
45//! - [`service_fn`] — create a service from a closure
46//! - [`StaticService`] — hardened static file service
47//! - [`ServerError`] — startup and lifecycle errors
48//! - [`ServiceError`] — per-request service errors
49//! - [`ShutdownResult`] — outcome of a shutdown operation
50//! - [`LifecycleState`] — server lifecycle state
51
52pub mod config;
53pub mod connection;
54pub mod errors;
55pub mod handle;
56pub mod lifecycle;
57pub mod service;
58pub mod static_service;
59
60pub use crate::primitives::request::Request;
61pub use config::{try_from_serve_config, RuntimeConfig, RuntimeConfigBuilder};
62pub use errors::{ServerError, ShutdownResult};
63pub use handle::ServerHandle;
64pub use lifecycle::LifecycleState;
65pub use service::{
66    service_fn, service_fn_head, service_fn_with_policy, Service, ServiceError, ServiceFn,
67};
68pub use static_service::{StaticService, StaticServiceBuilder};
69
70use std::sync::Arc;
71
72use hyper_util::rt::TokioIo;
73use tokio::net::TcpListener;
74use tokio::sync::broadcast;
75
76use crate::config::ServeConfig;
77use crate::server::lifecycle::Lifecycle;
78
79/// A reusable HTTP runtime server.
80///
81/// This type is experimental and its API may change without notice.
82///
83/// The server binds a TCP listener, accepts connections, and dispatches them
84/// to a [`Service`] implementation. It owns the full connection lifecycle:
85/// parsing, normalization, timeouts, connection tracking, and graceful shutdown.
86///
87/// # Example
88///
89/// ```no_run
90/// use eggserve_core::server::{Server, RuntimeConfig, service_fn, Request};
91/// use eggserve_core::primitives::canonical::{Response, StatusCode, ResponseBody};
92///
93/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
94/// let server = Server::builder()
95///     .runtime(RuntimeConfig::builder()
96///         .bind("127.0.0.1:8000".parse().unwrap())
97///         .build()?)
98///     .build()?;
99///
100/// let handle = server.start_with_service(service_fn(|_req: Request| async {
101///     Ok(Response::builder()
102///         .status(StatusCode::OK)
103///         .body(ResponseBody::Bytes(b"hello".to_vec()))
104///         .unwrap())
105/// })).await?;
106/// handle.ready().await?;
107/// println!("listening on {}", handle.local_addr());
108///
109/// // ... serve requests ...
110///
111/// handle.shutdown();
112/// handle.wait().await?;
113/// # Ok(())
114/// # }
115/// ```
116pub struct Server {
117    config: RuntimeConfig,
118    builtin_static_service: Option<StaticService>,
119    lifecycle: Arc<Lifecycle>,
120    listener_source: Option<ListenerSource>,
121}
122
123/// Transport state shared by every connection in one running server.
124///
125/// In particular, file-stream admission is created once here and cloned into
126/// connection tasks. Static services never own or acquire this semaphore.
127#[derive(Debug)]
128pub struct RuntimeState {
129    pub(crate) file_stream_semaphore: Arc<tokio::sync::Semaphore>,
130}
131
132impl RuntimeState {
133    pub(crate) fn new(config: &RuntimeConfig) -> Self {
134        Self {
135            file_stream_semaphore: Arc::new(tokio::sync::Semaphore::new(config.max_file_streams)),
136        }
137    }
138
139    /// Construct an explicit admission context for legacy adapter migration
140    /// and low-level tests. Running servers must obtain their context from
141    /// [`Server::start`] or [`Server::start_with_service`].
142    #[doc(hidden)]
143    pub fn new_for_testing(max_file_streams: usize) -> Self {
144        Self {
145            file_stream_semaphore: Arc::new(tokio::sync::Semaphore::new(max_file_streams)),
146        }
147    }
148
149    /// Return the server-wide file-stream admission pool.
150    pub fn file_stream_semaphore(&self) -> &Arc<tokio::sync::Semaphore> {
151        &self.file_stream_semaphore
152    }
153}
154
155/// Source for the TCP listener.
156#[derive(Debug)]
157enum ListenerSource {
158    /// Bind to this address on start.
159    Bind(std::net::SocketAddr),
160    /// Use this pre-bound listener.
161    Listener(TcpListener),
162}
163
164impl Server {
165    /// Create a new server builder with default configuration.
166    pub fn builder() -> ServerBuilder {
167        ServerBuilder {
168            runtime_config: None,
169            serve_config: None,
170            listener_source: None,
171        }
172    }
173}
174
175/// Builder for constructing a [`Server`].
176///
177/// This type is experimental and its API may change without notice.
178///
179/// # Example
180///
181/// ```no_run
182/// use eggserve_core::server::{RuntimeConfig, Server};
183/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
184///
185/// let server = Server::builder()
186///     .runtime(RuntimeConfig::default())
187///     .static_service("/var/www")?;
188/// # Ok(())
189/// # }
190/// ```
191#[derive(Debug)]
192#[must_use]
193pub struct ServerBuilder {
194    runtime_config: Option<RuntimeConfig>,
195    serve_config: Option<Arc<ServeConfig>>,
196    listener_source: Option<ListenerSource>,
197}
198
199impl ServerBuilder {
200    /// Set the runtime configuration.
201    pub fn runtime(mut self, config: RuntimeConfig) -> Self {
202        self.runtime_config = Some(config);
203        self
204    }
205
206    /// Set a pre-built serve configuration.
207    ///
208    /// This bridges the CLI/Python configuration model. The runtime config
209    /// is derived from the serve config's limits and bind address.
210    pub fn serve_config(mut self, config: Arc<ServeConfig>) -> Self {
211        self.serve_config = Some(config);
212        self
213    }
214
215    /// Set the bind address for the listener.
216    ///
217    /// This overrides the bind address from `RuntimeConfig`. The server will
218    /// bind to this address when `start()` is called.
219    pub fn bind(mut self, addr: std::net::SocketAddr) -> Self {
220        self.listener_source = Some(ListenerSource::Bind(addr));
221        self
222    }
223
224    /// Use a pre-bound TCP listener instead of binding on start.
225    ///
226    /// The listener must already be bound to an address. The runtime will
227    /// take ownership of the listener after a successful `start()`.
228    ///
229    /// # Blocking/nonblocking
230    ///
231    /// The listener should be in nonblocking mode (as returned by
232    /// [`TcpListener::bind`] and [`TcpListener::from_std`]).
233    /// The runtime will normalize to nonblocking if needed.
234    ///
235    /// # Ownership
236    ///
237    /// After `start()`, the runtime owns the listener. The caller must not
238    /// use the listener after passing it to the builder.
239    pub fn from_listener(mut self, listener: TcpListener) -> Self {
240        self.listener_source = Some(ListenerSource::Listener(listener));
241        self
242    }
243
244    /// Build the server, eagerly constructing the built-in static file service
245    /// when a serve configuration was supplied.
246    ///
247    /// Invalid static roots therefore fail during `build()`, before listener
248    /// preparation or startup. The serve config must have been set via
249    /// [`ServerBuilder::serve_config`] for [`Server::start`] to be available.
250    pub fn build(self) -> Result<Server, ServerError> {
251        let serve_config = self.serve_config;
252        let config = match self.runtime_config {
253            Some(c) => c,
254            None => match &serve_config {
255                Some(sc) => config::try_from_serve_config(sc)?,
256                None => {
257                    return Err(ServerError::Config(
258                        "runtime configuration or serve configuration required".into(),
259                    ))
260                }
261            },
262        };
263        let builtin_static_service = serve_config
264            .map(StaticService::from_serve_config)
265            .transpose()
266            .map_err(|e| ServerError::Config(e.to_string()))?;
267        Ok(Server {
268            config,
269            builtin_static_service,
270            lifecycle: Arc::new(Lifecycle::new()),
271            listener_source: self.listener_source,
272        })
273    }
274
275    /// Build the server with a static service rooted at the given path.
276    ///
277    /// Convenience method that creates both the serve config and runtime config.
278    pub fn static_service(self, root: impl AsRef<std::path::Path>) -> Result<Server, ServerError> {
279        let serve_config = Arc::new(ServeConfig {
280            root: root.as_ref().to_path_buf(),
281            ..ServeConfig::default()
282        });
283        let config = match self.runtime_config {
284            Some(c) => c,
285            None => config::try_from_serve_config(&serve_config)?,
286        };
287        let builtin_static_service = StaticService::from_serve_config(serve_config)
288            .map_err(|e| ServerError::Config(e.to_string()))?;
289        Ok(Server {
290            config,
291            builtin_static_service: Some(builtin_static_service),
292            lifecycle: Arc::new(Lifecycle::new()),
293            listener_source: self.listener_source,
294        })
295    }
296}
297
298impl Server {
299    /// Start the server with the built-in static file service.
300    ///
301    /// Starts the statically constructed service using the shared generic
302    /// accept loop. The serve config must have been set via
303    /// [`ServerBuilder::serve_config`].
304    pub async fn start(self) -> Result<ServerHandle, ServerError> {
305        let Server {
306            config,
307            builtin_static_service,
308            lifecycle,
309            listener_source,
310        } = self;
311        let service = builtin_static_service.ok_or_else(|| {
312            ServerError::Config("serve configuration required for static service".into())
313        })?;
314
315        Server {
316            config,
317            builtin_static_service: None,
318            lifecycle,
319            listener_source,
320        }
321        .start_with_service(service)
322        .await
323    }
324
325    /// Start the server with a custom service.
326    ///
327    /// The custom service does not require a static root or serve configuration.
328    /// The runtime creates only transport state (semaphores, lifecycle) and
329    /// passes it to the accept loop and connection pipeline.
330    pub async fn start_with_service<S: Service>(
331        self,
332        service: S,
333    ) -> Result<ServerHandle, ServerError> {
334        let Server {
335            config: runtime_config,
336            builtin_static_service: _,
337            lifecycle,
338            listener_source,
339        } = self;
340        lifecycle.start()?;
341
342        let listener = match listener_source {
343            Some(ListenerSource::Listener(l)) => l,
344            Some(ListenerSource::Bind(addr)) => {
345                TcpListener::bind(addr).await.map_err(ServerError::Bind)?
346            }
347            None => TcpListener::bind(runtime_config.bind)
348                .await
349                .map_err(ServerError::Bind)?,
350        };
351
352        let local_addr = listener.local_addr().map_err(ServerError::Bind)?;
353
354        let config = Arc::new(runtime_config);
355        let connection_semaphore = Arc::new(tokio::sync::Semaphore::new(config.max_connections));
356        let runtime_state = Arc::new(RuntimeState::new(&config));
357
358        let (shutdown_tx, shutdown_rx) = broadcast::channel::<()>(1);
359        let shutdown_tx_clone = shutdown_tx.clone();
360        let lifecycle = lifecycle.clone();
361
362        let join = tokio::spawn({
363            let lifecycle = lifecycle.clone();
364            async move {
365                accept_loop_generic(
366                    listener,
367                    local_addr,
368                    config,
369                    runtime_state,
370                    connection_semaphore,
371                    service,
372                    shutdown_rx,
373                    lifecycle,
374                )
375                .await
376            }
377        });
378
379        Ok(ServerHandle::new(
380            local_addr,
381            shutdown_tx_clone,
382            join,
383            lifecycle,
384        ))
385    }
386}
387
388/// Unified accept loop for both static and custom services.
389///
390#[allow(clippy::too_many_arguments)]
391async fn accept_loop_generic<S: Service>(
392    listener: TcpListener,
393    local_addr: std::net::SocketAddr,
394    config: Arc<RuntimeConfig>,
395    runtime_state: Arc<RuntimeState>,
396    connection_semaphore: Arc<tokio::sync::Semaphore>,
397    service: S,
398    mut shutdown_rx: broadcast::Receiver<()>,
399    lifecycle: Arc<Lifecycle>,
400) -> ShutdownResult {
401    let service = Arc::new(service);
402
403    // Signal that we're running (listener bound, accept loop about to poll).
404    if lifecycle.mark_running().is_err() {
405        let _ = lifecycle.mark_failed();
406        return ShutdownResult::Clean;
407    }
408
409    crate::ops::Logger::global().emit(crate::ops::Event::new(
410        crate::ops::Severity::Info,
411        crate::ops::EventKind::ListenerReady,
412        "accept loop started",
413    ));
414
415    let correlation = crate::ops::CorrelationId::new();
416    let counters = crate::ops::global_counters();
417
418    // Track spawned connection tasks for graceful drain.
419    let mut tasks = tokio::task::JoinSet::new();
420    let mut backoff_idx: usize = 0;
421    let mut error_repeat_count: usize = 0;
422    let mut last_error_kind: Option<String> = None;
423
424    loop {
425        tokio::select! {
426            result = listener.accept() => {
427                match result {
428                    Ok((stream, peer_addr)) => {
429                        let _ = stream.set_nodelay(true);
430                        backoff_idx = 0;
431                        error_repeat_count = 0;
432                        last_error_kind = None;
433                        let conn_id = correlation.next();
434                        counters.connections_accepted.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
435                        counters.active_connections.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
436
437                        crate::ops::Logger::global().emit(
438                            crate::ops::Event::new(
439                                crate::ops::Severity::Debug,
440                                crate::ops::EventKind::ConnectionAccepted,
441                                "connection accepted",
442                            )
443                            .connection_id(conn_id),
444                        );
445
446                        let permit = match connection_semaphore.clone().try_acquire_owned() {
447                            Ok(p) => p,
448                            Err(_) => {
449                                counters.connections_rejected.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
450                                counters.active_connections.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
451                                crate::ops::Logger::global().emit(
452                                    crate::ops::Event::new(
453                                        crate::ops::Severity::Debug,
454                                        crate::ops::EventKind::ConnectionRejected,
455                                        "connection rejected: admission limit",
456                                    )
457                                    .connection_id(conn_id),
458                                );
459                                drop(stream);
460                                continue;
461                            }
462                        };
463
464                        let mut shutdown_rx = shutdown_rx.resubscribe();
465                        let runtime_state = runtime_state.clone();
466                        let config = config.clone();
467                        let service = service.clone();
468                        let remote_addr = peer_addr;
469                        let local_addr_pre_tls = stream.local_addr().unwrap_or(local_addr);
470
471                        tasks.spawn(async move {
472                            let _permit = permit;
473                            let _active_connection = ActiveConnectionGuard;
474
475                            #[cfg(feature = "tls")]
476                            {
477                                if let Some(tls_config) = &config.tls_config {
478                                    let tls_acceptor = tokio_rustls::TlsAcceptor::from(tls_config.clone());
479                                    match accept_tls(stream, &tls_acceptor, config.header_read_timeout, conn_id).await {
480                                        Some((tls_stream, tls_info)) => {
481                                            crate::ops::Logger::global().emit(
482                                                crate::ops::Event::new(
483                                                    crate::ops::Severity::Debug,
484                                                    crate::ops::EventKind::TlsHandshakeSuccess,
485                                                    "TLS handshake completed",
486                                                )
487                                                .connection_id(conn_id),
488                                            );
489                                            let io = TokioIo::new(tls_stream);
490                                            connection::serve_connection_with_runtime_state(
491                                                io,
492                                                ArcService(service),
493                                                &config,
494                                                runtime_state.clone(),
495                                                &mut shutdown_rx,
496                                                conn_id,
497                                                local_addr_pre_tls,
498                                                remote_addr,
499                                                true,
500                                                Some(tls_info),
501                                            ).await;
502                                            return;
503                                        }
504                                        None => {
505                                            return;
506                                        }
507                                    }
508                                }
509                            }
510
511                            let io = TokioIo::new(stream);
512                            connection::serve_connection_with_runtime_state(
513                                io,
514                                ArcService(service),
515                                &config,
516                                runtime_state.clone(),
517                                &mut shutdown_rx,
518                                conn_id,
519                                local_addr_pre_tls,
520                                remote_addr,
521                                false,
522                                None,
523                            ).await;
524                        });
525                    }
526                    Err(e) => {
527                        let fatal = classify_accept_error(&e, &mut shutdown_rx, &mut backoff_idx, &mut error_repeat_count, &mut last_error_kind).await;
528                        if fatal {
529                            break;
530                        }
531                    }
532                }
533            }
534            _ = shutdown_rx.recv() => {
535                break;
536            }
537        }
538    }
539
540    crate::ops::Logger::global().emit(crate::ops::Event::new(
541        crate::ops::Severity::Info,
542        crate::ops::EventKind::ShutdownRequested,
543        "shutdown requested",
544    ));
545
546    // Transition to Draining.
547    let _ = lifecycle.drain();
548
549    // Wait for in-flight connections to drain.
550    let drain_timeout = config.graceful_shutdown_timeout;
551    let deadline = tokio::time::Instant::now() + drain_timeout;
552    let mut timed_out = false;
553
554    loop {
555        let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
556        if remaining.is_zero() {
557            timed_out = true;
558            break;
559        }
560        match tokio::time::timeout(remaining, tasks.join_next()).await {
561            Ok(Some(result)) => {
562                if let Err(e) = result {
563                    if e.is_panic() {
564                        counters
565                            .connection_panics
566                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
567                        crate::ops::Logger::global().emit(crate::ops::Event::new(
568                            crate::ops::Severity::Error,
569                            crate::ops::EventKind::ConnectionPanic,
570                            "connection task panicked during drain",
571                        ));
572                    }
573                }
574            }
575            Ok(None) => break,
576            Err(_) => {
577                timed_out = true;
578                break;
579            }
580        }
581    }
582
583    let mut abort_count = 0usize;
584
585    if timed_out {
586        crate::ops::Logger::global().emit(crate::ops::Event::new(
587            crate::ops::Severity::Warn,
588            crate::ops::EventKind::ForcedShutdownStarted,
589            "grace deadline exceeded, aborting remaining tasks",
590        ));
591        tasks.abort_all();
592        while let Some(result) = tasks.join_next().await {
593            abort_count += 1;
594            if let Err(e) = result {
595                if e.is_panic() {
596                    counters
597                        .connection_panics
598                        .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
599                    crate::ops::Logger::global().emit(crate::ops::Event::new(
600                        crate::ops::Severity::Error,
601                        crate::ops::EventKind::ConnectionPanic,
602                        "connection task panicked during forced shutdown",
603                    ));
604                }
605            }
606        }
607    }
608
609    let _ = lifecycle.mark_stopped();
610
611    let result = if timed_out {
612        counters
613            .forced_shutdowns
614            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
615        ShutdownResult::Timeout
616    } else {
617        counters
618            .graceful_shutdowns
619            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
620        ShutdownResult::Clean
621    };
622
623    crate::ops::Logger::global().emit(crate::ops::Event::new(
624        crate::ops::Severity::Info,
625        crate::ops::EventKind::ShutdownComplete,
626        format!("shutdown complete: {:?} (aborted={})", result, abort_count),
627    ));
628
629    result
630}
631
632/// Accept a TLS connection with timeout.
633///
634/// Returns the TLS stream and TLS session metadata on success, or `None` if
635/// the handshake failed or timed out. Emits `TlsHandshakeFailure` or
636/// `TlsHandshakeTimeout` events on failure.
637#[cfg(feature = "tls")]
638async fn accept_tls(
639    stream: tokio::net::TcpStream,
640    tls_acceptor: &tokio_rustls::TlsAcceptor,
641    timeout: std::time::Duration,
642    conn_id: u64,
643) -> Option<(
644    tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
645    crate::primitives::connection_info::TlsInfo,
646)> {
647    match tokio::time::timeout(timeout, tls_acceptor.accept(stream)).await {
648        Ok(Ok(tls_stream)) => {
649            let tls_info = extract_tls_info(&tls_stream);
650            Some((tls_stream, tls_info))
651        }
652        Ok(Err(_)) => {
653            crate::ops::Logger::global().emit(
654                crate::ops::Event::new(
655                    crate::ops::Severity::Warn,
656                    crate::ops::EventKind::TlsHandshakeFailure,
657                    "TLS handshake failed",
658                )
659                .connection_id(conn_id),
660            );
661            None
662        }
663        Err(_) => {
664            crate::ops::Logger::global().emit(
665                crate::ops::Event::new(
666                    crate::ops::Severity::Warn,
667                    crate::ops::EventKind::TlsHandshakeTimeout,
668                    "TLS handshake timeout",
669                )
670                .connection_id(conn_id),
671            );
672            None
673        }
674    }
675}
676
677/// Extract TLS session metadata from a completed TLS stream.
678#[cfg(feature = "tls")]
679fn extract_tls_info(
680    tls_stream: &tokio_rustls::server::TlsStream<tokio::net::TcpStream>,
681) -> crate::primitives::connection_info::TlsInfo {
682    use crate::primitives::connection_info::TlsInfo;
683
684    let (_io, conn) = tls_stream.get_ref();
685    let protocol_version = conn.protocol_version().map(|v| format!("{v:?}"));
686    let server_name = conn.server_name().map(|n| n.to_owned());
687    TlsInfo {
688        protocol_version,
689        server_name,
690    }
691}
692
693/// Classify an accept loop error, emit a structured log event, and apply
694/// bounded exponential backoff for transient errors. The backoff is
695/// interruptible by shutdown via the provided receiver.
696///
697/// Rate-limits repeated identical errors: emits the first occurrence, then
698/// a summary every 10 consecutive identical errors, resetting on success
699/// or a different error kind.
700///
701/// Returns `true` if the error is fatal and the accept loop should terminate.
702#[allow(clippy::collapsible_match)]
703async fn classify_accept_error(
704    e: &std::io::Error,
705    shutdown_rx: &mut broadcast::Receiver<()>,
706    backoff_idx: &mut usize,
707    error_repeat_count: &mut usize,
708    last_error_kind: &mut Option<String>,
709) -> bool {
710    use crate::ops::{Event, EventKind, Logger, Severity};
711
712    let err_str = e.to_string();
713    let kind = e.kind();
714    let fd_exhausted = is_fd_exhaustion(e);
715
716    let (severity, event_kind, should_backoff, is_fatal) = match kind {
717        std::io::ErrorKind::Interrupted => (
718            Severity::Debug,
719            EventKind::ListenerTransientError,
720            true,
721            false,
722        ),
723        std::io::ErrorKind::ConnectionRefused
724        | std::io::ErrorKind::ConnectionReset
725        | std::io::ErrorKind::ConnectionAborted
726        | std::io::ErrorKind::BrokenPipe => (
727            Severity::Debug,
728            EventKind::ListenerTransientError,
729            true,
730            false,
731        ),
732        std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut => (
733            Severity::Warn,
734            EventKind::ListenerTransientError,
735            true,
736            false,
737        ),
738        std::io::ErrorKind::OutOfMemory | std::io::ErrorKind::Other if fd_exhausted => {
739            (Severity::Error, EventKind::ResourceExhaustion, true, false)
740        }
741        std::io::ErrorKind::OutOfMemory | std::io::ErrorKind::Other => (
742            Severity::Error,
743            EventKind::ListenerPersistentError,
744            false,
745            true,
746        ),
747        _ if fd_exhausted => (Severity::Error, EventKind::ResourceExhaustion, true, false),
748        _ => (
749            Severity::Error,
750            EventKind::ListenerPersistentError,
751            false,
752            true,
753        ),
754    };
755
756    crate::ops::global_counters()
757        .listener_errors
758        .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
759
760    // Rate-limit repeated identical errors.
761    let current_kind = format!("{}", event_kind);
762    let is_same_kind = last_error_kind.as_deref() == Some(&current_kind);
763    if is_same_kind {
764        *error_repeat_count += 1;
765    } else {
766        *error_repeat_count = 1;
767        *last_error_kind = Some(current_kind);
768    }
769
770    // Emit on first occurrence, then every 10th.
771    let should_emit = *error_repeat_count == 1 || (*error_repeat_count).is_multiple_of(10);
772    if should_emit {
773        let message = if *error_repeat_count > 1 {
774            format!(
775                "accept error ({} consecutive): {}",
776                error_repeat_count, err_str
777            )
778        } else {
779            format!("accept error: {}", err_str)
780        };
781        Logger::global().emit(Event::new(severity, event_kind, message).field(
782            crate::ops::Field::Str("error_kind".into(), format!("{:?}", kind)),
783        ));
784    }
785
786    if should_backoff {
787        static BACKOFF_MS: [u64; 5] = [1, 2, 4, 8, 50];
788        let idx = (*backoff_idx).min(BACKOFF_MS.len() - 1);
789        *backoff_idx = backoff_idx.saturating_add(1);
790        let backoff = std::time::Duration::from_millis(BACKOFF_MS[idx]);
791        tokio::select! {
792            _ = tokio::time::sleep(backoff) => {}
793            _ = shutdown_rx.recv() => {}
794        }
795    }
796
797    is_fatal
798}
799
800fn is_fd_exhaustion(error: &std::io::Error) -> bool {
801    #[cfg(unix)]
802    if let Some(raw) = error.raw_os_error() {
803        return raw == rustix::io::Errno::MFILE.raw_os_error().abs()
804            || raw == rustix::io::Errno::NFILE.raw_os_error().abs();
805    }
806
807    if error.raw_os_error().is_some() {
808        return false;
809    }
810
811    let message = error.to_string().to_ascii_lowercase();
812    message.contains("too many open files")
813        || message.contains("emfile")
814        || message.contains("enfile")
815}
816
817struct ActiveConnectionGuard;
818
819impl Drop for ActiveConnectionGuard {
820    fn drop(&mut self) {
821        crate::ops::global_counters()
822            .active_connections
823            .fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
824    }
825}
826
827/// Wrapper to implement `Service` for `Arc<S>`.
828struct ArcService<S>(Arc<S>);
829
830impl<S: Service> Service for ArcService<S> {
831    fn request_body_policy(
832        &self,
833        head: &crate::primitives::request_head::RequestHead,
834    ) -> crate::primitives::request_body_policy::RequestBodyPolicy {
835        self.0.request_body_policy(head)
836    }
837
838    fn call(
839        &self,
840        request: crate::primitives::request::Request,
841    ) -> std::pin::Pin<
842        Box<
843            dyn std::future::Future<
844                    Output = Result<crate::primitives::canonical::Response, ServiceError>,
845                > + Send
846                + '_,
847        >,
848    > {
849        self.0.call(request)
850    }
851}
852
853#[cfg(test)]
854mod tests {
855    use super::*;
856
857    #[cfg(unix)]
858    #[tokio::test]
859    async fn classify_accept_error_uses_os_error_for_fd_exhaustion() {
860        let error = std::io::Error::from_raw_os_error(libc::EMFILE);
861        let (tx, mut rx) = broadcast::channel(1);
862        let mut backoff = 0;
863        let mut repeats = 0;
864        let mut last = None;
865        assert!(
866            !classify_accept_error(&error, &mut rx, &mut backoff, &mut repeats, &mut last,).await
867        );
868        let _ = tx.send(());
869    }
870}