From 839b87e3941c06e59fc3c26836a40c09940ea63e Mon Sep 17 00:00:00 2001 From: Tim Kuehn Date: Thu, 26 May 2022 14:14:49 -0700 Subject: [PATCH] Serialize RPC deadline as a Duration. Duration was previously serialized as SystemTime. However, absolute times run into problems with clock skew: if the remote machine's clock is too far in the future, the RPC deadline will be exceeded before request processing can begin. Conversely, if the remote machine's clock is too far in the past, the RPC deadline will not be enforced. By converting the absolute deadline to a relative duration, clock skew is no longer relevant, as the remote machine will convert the deadline into a time relative to its own clock. This mirrors how the gRPC HTTP2 protocol includes a Timeout in the request headers [0] but the SDK uses timestamps [1]. Keeping the absolute time in the core APIs maintains all the benefits of today, namely, natural deadline propagation between RPC hops when using the current context. This serialization strategy means that, generally, the remote machine's deadline will be slightly in the future compared to the local machine. Depending on network transfer latencies, this could be microseconds to milliseconds, or worse in the worst case. Because the deadline is not intended for high-precision scenarios, I don't view this is as problematic. Because this change only affects the serialization layer, local transports that bypass serialization are not affected. [0] https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md [1] https://grpc.io/blog/deadlines/#setting-a-deadline --- tarpc/src/context.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tarpc/src/context.rs b/tarpc/src/context.rs index b922108..9fe88c0 100644 --- a/tarpc/src/context.rs +++ b/tarpc/src/context.rs @@ -28,6 +28,7 @@ pub struct Context { /// When the client expects the request to be complete by. The server should cancel the request /// if it is not complete by this time. #[cfg_attr(feature = "serde1", serde(default = "ten_seconds_from_now"))] + #[cfg_attr(feature = "serde1", serde(with = "absolute_to_relative_time"))] pub deadline: SystemTime, /// Uniquely identifies requests originating from the same source. /// When a service handles a request by making requests itself, those requests should @@ -36,6 +37,54 @@ pub struct Context { pub trace_context: trace::Context, } +#[cfg(feature = "serde1")] +mod absolute_to_relative_time { + pub use serde::{Deserialize, Deserializer, Serialize, Serializer}; + pub use std::time::{Duration, SystemTime}; + + pub fn serialize(deadline: &SystemTime, serializer: S) -> Result + where + S: Serializer, + { + let deadline = deadline + .duration_since(SystemTime::now()) + .unwrap_or(Duration::ZERO); + deadline.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let deadline = Duration::deserialize(deserializer)?; + Ok(SystemTime::now() + deadline) + } + + #[cfg(test)] + #[derive(serde::Serialize, serde::Deserialize)] + struct AbsoluteToRelative(#[serde(with = "self")] SystemTime); + + #[test] + fn test_serialize() { + let now = SystemTime::now(); + let deadline = now + Duration::from_secs(10); + let serialized_deadline = bincode::serialize(&AbsoluteToRelative(deadline)).unwrap(); + let deserialized_deadline: Duration = bincode::deserialize(&serialized_deadline).unwrap(); + // TODO: how to avoid flakiness? + assert!(deserialized_deadline > Duration::from_secs(9)); + } + + #[test] + fn test_deserialize() { + let deadline = Duration::from_secs(10); + let serialized_deadline = bincode::serialize(&deadline).unwrap(); + let AbsoluteToRelative(deserialized_deadline) = + bincode::deserialize(&serialized_deadline).unwrap(); + // TODO: how to avoid flakiness? + assert!(deserialized_deadline > SystemTime::now() + Duration::from_secs(9)); + } +} + assert_impl_all!(Context: Send, Sync); fn ten_seconds_from_now() -> SystemTime {