2
0

GRPCTimeout.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. /*
  2. * Copyright 2019, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import NIO
  17. /// Errors thrown when constructing a timeout.
  18. public struct GRPCTimeoutError: Error, Equatable, CustomStringConvertible {
  19. private enum BaseError {
  20. case negative
  21. case tooManyDigits
  22. }
  23. private var error: BaseError
  24. private init(_ error: BaseError) {
  25. self.error = error
  26. }
  27. public var description: String {
  28. switch self.error {
  29. case .negative:
  30. return "GRPCTimeoutError: time amount must not be negative"
  31. case .tooManyDigits:
  32. return "GRPCTimeoutError: too many digits to represent using the gRPC wire-format"
  33. }
  34. }
  35. /// The timeout is negative.
  36. public static let negative = GRPCTimeoutError(.negative)
  37. /// The number of digits in the timeout amount is more than 8-digits and cannot be encoded in
  38. /// the gRPC wire-format.
  39. public static let tooManyDigits = GRPCTimeoutError(.tooManyDigits)
  40. }
  41. /// A timeout for a gRPC call.
  42. ///
  43. /// Timeouts must be positive and at most 8-digits long.
  44. public struct GRPCTimeout: CustomStringConvertible, Equatable {
  45. public static let `default`: GRPCTimeout = try! .minutes(1)
  46. /// Creates an infinite timeout. This is a sentinel value which must __not__ be sent to a gRPC service.
  47. public static let infinite: GRPCTimeout = GRPCTimeout(nanoseconds: Int64.max, wireEncoding: "infinite")
  48. /// The largest amount of any unit of time which may be represented by a gRPC timeout.
  49. private static let maxAmount: Int64 = 99_999_999
  50. /// The wire encoding of this timeout as described in the gRPC protocol.
  51. /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md.
  52. public let wireEncoding: String
  53. public let nanoseconds: Int64
  54. public var description: String {
  55. return wireEncoding
  56. }
  57. private init(nanoseconds: Int64, wireEncoding: String) {
  58. self.nanoseconds = nanoseconds
  59. self.wireEncoding = wireEncoding
  60. }
  61. /// Creates a `GRPCTimeout`.
  62. ///
  63. /// - Precondition: The amount should be greater than or equal to zero and less than or equal
  64. /// to `GRPCTimeout.maxAmount`.
  65. private init(amount: Int64, unit: GRPCTimeoutUnit) {
  66. precondition(amount >= 0 && amount <= GRPCTimeout.maxAmount)
  67. // See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
  68. // If we overflow at this point, which is certainly possible if `amount` is sufficiently large
  69. // and `unit` is `.hours`, clamp the nanosecond timeout to `Int64.max`. It's about 292 years so
  70. // it should be long enough for the user not to notice the difference should the rpc time out.
  71. let (partial, overflow) = amount.multipliedReportingOverflow(by: unit.asNanoseconds)
  72. self.init(
  73. nanoseconds: overflow ? Int64.max : partial,
  74. wireEncoding: "\(amount)\(unit.rawValue)"
  75. )
  76. }
  77. /// Create a timeout by rounding up the timeout so that it may be represented in the gRPC
  78. /// wire format.
  79. private init(rounding amount: Int64, unit: GRPCTimeoutUnit) {
  80. var roundedAmount = amount
  81. var roundedUnit = unit
  82. if roundedAmount <= 0 {
  83. roundedAmount = 0
  84. } else {
  85. while roundedAmount > GRPCTimeout.maxAmount {
  86. switch roundedUnit {
  87. case .nanoseconds:
  88. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1_000)
  89. roundedUnit = .microseconds
  90. case .microseconds:
  91. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1_000)
  92. roundedUnit = .milliseconds
  93. case .milliseconds:
  94. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1_000)
  95. roundedUnit = .seconds
  96. case .seconds:
  97. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
  98. roundedUnit = .minutes
  99. case .minutes:
  100. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
  101. roundedUnit = .hours
  102. case .hours:
  103. roundedAmount = GRPCTimeout.maxAmount
  104. roundedUnit = .hours
  105. }
  106. }
  107. }
  108. self.init(amount: roundedAmount, unit: roundedUnit)
  109. }
  110. private static func makeTimeout(_ amount: Int64, _ unit: GRPCTimeoutUnit) throws -> GRPCTimeout {
  111. // Timeouts must be positive and at most 8-digits.
  112. if amount < 0 {
  113. throw GRPCTimeoutError.negative
  114. }
  115. if amount > GRPCTimeout.maxAmount {
  116. throw GRPCTimeoutError.tooManyDigits
  117. }
  118. return .init(amount: amount, unit: unit)
  119. }
  120. /// Creates a new GRPCTimeout for the given amount of hours.
  121. ///
  122. /// `amount` must be positive and at most 8-digits.
  123. ///
  124. /// - Parameter amount: the amount of hours this `GRPCTimeout` represents.
  125. /// - Returns: A `GRPCTimeout` representing the given number of hours.
  126. /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
  127. public static func hours(_ amount: Int) throws -> GRPCTimeout {
  128. return try makeTimeout(Int64(amount), .hours)
  129. }
  130. /// Creates a new GRPCTimeout for the given amount of hours.
  131. ///
  132. /// The timeout will be rounded up if it may not be represented in the wire format.
  133. ///
  134. /// - Parameter amount: The number of hours to represent.
  135. public static func hours(rounding amount: Int) -> GRPCTimeout {
  136. return .init(rounding: Int64(amount), unit: .hours)
  137. }
  138. /// Creates a new GRPCTimeout for the given amount of minutes.
  139. ///
  140. /// `amount` must be positive and at most 8-digits.
  141. ///
  142. /// - Parameter amount: the amount of minutes this `GRPCTimeout` represents.
  143. /// - Returns: A `GRPCTimeout` representing the given number of minutes.
  144. /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
  145. public static func minutes(_ amount: Int) throws -> GRPCTimeout {
  146. return try makeTimeout(Int64(amount), .minutes)
  147. }
  148. /// Creates a new GRPCTimeout for the given amount of minutes.
  149. ///
  150. /// The timeout will be rounded up if it may not be represented in the wire format.
  151. ///
  152. /// - Parameter amount: The number of minutes to represent.
  153. public static func minutes(rounding amount: Int) -> GRPCTimeout {
  154. return .init(rounding: Int64(amount), unit: .minutes)
  155. }
  156. /// Creates a new GRPCTimeout for the given amount of seconds.
  157. ///
  158. /// `amount` must be positive and at most 8-digits.
  159. ///
  160. /// - Parameter amount: the amount of seconds this `GRPCTimeout` represents.
  161. /// - Returns: A `GRPCTimeout` representing the given number of seconds.
  162. /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
  163. public static func seconds(_ amount: Int) throws -> GRPCTimeout {
  164. return try makeTimeout(Int64(amount), .seconds)
  165. }
  166. /// Creates a new GRPCTimeout for the given amount of seconds.
  167. ///
  168. /// The timeout will be rounded up if it may not be represented in the wire format.
  169. ///
  170. /// - Parameter amount: The number of seconds to represent.
  171. public static func seconds(rounding amount: Int) -> GRPCTimeout {
  172. return .init(rounding: Int64(amount), unit: .seconds)
  173. }
  174. /// Creates a new GRPCTimeout for the given amount of milliseconds.
  175. ///
  176. /// `amount` must be positive and at most 8-digits.
  177. ///
  178. /// - Parameter amount: the amount of milliseconds this `GRPCTimeout` represents.
  179. /// - Returns: A `GRPCTimeout` representing the given number of milliseconds.
  180. /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
  181. public static func milliseconds(_ amount: Int) throws -> GRPCTimeout {
  182. return try makeTimeout(Int64(amount), .milliseconds)
  183. }
  184. /// Creates a new GRPCTimeout for the given amount of milliseconds.
  185. ///
  186. /// The timeout will be rounded up if it may not be represented in the wire format.
  187. ///
  188. /// - Parameter amount: The number of milliseconds to represent.
  189. public static func milliseconds(rounding amount: Int) -> GRPCTimeout {
  190. return .init(rounding: Int64(amount), unit: .milliseconds)
  191. }
  192. /// Creates a new GRPCTimeout for the given amount of microseconds.
  193. ///
  194. /// `amount` must be positive and at most 8-digits.
  195. ///
  196. /// - Parameter amount: the amount of microseconds this `GRPCTimeout` represents.
  197. /// - Returns: A `GRPCTimeout` representing the given number of microseconds.
  198. /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
  199. public static func microseconds(_ amount: Int) throws -> GRPCTimeout {
  200. return try makeTimeout(Int64(amount), .microseconds)
  201. }
  202. /// Creates a new GRPCTimeout for the given amount of microseconds.
  203. ///
  204. /// The timeout will be rounded up if it may not be represented in the wire format.
  205. ///
  206. /// - Parameter amount: The number of microseconds to represent.
  207. public static func microseconds(rounding amount: Int) -> GRPCTimeout {
  208. return .init(rounding: Int64(amount), unit: .microseconds)
  209. }
  210. /// Creates a new GRPCTimeout for the given amount of nanoseconds.
  211. ///
  212. /// `amount` must be positive and at most 8-digits.
  213. ///
  214. /// - Parameter amount: the amount of nanoseconds this `GRPCTimeout` represents.
  215. /// - Returns: A `GRPCTimeout` representing the given number of nanoseconds.
  216. /// - Throws: `GRPCTimeoutError` if the amount was negative or more than 8 digits long.
  217. public static func nanoseconds(_ amount: Int) throws -> GRPCTimeout {
  218. return try makeTimeout(Int64(amount), .nanoseconds)
  219. }
  220. /// Creates a new GRPCTimeout for the given amount of nanoseconds.
  221. ///
  222. /// The timeout will be rounded up if it may not be represented in the wire format.
  223. ///
  224. /// - Parameter amount: The number of nanoseconds to represent.
  225. public static func nanoseconds(rounding amount: Int) -> GRPCTimeout {
  226. return .init(rounding: Int64(amount), unit: .nanoseconds)
  227. }
  228. }
  229. public extension GRPCTimeout {
  230. /// Returns a NIO `TimeAmount` representing the amount of time as this timeout.
  231. var asNIOTimeAmount: TimeAmount {
  232. return TimeAmount.nanoseconds(numericCast(nanoseconds))
  233. }
  234. }
  235. fileprivate extension Int64 {
  236. /// Returns the quotient of this value when divided by `divisor` rounded up to the nearest
  237. /// multiple of `divisor` if the remainder is non-zero.
  238. ///
  239. /// - Parameter divisor: The value to divide this value by.
  240. func quotientRoundedUp(dividingBy divisor: Int64) -> Int64 {
  241. let (quotient, remainder) = self.quotientAndRemainder(dividingBy: divisor)
  242. return quotient + (remainder != 0 ? 1 : 0)
  243. }
  244. }
  245. fileprivate enum GRPCTimeoutUnit: String {
  246. case hours = "H"
  247. case minutes = "M"
  248. case seconds = "S"
  249. case milliseconds = "m"
  250. case microseconds = "u"
  251. case nanoseconds = "n"
  252. internal var asNanoseconds: Int64 {
  253. switch self {
  254. case .hours:
  255. return 60 * 60 * 1000 * 1000 * 1000
  256. case .minutes:
  257. return 60 * 1000 * 1000 * 1000
  258. case .seconds:
  259. return 1000 * 1000 * 1000
  260. case .milliseconds:
  261. return 1000 * 1000
  262. case .microseconds:
  263. return 1000
  264. case .nanoseconds:
  265. return 1
  266. }
  267. }
  268. }