Skip to main content

volo_grpc/client/layer/
timeout.rs

1use std::{
2    future::Future,
3    pin::Pin,
4    task::{Context, Poll},
5    time::Duration,
6};
7
8use metainfo::METAINFO;
9use motore::{Service, layer::Layer};
10use pin_project::pin_project;
11use tokio::time::{self, Sleep};
12
13use crate::{Request, context::ClientContext, metadata::MetadataValue, status::Status};
14
15/// Timeout middleware that enforces deadlines from ClientContext.
16#[derive(Debug, Clone)]
17pub struct Timeout<S> {
18    inner: S,
19}
20
21impl<S> Timeout<S> {
22    pub fn new(inner: S) -> Self {
23        Self { inner }
24    }
25}
26
27#[derive(Clone, Default, Copy)]
28pub struct TimeoutLayer;
29
30impl TimeoutLayer {
31    pub fn new() -> Self {
32        Self
33    }
34}
35
36impl<S> Layer<S> for TimeoutLayer {
37    type Service = Timeout<S>;
38
39    fn layer(self, inner: S) -> Self::Service {
40        Timeout { inner }
41    }
42}
43
44impl<S, T> Service<ClientContext, Request<T>> for Timeout<S>
45where
46    S: Service<ClientContext, Request<T>, Error = Status> + Send + Sync,
47    T: Send + 'static,
48{
49    type Response = S::Response;
50    type Error = Status;
51
52    async fn call(
53        &self,
54        cx: &mut ClientContext,
55        mut req: Request<T>,
56    ) -> Result<Self::Response, Self::Error> {
57        let config_timeout = cx.rpc_info.config().rpc_timeout();
58
59        let mi_timeout = METAINFO.with(|m| m.borrow().get::<Duration>().cloned());
60
61        // get the shorter timeout
62        let timeout_duration = match (config_timeout, mi_timeout) {
63            (None, None) => None,
64            (None, Some(t)) | (Some(t), None) => Some(t),
65            (Some(t1), Some(t2)) => Some(t1.min(t2)),
66        };
67
68        if let Some(timeout) = timeout_duration {
69            let header_val = duration_to_grpc_timeout(timeout);
70            // Convert to gRPC metadata value and add to outgoing request with header
71            if let Ok(meta_val) = MetadataValue::from_str(&header_val) {
72                req.metadata_mut()
73                    .insert(crate::metadata::GRPC_TIMEOUT_HEADER, meta_val);
74            } else {
75                tracing::warn!("Invalid grpc-timeout value: {}", header_val);
76            }
77        }
78
79        let sleep = timeout_duration.map(time::sleep);
80        let inner = self.inner.call(cx, req);
81
82        ResponseFuture {
83            inner,
84            sleep: sleep.map(OptionPin::Some).unwrap_or(OptionPin::None),
85        }
86        .await
87    }
88}
89
90/// Converts a `std::time::Duration` to a `String` in gRPC timeout format.
91///
92/// The gRPC timeout format specifies a duration with a time unit suffix:
93/// - `"H"` for hours
94/// - `"M"` for minutes
95/// - `"S"` for seconds
96/// - `"m"` for milliseconds
97/// - `"u"` for microseconds
98/// - `"n"` for nanoseconds
99///
100/// This function chooses the largest possible time unit that evenly divides the duration
101/// (e.g., 3600 seconds becomes `"1H"`, 60 seconds becomes `"1M"`, 13 milliseconds becomes `"13m"`).
102///
103/// # Parameters
104/// - `duration`: The `Duration` to convert.
105///
106/// # Returns
107/// A `String` representing the gRPC timeout format.
108fn duration_to_grpc_timeout(duration: Duration) -> String {
109    let secs = duration.as_secs();
110    let nanos = duration.subsec_nanos();
111
112    if nanos == 0 {
113        if secs % 3600 == 0 {
114            let hrs = secs / 3600;
115            format!("{hrs}H")
116        } else if secs % 60 == 0 {
117            let mins = secs / 60;
118            format!("{mins}M")
119        } else {
120            format!("{secs}S")
121        }
122    } else if secs == 0 && nanos % 1_000_000 == 0 {
123        let millis = nanos / 1_000_000;
124        format!("{millis}m")
125    } else if secs == 0 && nanos % 1_000 == 0 {
126        let micros = nanos / 1_000;
127        format!("{micros}u")
128    } else if secs == 0 {
129        format!("{nanos}n")
130    } else {
131        let total_nanos = secs * 1_000_000_000 + nanos as u64;
132        format!("{total_nanos}n")
133    }
134}
135
136#[pin_project]
137pub struct ResponseFuture<F> {
138    #[pin]
139    inner: F,
140    #[pin]
141    sleep: OptionPin<Sleep>,
142}
143
144#[pin_project(project = OptionPinProj)]
145pub enum OptionPin<T> {
146    Some(#[pin] T),
147    None,
148}
149
150impl<F, R> Future for ResponseFuture<F>
151where
152    F: Future<Output = Result<R, Status>>,
153{
154    type Output = Result<R, Status>;
155
156    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
157        let this = self.project();
158
159        if let Poll::Ready(res) = this.inner.poll(cx) {
160            return Poll::Ready(res);
161        }
162
163        if let OptionPinProj::Some(sleep) = this.sleep.project() {
164            futures_util::ready!(sleep.poll(cx));
165            let err = Status::deadline_exceeded("timeout");
166            return Poll::Ready(Err(err));
167        }
168
169        Poll::Pending
170    }
171}
172
173#[cfg(test)]
174mod tests {
175
176    use super::*;
177    #[test]
178    fn test_hours() {
179        let converted_duration = duration_to_grpc_timeout(Duration::from_secs(3 * 3600));
180        assert_eq!("3H", converted_duration);
181    }
182
183    #[test]
184    fn test_minutes() {
185        let converted_duration = duration_to_grpc_timeout(Duration::from_secs(60));
186        assert_eq!("1M", converted_duration);
187    }
188
189    #[test]
190    fn test_seconds() {
191        let converted_duration = duration_to_grpc_timeout(Duration::from_secs(42));
192        assert_eq!("42S", converted_duration);
193    }
194
195    #[test]
196    fn test_milliseconds() {
197        let converted_duration = duration_to_grpc_timeout(Duration::from_millis(13));
198        assert_eq!("13m", converted_duration);
199    }
200
201    #[test]
202    fn test_microseconds() {
203        let converted_duration = duration_to_grpc_timeout(Duration::from_micros(2));
204        assert_eq!("2u", converted_duration);
205    }
206
207    #[test]
208    fn test_nanoseconds() {
209        let converted_duration = duration_to_grpc_timeout(Duration::from_nanos(82));
210        assert_eq!("82n", converted_duration);
211    }
212}