Skip to main content

arcly_http/resilience/
timeout.rs

1//! Route-level deadline enforcement for `#[Timeout("…")]`.
2//!
3//! Wraps the whole handler thunk (guards + extraction + body) in
4//! `tokio::time::timeout`: on expiry the future is **dropped** — the worker
5//! is released immediately — and the client receives `504 Gateway Timeout`
6//! as RFC-7807 ProblemDetails. This is the backstop that keeps a slow
7//! dependency from holding request tasks hostage past their SLA.
8
9use std::time::Duration;
10
11use axum::response::Response;
12
13use crate::web::error::{GatewayTimeout, HttpException};
14
15/// Called by the `#[Timeout]` macro expansion. Not part of the public API.
16#[doc(hidden)]
17pub async fn run_with_timeout<F>(millis: u64, route: &'static str, fut: F) -> Response
18where
19    F: std::future::Future<Output = Response>,
20{
21    match tokio::time::timeout(Duration::from_millis(millis), fut).await {
22        Ok(resp) => resp,
23        Err(_elapsed) => {
24            metrics::counter!("handler_timeouts_total", "route" => route).increment(1);
25            crate::http::IntoResponse::into_response(HttpException::from(GatewayTimeout::new(
26                "handler exceeded its deadline",
27            )))
28        }
29    }
30}
31
32/// Parse `"250ms"`, `"2s"`, `"1m"` (used at macro-expansion time via the
33/// macro crate's own copy; kept here for runtime construction too).
34pub fn parse_duration_millis(s: &str) -> Option<u64> {
35    let s = s.trim();
36    if let Some(v) = s.strip_suffix("ms") {
37        return v.trim().parse().ok();
38    }
39    if let Some(v) = s.strip_suffix('s') {
40        return v.trim().parse::<u64>().ok().map(|n| n * 1_000);
41    }
42    if let Some(v) = s.strip_suffix('m') {
43        return v.trim().parse::<u64>().ok().map(|n| n * 60_000);
44    }
45    s.parse().ok()
46}