2
0

GRPCTimeout.swift 11 KB

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