|
|
@@ -16,7 +16,7 @@
|
|
|
import Foundation
|
|
|
import NIO
|
|
|
|
|
|
-public enum GRPCTimeoutError: String, Error {
|
|
|
+public enum GRPCTimeoutError: String, Error, Equatable {
|
|
|
case negative = "GRPCTimeout must be non-negative"
|
|
|
case tooManyDigits = "GRPCTimeout must be at most 8 digits"
|
|
|
}
|
|
|
@@ -27,28 +27,89 @@ public enum GRPCTimeoutError: String, Error {
|
|
|
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, description: "infinite")
|
|
|
+ public static let infinite: GRPCTimeout = GRPCTimeout(nanoseconds: Int64.max, wireEncoding: "infinite")
|
|
|
|
|
|
- /// A description of the timeout in the format described in the
|
|
|
- /// [gRPC protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md).
|
|
|
- public let description: String
|
|
|
+ /// The largest amount of any unit of time which may be represented by a gRPC timeout.
|
|
|
+ private 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.
|
|
|
+ public let wireEncoding: String
|
|
|
public let nanoseconds: Int64
|
|
|
|
|
|
- private init(nanoseconds: Int64, description: String) {
|
|
|
- self.nanoseconds = nanoseconds
|
|
|
- self.description = description
|
|
|
+ public var description: String {
|
|
|
+ return wireEncoding
|
|
|
}
|
|
|
|
|
|
- private static func makeTimeout(_ amount: Int, _ unit: GRPCTimeoutUnit) throws -> GRPCTimeout {
|
|
|
- // Timeouts must be positive and at most 8-digits.
|
|
|
- if amount < 0 { throw GRPCTimeoutError.negative }
|
|
|
- if amount >= 100_000_000 { throw GRPCTimeoutError.tooManyDigits }
|
|
|
+ private init(nanoseconds: Int64, wireEncoding: String) {
|
|
|
+ self.nanoseconds = nanoseconds
|
|
|
+ self.wireEncoding = wireEncoding
|
|
|
+ }
|
|
|
|
|
|
+ /// Creates a `GRPCTimeout`.
|
|
|
+ ///
|
|
|
+ /// - 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) {
|
|
|
+ precondition(amount >= 0 && amount <= GRPCTimeout.maxAmount)
|
|
|
// See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
|
|
|
- let description = "\(amount)\(unit.rawValue)"
|
|
|
- let nanoseconds = Int64(amount) * unit.asNanoseconds
|
|
|
|
|
|
- return GRPCTimeout(nanoseconds: nanoseconds, description: description)
|
|
|
+ // If we overflow at this point, which is certainly possible if `amount` is sufficiently large
|
|
|
+ // and `unit` is `.hours`, clamp the nanosecond timeout to `Int64.max`. It's about 292 years so
|
|
|
+ // it should be long enough for the user not to notice the difference should the rpc time out.
|
|
|
+ let (partial, overflow) = amount.multipliedReportingOverflow(by: unit.asNanoseconds)
|
|
|
+
|
|
|
+ self.init(
|
|
|
+ nanoseconds: overflow ? Int64.max : partial,
|
|
|
+ wireEncoding: "\(amount)\(unit.rawValue)"
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 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) {
|
|
|
+ var roundedAmount = amount
|
|
|
+ var roundedUnit = unit
|
|
|
+
|
|
|
+ if roundedAmount <= 0 {
|
|
|
+ roundedAmount = 0
|
|
|
+ } else {
|
|
|
+ while roundedAmount > GRPCTimeout.maxAmount {
|
|
|
+ switch roundedUnit {
|
|
|
+ case .nanoseconds:
|
|
|
+ roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1_000)
|
|
|
+ roundedUnit = .microseconds
|
|
|
+ case .microseconds:
|
|
|
+ roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1_000)
|
|
|
+ roundedUnit = .milliseconds
|
|
|
+ case .milliseconds:
|
|
|
+ roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1_000)
|
|
|
+ roundedUnit = .seconds
|
|
|
+ case .seconds:
|
|
|
+ roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
|
|
|
+ roundedUnit = .minutes
|
|
|
+ case .minutes:
|
|
|
+ roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
|
|
|
+ roundedUnit = .hours
|
|
|
+ case .hours:
|
|
|
+ roundedAmount = GRPCTimeout.maxAmount
|
|
|
+ roundedUnit = .hours
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ 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.
|
|
|
@@ -59,7 +120,16 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
|
|
|
/// - 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(amount, .hours)
|
|
|
+ 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.
|
|
|
@@ -70,7 +140,16 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
|
|
|
/// - 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(amount, .minutes)
|
|
|
+ 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.
|
|
|
@@ -81,7 +160,16 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
|
|
|
/// - 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(amount, .seconds)
|
|
|
+ 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.
|
|
|
@@ -92,7 +180,16 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
|
|
|
/// - 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(amount, .milliseconds)
|
|
|
+ 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.
|
|
|
@@ -103,7 +200,16 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
|
|
|
/// - 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(amount, .microseconds)
|
|
|
+ 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.
|
|
|
@@ -114,18 +220,38 @@ public struct GRPCTimeout: CustomStringConvertible, Equatable {
|
|
|
/// - 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(amount, .nanoseconds)
|
|
|
+ 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)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-extension GRPCTimeout {
|
|
|
+public extension GRPCTimeout {
|
|
|
/// Returns a NIO `TimeAmount` representing the amount of time as this timeout.
|
|
|
- public var asNIOTimeAmount: TimeAmount {
|
|
|
+ var asNIOTimeAmount: TimeAmount {
|
|
|
return TimeAmount.nanoseconds(numericCast(nanoseconds))
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-private enum GRPCTimeoutUnit: String {
|
|
|
+fileprivate extension Int64 {
|
|
|
+ /// Returns the quotient of this value when divided by `divisor` rounded up to the nearest
|
|
|
+ /// multiple of `divisor` if the remainder is non-zero.
|
|
|
+ ///
|
|
|
+ /// - Parameter divisor: The value to divide this value by.
|
|
|
+ func quotientRoundedUp(dividingBy divisor: Int64) -> Int64 {
|
|
|
+ let (quotient, remainder) = self.quotientAndRemainder(dividingBy: divisor)
|
|
|
+ return quotient + (remainder != 0 ? 1 : 0)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+fileprivate enum GRPCTimeoutUnit: String {
|
|
|
case hours = "H"
|
|
|
case minutes = "M"
|
|
|
case seconds = "S"
|