| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- /*
- * Copyright 2023, gRPC Authors All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- import Dispatch
- /// A timeout for a gRPC call.
- ///
- /// It's a combination of an amount (expressed as an integer of at maximum 8 digits), and a unit, which is
- /// one of ``Timeout/Unit`` (hours, minutes, seconds, milliseconds, microseconds or nanoseconds).
- ///
- /// Timeouts must be positive and at most 8-digits long.
- @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
- @usableFromInline
- struct Timeout: CustomStringConvertible, Hashable, Sendable {
- /// Possible units for a ``Timeout``.
- internal enum Unit: Character {
- case hours = "H"
- case minutes = "M"
- case seconds = "S"
- case milliseconds = "m"
- case microseconds = "u"
- case nanoseconds = "n"
- }
- /// The largest amount of any unit of time which may be represented by a gRPC timeout.
- static let maxAmount: Int64 = 99_999_999
- private let amount: Int64
- private let unit: Unit
- @usableFromInline
- var duration: Duration {
- Duration(amount: amount, unit: unit)
- }
- /// The wire encoding of this timeout as described in the gRPC protocol.
- /// See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
- var wireEncoding: String {
- "\(amount)\(unit.rawValue)"
- }
- @usableFromInline
- var description: String {
- return self.wireEncoding
- }
- @usableFromInline
- init?(decoding value: String) {
- guard (2 ... 8).contains(value.count) else {
- return nil
- }
- if let amount = Int64(value.dropLast()),
- let unit = Unit(rawValue: value.last!)
- {
- self = Self.init(amount: amount, unit: unit)
- } else {
- return nil
- }
- }
- /// Create a ``Timeout`` from a ``Duration``.
- ///
- /// - Important: It's not possible to know with what precision the duration was created: that is,
- /// it's not possible to know whether `Duration.seconds(value)` or `Duration.milliseconds(value)`
- /// was used. For this reason, the unit chosen for the ``Timeout`` (and thus the wire encoding) may be
- /// different from the one originally used to create the ``Duration``. Despite this, we guarantee that
- /// both durations will be equivalent if there was no loss in precision during the transformation.
- /// For example, `Duration.hours(123)` will yield a ``Timeout`` with `wireEncoding` equal to
- /// `"442800S"`, which is in seconds. However, 442800 seconds and 123 hours are equivalent.
- /// However, you must note that there may be some loss of precision when dealing with transforming
- /// between units. For example, for very low precisions, such as a duration of only a few attoseconds,
- /// given the smallest unit we have is whole nanoseconds, we cannot represent it. Same when converting
- /// for instance, milliseconds to seconds. In these scenarios, we'll round to the closest whole number in
- /// the target unit.
- @usableFromInline
- init(duration: Duration) {
- let (seconds, attoseconds) = duration.components
- if seconds == 0 {
- // There is no seconds component, so only pay attention to the attoseconds.
- // Try converting to nanoseconds first, and continue rounding up if the
- // max amount of digits is exceeded.
- let nanoseconds = Int64(Double(attoseconds) / 1e+9)
- self.init(rounding: nanoseconds, unit: .nanoseconds)
- } else if Self.exceedsDigitLimit(seconds) {
- // We don't have enough digits to represent this amount in seconds, so
- // we will have to use minutes or hours.
- // We can also ignore attoseconds, since we won't have enough precision
- // anyways to represent the (at most) one second that the attoseconds
- // component can express.
- self.init(rounding: seconds, unit: .seconds)
- } else {
- // We can't convert seconds to nanoseconds because that would take us
- // over the 8 digit limit (1 second = 1e+9 nanoseconds).
- // We can however, try converting to microseconds or milliseconds.
- let nanoseconds = Int64(Double(attoseconds) / 1e+9)
- let microseconds = nanoseconds / 1000
- if microseconds == 0 {
- self.init(amount: seconds, unit: .seconds)
- } else {
- let secondsInMicroseconds = seconds * 1000 * 1000
- let totalMicroseconds = microseconds + secondsInMicroseconds
- self.init(rounding: totalMicroseconds, unit: .microseconds)
- }
- }
- }
- /// 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: Unit) {
- var roundedAmount = amount
- var roundedUnit = unit
- if roundedAmount <= 0 {
- roundedAmount = 0
- } else {
- while roundedAmount > Timeout.maxAmount {
- switch roundedUnit {
- case .nanoseconds:
- roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
- roundedUnit = .microseconds
- case .microseconds:
- roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
- roundedUnit = .milliseconds
- case .milliseconds:
- roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
- roundedUnit = .seconds
- case .seconds:
- roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
- roundedUnit = .minutes
- case .minutes:
- roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
- roundedUnit = .hours
- case .hours:
- roundedAmount = Timeout.maxAmount
- roundedUnit = .hours
- }
- }
- }
- self.init(amount: roundedAmount, unit: roundedUnit)
- }
- private static func exceedsDigitLimit(_ value: Int64) -> Bool {
- value > Timeout.maxAmount
- }
- /// Creates a `GRPCTimeout`.
- ///
- /// - Precondition: The amount should be greater than or equal to zero and less than or equal
- /// to `GRPCTimeout.maxAmount`.
- internal init(amount: Int64, unit: Unit) {
- precondition((0 ... Timeout.maxAmount).contains(amount))
- self.amount = amount
- self.unit = unit
- }
- }
- 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.
- fileprivate func quotientRoundedUp(dividingBy divisor: Int64) -> Int64 {
- let (quotient, remainder) = self.quotientAndRemainder(dividingBy: divisor)
- return quotient + (remainder != 0 ? 1 : 0)
- }
- }
- @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
- extension Duration {
- /// Construct a `Duration` given a number of minutes represented as an `Int64`.
- ///
- /// let d: Duration = .minutes(5)
- ///
- /// - Returns: A `Duration` representing a given number of minutes.
- internal static func minutes(_ minutes: Int64) -> Duration {
- return Self.init(secondsComponent: 60 * minutes, attosecondsComponent: 0)
- }
- /// Construct a `Duration` given a number of hours represented as an `Int64`.
- ///
- /// let d: Duration = .hours(3)
- ///
- /// - Returns: A `Duration` representing a given number of hours.
- internal static func hours(_ hours: Int64) -> Duration {
- return Self.init(secondsComponent: 60 * 60 * hours, attosecondsComponent: 0)
- }
- internal init(amount: Int64, unit: Timeout.Unit) {
- switch unit {
- case .hours:
- self = Self.hours(amount)
- case .minutes:
- self = Self.minutes(amount)
- case .seconds:
- self = Self.seconds(amount)
- case .milliseconds:
- self = Self.milliseconds(amount)
- case .microseconds:
- self = Self.microseconds(amount)
- case .nanoseconds:
- self = Self.nanoseconds(amount)
- }
- }
- }
|