2
0

Timeout.swift 7.9 KB

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