Procházet zdrojové kódy

Allow deadlines or timeouts to be set on RPCs (#842)

Motivation:

Sometimes timeouts are useful, other times deadlines are useful. We
should support both.

Modifications:

- Add a `timeLimit` to `CallOptions`, which is essentially a deadline or
  a timeout. When we start a call we generate a deadline from the time
  limit and forward that timeline through the pipeline and then compute
  a timeout to send to the server.
- For timeouts, `GRPCTimeout` has been replaced by `TimeAmount`, the
  user doesn't care how the timeout is serialized over the wire so it
  makes more sense to use a type which provides better interoperability
  with the rest of the community.
- Shims for deprecating `CallOptions(timeout:)`

Result:

- Users can set timeouts or deadlines
- Better deadline/timeout interoperability with other NIO applications
George Barnett před 5 roky
rodič
revize
8196439d23

+ 1 - 1
Sources/Examples/RouteGuide/Client/main.swift

@@ -105,7 +105,7 @@ public func recordRoute(
   featuresToVisit: Int
 ) {
   print("→ RecordRoute")
-  let options = CallOptions(timeout: .minutes(rounding: 1))
+  let options = CallOptions(timeLimit: .timeout(.minutes(1)))
   let call = client.recordRoute(callOptions: options)
 
   call.response.whenSuccess { summary in

+ 1 - 1
Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift

@@ -153,7 +153,7 @@ public final class BidirectionalStreamingCall<
       multiplexer: multiplexer,
       responseContainer: .init(eventLoop: eventLoop, streamingResponseHandler: handler),
       callType: .bidirectionalStreaming,
-      timeout: callOptions.timeout,
+      timeLimit: callOptions.timeLimit,
       errorDelegate: errorDelegate,
       logger: logger
     )

+ 9 - 8
Sources/GRPC/ClientCalls/ClientCallTransport.swift

@@ -120,7 +120,7 @@ internal class ChannelTransport<Request: GRPCPayload, Response: GRPCPayload> {
   internal convenience init(
     eventLoop: EventLoop,
     responseContainer: ResponsePartContainer<Response>,
-    timeout: GRPCTimeout,
+    timeLimit: TimeLimit,
     errorDelegate: ClientErrorDelegate?,
     logger: Logger,
     channelProvider: (ChannelTransport<Request, Response>, EventLoopPromise<Channel>) -> ()
@@ -142,9 +142,10 @@ internal class ChannelTransport<Request: GRPCPayload, Response: GRPCPayload> {
     }
 
     // Schedule the timeout.
-    if timeout != .infinite {
-      self.scheduledTimeout = eventLoop.scheduleTask(in: timeout.asNIOTimeAmount) {
-        self.timedOut(after: timeout)
+    let deadline = timeLimit.makeDeadline()
+    if deadline != .distantFuture {
+      self.scheduledTimeout = eventLoop.scheduleTask(deadline: deadline) {
+        self.timedOut(after: timeLimit)
       }
     }
 
@@ -156,14 +157,14 @@ internal class ChannelTransport<Request: GRPCPayload, Response: GRPCPayload> {
     multiplexer: EventLoopFuture<HTTP2StreamMultiplexer>,
     responseContainer: ResponsePartContainer<Response>,
     callType: GRPCCallType,
-    timeout: GRPCTimeout,
+    timeLimit: TimeLimit,
     errorDelegate: ClientErrorDelegate?,
     logger: Logger
   ) {
     self.init(
       eventLoop: multiplexer.eventLoop,
       responseContainer: responseContainer,
-      timeout: timeout,
+      timeLimit: timeLimit,
       errorDelegate: errorDelegate,
       logger: logger
     ) { call, streamPromise in
@@ -357,10 +358,10 @@ extension ChannelTransport {
   /// The scheduled timeout triggered: timeout the RPC if it's not yet finished.
   ///
   /// Must be called from the event loop.
-  private func timedOut(after timeout: GRPCTimeout) {
+  private func timedOut(after timeLimit: TimeLimit) {
     self.eventLoop.preconditionInEventLoop()
 
-    let error = GRPCError.RPCTimedOut(timeout).captureContext()
+    let error = GRPCError.RPCTimedOut(timeLimit).captureContext()
     self.handleError(error, promise: nil)
   }
 

+ 1 - 1
Sources/GRPC/ClientCalls/ClientStreamingCall.swift

@@ -156,7 +156,7 @@ public final class ClientStreamingCall<
       multiplexer: multiplexer,
       responseContainer: .init(eventLoop: eventLoop, unaryResponsePromise: responsePromise),
       callType: .clientStreaming,
-      timeout: callOptions.timeout,
+      timeLimit: callOptions.timeLimit,
       errorDelegate: errorDelegate,
       logger: logger
     )

+ 1 - 1
Sources/GRPC/ClientCalls/ServerStreamingCall.swift

@@ -100,7 +100,7 @@ public final class ServerStreamingCall<
       multiplexer: multiplexer,
       responseContainer: .init(eventLoop: eventLoop, streamingResponseHandler: handler),
       callType: .serverStreaming,
-      timeout: callOptions.timeout,
+      timeLimit: callOptions.timeLimit,
       errorDelegate: errorDelegate,
       logger: logger
     )

+ 1 - 1
Sources/GRPC/ClientCalls/UnaryCall.swift

@@ -105,7 +105,7 @@ public final class UnaryCall<
       multiplexer: multiplexer,
       responseContainer: .init(eventLoop: eventLoop, unaryResponsePromise: responsePromise),
       callType: .unary,
-      timeout: callOptions.timeout,
+      timeLimit: callOptions.timeLimit,
       errorDelegate: errorDelegate,
       logger: logger
     )

+ 7 - 5
Sources/GRPC/ClientOptions.swift

@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import Foundation
+import struct Foundation.UUID
 import NIOHTTP1
 import NIOHTTP2
 import NIOHPACK
@@ -23,8 +23,10 @@ public struct CallOptions {
   /// Additional metadata to send to the service.
   public var customMetadata: HPACKHeaders
 
-  /// The call timeout.
-  public var timeout: GRPCTimeout
+  /// The time limit for the RPC.
+  ///
+  /// - Note: timeouts are treated as deadlines as soon as an RPC has been invoked.
+  public var timeLimit: TimeLimit
 
   /// The compression used for requests, and the compression algorithms to advertise as acceptable
   /// for the remote peer to use for encoding responses.
@@ -60,18 +62,18 @@ public struct CallOptions {
 
   public init(
     customMetadata: HPACKHeaders = HPACKHeaders(),
-    timeout: GRPCTimeout = GRPCTimeout.infinite,
+    timeLimit: TimeLimit = .none,
     messageEncoding: ClientMessageEncoding = .disabled,
     requestIDProvider: RequestIDProvider = .autogenerated,
     requestIDHeader: String? = nil,
     cacheable: Bool = false
   ) {
     self.customMetadata = customMetadata
-    self.timeout = timeout
     self.messageEncoding = messageEncoding
     self.requestIDProvider = requestIDProvider
     self.requestIDHeader = requestIDHeader
     self.cacheable = false
+    self.timeLimit = timeLimit
   }
 
   public struct RequestIDProvider {

+ 1 - 1
Sources/GRPC/GRPCClientStateMachine.swift

@@ -337,7 +337,7 @@ extension GRPCClientStateMachine.State {
         scheme: requestHead.scheme,
         host: requestHead.host,
         path: requestHead.path,
-        timeout: requestHead.timeout,
+        timeout: GRPCTimeout(deadline: requestHead.deadline),
         customMetadata: requestHead.customMetadata,
         compression: requestHead.encoding
       )

+ 5 - 5
Sources/GRPC/GRPCError.swift

@@ -55,15 +55,15 @@ public enum GRPCError {
 
 /// The RPC did not complete before the timeout.
   public struct RPCTimedOut: GRPCErrorProtocol {
-    /// The timeout used for the RPC.
-    public var timeout: GRPCTimeout
+    /// The time limit which was exceeded by the RPC.
+    public var timeLimit: TimeLimit
 
-    public init(_ timeout: GRPCTimeout) {
-      self.timeout = timeout
+    public init(_ timeLimit: TimeLimit) {
+      self.timeLimit = timeLimit
     }
 
     public var description: String {
-      return "RPC timed out (timeout=\(self.timeout.wireEncoding)) before completing"
+      return "RPC timed out before completing (\(self.timeLimit))"
     }
 
     public func makeGRPCStatus() -> GRPCStatus {

+ 14 - 172
Sources/GRPC/GRPCTimeout.swift

@@ -13,48 +13,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import Dispatch
 import NIO
 
-/// Errors thrown when constructing a timeout.
-public struct GRPCTimeoutError: Error, Equatable, CustomStringConvertible {
-  private enum BaseError {
-    case negative
-    case tooManyDigits
-  }
-
-  private var error: BaseError
-
-  private init(_ error: BaseError) {
-    self.error = error
-  }
-
-  public var description: String {
-    switch self.error {
-    case .negative:
-      return "GRPCTimeoutError: time amount must not be negative"
-    case .tooManyDigits:
-      return "GRPCTimeoutError: too many digits to represent using the gRPC wire-format"
-    }
-  }
-
-  /// The timeout is negative.
-  public static let negative = GRPCTimeoutError(.negative)
-
-  /// The number of digits in the timeout amount is more than 8-digits and cannot be encoded in
-  /// the gRPC wire-format.
-  public static let tooManyDigits = GRPCTimeoutError(.tooManyDigits)
-}
 
 /// A timeout for a gRPC call.
 ///
 /// Timeouts must be positive and at most 8-digits long.
 public struct GRPCTimeout: CustomStringConvertible, Equatable {
-  public static let `default`: GRPCTimeout = try! .minutes(1)
   /// Creates an infinite timeout. This is a sentinel value which must __not__ be sent to a gRPC service.
   public static let infinite: GRPCTimeout = GRPCTimeout(nanoseconds: Int64.max, wireEncoding: "infinite")
 
   /// The largest amount of any unit of time which may be represented by a gRPC timeout.
-  private static let maxAmount: Int64 = 99_999_999
+  internal static let maxAmount: Int64 = 99_999_999
 
   /// The wire encoding of this timeout as described in the gRPC protocol.
   /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md.
@@ -65,6 +36,15 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
     return wireEncoding
   }
 
+  /// Creates a timeout from the given deadline.
+  ///
+  /// - Parameter deadline: The deadline to create a timeout from.
+  internal init(deadline: NIODeadline, testingOnlyNow: NIODeadline? = nil) {
+    let timeAmountUntilDeadline = deadline - (testingOnlyNow ?? .now())
+    self.init(rounding: timeAmountUntilDeadline.nanoseconds, unit: .nanoseconds)
+
+  }
+
   private init(nanoseconds: Int64, wireEncoding: String) {
     self.nanoseconds = nanoseconds
     self.wireEncoding = wireEncoding
@@ -74,7 +54,7 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
   ///
   /// - Precondition: The amount should be greater than or equal to zero and less than or equal
   ///   to `GRPCTimeout.maxAmount`.
-  private init(amount: Int64, unit: GRPCTimeoutUnit) {
+  internal init(amount: Int64, unit: GRPCTimeoutUnit) {
     precondition(amount >= 0 && amount <= GRPCTimeout.maxAmount)
     // See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
 
@@ -91,7 +71,7 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
 
   /// Create a timeout by rounding up the timeout so that it may be represented in the gRPC
   /// wire format.
-  private init(rounding amount: Int64, unit: GRPCTimeoutUnit) {
+  internal init(rounding amount: Int64, unit: GRPCTimeoutUnit) {
     var roundedAmount = amount
     var roundedUnit = unit
 
@@ -124,144 +104,6 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
 
     self.init(amount: roundedAmount, unit: roundedUnit)
   }
-
-  private static func makeTimeout(_ amount: Int64, _ unit: GRPCTimeoutUnit) throws -> GRPCTimeout {
-    // Timeouts must be positive and at most 8-digits.
-    if amount < 0 {
-      throw GRPCTimeoutError.negative
-    }
-    if amount > GRPCTimeout.maxAmount {
-      throw GRPCTimeoutError.tooManyDigits
-    }
-    return .init(amount: amount, unit: unit)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of hours.
-  ///
-  /// `amount` must be positive and at most 8-digits.
-  ///
-  /// - Parameter amount: the amount of hours this `GRPCTimeout` represents.
-  /// - Returns: A `GRPCTimeout` representing the given number of hours.
-  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
-  public static func hours(_ amount: Int) throws -> GRPCTimeout {
-    return try makeTimeout(Int64(amount), .hours)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of hours.
-  ///
-  /// The timeout will be rounded up if it may not be represented in the wire format.
-  ///
-  /// - Parameter amount: The number of hours to represent.
-  public static func hours(rounding amount: Int) -> GRPCTimeout {
-    return .init(rounding: Int64(amount), unit: .hours)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of minutes.
-  ///
-  /// `amount` must be positive and at most 8-digits.
-  ///
-  /// - Parameter amount: the amount of minutes this `GRPCTimeout` represents.
-  /// - Returns: A `GRPCTimeout` representing the given number of minutes.
-  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
-  public static func minutes(_ amount: Int) throws -> GRPCTimeout {
-    return try makeTimeout(Int64(amount), .minutes)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of minutes.
-  ///
-  /// The timeout will be rounded up if it may not be represented in the wire format.
-  ///
-  /// - Parameter amount: The number of minutes to represent.
-  public static func minutes(rounding amount: Int) -> GRPCTimeout {
-    return .init(rounding: Int64(amount), unit: .minutes)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of seconds.
-  ///
-  /// `amount` must be positive and at most 8-digits.
-  ///
-  /// - Parameter amount: the amount of seconds this `GRPCTimeout` represents.
-  /// - Returns: A `GRPCTimeout` representing the given number of seconds.
-  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
-  public static func seconds(_ amount: Int) throws -> GRPCTimeout {
-    return try makeTimeout(Int64(amount), .seconds)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of seconds.
-  ///
-  /// The timeout will be rounded up if it may not be represented in the wire format.
-  ///
-  /// - Parameter amount: The number of seconds to represent.
-  public static func seconds(rounding amount: Int) -> GRPCTimeout {
-    return .init(rounding: Int64(amount), unit: .seconds)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of milliseconds.
-  ///
-  /// `amount` must be positive and at most 8-digits.
-  ///
-  /// - Parameter amount: the amount of milliseconds this `GRPCTimeout` represents.
-  /// - Returns: A `GRPCTimeout` representing the given number of milliseconds.
-  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
-  public static func milliseconds(_ amount: Int) throws -> GRPCTimeout {
-    return try makeTimeout(Int64(amount), .milliseconds)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of milliseconds.
-  ///
-  /// The timeout will be rounded up if it may not be represented in the wire format.
-  ///
-  /// - Parameter amount: The number of milliseconds to represent.
-  public static func milliseconds(rounding amount: Int) -> GRPCTimeout {
-    return .init(rounding: Int64(amount), unit: .milliseconds)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of microseconds.
-  ///
-  /// `amount` must be positive and at most 8-digits.
-  ///
-  /// - Parameter amount: the amount of microseconds this `GRPCTimeout` represents.
-  /// - Returns: A `GRPCTimeout` representing the given number of microseconds.
-  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
-  public static func microseconds(_ amount: Int) throws -> GRPCTimeout {
-    return try makeTimeout(Int64(amount), .microseconds)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of microseconds.
-  ///
-  /// The timeout will be rounded up if it may not be represented in the wire format.
-  ///
-  /// - Parameter amount: The number of microseconds to represent.
-  public static func microseconds(rounding amount: Int) -> GRPCTimeout {
-    return .init(rounding: Int64(amount), unit: .microseconds)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of nanoseconds.
-  ///
-  /// `amount` must be positive and at most 8-digits.
-  ///
-  /// - Parameter amount: the amount of nanoseconds this `GRPCTimeout` represents.
-  /// - Returns: A `GRPCTimeout` representing the given number of nanoseconds.
-  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
-  public static func nanoseconds(_ amount: Int) throws -> GRPCTimeout {
-    return try makeTimeout(Int64(amount), .nanoseconds)
-  }
-
-  /// Creates a new GRPCTimeout for the given amount of nanoseconds.
-  ///
-  /// The timeout will be rounded up if it may not be represented in the wire format.
-  ///
-  /// - Parameter amount: The number of nanoseconds to represent.
-  public static func nanoseconds(rounding amount: Int) -> GRPCTimeout {
-    return .init(rounding: Int64(amount), unit: .nanoseconds)
-  }
-}
-
-public extension GRPCTimeout {
-  /// Returns a NIO `TimeAmount` representing the amount of time as this timeout.
-  var asNIOTimeAmount: TimeAmount {
-    return TimeAmount.nanoseconds(numericCast(nanoseconds))
-  }
 }
 
 fileprivate extension Int64 {
@@ -275,7 +117,7 @@ fileprivate extension Int64 {
   }
 }
 
-fileprivate enum GRPCTimeoutUnit: String {
+internal enum GRPCTimeoutUnit: String {
   case hours = "H"
   case minutes = "M"
   case seconds = "S"

+ 234 - 0
Sources/GRPC/Shims.swift

@@ -13,7 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import NIO
 import NIOSSL
+import NIOHPACK
 
 // This file contains shims to notify users of API changes between v1.0.0-alpha.1 and v1.0.0.
 
@@ -59,3 +61,235 @@ extension GRPCClient {
     return self.channel
   }
 }
+
+extension CallOptions {
+  @available(*, deprecated, renamed: "init(customMetadata:timeLimit:messageEncoding:requestIDProvider:requestIDHeader:cacheable:)")
+  public init(
+    customMetadata: HPACKHeaders = HPACKHeaders(),
+    timeout: GRPCTimeout,
+    messageEncoding: ClientMessageEncoding = .disabled,
+    requestIDProvider: RequestIDProvider = .autogenerated,
+    requestIDHeader: String? = nil,
+    cacheable: Bool = false
+  ) {
+    self.init(
+      customMetadata: customMetadata,
+      timeLimit: .timeout(timeout.asNIOTimeAmount),
+      messageEncoding: messageEncoding,
+      requestIDProvider: requestIDProvider,
+      requestIDHeader: requestIDHeader,
+      cacheable: cacheable
+    )
+  }
+
+  // TODO: `timeLimit.wrapped` can be private when the shims are removed.
+  @available(*, deprecated, renamed: "timeLimit")
+  public var timeout: GRPCTimeout {
+    get {
+      switch self.timeLimit.wrapped {
+      case .none:
+        return .infinite
+
+      case .timeout(let timeout) where timeout.nanoseconds == .max:
+        return .infinite
+
+      case .deadline(let deadline) where deadline == .distantFuture:
+        return .infinite
+
+      case .timeout(let timeout):
+        return GRPCTimeout.nanoseconds(rounding: Int(timeout.nanoseconds))
+
+      case .deadline(let deadline):
+        return GRPCTimeout(deadline: deadline)
+      }
+    }
+    set {
+      self.timeLimit = .timeout(newValue.asNIOTimeAmount)
+    }
+  }
+}
+
+extension GRPCTimeout {
+  /// Creates a new GRPCTimeout for the given amount of hours.
+  ///
+  /// `amount` must be positive and at most 8-digits.
+  ///
+  /// - Parameter amount: the amount of hours this `GRPCTimeout` represents.
+  /// - Returns: A `GRPCTimeout` representing the given number of hours.
+  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func hours(_ amount: Int) throws -> GRPCTimeout {
+    return try makeTimeout(Int64(amount), .hours)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of hours.
+  ///
+  /// The timeout will be rounded up if it may not be represented in the wire format.
+  ///
+  /// - Parameter amount: The number of hours to represent.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func hours(rounding amount: Int) -> GRPCTimeout {
+    return .init(rounding: Int64(amount), unit: .hours)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of minutes.
+  ///
+  /// `amount` must be positive and at most 8-digits.
+  ///
+  /// - Parameter amount: the amount of minutes this `GRPCTimeout` represents.
+  /// - Returns: A `GRPCTimeout` representing the given number of minutes.
+  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func minutes(_ amount: Int) throws -> GRPCTimeout {
+    return try makeTimeout(Int64(amount), .minutes)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of minutes.
+  ///
+  /// The timeout will be rounded up if it may not be represented in the wire format.
+  ///
+  /// - Parameter amount: The number of minutes to represent.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func minutes(rounding amount: Int) -> GRPCTimeout {
+    return .init(rounding: Int64(amount), unit: .minutes)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of seconds.
+  ///
+  /// `amount` must be positive and at most 8-digits.
+  ///
+  /// - Parameter amount: the amount of seconds this `GRPCTimeout` represents.
+  /// - Returns: A `GRPCTimeout` representing the given number of seconds.
+  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func seconds(_ amount: Int) throws -> GRPCTimeout {
+    return try makeTimeout(Int64(amount), .seconds)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of seconds.
+  ///
+  /// The timeout will be rounded up if it may not be represented in the wire format.
+  ///
+  /// - Parameter amount: The number of seconds to represent.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func seconds(rounding amount: Int) -> GRPCTimeout {
+    return .init(rounding: Int64(amount), unit: .seconds)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of milliseconds.
+  ///
+  /// `amount` must be positive and at most 8-digits.
+  ///
+  /// - Parameter amount: the amount of milliseconds this `GRPCTimeout` represents.
+  /// - Returns: A `GRPCTimeout` representing the given number of milliseconds.
+  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func milliseconds(_ amount: Int) throws -> GRPCTimeout {
+    return try makeTimeout(Int64(amount), .milliseconds)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of milliseconds.
+  ///
+  /// The timeout will be rounded up if it may not be represented in the wire format.
+  ///
+  /// - Parameter amount: The number of milliseconds to represent.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func milliseconds(rounding amount: Int) -> GRPCTimeout {
+    return .init(rounding: Int64(amount), unit: .milliseconds)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of microseconds.
+  ///
+  /// `amount` must be positive and at most 8-digits.
+  ///
+  /// - Parameter amount: the amount of microseconds this `GRPCTimeout` represents.
+  /// - Returns: A `GRPCTimeout` representing the given number of microseconds.
+  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func microseconds(_ amount: Int) throws -> GRPCTimeout {
+    return try makeTimeout(Int64(amount), .microseconds)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of microseconds.
+  ///
+  /// The timeout will be rounded up if it may not be represented in the wire format.
+  ///
+  /// - Parameter amount: The number of microseconds to represent.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func microseconds(rounding amount: Int) -> GRPCTimeout {
+    return .init(rounding: Int64(amount), unit: .microseconds)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of nanoseconds.
+  ///
+  /// `amount` must be positive and at most 8-digits.
+  ///
+  /// - Parameter amount: the amount of nanoseconds this `GRPCTimeout` represents.
+  /// - Returns: A `GRPCTimeout` representing the given number of nanoseconds.
+  /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func nanoseconds(_ amount: Int) throws -> GRPCTimeout {
+    return try makeTimeout(Int64(amount), .nanoseconds)
+  }
+
+  /// Creates a new GRPCTimeout for the given amount of nanoseconds.
+  ///
+  /// The timeout will be rounded up if it may not be represented in the wire format.
+  ///
+  /// - Parameter amount: The number of nanoseconds to represent.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public static func nanoseconds(rounding amount: Int) -> GRPCTimeout {
+    return .init(rounding: Int64(amount), unit: .nanoseconds)
+  }
+}
+
+extension GRPCTimeout {
+  /// Returns a NIO `TimeAmount` representing the amount of time as this timeout.
+  @available(*, deprecated, message: "Use 'TimeLimit.timeout(_:)' or 'TimeLimit.deadline(_:)' instead.")
+  public var asNIOTimeAmount: TimeAmount {
+    return TimeAmount.nanoseconds(numericCast(nanoseconds))
+  }
+
+  internal static func makeTimeout(_ amount: Int64, _ unit: GRPCTimeoutUnit) throws -> GRPCTimeout {
+    // Timeouts must be positive and at most 8-digits.
+    if amount < 0 {
+      throw GRPCTimeoutError.negative
+    }
+    if amount > GRPCTimeout.maxAmount {
+      throw GRPCTimeoutError.tooManyDigits
+    }
+    return .init(amount: amount, unit: unit)
+  }
+}
+
+// These will be obsoleted when the shims are removed.
+
+/// Errors thrown when constructing a timeout.
+public struct GRPCTimeoutError: Error, Equatable, CustomStringConvertible {
+  private enum BaseError {
+    case negative
+    case tooManyDigits
+  }
+
+  private var error: BaseError
+
+  private init(_ error: BaseError) {
+    self.error = error
+  }
+
+  public var description: String {
+    switch self.error {
+    case .negative:
+      return "GRPCTimeoutError: time amount must not be negative"
+    case .tooManyDigits:
+      return "GRPCTimeoutError: too many digits to represent using the gRPC wire-format"
+    }
+  }
+
+  /// The timeout is negative.
+  public static let negative = GRPCTimeoutError(.negative)
+
+  /// The number of digits in the timeout amount is more than 8-digits and cannot be encoded in
+  /// the gRPC wire-format.
+  public static let tooManyDigits = GRPCTimeoutError(.tooManyDigits)
+}

+ 121 - 0
Sources/GRPC/TimeLimit.swift

@@ -0,0 +1,121 @@
+/*
+ * Copyright 2020, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import Dispatch
+import NIO
+
+/// A time limit for an RPC.
+///
+/// RPCs may have a time limit imposed on them by a caller which may be timeout or deadline based.
+/// If the RPC has not completed before the limit is reached then the call will be cancelled and
+/// completed with a `.deadlineExceeded` status code.
+///
+/// - Note: Servers may impose a time limit on an RPC independent of the client's time limit; RPCs
+///   may therefore complete with `.deadlineExceeded` even if no time limit was set by the client.
+public struct TimeLimit: Equatable, CustomStringConvertible {
+  // private but for shimming.
+  internal enum Wrapped: Equatable {
+    case none
+    case timeout(TimeAmount)
+    case deadline(NIODeadline)
+  }
+
+  // private but for shimming.
+  internal var wrapped: Wrapped
+
+  private init(_ wrapped: Wrapped) {
+    self.wrapped = wrapped
+  }
+
+  /// No time limit, the RPC will not be automatically cancelled by the client. Note: some services
+  /// may impose a time limit on RPCs independent of the client's time limit.
+  public static let none = TimeLimit(.none)
+
+  /// Create a timeout before which the RPC must have completed. Failure to complete before the
+  /// deadline will result in the RPC being cancelled.
+  ///
+  /// - Note: The timeout is started once the call has been invoked and the call may timeout waiting
+  ///   for an active connection.
+  public static func timeout(_ timeout: TimeAmount) -> TimeLimit {
+    return TimeLimit(.timeout(timeout))
+  }
+
+  /// Create a point in time by which the RPC must have completed. Failure to complete before the
+  /// deadline will result in the RPC being cancelled.
+  public static func deadline(_ deadline: NIODeadline) -> TimeLimit {
+    return TimeLimit(.deadline(deadline))
+  }
+
+  /// Return the timeout, if one was set.
+  public var timeout: TimeAmount? {
+    switch self.wrapped {
+    case .timeout(let timeout):
+      return timeout
+
+    case .none, .deadline:
+      return nil
+    }
+  }
+
+  /// Return the deadline, if one was set.
+  public var deadline: NIODeadline? {
+    switch self.wrapped {
+    case .deadline(let deadline):
+      return deadline
+
+    case .none, .timeout:
+      return nil
+    }
+
+  }
+}
+
+extension TimeLimit {
+  /// Make a non-distant-future deadline from the give time limit.
+  internal func makeDeadline() -> NIODeadline {
+    switch self.wrapped {
+    case .none:
+      return .distantFuture
+
+    case .timeout(let timeout) where timeout.nanoseconds == .max:
+      return .distantFuture
+
+    case .timeout(let timeout):
+      return .now() + timeout
+
+    case .deadline(let deadline):
+      return deadline
+    }
+  }
+
+  public var description: String {
+    switch self.wrapped {
+    case .none:
+      return "none"
+
+    case .timeout(let timeout) where timeout.nanoseconds == .max:
+      return "timeout=never"
+
+    case .timeout(let timeout):
+      return "timeout=\(timeout.nanoseconds) nanoseconds"
+
+    case .deadline(let deadline) where deadline == .distantFuture:
+      return "deadline=.distantFuture"
+
+    case .deadline(let deadline):
+      return "deadline=\(deadline.uptimeNanoseconds) uptimeNanoseconds"
+    }
+  }
+}

+ 10 - 10
Sources/GRPC/_GRPCClientChannelHandler.swift

@@ -43,7 +43,7 @@ public struct _GRPCRequestHead {
     var scheme: String
     var path: String
     var host: String
-    var timeout: GRPCTimeout
+    var deadline: NIODeadline
     var encoding: ClientMessageEncoding
 
     init(
@@ -51,14 +51,14 @@ public struct _GRPCRequestHead {
       scheme: String,
       path: String,
       host: String,
-      timeout: GRPCTimeout,
+      deadline: NIODeadline,
       encoding: ClientMessageEncoding
     ) {
       self.method = method
       self.scheme = scheme
       self.path = path
       self.host = host
-      self.timeout = timeout
+      self.deadline = deadline
       self.encoding = encoding
     }
 
@@ -68,7 +68,7 @@ public struct _GRPCRequestHead {
         scheme: self.scheme,
         path: self.path,
         host: self.host,
-        timeout: self.timeout,
+        deadline: self.deadline,
         encoding: self.encoding
       )
     }
@@ -126,15 +126,15 @@ public struct _GRPCRequestHead {
     }
   }
 
-  internal var timeout: GRPCTimeout {
+  internal var deadline: NIODeadline {
     get {
-      return self._storage.timeout
+      return self._storage.deadline
     }
     set {
       if !isKnownUniquelyReferenced(&self._storage) {
         self._storage = self._storage.copy()
       }
-      self._storage.timeout = newValue
+      self._storage.deadline = newValue
     }
   }
 
@@ -155,7 +155,7 @@ public struct _GRPCRequestHead {
     scheme: String,
     path: String,
     host: String,
-    timeout: GRPCTimeout,
+    deadline: NIODeadline,
     customMetadata: HPACKHeaders,
     encoding: ClientMessageEncoding
   ) {
@@ -164,7 +164,7 @@ public struct _GRPCRequestHead {
       scheme: scheme,
       path: path,
       host: host,
-      timeout: timeout,
+      deadline: deadline,
       encoding: encoding
     )
     self.customMetadata = customMetadata
@@ -189,7 +189,7 @@ extension _GRPCRequestHead {
       scheme: scheme,
       path: path,
       host: host,
-      timeout: options.timeout,
+      deadline: options.timeLimit.makeDeadline(),
       customMetadata: customMetadata,
       encoding: options.messageEncoding
     )

+ 2 - 2
Sources/GRPCConnectionBackoffInteropTest/main.swift

@@ -56,7 +56,7 @@ let controlConnection = ClientConnection.insecure(group: group)
   .connect(host: "localhost", port: controlPort)
 let controlClient = Grpc_Testing_ReconnectServiceClient(channel: controlConnection)
 print("[\(Date())] Control 'Start' call started")
-let controlStart = controlClient.start(.init(), callOptions: .init(timeout: .infinite))
+let controlStart = controlClient.start(.init(), callOptions: .init(timeLimit: .none))
 let controlStartStatus = try controlStart.status.wait()
 assert(controlStartStatus.code == .ok, "Control Start rpc failed: \(controlStartStatus.code)")
 print("[\(Date())] Control 'Start' call succeeded")
@@ -70,7 +70,7 @@ let retryConnection = ClientConnection.secure(group: group)
   .connect(host: "localhost", port: retryPort)
 let retryClient = Grpc_Testing_ReconnectServiceClient(
   channel: retryConnection,
-  defaultCallOptions: CallOptions(timeout: try! .seconds(540))
+  defaultCallOptions: CallOptions(timeLimit: .timeout(.seconds(540)))
 )
 let retryStart = retryClient.start(.init())
 // We expect this to take some time!

+ 1 - 1
Sources/GRPCInteroperabilityTestsImplementation/InteroperabilityTestCases.swift

@@ -1008,7 +1008,7 @@ class TimeoutOnSleepingServer: InteroperabilityTest {
   func run(using connection: ClientConnection) throws {
     let client = Grpc_Testing_TestServiceClient(channel: connection)
 
-    let callOptions = CallOptions(timeout: try .milliseconds(1))
+    let callOptions = CallOptions(timeLimit: .timeout(.milliseconds(1)))
     let call = client.fullDuplexCall(callOptions: callOptions) { _ in }
 
     try waitAndAssertEqual(call.status.map { $0.code }, .deadlineExceeded)

+ 1 - 1
Sources/GRPCPerformanceTests/Benchmarks/EmbeddedClientThroughput.swift

@@ -45,7 +45,7 @@ class EmbeddedClientThroughput: Benchmark {
       scheme: "http",
       path: "/echo.Echo/Get",
       host: "localhost",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: [:],
       encoding: .disabled
     )

+ 13 - 12
Tests/GRPCTests/ChannelTransportTests.swift

@@ -29,12 +29,12 @@ class ChannelTransportTests: GRPCTestCase {
   private func makeEmbeddedTransport(
     channel: EmbeddedChannel,
     container: ResponsePartContainer<Response>,
-    timeout: GRPCTimeout = .infinite
+    deadline: NIODeadline = .distantFuture
   ) -> ChannelTransport<Request, Response> {
     let transport = ChannelTransport<Request, Response>(
       eventLoop: channel.eventLoop,
       responseContainer: container,
-      timeout: timeout,
+      timeLimit: .deadline(deadline),
       errorDelegate: nil,
       logger: self.logger
     ) { call, promise in
@@ -57,7 +57,7 @@ class ChannelTransportTests: GRPCTestCase {
       scheme: "http",
       path: "/foo/bar",
       host: "localhost",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: [:],
       encoding: .disabled
     )
@@ -147,14 +147,14 @@ class ChannelTransportTests: GRPCTestCase {
   // MARK: - Timeout
 
   func testTimeoutBeforeActivating() throws {
-    let timeout = try GRPCTimeout.minutes(42)
+    let deadline = NIODeadline.uptimeNanoseconds(0) + .minutes(42)
     let channel = EmbeddedChannel()
     let responsePromise = channel.eventLoop.makePromise(of: Response.self)
     let container = ResponsePartContainer<Response>(eventLoop: channel.eventLoop, unaryResponsePromise: responsePromise)
-    let transport = self.makeEmbeddedTransport(channel: channel, container: container, timeout: timeout)
+    let transport = self.makeEmbeddedTransport(channel: channel, container: container, deadline: deadline)
 
     // Advance time beyond the timeout.
-    channel.embeddedEventLoop.advanceTime(by: timeout.asNIOTimeAmount)
+    channel.embeddedEventLoop.advanceTime(by: .minutes(42))
 
     XCTAssertThrowsError(try transport.responseContainer.lazyInitialMetadataPromise.getFutureResult().wait())
     XCTAssertThrowsError(try responsePromise.futureResult.wait())
@@ -169,17 +169,18 @@ class ChannelTransportTests: GRPCTestCase {
   }
 
   func testTimeoutAfterActivating() throws {
-    let timeout = try GRPCTimeout.minutes(42)
+    let deadline = NIODeadline.uptimeNanoseconds(0) + .minutes(42)
     let channel = EmbeddedChannel()
+    
     let responsePromise = channel.eventLoop.makePromise(of: Response.self)
     let container = ResponsePartContainer<Response>(eventLoop: channel.eventLoop, unaryResponsePromise: responsePromise)
-    let transport = self.makeEmbeddedTransport(channel: channel, container: container, timeout: timeout)
+    let transport = self.makeEmbeddedTransport(channel: channel, container: container, deadline: deadline)
 
     // Activate the channel.
     channel.pipeline.fireChannelActive()
 
     // Advance time beyond the timeout.
-    channel.embeddedEventLoop.advanceTime(by: timeout.asNIOTimeAmount)
+    channel.embeddedEventLoop.advanceTime(by: .minutes(42))
 
     XCTAssertThrowsError(try transport.responseContainer.lazyInitialMetadataPromise.getFutureResult().wait())
     XCTAssertThrowsError(try responsePromise.futureResult.wait())
@@ -193,13 +194,13 @@ class ChannelTransportTests: GRPCTestCase {
   }
 
   func testTimeoutMidRPC() throws {
-    let timeout = try GRPCTimeout.minutes(42)
+    let deadline = NIODeadline.uptimeNanoseconds(0) + .minutes(42)
     let channel = EmbeddedChannel()
     let container = ResponsePartContainer<Response>(eventLoop: channel.eventLoop) { (response: Response) in
       XCTFail("No response expected but got: \(response)")
     }
 
-    let transport = self.makeEmbeddedTransport(channel: channel, container: container, timeout: timeout)
+    let transport = self.makeEmbeddedTransport(channel: channel, container: container, deadline: deadline)
 
     // Activate the channel.
     channel.pipeline.fireChannelActive()
@@ -219,7 +220,7 @@ class ChannelTransportTests: GRPCTestCase {
     XCTAssertNoThrow(try transport.responseContainer.lazyInitialMetadataPromise.getFutureResult().wait())
 
     // Advance time beyond the timeout.
-    channel.embeddedEventLoop.advanceTime(by: timeout.asNIOTimeAmount)
+    channel.embeddedEventLoop.advanceTime(by: .minutes(42))
 
     // Check the remaining response parts.
     XCTAssertThrowsError(try transport.responseContainer.lazyTrailingMetadataPromise.getFutureResult().wait())

+ 12 - 9
Tests/GRPCTests/ClientTimeoutTests.swift

@@ -23,9 +23,12 @@ class ClientTimeoutTests: GRPCTestCase {
   var channel: EmbeddedChannel!
   var client: Echo_EchoClient!
 
-  let callOptions = CallOptions(timeout: try! .milliseconds(100))
-  var timeout: GRPCTimeout {
-    return self.callOptions.timeout
+  let timeout = TimeAmount.milliseconds(100)
+  var callOptions: CallOptions {
+    // We use a deadline here because internally we convert timeouts into deadlines by diffing
+    // with `DispatchTime.now()`. We therefore need the deadline to be known in advance. Note we
+    // use zero because `EmbeddedEventLoop`s time starts at zero.
+    return CallOptions(timeLimit: .deadline(.uptimeNanoseconds(0) + timeout))
   }
 
   // Note: this is not related to the call timeout since we're using an EmbeddedChannel. We require
@@ -79,7 +82,7 @@ class ClientTimeoutTests: GRPCTestCase {
     let statusExpectation = self.expectation(description: "status fulfilled")
 
     let call = self.client.get(Echo_EchoRequest(text: "foo"))
-    channel.embeddedEventLoop.advanceTime(by: self.timeout.asNIOTimeAmount)
+    channel.embeddedEventLoop.advanceTime(by: self.timeout)
 
     self.assertDeadlineExceeded(call.status, expectation: statusExpectation)
     self.wait(for: [statusExpectation], timeout: self.testTimeout)
@@ -89,7 +92,7 @@ class ClientTimeoutTests: GRPCTestCase {
     let statusExpectation = self.expectation(description: "status fulfilled")
 
     let call = client.expand(Echo_EchoRequest(text: "foo bar baz")) { _ in }
-    channel.embeddedEventLoop.advanceTime(by: self.timeout.asNIOTimeAmount)
+    channel.embeddedEventLoop.advanceTime(by: self.timeout)
 
     self.assertDeadlineExceeded(call.status, expectation: statusExpectation)
     self.wait(for: [statusExpectation], timeout: self.testTimeout)
@@ -100,7 +103,7 @@ class ClientTimeoutTests: GRPCTestCase {
     let statusExpectation = self.expectation(description: "status fulfilled")
 
     let call = client.collect()
-    channel.embeddedEventLoop.advanceTime(by: self.timeout.asNIOTimeAmount)
+    channel.embeddedEventLoop.advanceTime(by: self.timeout)
 
     self.assertDeadlineExceeded(call.response, expectation: responseExpectation)
     self.assertDeadlineExceeded(call.status, expectation: statusExpectation)
@@ -118,7 +121,7 @@ class ClientTimeoutTests: GRPCTestCase {
 
     call.sendMessage(Echo_EchoRequest(text: "foo"), promise: nil)
     call.sendEnd(promise: nil)
-    channel.embeddedEventLoop.advanceTime(by: self.timeout.asNIOTimeAmount)
+    channel.embeddedEventLoop.advanceTime(by: self.timeout)
 
     self.wait(for: [responseExpectation, statusExpectation], timeout: 1.0)
   }
@@ -128,7 +131,7 @@ class ClientTimeoutTests: GRPCTestCase {
 
     let call = client.update { _ in }
 
-    channel.embeddedEventLoop.advanceTime(by: self.timeout.asNIOTimeAmount)
+    channel.embeddedEventLoop.advanceTime(by: self.timeout)
 
     self.assertDeadlineExceeded(call.status, expectation: statusExpectation)
     self.wait(for: [statusExpectation], timeout: self.testTimeout)
@@ -143,7 +146,7 @@ class ClientTimeoutTests: GRPCTestCase {
 
     call.sendMessage(Echo_EchoRequest(text: "foo"), promise: nil)
     call.sendEnd(promise: nil)
-    channel.embeddedEventLoop.advanceTime(by: self.timeout.asNIOTimeAmount)
+    channel.embeddedEventLoop.advanceTime(by: self.timeout)
 
     self.wait(for: [statusExpectation], timeout: self.testTimeout)
   }

+ 1 - 1
Tests/GRPCTests/FunctionalTests.swift

@@ -114,7 +114,7 @@ class FunctionalTestsInsecureTransport: EchoTestCaseBase {
     let responseExpectation = self.makeResponseExpectation()
     let statusExpectation = self.makeStatusExpectation()
 
-    let call = client.collect(callOptions: CallOptions(timeout: .infinite))
+    let call = client.collect(callOptions: CallOptions(timeLimit: .none))
     call.status.map { $0.code }.assertEqual(.ok, fulfill: statusExpectation, file: file, line: line)
     call.response.assertEqual(Echo_EchoResponse(text: "Swift echo collect: \(messages.joined(separator: " "))"), fulfill: responseExpectation)
 

+ 13 - 12
Tests/GRPCTests/GRPCClientStateMachineTests.swift

@@ -78,7 +78,7 @@ extension GRPCClientStateMachineTests {
       scheme: "http",
       path: "/echo/Get",
       host: "host",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: [:],
       encoding: .disabled
     )).assertFailure {
@@ -93,7 +93,7 @@ extension GRPCClientStateMachineTests {
       scheme: "http",
       path: "/echo/Get",
       host: "host",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: [:],
       encoding: .disabled
     )).assertSuccess()
@@ -449,7 +449,7 @@ extension GRPCClientStateMachineTests {
       scheme: "https",
       path: "/echo/Get",
       host: "foo",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: [:],
       encoding: .disabled
     )).assertSuccess()
@@ -480,7 +480,7 @@ extension GRPCClientStateMachineTests {
       scheme: "https",
       path: "/echo/Get",
       host: "foo",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: [:],
       encoding: .disabled
     )).assertSuccess()
@@ -513,7 +513,7 @@ extension GRPCClientStateMachineTests {
       scheme: "https",
       path: "/echo/Get",
       host: "foo",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: [:],
       encoding: .disabled
     )).assertSuccess()
@@ -549,7 +549,7 @@ extension GRPCClientStateMachineTests {
       scheme: "https",
       path: "/echo/Get",
       host: "foo",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: [:],
       encoding: .disabled
     )).assertSuccess()
@@ -660,7 +660,7 @@ extension GRPCClientStateMachineTests {
       scheme: "http",
       path: "/echo/Get",
       host: "localhost",
-      timeout: .hours(rounding: 1),
+      deadline: .now() + .hours(1),
       customMetadata: ["x-grpc-id": "request-id"],
       encoding: .enabled(.init(forRequests: .identity, acceptableForResponses: [.identity], decompressionLimit: .ratio(10)))
     )).assertSuccess { headers in
@@ -670,7 +670,8 @@ extension GRPCClientStateMachineTests {
       XCTAssertEqual(headers[":scheme"], ["http"])
       XCTAssertEqual(headers["content-type"], ["application/grpc"])
       XCTAssertEqual(headers["te"], ["trailers"])
-      XCTAssertEqual(headers["grpc-timeout"], ["1H"])
+      // We convert the deadline into a timeout, we can't be exactly sure what that timeout is.
+      XCTAssertTrue(headers.contains(name: "grpc-timeout"))
       XCTAssertEqual(headers["x-grpc-id"], ["request-id"])
       XCTAssertEqual(headers["grpc-encoding"], ["identity"])
       XCTAssertTrue(headers["grpc-accept-encoding"].contains("identity"))
@@ -694,7 +695,7 @@ extension GRPCClientStateMachineTests {
       scheme: "http",
       path: "/echo/Get",
       host: "localhost",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: customMetadata,
       encoding: .disabled
     )).assertSuccess { headers in
@@ -722,7 +723,7 @@ extension GRPCClientStateMachineTests {
       scheme: "http",
       path: "/echo/Get",
       host: "localhost",
-      timeout: .hours(rounding: 1),
+      deadline: .distantFuture,
       customMetadata: ["x-grpc-id": "request-id"],
       encoding: .enabled(.init(forRequests: nil, acceptableForResponses: [], decompressionLimit: .ratio(10)))
     )).assertSuccess { headers in
@@ -738,7 +739,7 @@ extension GRPCClientStateMachineTests {
       scheme: "http",
       path: "/echo/Get",
       host: "localhost",
-      timeout: .hours(rounding: 1),
+      deadline: .distantFuture,
       customMetadata: ["x-grpc-id": "request-id"],
       encoding: .enabled(.init(forRequests: nil, acceptableForResponses: [.identity, .gzip], decompressionLimit: .ratio(10)))
     )).assertSuccess { headers in
@@ -754,7 +755,7 @@ extension GRPCClientStateMachineTests {
       scheme: "http",
       path: "/echo/Get",
       host: "localhost",
-      timeout: .hours(rounding: 1),
+      deadline: .distantFuture,
       customMetadata: ["x-grpc-id": "request-id"],
       encoding: .enabled(.init(forRequests: .gzip, acceptableForResponses: [], decompressionLimit: .ratio(10)))
     )).assertSuccess { headers in

+ 1 - 1
Tests/GRPCTests/GRPCStatusCodeTests.swift

@@ -49,7 +49,7 @@ class GRPCStatusCodeTests: GRPCTestCase {
       scheme: "http",
       path: "/foo/bar",
       host: "localhost",
-      timeout: .infinite,
+      deadline: .distantFuture,
       customMetadata: [:],
       encoding: .disabled
     )

+ 33 - 26
Tests/GRPCTests/GRPCTimeoutTests.swift

@@ -14,31 +14,21 @@
  * limitations under the License.
  */
 import Foundation
-import GRPC
+import Dispatch
+@testable import GRPC
+import NIO
 import XCTest
 
 class GRPCTimeoutTests: GRPCTestCase {
-  func testNegativeTimeoutThrows() throws {
-    XCTAssertThrowsError(try GRPCTimeout.seconds(-10)) { error in
-      XCTAssertEqual(error as? GRPCTimeoutError, GRPCTimeoutError.negative)
-    }
-  }
-
-  func testTooLargeTimeout() throws {
-    XCTAssertThrowsError(try GRPCTimeout.seconds(100_000_000)) { error in
-      XCTAssertEqual(error as? GRPCTimeoutError, GRPCTimeoutError.tooManyDigits)
-    }
-  }
-
   func testRoundingNegativeTimeout() {
-    let timeout: GRPCTimeout = .seconds(rounding: -10)
+    let timeout = GRPCTimeout(rounding: -10, unit: .seconds)
     XCTAssertEqual(String(describing: timeout), "0S")
     XCTAssertEqual(timeout.nanoseconds, 0)
   }
 
   func testRoundingNanosecondsTimeout() throws {
-    let timeout: GRPCTimeout = .nanoseconds(rounding: 123_456_789)
-    XCTAssertEqual(timeout, try .microseconds(123457))
+    let timeout = GRPCTimeout(rounding: 123_456_789, unit: .nanoseconds)
+    XCTAssertEqual(timeout, GRPCTimeout(amount: 123457, unit: .microseconds))
 
     // 123_456_789 (nanoseconds) / 1_000
     //   = 123_456.789
@@ -51,8 +41,8 @@ class GRPCTimeoutTests: GRPCTestCase {
   }
 
   func testRoundingMicrosecondsTimeout() throws {
-    let timeout: GRPCTimeout = .microseconds(rounding: 123_456_789)
-    XCTAssertEqual(timeout, try .milliseconds(123457))
+    let timeout = GRPCTimeout(rounding: 123_456_789, unit: .microseconds)
+    XCTAssertEqual(timeout, GRPCTimeout(amount: 123457, unit: .milliseconds))
 
     // 123_456_789 (microseconds) / 1_000
     //   = 123_456.789
@@ -65,8 +55,8 @@ class GRPCTimeoutTests: GRPCTestCase {
   }
 
   func testRoundingMillisecondsTimeout() throws {
-    let timeout: GRPCTimeout = .milliseconds(rounding: 123_456_789)
-    XCTAssertEqual(timeout, try .seconds(123457))
+    let timeout = GRPCTimeout(rounding: 123_456_789, unit: .milliseconds)
+    XCTAssertEqual(timeout, GRPCTimeout(amount: 123457, unit: .seconds))
 
     // 123_456_789 (milliseconds) / 1_000
     //   = 123_456.789
@@ -79,8 +69,8 @@ class GRPCTimeoutTests: GRPCTestCase {
   }
 
   func testRoundingSecondsTimeout() throws {
-    let timeout: GRPCTimeout = .seconds(rounding: 123_456_789)
-    XCTAssertEqual(timeout, try .minutes(2057614))
+    let timeout = GRPCTimeout(rounding: 123_456_789, unit: .seconds)
+    XCTAssertEqual(timeout, GRPCTimeout(amount: 2057614, unit: .minutes))
 
     // 123_456_789 (seconds) / 60
     //   = 2_057_613.15
@@ -93,8 +83,8 @@ class GRPCTimeoutTests: GRPCTestCase {
   }
 
   func testRoundingMinutesTimeout() throws {
-    let timeout: GRPCTimeout = .minutes(rounding: 123_456_789)
-    XCTAssertEqual(timeout, try .hours(2057614))
+    let timeout = GRPCTimeout(rounding: 123_456_789, unit: .minutes)
+    XCTAssertEqual(timeout, GRPCTimeout(amount: 2057614, unit: .hours))
 
     // 123_456_789 (minutes) / 60
     //   = 2_057_613.15
@@ -107,8 +97,8 @@ class GRPCTimeoutTests: GRPCTestCase {
   }
 
   func testRoundingHoursTimeout() throws {
-    let timeout: GRPCTimeout = .hours(rounding: 123_456_789)
-    XCTAssertEqual(timeout, try .hours(99_999_999))
+    let timeout = GRPCTimeout(rounding: 123_456_789, unit: .hours)
+    XCTAssertEqual(timeout, GRPCTimeout(amount: 99_999_999, unit: .hours))
 
     // Hours are the largest unit of time we have (as per the gRPC spec) so we can't round to a
     // different unit. In this case we clamp to the largest value.
@@ -117,4 +107,21 @@ class GRPCTimeoutTests: GRPCTestCase {
     // in nanoseconds within 64 bits, again the value is clamped.
     XCTAssertEqual(timeout.nanoseconds, Int64.max)
   }
+
+  func testTimeoutFromDeadline() throws {
+    let deadline = NIODeadline.uptimeNanoseconds(0) + .seconds(42)
+    let timeout = GRPCTimeout(deadline: deadline, testingOnlyNow: .uptimeNanoseconds(0))
+    XCTAssertEqual(timeout.nanoseconds, 42_000_000_000)
+
+    // Wire encoding may have at most 8 digits, we should automatically coarsen the resolution until
+    // we're within that limit.
+    XCTAssertEqual(timeout.wireEncoding, "42000000u")
+  }
+
+  func testTimeoutFromPastDeadline() throws {
+    let deadline = NIODeadline.uptimeNanoseconds(100) + .nanoseconds(50)
+    // testingOnlyNow >= deadline: timeout should be zero.
+    let timeout = GRPCTimeout(deadline: deadline, testingOnlyNow: .uptimeNanoseconds(200))
+    XCTAssertEqual(timeout.nanoseconds, 0)
+  }
 }

+ 41 - 0
Tests/GRPCTests/TimeLimitTests.swift

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020, gRPC Authors All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@testable import GRPC
+import NIO
+import XCTest
+
+class TimeLimitTests: GRPCTestCase {
+  func testTimeout() {
+    XCTAssertEqual(TimeLimit.timeout(.seconds(42)).timeout, .seconds(42))
+    XCTAssertNil(TimeLimit.none.timeout)
+    XCTAssertNil(TimeLimit.deadline(.now()).timeout)
+  }
+
+  func testDeadline() {
+    XCTAssertEqual(TimeLimit.deadline(.uptimeNanoseconds(42)).deadline, .uptimeNanoseconds(42))
+    XCTAssertNil(TimeLimit.none.deadline)
+    XCTAssertNil(TimeLimit.timeout(.milliseconds(31415)).deadline)
+  }
+
+  func testMakeDeadline() {
+    XCTAssertEqual(TimeLimit.none.makeDeadline(), .distantFuture)
+    XCTAssertEqual(TimeLimit.timeout(.nanoseconds(.max)).makeDeadline(), .distantFuture)
+
+    let now = NIODeadline.now()
+    XCTAssertEqual(TimeLimit.deadline(now).makeDeadline(), now)
+    XCTAssertEqual(TimeLimit.deadline(.distantFuture).makeDeadline(), .distantFuture)
+  }
+}

+ 14 - 2
Tests/GRPCTests/XCTestManifests.swift

@@ -489,7 +489,6 @@ extension GRPCTimeoutTests {
     //   `swift test --generate-linuxmain`
     // to regenerate.
     static let __allTests__GRPCTimeoutTests = [
-        ("testNegativeTimeoutThrows", testNegativeTimeoutThrows),
         ("testRoundingHoursTimeout", testRoundingHoursTimeout),
         ("testRoundingMicrosecondsTimeout", testRoundingMicrosecondsTimeout),
         ("testRoundingMillisecondsTimeout", testRoundingMillisecondsTimeout),
@@ -497,7 +496,8 @@ extension GRPCTimeoutTests {
         ("testRoundingNanosecondsTimeout", testRoundingNanosecondsTimeout),
         ("testRoundingNegativeTimeout", testRoundingNegativeTimeout),
         ("testRoundingSecondsTimeout", testRoundingSecondsTimeout),
-        ("testTooLargeTimeout", testTooLargeTimeout),
+        ("testTimeoutFromDeadline", testTimeoutFromDeadline),
+        ("testTimeoutFromPastDeadline", testTimeoutFromPastDeadline),
     ]
 }
 
@@ -733,6 +733,17 @@ extension StreamingRequestClientCallTests {
     ]
 }
 
+extension TimeLimitTests {
+    // DO NOT MODIFY: This is autogenerated, use:
+    //   `swift test --generate-linuxmain`
+    // to regenerate.
+    static let __allTests__TimeLimitTests = [
+        ("testDeadline", testDeadline),
+        ("testMakeDeadline", testMakeDeadline),
+        ("testTimeout", testTimeout),
+    ]
+}
+
 extension ZlibTests {
     // DO NOT MODIFY: This is autogenerated, use:
     //   `swift test --generate-linuxmain`
@@ -800,6 +811,7 @@ public func __allTests() -> [XCTestCaseEntry] {
         testCase(ServerWebTests.__allTests__ServerWebTests),
         testCase(StopwatchTests.__allTests__StopwatchTests),
         testCase(StreamingRequestClientCallTests.__allTests__StreamingRequestClientCallTests),
+        testCase(TimeLimitTests.__allTests__TimeLimitTests),
         testCase(ZlibTests.__allTests__ZlibTests),
     ]
 }