Timeout.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /*
  2. * Copyright 2023, 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 Dispatch
  17. /// A timeout for a gRPC call.
  18. ///
  19. /// It's a combination of an amount (expressed as an integer of at maximum 8 digits), and a unit, which is
  20. /// one of ``Timeout/Unit`` (hours, minutes, seconds, milliseconds, microseconds or nanoseconds).
  21. ///
  22. /// Timeouts must be positive and at most 8-digits long.
  23. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  24. @usableFromInline
  25. struct Timeout: CustomStringConvertible, Hashable, Sendable {
  26. /// Possible units for a ``Timeout``.
  27. internal enum Unit: Character {
  28. case hours = "H"
  29. case minutes = "M"
  30. case seconds = "S"
  31. case milliseconds = "m"
  32. case microseconds = "u"
  33. case nanoseconds = "n"
  34. }
  35. /// The largest amount of any unit of time which may be represented by a gRPC timeout.
  36. static let maxAmount: Int64 = 99_999_999
  37. private let amount: Int64
  38. private let unit: Unit
  39. @usableFromInline
  40. var duration: Duration {
  41. Duration(amount: amount, unit: unit)
  42. }
  43. /// The wire encoding of this timeout as described in the gRPC protocol.
  44. /// See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
  45. var wireEncoding: String {
  46. "\(amount)\(unit.rawValue)"
  47. }
  48. @usableFromInline
  49. var description: String {
  50. return self.wireEncoding
  51. }
  52. @usableFromInline
  53. init?(decoding value: String) {
  54. guard (2 ... 8).contains(value.count) else {
  55. return nil
  56. }
  57. if let amount = Int64(value.dropLast()),
  58. let unit = Unit(rawValue: value.last!)
  59. {
  60. self = Self.init(amount: amount, unit: unit)
  61. } else {
  62. return nil
  63. }
  64. }
  65. /// Create a ``Timeout`` from a ``Duration``.
  66. ///
  67. /// - Important: It's not possible to know with what precision the duration was created: that is,
  68. /// it's not possible to know whether `Duration.seconds(value)` or `Duration.milliseconds(value)`
  69. /// was used. For this reason, the unit chosen for the ``Timeout`` (and thus the wire encoding) may be
  70. /// different from the one originally used to create the ``Duration``. Despite this, we guarantee that
  71. /// both durations will be equivalent if there was no loss in precision during the transformation.
  72. /// For example, `Duration.hours(123)` will yield a ``Timeout`` with `wireEncoding` equal to
  73. /// `"442800S"`, which is in seconds. However, 442800 seconds and 123 hours are equivalent.
  74. /// However, you must note that there may be some loss of precision when dealing with transforming
  75. /// between units. For example, for very low precisions, such as a duration of only a few attoseconds,
  76. /// given the smallest unit we have is whole nanoseconds, we cannot represent it. Same when converting
  77. /// for instance, milliseconds to seconds. In these scenarios, we'll round to the closest whole number in
  78. /// the target unit.
  79. @usableFromInline
  80. init(duration: Duration) {
  81. let (seconds, attoseconds) = duration.components
  82. if seconds == 0 {
  83. // There is no seconds component, so only pay attention to the attoseconds.
  84. // Try converting to nanoseconds first, and continue rounding up if the
  85. // max amount of digits is exceeded.
  86. let nanoseconds = Int64(Double(attoseconds) / 1e+9)
  87. self.init(rounding: nanoseconds, unit: .nanoseconds)
  88. } else if Self.exceedsDigitLimit(seconds) {
  89. // We don't have enough digits to represent this amount in seconds, so
  90. // we will have to use minutes or hours.
  91. // We can also ignore attoseconds, since we won't have enough precision
  92. // anyways to represent the (at most) one second that the attoseconds
  93. // component can express.
  94. self.init(rounding: seconds, unit: .seconds)
  95. } else {
  96. // We can't convert seconds to nanoseconds because that would take us
  97. // over the 8 digit limit (1 second = 1e+9 nanoseconds).
  98. // We can however, try converting to microseconds or milliseconds.
  99. let nanoseconds = Int64(Double(attoseconds) / 1e+9)
  100. let microseconds = nanoseconds / 1000
  101. if microseconds == 0 {
  102. self.init(amount: seconds, unit: .seconds)
  103. } else {
  104. let secondsInMicroseconds = seconds * 1000 * 1000
  105. let totalMicroseconds = microseconds + secondsInMicroseconds
  106. self.init(rounding: totalMicroseconds, unit: .microseconds)
  107. }
  108. }
  109. }
  110. /// Create a timeout by rounding up the timeout so that it may be represented in the gRPC
  111. /// wire format.
  112. private init(rounding amount: Int64, unit: Unit) {
  113. var roundedAmount = amount
  114. var roundedUnit = unit
  115. if roundedAmount <= 0 {
  116. roundedAmount = 0
  117. } else {
  118. while roundedAmount > Timeout.maxAmount {
  119. switch roundedUnit {
  120. case .nanoseconds:
  121. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
  122. roundedUnit = .microseconds
  123. case .microseconds:
  124. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
  125. roundedUnit = .milliseconds
  126. case .milliseconds:
  127. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
  128. roundedUnit = .seconds
  129. case .seconds:
  130. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
  131. roundedUnit = .minutes
  132. case .minutes:
  133. roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
  134. roundedUnit = .hours
  135. case .hours:
  136. roundedAmount = Timeout.maxAmount
  137. roundedUnit = .hours
  138. }
  139. }
  140. }
  141. self.init(amount: roundedAmount, unit: roundedUnit)
  142. }
  143. private static func exceedsDigitLimit(_ value: Int64) -> Bool {
  144. value > Timeout.maxAmount
  145. }
  146. /// Creates a `GRPCTimeout`.
  147. ///
  148. /// - Precondition: The amount should be greater than or equal to zero and less than or equal
  149. /// to `GRPCTimeout.maxAmount`.
  150. internal init(amount: Int64, unit: Unit) {
  151. precondition((0 ... Timeout.maxAmount).contains(amount))
  152. self.amount = amount
  153. self.unit = unit
  154. }
  155. }
  156. extension Int64 {
  157. /// Returns the quotient of this value when divided by `divisor` rounded up to the nearest
  158. /// multiple of `divisor` if the remainder is non-zero.
  159. ///
  160. /// - Parameter divisor: The value to divide this value by.
  161. fileprivate func quotientRoundedUp(dividingBy divisor: Int64) -> Int64 {
  162. let (quotient, remainder) = self.quotientAndRemainder(dividingBy: divisor)
  163. return quotient + (remainder != 0 ? 1 : 0)
  164. }
  165. }
  166. @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
  167. extension Duration {
  168. /// Construct a `Duration` given a number of minutes represented as an `Int64`.
  169. ///
  170. /// let d: Duration = .minutes(5)
  171. ///
  172. /// - Returns: A `Duration` representing a given number of minutes.
  173. internal static func minutes(_ minutes: Int64) -> Duration {
  174. return Self.init(secondsComponent: 60 * minutes, attosecondsComponent: 0)
  175. }
  176. /// Construct a `Duration` given a number of hours represented as an `Int64`.
  177. ///
  178. /// let d: Duration = .hours(3)
  179. ///
  180. /// - Returns: A `Duration` representing a given number of hours.
  181. internal static func hours(_ hours: Int64) -> Duration {
  182. return Self.init(secondsComponent: 60 * 60 * hours, attosecondsComponent: 0)
  183. }
  184. internal init(amount: Int64, unit: Timeout.Unit) {
  185. switch unit {
  186. case .hours:
  187. self = Self.hours(amount)
  188. case .minutes:
  189. self = Self.minutes(amount)
  190. case .seconds:
  191. self = Self.seconds(amount)
  192. case .milliseconds:
  193. self = Self.milliseconds(amount)
  194. case .microseconds:
  195. self = Self.microseconds(amount)
  196. case .nanoseconds:
  197. self = Self.nanoseconds(amount)
  198. }
  199. }
  200. }