axum_conf/fluent/builder.rs
1//! Orchestration and router delegation: setup_middleware(), start(), layer(), route(), etc.
2
3use super::router::FluentRouter;
4use super::shutdown::{ShutdownNotifier, ShutdownPhase};
5use crate::Result;
6
7use {
8 axum::{Router, body::Body, routing::Route},
9 http::Request,
10 std::{convert::Infallible, env, net::SocketAddr, time::Duration},
11 tokio::signal,
12 tower::{Layer, Service},
13};
14
15impl<State> FluentRouter<State>
16where
17 State: Clone + Send + Sync + 'static,
18{
19 /// Sets up all standard middleware layers in the correct order.
20 ///
21 /// This is the **recommended way** to configure middleware. It handles the complex
22 /// ordering requirements automatically, ensuring all layers work correctly together.
23 ///
24 /// # When to Use This Method
25 ///
26 /// **Use `setup_middleware()`** for most applications. It provides production-ready
27 /// defaults and handles middleware dependencies automatically.
28 ///
29 /// **Use individual `setup_*` methods** only when you need:
30 /// - Custom middleware ordering
31 /// - Middleware between specific layers
32 /// - Partial middleware stack (though `exclude` config is preferred)
33 ///
34 /// # What It Configures
35 ///
36 /// - Liveness/readiness probes
37 /// - OIDC authentication (if `keycloak` feature enabled)
38 /// - Request deduplication
39 /// - Concurrency limits
40 /// - Payload size limits
41 /// - Compression/decompression
42 /// - Path normalization
43 /// - Sensitive header protection
44 /// - Request ID generation
45 /// - API versioning
46 /// - CORS headers
47 /// - Security headers (Helmet)
48 /// - Logging and tracing
49 /// - Metrics collection (Prometheus)
50 /// - Request timeouts
51 /// - Rate limiting
52 /// - Panic recovery
53 ///
54 /// # Middleware Order
55 ///
56 /// **CRITICAL**: Middleware is processed outside-in for requests and inside-out for responses.
57 /// The **last layer added is the outermost layer** and executes **first** on incoming requests.
58 ///
59 /// The current order (from innermost to outermost):
60 /// 1. **OIDC Authentication** - Check auth after infrastructure layers
61 /// 2. **Deduplication** - Check for duplicate requests
62 /// 3. **Concurrency limit** - Control concurrent processing
63 /// 4. **Max payload size** - Limit body size
64 /// 5. **Compression/Decompression** - Handle encoding
65 /// 6. **Path normalization** - Normalize before routing
66 /// 7. **Sensitive headers** - Filter before logging
67 /// 8. **API versioning** - Extract version from path/headers/query
68 /// 9. **CORS** - Handle preflight & add headers
69 /// 10. **Security headers (Helmet)** - Apply to all responses
70 /// 11. **Logging** - Log all requests
71 /// 12. **Metrics** - Measure all requests
72 /// 13. **Readiness** - Database health check (benefits from timeout/rate limiting)
73 /// 14. **Timeout** - Set timeout boundary for everything (optional)
74 /// 15. **Rate limiting** - Reject excessive requests early
75 /// 16. **Request ID** - Generate/extract ID for tracing (early for observability)
76 /// 17. **Liveness** - Simple health check (always accessible, very early)
77 /// 18. **Panic catching** - Catch ALL panics from inner layers (outermost)
78 ///
79 /// # Manual Setup (Advanced)
80 ///
81 /// If you need custom ordering, call individual `setup_*` methods. **Important rules**:
82 ///
83 /// - **Call order matters**: Methods must be called in reverse execution order
84 /// (first method called = innermost layer = executes last on request)
85 /// - **Dependencies**: Some middleware depends on others:
86 /// - `setup_request_id()` must be called **after** `setup_deduplication()` so the
87 /// request ID is available when deduplication checks for duplicates
88 /// - `setup_oidc()` requires `setup_session_handling()` (when using sessions)
89 /// - **Don't call twice**: Each `setup_*` method should only be called once
90 /// - **Configuration controls**: Use `[http.middleware] exclude/include` instead of
91 /// skipping methods, as this ensures proper dependency handling
92 ///
93 /// ```rust,no_run
94 /// # use axum_conf::{Config, FluentRouter, Result};
95 /// # async fn example() -> Result<()> {
96 /// // Manual setup example (not recommended unless you need custom ordering)
97 /// let router = FluentRouter::without_state(Config::default())?
98 /// // Innermost layers first (execute last on request)
99 /// .setup_deduplication()
100 /// .setup_logging()
101 /// .setup_readiness() // /ready - after timeout/rate limiting (benefits from protection)
102 /// .setup_timeout()
103 /// .setup_rate_limiting()
104 /// .setup_request_id() // Outer to deduplication, generates ID early
105 /// .setup_liveness() // /live - always accessible, very early
106 /// .setup_catch_panic(); // Outermost (executes first on request)
107 /// # Ok(())
108 /// # }
109 /// ```
110 ///
111 /// # Returns
112 ///
113 /// A `Result` containing the configured router or an error if setup fails.
114 ///
115 /// # Errors
116 ///
117 /// Returns an error if:
118 /// - OIDC configuration is invalid (when `keycloak` feature enabled)
119 /// - Configuration validation fails
120 ///
121 /// # Note
122 ///
123 /// Disable Prometheus in tests to avoid global registry conflicts:
124 /// ```rust
125 /// # use axum_conf::Config;
126 /// let mut config = Config::default();
127 /// config.http.with_metrics = false;
128 /// ```
129 pub async fn setup_middleware(self) -> Result<Self> {
130 // Output the current version of the service
131 const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
132 const VERSION: &str = env!("CARGO_PKG_VERSION");
133 tracing::info!("Starting {PACKAGE_NAME} version {VERSION}...");
134
135 // Capture config values before moving self
136 let default_api_version = self.config.http.default_api_version;
137
138 // Middleware is added from innermost to outermost
139 // The last layer added executes FIRST on incoming requests
140 // Note: route_layer applies to routes added BEFORE it, so OIDC auth is applied first,
141 // then health endpoints are added AFTER (so they're not protected by auth)
142
143 // Protected static files must be added BEFORE auth so route_layer applies to them
144 let router = self.setup_protected_files()?;
145
146 #[cfg(feature = "keycloak")]
147 let router = router.setup_oidc()?; // 1a. OIDC Authentication (route_layer - applies to existing routes)
148
149 #[cfg(feature = "basic-auth")]
150 let router = router.setup_basic_auth()?; // 1b. Basic Auth (route_layer - applies to existing routes)
151
152 // Public static files added AFTER auth so they're accessible without authentication
153 let router = router.setup_public_files()?;
154
155 let router = router.setup_user_span(); // 1c. Record username to span (after auth)
156
157 let router = router
158 .setup_deduplication() // 2. Deduplication
159 .setup_concurrency_limit() // 3. Concurrency control
160 .setup_max_payload_size() // 4. Body size limits
161 .setup_compression() // 5. Compression/decompression
162 .setup_path_normalization() // 6. Path normalization
163 .setup_sensitive_headers() // 7. Filter sensitive headers
164 .setup_api_versioning(default_api_version) // 8. API versioning
165 .setup_cors() // 9. CORS handling
166 .setup_helmet() // 10. Security headers
167 .setup_logging() // 11. Request/response logging
168 .setup_metrics() // 12. Metrics collection
169 .setup_readiness() // 13. Readiness endpoint (benefits from timeout/rate limiting)
170 .setup_timeout() // 14. Request timeout (optional)
171 .setup_rate_limiting() // 15. Rate limiting
172 .setup_request_id() // 16. Request ID - early so all requests get IDs
173 .setup_liveness() // 17. Liveness endpoint (always accessible, very early)
174 .setup_catch_panic() // 18. Outermost - panic recovery
175 .setup_fallback_files()?; // 19. Fallback static files (must be last)
176
177 Ok(router)
178 }
179
180 /// Adds the remaining standard middleware layers in the correct order.
181 /// These layers should be added last as they handle security, errors and panics.
182 /// Since they are added last, they are the outermost layers and thus executed first.
183 ///
184 /// # Deprecated
185 ///
186 /// This method is deprecated. Use `setup_middleware()` instead, which now includes
187 /// all middleware layers in the optimal order. This method is kept for backward
188 /// compatibility but does nothing.
189 #[must_use]
190 #[deprecated(
191 since = "0.2.2",
192 note = "Use setup_middleware() instead, which now includes all layers"
193 )]
194 pub fn build(self) -> Self {
195 // All middleware is now configured in setup_middleware()
196 // This method is a no-op for backward compatibility
197 self
198 }
199
200 /// Starts the HTTP server based on the current configuration.
201 ///
202 /// The server supports both HTTP/1.1 and HTTP/2 protocols automatically.
203 /// HTTP/2 will be used when clients request it via ALPN negotiation.
204 ///
205 /// # Graceful Shutdown
206 ///
207 /// When a shutdown signal is received (SIGTERM or SIGINT), the server:
208 ///
209 /// 1. Emits [`ShutdownPhase::Initiated`] to all subscribers
210 /// 2. Triggers the cancellation token (stopping background tasks)
211 /// 3. Stops accepting new connections
212 /// 4. Emits [`ShutdownPhase::GracePeriodStarted`] with the configured timeout
213 /// 5. Waits for in-flight requests to complete (up to `shutdown_timeout`)
214 /// 6. Emits [`ShutdownPhase::GracePeriodEnded`] if timeout expires
215 /// 7. Exits
216 ///
217 /// If all connections drain before the timeout, shutdown completes early
218 /// without waiting for the full timeout duration.
219 ///
220 /// Components can subscribe to these phases before calling `start()`:
221 ///
222 /// ```rust,no_run
223 /// use axum_conf::{Config, FluentRouter, ShutdownPhase};
224 ///
225 /// # async fn example() -> axum_conf::Result<()> {
226 /// let router = FluentRouter::without_state(Config::default())?;
227 ///
228 /// // Set up shutdown handlers BEFORE starting
229 /// let mut shutdown_rx = router.subscribe_to_shutdown();
230 ///
231 /// tokio::spawn(async move {
232 /// while let Ok(phase) = shutdown_rx.recv().await {
233 /// tracing::info!("Shutdown phase: {:?}", phase);
234 /// }
235 /// });
236 ///
237 /// // Now start the server
238 /// router.setup_middleware().await?.start().await
239 /// # }
240 /// ```
241 pub async fn start(self) -> Result<()>
242 where
243 State: Clone + Send + Sync + 'static,
244 {
245 let bind_addr = self.config.http.full_bind_addr();
246 let listener = tokio::net::TcpListener::bind(&bind_addr).await?;
247
248 tracing::info!("Bound to {}", &bind_addr);
249 tracing::info!("Waiting for connections");
250 tracing::info!("Max req/s: {}", self.config.http.max_requests_per_sec);
251
252 let service = self
253 .inner
254 .with_state(self.state)
255 .into_make_service_with_connect_info::<SocketAddr>();
256
257 let shutdown_timeout = self.config.http.shutdown_timeout;
258 let shutdown_notifier = self.shutdown_notifier.clone();
259
260 // Subscribe to shutdown notifications to know when signal is received
261 let mut shutdown_rx = shutdown_notifier.subscribe();
262
263 let serve_future = axum::serve(listener, service).with_graceful_shutdown(
264 shutdown_signal_with_notifications(shutdown_timeout, shutdown_notifier.clone()),
265 );
266
267 // Wait for graceful shutdown with timeout enforcement.
268 // The timeout only starts AFTER a shutdown signal is received, not immediately.
269 // If connections drain before timeout, we complete early.
270 // If timeout expires first, we emit GracePeriodEnded and force shutdown.
271 tokio::select! {
272 result = serve_future => {
273 // Server shut down gracefully (connections drained)
274 tracing::info!("Graceful shutdown completed");
275 result?;
276 }
277 _ = async {
278 // Wait for shutdown to be initiated before starting the timeout
279 loop {
280 match shutdown_rx.recv().await {
281 Ok(ShutdownPhase::Initiated) => break,
282 Ok(_) => continue,
283 Err(_) => return, // Channel closed
284 }
285 }
286 // Now start the timeout (only after signal received)
287 tokio::time::sleep(shutdown_timeout).await;
288 } => {
289 // Timeout expired after shutdown was initiated
290 tracing::warn!("Graceful shutdown timeout expired, forcing shutdown");
291 shutdown_notifier.emit(ShutdownPhase::GracePeriodEnded);
292 }
293 }
294
295 Ok(())
296 }
297
298 /// Adds a custom Tower middleware layer to the router.
299 ///
300 /// This is a low-level method that forwards to `axum::Router::layer()`,
301 /// allowing you to add custom middleware that isn't provided by the library.
302 ///
303 /// # Type Parameters
304 ///
305 /// * `L` - A Tower Layer that produces services compatible with Axum
306 ///
307 /// # Examples
308 ///
309 /// ```rust,no_run
310 /// use tower::limit::ConcurrencyLimitLayer;
311 /// # use axum_conf::{Config, FluentRouter};
312 /// # fn example() -> axum_conf::Result<()> {
313 ///
314 /// let router = FluentRouter::without_state(Config::default())?
315 /// .layer(ConcurrencyLimitLayer::new(100));
316 /// # Ok(())
317 /// # }
318 /// ```
319 #[must_use]
320 pub fn layer<L>(mut self, layer: L) -> Self
321 where
322 L: Layer<Route> + Clone + Send + Sync + 'static,
323 L::Service: Service<Request<Body>> + Clone + Send + Sync + 'static,
324 <L::Service as Service<Request<Body>>>::Response: axum::response::IntoResponse + 'static,
325 <L::Service as Service<Request<Body>>>::Error: Into<Infallible> + 'static,
326 <L::Service as Service<Request<Body>>>::Future: Send + 'static,
327 {
328 self.inner = self.inner.layer(layer);
329 self
330 }
331
332 /// Adds a new route to the router at the specified path.
333 ///
334 /// Routes define how HTTP requests to specific paths are handled.
335 /// Use the routing helpers from `axum::routing` to create method routers:
336 /// - `get()` - Handle GET requests
337 /// - `post()` - Handle POST requests
338 /// - `put()` - Handle PUT requests
339 /// - `delete()` - Handle DELETE requests
340 /// - And more...
341 ///
342 /// # Arguments
343 ///
344 /// * `path` - The URL path pattern for this route (e.g., "/users/:id")
345 /// * `route` - A `MethodRouter` created with `axum::routing` helpers
346 ///
347 /// # Examples
348 ///
349 /// ```
350 /// use axum_conf::{Config, FluentRouter};
351 /// use axum::routing::get;
352 ///
353 /// async fn handler() -> &'static str {
354 /// "Hello, World!"
355 /// }
356 ///
357 /// # async fn example() {
358 /// let config = Config::default();
359 /// let router = FluentRouter::without_state(config)
360 /// .unwrap()
361 /// .route("/hello", get(handler))
362 /// .into_inner();
363 /// # }
364 /// ```
365 #[must_use]
366 pub fn route(mut self, path: &str, route: axum::routing::MethodRouter<State>) -> Self {
367 self.inner = self.inner.route(path, route);
368 self
369 }
370
371 /// Adds a middleware layer that only applies to routes, not services.
372 ///
373 /// This is a low-level method that forwards to `axum::Router::route_layer()`.
374 /// Unlike `layer()`, this only affects route handlers and doesn't wrap
375 /// nested services.
376 ///
377 /// # Type Parameters
378 ///
379 /// * `L` - A Tower Layer that produces services compatible with Axum
380 ///
381 /// # Use Cases
382 ///
383 /// Use this when you want middleware to only affect your route handlers
384 /// but not services like `ServeDir` or nested routers.
385 #[must_use]
386 pub fn route_layer<L>(mut self, layer: L) -> Self
387 where
388 L: Layer<Route> + Clone + Send + Sync + 'static,
389 L::Service: Service<Request<Body>> + Clone + Send + Sync + 'static,
390 <L::Service as Service<Request<Body>>>::Response: axum::response::IntoResponse + 'static,
391 <L::Service as Service<Request<Body>>>::Error: Into<Infallible> + 'static,
392 <L::Service as Service<Request<Body>>>::Future: Send + 'static,
393 {
394 self.inner = self.inner.route_layer(layer);
395 self
396 }
397
398 /// Nests another router at a specific path prefix.
399 ///
400 /// All routes in the nested router will be prefixed with the given path.
401 /// Middleware added to the nested router only affects its own routes.
402 ///
403 /// # Arguments
404 ///
405 /// * `path` - The path prefix (must start with `/`)
406 /// * `router` - The router to nest
407 ///
408 /// # Examples
409 ///
410 /// ```rust,no_run
411 /// use axum::{Router, routing::get};
412 /// # use axum_conf::{Config, FluentRouter};
413 /// # fn example() -> axum_conf::Result<()> {
414 ///
415 /// let api_v1 = Router::new()
416 /// .route("/users", get(|| async { "users" }));
417 ///
418 /// let app = FluentRouter::without_state(Config::default())?
419 /// .nest("/api/v1", api_v1); // Routes at /api/v1/users
420 /// # Ok(())
421 /// # }
422 /// ```
423 #[must_use]
424 pub fn nest(mut self, path: &str, router: Router<State>) -> Self {
425 self.inner = self.inner.nest(path, router);
426 self
427 }
428
429 /// Nests a Tower service at a specific path prefix.
430 ///
431 /// Similar to `nest()` but for raw Tower services instead of Axum routers.
432 /// Commonly used for serving static files with `ServeDir`.
433 ///
434 /// # Arguments
435 ///
436 /// * `path` - The path prefix (must start with `/`)
437 /// * `service` - The Tower service to nest
438 ///
439 /// # Examples
440 ///
441 /// ```rust,no_run
442 /// use axum::{Router, routing::get};
443 /// # use axum_conf::{Config, FluentRouter};
444 /// # fn example() -> axum_conf::Result<()> {
445 ///
446 /// let service = Router::new().route("/health", get(|| async { "OK" }));
447 /// let app = FluentRouter::without_state(Config::default())?
448 /// .nest_service("/api", service);
449 /// # Ok(())
450 /// # }
451 /// ```
452 #[must_use]
453 pub fn nest_service<T>(mut self, path: &str, service: T) -> Self
454 where
455 T: Service<Request<Body>, Response = axum::response::Response, Error = Infallible>
456 + Clone
457 + Send
458 + Sync
459 + 'static,
460 T::Future: Send + 'static,
461 {
462 self.inner = self.inner.nest_service(path, service);
463 self
464 }
465
466 /// Merges another router into this one.
467 ///
468 /// Routes and services from the other router are added to this router.
469 /// Unlike `nest()`, routes are not prefixed - they're added at the same level.
470 ///
471 /// # Arguments
472 ///
473 /// * `other` - The router to merge
474 ///
475 /// # Examples
476 ///
477 /// ```rust,no_run
478 /// use axum::{Router, routing::get};
479 /// # use axum_conf::{Config, FluentRouter};
480 /// # fn example() -> axum_conf::Result<()> {
481 ///
482 /// let user_routes = Router::new()
483 /// .route("/users", get(|| async { "users" }));
484 ///
485 /// let app = FluentRouter::without_state(Config::default())?
486 /// .merge(user_routes); // Routes directly at /users
487 /// # Ok(())
488 /// # }
489 /// ```
490 ///
491 /// # Common Pattern
492 ///
493 /// Use `merge()` to combine route modules:
494 /// ```rust,no_run
495 /// # use axum::Router;
496 /// # use axum_conf::{Config, FluentRouter};
497 /// # fn example() -> axum_conf::Result<()> {
498 /// # fn api_routes() -> Router { Router::new() }
499 /// # fn admin_routes() -> Router { Router::new() }
500 /// FluentRouter::without_state(Config::default())?
501 /// .merge(api_routes())
502 /// .merge(admin_routes());
503 /// # Ok(())
504 /// # }
505 /// ```
506 #[must_use]
507 pub fn merge(mut self, other: Router<State>) -> Self {
508 self.inner = self.inner.merge(other);
509 self
510 }
511
512 /// Adds a Tower service at a specific route.
513 ///
514 /// Unlike `nest_service()`, this adds the service at an exact path rather
515 /// than a path prefix.
516 ///
517 /// # Arguments
518 ///
519 /// * `path` - The exact route path
520 /// * `service` - The Tower service to add
521 ///
522 /// # Examples
523 ///
524 /// ```rust,no_run
525 /// use tower::service_fn;
526 /// use http::Response;
527 /// # use axum_conf::{Config, FluentRouter};
528 /// # async fn example() -> axum_conf::Result<()> {
529 ///
530 /// let service = service_fn(|_req| async {
531 /// Ok::<_, std::convert::Infallible>(Response::new("Hello".into()))
532 /// });
533 ///
534 /// let app = FluentRouter::without_state(Config::default())?
535 /// .route_service("/custom", service);
536 /// # Ok(())
537 /// # }
538 /// ```
539 #[must_use]
540 pub fn route_service<T>(mut self, path: &str, service: T) -> Self
541 where
542 T: Service<Request<Body>, Response = axum::response::Response, Error = Infallible>
543 + Clone
544 + Send
545 + Sync
546 + 'static,
547 T::Future: Send + 'static,
548 {
549 self.inner = self.inner.route_service(path, service);
550 self
551 }
552
553 /// Consumes the `FluentRouter` and returns the underlying `axum::Router`.
554 ///
555 /// Use this when you need direct access to the Axum router, typically for
556 /// testing or when you want to add additional middleware that requires
557 /// the concrete `Router` type.
558 ///
559 /// # Examples
560 ///
561 /// ```rust,no_run
562 /// # use axum_conf::{Config, FluentRouter};
563 /// # fn example() -> axum_conf::Result<()> {
564 /// let fluent = FluentRouter::without_state(Config::default())?;
565 /// let axum_router: axum::Router = fluent.into_inner();
566 /// # Ok(())
567 /// # }
568 /// ```
569 pub fn into_inner(self) -> Router<State> {
570 self.inner
571 }
572}
573
574/// Returns a signal handler that emits shutdown phase notifications.
575///
576/// This function:
577/// 1. Waits for SIGTERM or SIGINT (Ctrl+C)
578/// 2. Emits [`ShutdownPhase::Initiated`] (and triggers the cancellation token)
579/// 3. Emits [`ShutdownPhase::GracePeriodStarted`] with the configured timeout
580/// 4. Returns immediately to let axum start graceful shutdown
581///
582/// The grace period timeout is enforced by the caller (see [`FluentRouter::start`]),
583/// which wraps the serve call with a timeout. When connections drain before the
584/// timeout, shutdown completes early. If the timeout expires first,
585/// [`ShutdownPhase::GracePeriodEnded`] is emitted and shutdown is forced.
586///
587/// Components can subscribe to these phases to perform coordinated cleanup.
588///
589/// If signal registration fails, the function logs a warning and falls back to
590/// waiting indefinitely. This ensures the server continues running even if signal
591/// handlers cannot be installed (e.g., in restricted environments).
592pub(crate) async fn shutdown_signal_with_notifications(
593 timeout: Duration,
594 notifier: ShutdownNotifier,
595) {
596 let ctrl_c = async {
597 match signal::ctrl_c().await {
598 Ok(()) => {
599 tracing::debug!("Ctrl+C signal received");
600 }
601 Err(err) => {
602 tracing::warn!("Failed to install Ctrl+C handler: {}", err);
603 // Wait indefinitely if we can't install the handler
604 std::future::pending::<()>().await;
605 }
606 }
607 };
608
609 #[cfg(unix)]
610 let terminate = async {
611 match signal::unix::signal(signal::unix::SignalKind::terminate()) {
612 Ok(mut signal_handler) => {
613 signal_handler.recv().await;
614 tracing::debug!("SIGTERM signal received");
615 }
616 Err(err) => {
617 tracing::warn!("Failed to install SIGTERM handler: {}", err);
618 // Wait indefinitely if we can't install the handler
619 std::future::pending::<()>().await;
620 }
621 }
622 };
623
624 #[cfg(not(unix))]
625 let terminate = std::future::pending::<()>();
626
627 tokio::select! {
628 _ = ctrl_c => {},
629 _ = terminate => {},
630 }
631
632 // Phase 1: Initiated - signal received, cancellation token triggered
633 tracing::info!(
634 "Shutdown signal received, starting graceful shutdown (timeout: {}s)",
635 timeout.as_secs()
636 );
637 let subscriber_count = notifier.emit(ShutdownPhase::Initiated);
638 tracing::debug!(
639 "Shutdown initiated notification sent to {} subscriber(s)",
640 subscriber_count
641 );
642
643 // Phase 2: Grace period started - in-flight requests draining
644 // Return immediately to let axum start graceful shutdown.
645 // The timeout is enforced by the caller wrapping the serve call.
646 notifier.emit(ShutdownPhase::GracePeriodStarted { timeout });
647}
648
649/// Returns a signal handler that allows us to stop the server using Ctrl+C
650/// or the terminate signal, which in turn allows us to perform a graceful
651/// shutdown with a configurable timeout.
652///
653/// If signal registration fails, the function logs a warning and falls back to
654/// waiting indefinitely. This ensures the server continues running even if signal
655/// handlers cannot be installed (e.g., in restricted environments).
656///
657/// # Deprecated
658///
659/// This function is deprecated. Use [`shutdown_signal_with_notifications`] instead,
660/// which emits [`ShutdownPhase`] events for coordinated shutdown handling.
661/// Note: The timeout is now enforced by the caller, not within this function.
662#[allow(dead_code)]
663#[deprecated(
664 since = "0.4.0",
665 note = "Use shutdown_signal_with_notifications instead for shutdown phase notifications"
666)]
667pub(crate) async fn shutdown_signal_with_timeout(timeout: Duration) {
668 shutdown_signal_with_notifications(timeout, ShutdownNotifier::default()).await;
669}