Skip to content

Commit dae31d0

Browse files
authored
feat(tonic): Add Request::set_timeout (#615)
* feat(tonic): Add `Request::set_timeout` This will set the `grpc-timeout`. * Expand docs a bit
1 parent 4001665 commit dae31d0

File tree

3 files changed

+90
-4
lines changed

3 files changed

+90
-4
lines changed

tonic/src/metadata/map.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ pub struct OccupiedEntry<'a, VE: ValueEncoding> {
194194
phantom: PhantomData<VE>,
195195
}
196196

197-
#[cfg(feature = "transport")]
198197
pub(crate) const GRPC_TIMEOUT_HEADER: &str = "grpc-timeout";
199198

200199
// ===== impl MetadataMap =====

tonic/src/metadata/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ pub use self::value::AsciiMetadataValue;
2929
pub use self::value::BinaryMetadataValue;
3030
pub use self::value::MetadataValue;
3131

32-
#[cfg(feature = "transport")]
3332
pub(crate) use self::map::GRPC_TIMEOUT_HEADER;
3433

3534
/// The metadata::errors module contains types for errors that can occur

tonic/src/request.rs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
use crate::metadata::MetadataMap;
1+
use crate::metadata::{MetadataMap, MetadataValue};
22
#[cfg(feature = "transport")]
33
use crate::transport::Certificate;
44
use futures_core::Stream;
55
use http::Extensions;
6-
use std::net::SocketAddr;
76
#[cfg(feature = "transport")]
87
use std::sync::Arc;
8+
use std::{net::SocketAddr, time::Duration};
99

1010
/// A gRPC request and metadata from an RPC call.
1111
#[derive(Debug)]
@@ -221,6 +221,39 @@ impl<T> Request<T> {
221221
pub(crate) fn get<I: Send + Sync + 'static>(&self) -> Option<&I> {
222222
self.extensions.get::<I>()
223223
}
224+
225+
/// Set the max duration the request is allowed to take.
226+
///
227+
/// Requires the server to support the `grpc-timeout` metadata, which Tonic does.
228+
///
229+
/// The duration will be formatted according to [the spec] and use the most precise unit
230+
/// possible.
231+
///
232+
/// Example:
233+
///
234+
/// ```rust
235+
/// use std::time::Duration;
236+
/// use tonic::Request;
237+
///
238+
/// let mut request = Request::new(());
239+
///
240+
/// request.set_timeout(Duration::from_secs(30));
241+
///
242+
/// let value = request.metadata().get("grpc-timeout").unwrap();
243+
///
244+
/// assert_eq!(
245+
/// value,
246+
/// // equivalent to 30 seconds
247+
/// "30000000u"
248+
/// );
249+
/// ```
250+
///
251+
/// [the spec]: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
252+
pub fn set_timeout(&mut self, deadline: Duration) {
253+
let value = MetadataValue::from_str(&duration_to_grpc_timeout(deadline)).unwrap();
254+
self.metadata_mut()
255+
.insert(crate::metadata::GRPC_TIMEOUT_HEADER, value);
256+
}
224257
}
225258

226259
impl<T> IntoRequest<T> for T {
@@ -265,6 +298,40 @@ mod sealed {
265298
pub trait Sealed {}
266299
}
267300

301+
fn duration_to_grpc_timeout(duration: Duration) -> String {
302+
fn try_format<T: Into<u128>>(
303+
duration: Duration,
304+
unit: char,
305+
convert: impl FnOnce(Duration) -> T,
306+
) -> Option<String> {
307+
// The gRPC spec specifies that the timeout most be at most 8 digits. So this is the largest a
308+
// value can be before we need to use a bigger unit.
309+
let max_size: u128 = 99_999_999; // exactly 8 digits
310+
311+
let value = convert(duration).into();
312+
if value > max_size {
313+
None
314+
} else {
315+
Some(format!("{}{}", value, unit))
316+
}
317+
}
318+
319+
// pick the most precise unit that is less than or equal to 8 digits as per the gRPC spec
320+
try_format(duration, 'n', |d| d.as_nanos())
321+
.or_else(|| try_format(duration, 'u', |d| d.as_micros()))
322+
.or_else(|| try_format(duration, 'm', |d| d.as_millis()))
323+
.or_else(|| try_format(duration, 'S', |d| d.as_secs()))
324+
.or_else(|| try_format(duration, 'M', |d| d.as_secs() / 60))
325+
.or_else(|| {
326+
try_format(duration, 'H', |d| {
327+
let minutes = d.as_secs() / 60;
328+
minutes / 60
329+
})
330+
})
331+
// duration has to be more than 11_415 years for this to happen
332+
.expect("duration is unrealistically large")
333+
}
334+
268335
#[cfg(test)]
269336
mod tests {
270337
use super::*;
@@ -283,4 +350,25 @@ mod tests {
283350
let http_request = r.into_http(Uri::default());
284351
assert!(http_request.headers().is_empty());
285352
}
353+
354+
#[test]
355+
fn duration_to_grpc_timeout_less_than_second() {
356+
let timeout = Duration::from_millis(500);
357+
let value = duration_to_grpc_timeout(timeout);
358+
assert_eq!(value, format!("{}u", timeout.as_micros()));
359+
}
360+
361+
#[test]
362+
fn duration_to_grpc_timeout_more_than_second() {
363+
let timeout = Duration::from_secs(30);
364+
let value = duration_to_grpc_timeout(timeout);
365+
assert_eq!(value, format!("{}u", timeout.as_micros()));
366+
}
367+
368+
#[test]
369+
fn duration_to_grpc_timeout_a_very_long_time() {
370+
let one_hour = Duration::from_secs(60 * 60);
371+
let value = duration_to_grpc_timeout(one_hour);
372+
assert_eq!(value, format!("{}m", one_hour.as_millis()));
373+
}
286374
}

0 commit comments

Comments
 (0)