From 5dbf942c968fe189066630fa2e0f3d3fd426c66b Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 23 Nov 2023 13:34:59 +0000 Subject: [PATCH 01/14] Add Timeout type to GRPCCore --- .../Server/Internal/ServerRPCExecutor.swift | 2 +- Sources/GRPCCore/Internal/Metadata+GRPC.swift | 20 ++- Sources/GRPCCore/Timeout.swift | 153 ++++++++++++++++ Tests/GRPCCoreTests/TimeoutTests.swift | 163 ++++++++++++++++++ 4 files changed, 328 insertions(+), 10 deletions(-) create mode 100644 Sources/GRPCCore/Timeout.swift create mode 100644 Tests/GRPCCoreTests/TimeoutTests.swift diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index 54b775195..aa39bbe58 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -76,7 +76,7 @@ struct ServerRPCExecutor { if let timeout = metadata.timeout { group.addTask { let result = await Result { - try await Task.sleep(until: .now.advanced(by: timeout), clock: .continuous) + try await Task.sleep(for: timeout.duration, clock: .continuous) } return .timedOut(result) } diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index f28bfc966..a380bedc4 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -38,15 +38,17 @@ extension Metadata { } @inlinable - var timeout: Duration? { - // Temporary hack to support tests; only supports nanoseconds. - guard let value = self.firstString(forKey: .timeout) else { return nil } - guard value.utf8.last == UTF8.CodeUnit(ascii: "n") else { return nil } - var index = value.utf8.endIndex - value.utf8.formIndex(before: &index) - guard let digits = String(value.utf8[.. 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) + } +} + +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: TimeoutUnit) { + 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) + } + } +} + +internal enum TimeoutUnit: Character { + case hours = "H" + case minutes = "M" + case seconds = "S" + case milliseconds = "m" + case microseconds = "u" + case nanoseconds = "n" +} diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift new file mode 100644 index 000000000..f5aac26f0 --- /dev/null +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -0,0 +1,163 @@ +/* + * 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. + */ +@testable import GRPCCore +import XCTest + +final class TimeoutTests: XCTestCase { + func testDecodeInvalidTimeout_Empty() { + let timeoutHeader = "" + XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + } + + func testDecodeInvalidTimeout_NoAmount() { + let timeoutHeader = "H" + XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + } + + func testDecodeInvalidTimeout_NoUnit() { + let timeoutHeader = "123" + XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + } + + func testDecodeInvalidTimeout_TooLongAmount() { + let timeoutHeader = "100000000S" + XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + } + + func testDecodeInvalidTimeout_InvalidUnit() { + let timeoutHeader = "123j" + XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + } + + func testDecodeValidTimeout_Hours(){ + let timeoutHeader = "123H" + let timeout = Timeout(stringLiteral: timeoutHeader) + XCTAssertNotNil(timeout) + XCTAssertEqual(timeout!.duration, Duration.hours(123)) + XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Minutes(){ + let timeoutHeader = "123M" + let timeout = Timeout(stringLiteral: timeoutHeader) + XCTAssertNotNil(timeout) + XCTAssertEqual(timeout!.duration, Duration.minutes(123)) + XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Seconds(){ + let timeoutHeader = "123S" + let timeout = Timeout(stringLiteral: timeoutHeader) + XCTAssertNotNil(timeout) + XCTAssertEqual(timeout!.duration, Duration.seconds(123)) + XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Milliseconds(){ + let timeoutHeader = "123m" + let timeout = Timeout(stringLiteral: timeoutHeader) + XCTAssertNotNil(timeout) + XCTAssertEqual(timeout!.duration, Duration.milliseconds(123)) + XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Microseconds(){ + let timeoutHeader = "123u" + let timeout = Timeout(stringLiteral: timeoutHeader) + XCTAssertNotNil(timeout) + XCTAssertEqual(timeout!.duration, Duration.microseconds(123)) + XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + } + + func testDecodeValidTimeout_Nanoseconds(){ + let timeoutHeader = "123n" + let timeout = Timeout(stringLiteral: timeoutHeader) + XCTAssertNotNil(timeout) + XCTAssertEqual(timeout!.duration, Duration.nanoseconds(123)) + XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + } + + func testRoundingNegativeTimeout() { + let timeout = Timeout(rounding: -10, unit: .seconds) + XCTAssertEqual(String(describing: timeout), "0S") + XCTAssertEqual(timeout.duration, .seconds(0)) + } + + func testRoundingNanosecondsTimeout() throws { + let timeout = Timeout(rounding: 123_456_789, unit: .nanoseconds) + XCTAssertEqual(timeout, Timeout(amount: 123_457, unit: .microseconds)) + + // 123_456_789 (nanoseconds) / 1_000 + // = 123_456.789 + // = 123_457 (microseconds, rounded up) + XCTAssertEqual(String(describing: timeout), "123457u") + XCTAssertEqual(timeout.duration, .microseconds(123_457)) + } + + func testRoundingMicrosecondsTimeout() throws { + let timeout = Timeout(rounding: 123_456_789, unit: .microseconds) + XCTAssertEqual(timeout, Timeout(amount: 123_457, unit: .milliseconds)) + + // 123_456_789 (microseconds) / 1_000 + // = 123_456.789 + // = 123_457 (milliseconds, rounded up) + XCTAssertEqual(String(describing: timeout), "123457m") + XCTAssertEqual(timeout.duration, .milliseconds(123_457)) + } + + func testRoundingMillisecondsTimeout() throws { + let timeout = Timeout(rounding: 123_456_789, unit: .milliseconds) + XCTAssertEqual(timeout, Timeout(amount: 123_457, unit: .seconds)) + + // 123_456_789 (milliseconds) / 1_000 + // = 123_456.789 + // = 123_457 (seconds, rounded up) + XCTAssertEqual(String(describing: timeout), "123457S") + XCTAssertEqual(timeout.duration, .seconds(123_457)) + } + + func testRoundingSecondsTimeout() throws { + let timeout = Timeout(rounding: 123_456_789, unit: .seconds) + XCTAssertEqual(timeout, Timeout(amount: 2_057_614, unit: .minutes)) + + // 123_456_789 (seconds) / 60 + // = 2_057_613.15 + // = 2_057_614 (minutes, rounded up) + XCTAssertEqual(String(describing: timeout), "2057614M") + XCTAssertEqual(timeout.duration, .minutes(2057614)) + } + + func testRoundingMinutesTimeout() throws { + let timeout = Timeout(rounding: 123_456_789, unit: .minutes) + XCTAssertEqual(timeout, Timeout(amount: 2_057_614, unit: .hours)) + + // 123_456_789 (minutes) / 60 + // = 2_057_613.15 + // = 2_057_614 (hours, rounded up) + XCTAssertEqual(String(describing: timeout), "2057614H") + XCTAssertEqual(timeout.duration, .hours(2057614)) + } + + func testRoundingHoursTimeout() throws { + let timeout = Timeout(rounding: 123_456_789, unit: .hours) + XCTAssertEqual(timeout, Timeout(amount: 99_999_999, unit: .hours)) + + // Hours are the largest unit of time we have (as per the gRPC spec) so we can't round to a + // different unit. In this case we clamp to the largest value. + XCTAssertEqual(String(describing: timeout), "99999999H") + XCTAssertEqual(timeout.duration, .hours(Timeout.maxAmount)) + } +} From 653ddec80349f9f7a6b58df337f7afcae04b9f3e Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Thu, 23 Nov 2023 17:58:58 +0000 Subject: [PATCH 02/14] Formatting --- Sources/GRPCCore/Timeout.swift | 17 ++++++----- Tests/GRPCCoreTests/TimeoutTests.swift | 41 +++++++++++++------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index ddcc79378..c3a1e9cb0 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -32,14 +32,15 @@ public struct Timeout: CustomStringConvertible, Equatable { public var description: String { return self.wireEncoding } - + public init?(stringLiteral value: String) { - guard 2...8 ~= value.count else { + guard 2 ... 8 ~= value.count else { return nil } - if let amount = Int64(value.dropLast()), - let unit = TimeoutUnit(rawValue: value.last!) { + if let amount = Int64(value.dropLast()), + let unit = TimeoutUnit(rawValue: value.last!) + { self = Self.init(amount: amount, unit: unit) } else { return nil @@ -51,8 +52,8 @@ public struct Timeout: CustomStringConvertible, Equatable { /// - Precondition: The amount should be greater than or equal to zero and less than or equal /// to `GRPCTimeout.maxAmount`. internal init(amount: Int64, unit: TimeoutUnit) { - precondition(0...Timeout.maxAmount ~= amount) - + precondition(0 ... Timeout.maxAmount ~= amount) + self.duration = Duration(amount: amount, unit: unit) self.wireEncoding = "\(amount)\(unit.rawValue)" } @@ -115,7 +116,7 @@ extension Duration { 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) @@ -124,7 +125,7 @@ extension Duration { internal static func hours(_ hours: Int64) -> Duration { return Self.init(secondsComponent: 60 * 60 * hours, attosecondsComponent: 0) } - + internal init(amount: Int64, unit: TimeoutUnit) { switch unit { case .hours: diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index f5aac26f0..1eb511e74 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -1,3 +1,5 @@ +import XCTest + /* * Copyright 2023, gRPC Authors All rights reserved. * @@ -14,82 +16,81 @@ * limitations under the License. */ @testable import GRPCCore -import XCTest final class TimeoutTests: XCTestCase { func testDecodeInvalidTimeout_Empty() { let timeoutHeader = "" XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) } - + func testDecodeInvalidTimeout_NoAmount() { let timeoutHeader = "H" XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) } - + func testDecodeInvalidTimeout_NoUnit() { let timeoutHeader = "123" XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) } - + func testDecodeInvalidTimeout_TooLongAmount() { let timeoutHeader = "100000000S" XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) } - + func testDecodeInvalidTimeout_InvalidUnit() { let timeoutHeader = "123j" XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) } - - func testDecodeValidTimeout_Hours(){ + + func testDecodeValidTimeout_Hours() { let timeoutHeader = "123H" let timeout = Timeout(stringLiteral: timeoutHeader) XCTAssertNotNil(timeout) XCTAssertEqual(timeout!.duration, Duration.hours(123)) XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) } - - func testDecodeValidTimeout_Minutes(){ + + func testDecodeValidTimeout_Minutes() { let timeoutHeader = "123M" let timeout = Timeout(stringLiteral: timeoutHeader) XCTAssertNotNil(timeout) XCTAssertEqual(timeout!.duration, Duration.minutes(123)) XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) } - - func testDecodeValidTimeout_Seconds(){ + + func testDecodeValidTimeout_Seconds() { let timeoutHeader = "123S" let timeout = Timeout(stringLiteral: timeoutHeader) XCTAssertNotNil(timeout) XCTAssertEqual(timeout!.duration, Duration.seconds(123)) XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) } - - func testDecodeValidTimeout_Milliseconds(){ + + func testDecodeValidTimeout_Milliseconds() { let timeoutHeader = "123m" let timeout = Timeout(stringLiteral: timeoutHeader) XCTAssertNotNil(timeout) XCTAssertEqual(timeout!.duration, Duration.milliseconds(123)) XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) } - - func testDecodeValidTimeout_Microseconds(){ + + func testDecodeValidTimeout_Microseconds() { let timeoutHeader = "123u" let timeout = Timeout(stringLiteral: timeoutHeader) XCTAssertNotNil(timeout) XCTAssertEqual(timeout!.duration, Duration.microseconds(123)) XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) } - - func testDecodeValidTimeout_Nanoseconds(){ + + func testDecodeValidTimeout_Nanoseconds() { let timeoutHeader = "123n" let timeout = Timeout(stringLiteral: timeoutHeader) XCTAssertNotNil(timeout) XCTAssertEqual(timeout!.duration, Duration.nanoseconds(123)) XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) } - + func testRoundingNegativeTimeout() { let timeout = Timeout(rounding: -10, unit: .seconds) XCTAssertEqual(String(describing: timeout), "0S") @@ -137,7 +138,7 @@ final class TimeoutTests: XCTestCase { // = 2_057_613.15 // = 2_057_614 (minutes, rounded up) XCTAssertEqual(String(describing: timeout), "2057614M") - XCTAssertEqual(timeout.duration, .minutes(2057614)) + XCTAssertEqual(timeout.duration, .minutes(2_057_614)) } func testRoundingMinutesTimeout() throws { @@ -148,7 +149,7 @@ final class TimeoutTests: XCTestCase { // = 2_057_613.15 // = 2_057_614 (hours, rounded up) XCTAssertEqual(String(describing: timeout), "2057614H") - XCTAssertEqual(timeout.duration, .hours(2057614)) + XCTAssertEqual(timeout.duration, .hours(2_057_614)) } func testRoundingHoursTimeout() throws { From c76b555f58ca903f2d0e064097b5ca2edbb5f45f Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Sun, 26 Nov 2023 16:48:58 +0000 Subject: [PATCH 03/14] Expose timeout as a Duration --- .../Server/Internal/ServerRPCExecutor.swift | 2 +- Sources/GRPCCore/Internal/Metadata+GRPC.swift | 6 +- Sources/GRPCCore/Timeout.swift | 155 ++++++++++------ Tests/GRPCCoreTests/TimeoutTests.swift | 168 ++++++++++-------- 4 files changed, 202 insertions(+), 129 deletions(-) diff --git a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift index aa39bbe58..9bf0e75a5 100644 --- a/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift +++ b/Sources/GRPCCore/Call/Server/Internal/ServerRPCExecutor.swift @@ -76,7 +76,7 @@ struct ServerRPCExecutor { if let timeout = metadata.timeout { group.addTask { let result = await Result { - try await Task.sleep(for: timeout.duration, clock: .continuous) + try await Task.sleep(for: timeout, clock: .continuous) } return .timedOut(result) } diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index a380bedc4..0cfe67558 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -38,13 +38,13 @@ extension Metadata { } @inlinable - var timeout: Timeout? { + var timeout: Duration? { get { - self.firstString(forKey: .timeout).flatMap { Timeout(stringLiteral: $0) } + self.firstString(forKey: .timeout).flatMap { Timeout(stringLiteral: $0)?.duration } } set { if let newValue { - self.replaceOrAddString(String(describing: newValue), forKey: .timeout) + self.replaceOrAddString(String(describing: Timeout(duration: newValue)), forKey: .timeout) } else { self.removeAllValues(forKey: .timeout) } diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index c3a1e9cb0..c42848944 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -18,22 +18,26 @@ import Dispatch /// A timeout for a gRPC call. /// /// Timeouts must be positive and at most 8-digits long. -/// See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *) -public struct Timeout: CustomStringConvertible, Equatable { +@usableFromInline +struct Timeout: CustomStringConvertible, Equatable { /// The largest amount of any unit of time which may be represented by a gRPC timeout. - internal static let maxAmount: Int64 = 99_999_999 + static let maxAmount: Int64 = 99_999_999 /// The wire encoding of this timeout as described in the gRPC protocol. - /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md. - public let wireEncoding: String - public let duration: Duration + /// See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests + let wireEncoding: String + + @usableFromInline + let duration: Duration - public var description: String { + @usableFromInline + var description: String { return self.wireEncoding } - public init?(stringLiteral value: String) { + @usableFromInline + init?(stringLiteral value: String) { guard 2 ... 8 ~= value.count else { return nil } @@ -46,6 +50,92 @@ public struct Timeout: CustomStringConvertible, Equatable { 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. + /// 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. + @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... + let nanoseconds = Int64(round(Double(attoseconds) / 1e+9)) + if Self.exceedsDigitLimit(nanoseconds) { + // If the number of digits exceeds the max (8), try microseconds. + let microseconds = nanoseconds / 1000 + + // The max value for attoseconds, as per `Duration`'s docs is 1e+18. + // This means that, after dividing by 1e+9 and later by 1000 (1e+3), + // the max number of digits will be those in 1e+18 / 1e+12 = 1e+6 = 1.000.000 + // -> 7 digits. We don't need to check anymore and can represent this + // value as microseconds. + self.init(amount: microseconds, unit: .microseconds) + } else { + self.init(amount: 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. + + // Try with minutes first... + let minutes = seconds / 60 + if Self.exceedsDigitLimit(minutes) { + // We don't have enough digits to represent the amount using minutes. + // Try hours... + let hours = minutes / 60 + if Self.exceedsDigitLimit(hours) { + // We don't have enough digits to represent the amount using hours. + // Then initialize the timeout to the maximum possible value. + self.init(amount: Timeout.maxAmount, unit: .hours) + } else { + self.init(amount: hours, unit: .hours) + } + } else { + self.init(amount: minutes, unit: .minutes) + } + } 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 + if Self.exceedsDigitLimit(totalMicroseconds) { + let totalMilliseconds = totalMicroseconds / 1000 + if Self.exceedsDigitLimit(totalMilliseconds) { + let totalSeconds = totalMicroseconds / 1000 + self.init(amount: totalSeconds, unit: .seconds) + } else { + self.init(amount: totalMilliseconds, unit: .milliseconds) + } + } else { + self.init(amount: totalMicroseconds, unit: .microseconds) + } + } + } + } + } + + private static func exceedsDigitLimit(_ value: Int64) -> Bool { + (value == 0 ? 1 : floor(log10(Double(value))) + 1) > 8 + } /// Creates a `GRPCTimeout`. /// @@ -57,53 +147,6 @@ public struct Timeout: CustomStringConvertible, Equatable { self.duration = Duration(amount: amount, unit: unit) self.wireEncoding = "\(amount)\(unit.rawValue)" } - - /// Create a timeout by rounding up the timeout so that it may be represented in the gRPC - /// wire format. - internal init(rounding amount: Int64, unit: TimeoutUnit) { - 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) - } -} - -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, *) @@ -142,6 +185,8 @@ extension Duration { self = Self.nanoseconds(amount) } } + + } internal enum TimeoutUnit: Character { diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index 1eb511e74..b52de821c 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -90,75 +90,103 @@ final class TimeoutTests: XCTestCase { XCTAssertEqual(timeout!.duration, Duration.nanoseconds(123)) XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) } - - func testRoundingNegativeTimeout() { - let timeout = Timeout(rounding: -10, unit: .seconds) - XCTAssertEqual(String(describing: timeout), "0S") - XCTAssertEqual(timeout.duration, .seconds(0)) - } - - func testRoundingNanosecondsTimeout() throws { - let timeout = Timeout(rounding: 123_456_789, unit: .nanoseconds) - XCTAssertEqual(timeout, Timeout(amount: 123_457, unit: .microseconds)) - - // 123_456_789 (nanoseconds) / 1_000 - // = 123_456.789 - // = 123_457 (microseconds, rounded up) - XCTAssertEqual(String(describing: timeout), "123457u") - XCTAssertEqual(timeout.duration, .microseconds(123_457)) - } - - func testRoundingMicrosecondsTimeout() throws { - let timeout = Timeout(rounding: 123_456_789, unit: .microseconds) - XCTAssertEqual(timeout, Timeout(amount: 123_457, unit: .milliseconds)) - - // 123_456_789 (microseconds) / 1_000 - // = 123_456.789 - // = 123_457 (milliseconds, rounded up) - XCTAssertEqual(String(describing: timeout), "123457m") - XCTAssertEqual(timeout.duration, .milliseconds(123_457)) - } - - func testRoundingMillisecondsTimeout() throws { - let timeout = Timeout(rounding: 123_456_789, unit: .milliseconds) - XCTAssertEqual(timeout, Timeout(amount: 123_457, unit: .seconds)) - - // 123_456_789 (milliseconds) / 1_000 - // = 123_456.789 - // = 123_457 (seconds, rounded up) - XCTAssertEqual(String(describing: timeout), "123457S") - XCTAssertEqual(timeout.duration, .seconds(123_457)) - } - - func testRoundingSecondsTimeout() throws { - let timeout = Timeout(rounding: 123_456_789, unit: .seconds) - XCTAssertEqual(timeout, Timeout(amount: 2_057_614, unit: .minutes)) - - // 123_456_789 (seconds) / 60 - // = 2_057_613.15 - // = 2_057_614 (minutes, rounded up) - XCTAssertEqual(String(describing: timeout), "2057614M") - XCTAssertEqual(timeout.duration, .minutes(2_057_614)) - } - - func testRoundingMinutesTimeout() throws { - let timeout = Timeout(rounding: 123_456_789, unit: .minutes) - XCTAssertEqual(timeout, Timeout(amount: 2_057_614, unit: .hours)) - - // 123_456_789 (minutes) / 60 - // = 2_057_613.15 - // = 2_057_614 (hours, rounded up) - XCTAssertEqual(String(describing: timeout), "2057614H") - XCTAssertEqual(timeout.duration, .hours(2_057_614)) - } - - func testRoundingHoursTimeout() throws { - let timeout = Timeout(rounding: 123_456_789, unit: .hours) - XCTAssertEqual(timeout, Timeout(amount: 99_999_999, unit: .hours)) - - // Hours are the largest unit of time we have (as per the gRPC spec) so we can't round to a - // different unit. In this case we clamp to the largest value. - XCTAssertEqual(String(describing: timeout), "99999999H") - XCTAssertEqual(timeout.duration, .hours(Timeout.maxAmount)) + + func testEncodeValidTimeout_Hours() { + let duration = Duration.hours(123) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Minutes() { + let duration = Duration.minutes(43) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Seconds() { + let duration = Duration.seconds(12345) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Seconds_TooLong_Minutes() { + let duration = Duration.seconds(111_111_111) + let timeout = Timeout(duration: duration) + // The conversion from seconds to minutes results in a loss of precision. + // 111,111,111 seconds / 60 = 1,851,851.85 minutes -rounding-> 1,851,851 minutes * 60 = 111,111,060 seconds + let expectedRoundedDuration = Duration.minutes(1_851_851) + XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, expectedRoundedDuration.components.attoseconds) + } + + func testEncodeValidTimeout_Seconds_TooLong_Hours() { + let duration = Duration.seconds(9_999_999_999) + let timeout = Timeout(duration: duration) + // The conversion from seconds to hours results in a loss of precision. + // 9,999,999,999 seconds / 60 = 166,666,666.65 minutes -rounding-> + // 166,666,666 minutes / 60 = 2,777,777.77 hours -rounding-> + // 2,777,777 hours * 60 -> 166,666,620 minutes * 60 = 9,999,997,200 seconds + let expectedRoundedDuration = Duration.hours(2_777_777) + XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, expectedRoundedDuration.components.attoseconds) + } + + func testEncodeValidTimeout_Seconds_TooLong_MaxAmount() { + let duration = Duration.seconds(999_999_999_999) + let timeout = Timeout(duration: duration) + // The conversion from seconds to hours results in a number that still has + // more than the maximum allowed 8 digits, so we must clamp it. + // Make sure that `Timeout.maxAmount` is the amount used for the resulting timeout. + let expectedRoundedDuration = Duration.hours(Timeout.maxAmount) + XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, expectedRoundedDuration.components.attoseconds) + } + + func testEncodeValidTimeout_SecondsAndMilliseconds() { + let duration = Duration(secondsComponent: 100, attosecondsComponent: Int64(1e+17)) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_SecondsAndMicroseconds() { + let duration = Duration(secondsComponent: 1, attosecondsComponent: Int64(1e+14)) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_SecondsAndNanoseconds() { + let duration = Duration(secondsComponent: 1, attosecondsComponent: Int64(1e+11)) + let timeout = Timeout(duration: duration) + // We can't convert seconds to nanoseconds because that would require at least + // 9 digits, and the maximum allowed is 8: we expect to simply drop the nanoseconds. + let expectedRoundedDuration = Duration.seconds(1) + XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, expectedRoundedDuration.components.attoseconds) + } + + func testEncodeValidTimeout_Milliseconds() { + let duration = Duration.milliseconds(100) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Microseconds() { + let duration = Duration.microseconds(100) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) + } + + func testEncodeValidTimeout_Nanoseconds() { + let duration = Duration.nanoseconds(100) + let timeout = Timeout(duration: duration) + XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) + XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) } } From def006dee08ac79479988f177936b14940329133 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Sun, 26 Nov 2023 16:57:28 +0000 Subject: [PATCH 04/14] Formatting --- Sources/GRPCCore/Timeout.swift | 44 ++++++++++++++------------ Tests/GRPCCoreTests/TimeoutTests.swift | 44 ++++++++++++++++---------- 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index c42848944..d54eb21d0 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -17,17 +17,30 @@ 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, Equatable { +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 /// 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 let wireEncoding: String - + @usableFromInline let duration: Duration @@ -43,14 +56,14 @@ struct Timeout: CustomStringConvertible, Equatable { } if let amount = Int64(value.dropLast()), - let unit = TimeoutUnit(rawValue: value.last!) + 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, @@ -63,7 +76,7 @@ struct Timeout: CustomStringConvertible, Equatable { @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... @@ -107,11 +120,11 @@ struct Timeout: CustomStringConvertible, Equatable { } } else { // We can't convert seconds to nanoseconds because that would take us - // over the 8 digit limit (1 second = 1e+9 nanoseconds). + // 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 { @@ -132,7 +145,7 @@ struct Timeout: CustomStringConvertible, Equatable { } } } - + private static func exceedsDigitLimit(_ value: Int64) -> Bool { (value == 0 ? 1 : floor(log10(Double(value))) + 1) > 8 } @@ -141,7 +154,7 @@ struct Timeout: CustomStringConvertible, Equatable { /// /// - Precondition: The amount should be greater than or equal to zero and less than or equal /// to `GRPCTimeout.maxAmount`. - internal init(amount: Int64, unit: TimeoutUnit) { + internal init(amount: Int64, unit: Unit) { precondition(0 ... Timeout.maxAmount ~= amount) self.duration = Duration(amount: amount, unit: unit) @@ -169,7 +182,7 @@ extension Duration { return Self.init(secondsComponent: 60 * 60 * hours, attosecondsComponent: 0) } - internal init(amount: Int64, unit: TimeoutUnit) { + internal init(amount: Int64, unit: Timeout.Unit) { switch unit { case .hours: self = Self.hours(amount) @@ -185,15 +198,4 @@ extension Duration { self = Self.nanoseconds(amount) } } - - -} - -internal enum TimeoutUnit: Character { - case hours = "H" - case minutes = "M" - case seconds = "S" - case milliseconds = "m" - case microseconds = "u" - case nanoseconds = "n" } diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index b52de821c..c72e05d66 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -90,28 +90,28 @@ final class TimeoutTests: XCTestCase { XCTAssertEqual(timeout!.duration, Duration.nanoseconds(123)) XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) } - + func testEncodeValidTimeout_Hours() { let duration = Duration.hours(123) let timeout = Timeout(duration: duration) XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) } - + func testEncodeValidTimeout_Minutes() { let duration = Duration.minutes(43) let timeout = Timeout(duration: duration) XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) } - + func testEncodeValidTimeout_Seconds() { let duration = Duration.seconds(12345) let timeout = Timeout(duration: duration) XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) } - + func testEncodeValidTimeout_Seconds_TooLong_Minutes() { let duration = Duration.seconds(111_111_111) let timeout = Timeout(duration: duration) @@ -119,9 +119,12 @@ final class TimeoutTests: XCTestCase { // 111,111,111 seconds / 60 = 1,851,851.85 minutes -rounding-> 1,851,851 minutes * 60 = 111,111,060 seconds let expectedRoundedDuration = Duration.minutes(1_851_851) XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, expectedRoundedDuration.components.attoseconds) + XCTAssertEqual( + timeout.duration.components.attoseconds, + expectedRoundedDuration.components.attoseconds + ) } - + func testEncodeValidTimeout_Seconds_TooLong_Hours() { let duration = Duration.seconds(9_999_999_999) let timeout = Timeout(duration: duration) @@ -131,9 +134,12 @@ final class TimeoutTests: XCTestCase { // 2,777,777 hours * 60 -> 166,666,620 minutes * 60 = 9,999,997,200 seconds let expectedRoundedDuration = Duration.hours(2_777_777) XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, expectedRoundedDuration.components.attoseconds) + XCTAssertEqual( + timeout.duration.components.attoseconds, + expectedRoundedDuration.components.attoseconds + ) } - + func testEncodeValidTimeout_Seconds_TooLong_MaxAmount() { let duration = Duration.seconds(999_999_999_999) let timeout = Timeout(duration: duration) @@ -142,23 +148,26 @@ final class TimeoutTests: XCTestCase { // Make sure that `Timeout.maxAmount` is the amount used for the resulting timeout. let expectedRoundedDuration = Duration.hours(Timeout.maxAmount) XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, expectedRoundedDuration.components.attoseconds) + XCTAssertEqual( + timeout.duration.components.attoseconds, + expectedRoundedDuration.components.attoseconds + ) } - + func testEncodeValidTimeout_SecondsAndMilliseconds() { let duration = Duration(secondsComponent: 100, attosecondsComponent: Int64(1e+17)) let timeout = Timeout(duration: duration) XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) } - + func testEncodeValidTimeout_SecondsAndMicroseconds() { let duration = Duration(secondsComponent: 1, attosecondsComponent: Int64(1e+14)) let timeout = Timeout(duration: duration) XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) } - + func testEncodeValidTimeout_SecondsAndNanoseconds() { let duration = Duration(secondsComponent: 1, attosecondsComponent: Int64(1e+11)) let timeout = Timeout(duration: duration) @@ -166,23 +175,26 @@ final class TimeoutTests: XCTestCase { // 9 digits, and the maximum allowed is 8: we expect to simply drop the nanoseconds. let expectedRoundedDuration = Duration.seconds(1) XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) - XCTAssertEqual(timeout.duration.components.attoseconds, expectedRoundedDuration.components.attoseconds) + XCTAssertEqual( + timeout.duration.components.attoseconds, + expectedRoundedDuration.components.attoseconds + ) } - + func testEncodeValidTimeout_Milliseconds() { let duration = Duration.milliseconds(100) let timeout = Timeout(duration: duration) XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) } - + func testEncodeValidTimeout_Microseconds() { let duration = Duration.microseconds(100) let timeout = Timeout(duration: duration) XCTAssertEqual(timeout.duration.components.seconds, duration.components.seconds) XCTAssertEqual(timeout.duration.components.attoseconds, duration.components.attoseconds) } - + func testEncodeValidTimeout_Nanoseconds() { let duration = Duration.nanoseconds(100) let timeout = Timeout(duration: duration) From 0b8af2005317d853b3b52cff1cacef45555b92db Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Sun, 26 Nov 2023 17:05:22 +0000 Subject: [PATCH 05/14] Add missing import --- Sources/GRPCCore/Timeout.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index d54eb21d0..9f21c6fba 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -15,6 +15,12 @@ */ import Dispatch +#if canImport(Darwin) +import Darwin +#elseif canImport(Glibc) +import Glibc +#endif + /// 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 From b58ceb2da326a3a073264d52c22120bb2590bd04 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Sun, 26 Nov 2023 17:27:06 +0000 Subject: [PATCH 06/14] Formatting --- Tests/GRPCCoreTests/TimeoutTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index c72e05d66..7f98d481e 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -1,5 +1,3 @@ -import XCTest - /* * Copyright 2023, gRPC Authors All rights reserved. * @@ -15,6 +13,8 @@ import XCTest * See the License for the specific language governing permissions and * limitations under the License. */ +import XCTest + @testable import GRPCCore final class TimeoutTests: XCTestCase { From 70f4be02dea3135e88588a5f9b5951afb911479e Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 27 Nov 2023 10:50:12 +0000 Subject: [PATCH 07/14] Change ~= for contains --- Sources/GRPCCore/Timeout.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index 9f21c6fba..a34c1c7b5 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -57,7 +57,7 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { @usableFromInline init?(stringLiteral value: String) { - guard 2 ... 8 ~= value.count else { + guard (2 ... 8).contains(value.count) else { return nil } @@ -161,7 +161,7 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { /// - 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 ~= amount) + precondition((0 ... Timeout.maxAmount).contains(amount)) self.duration = Duration(amount: amount, unit: unit) self.wireEncoding = "\(amount)\(unit.rawValue)" From dfbec998f1b5287ae8eb1b3bb432b84c668a2811 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 27 Nov 2023 10:54:48 +0000 Subject: [PATCH 08/14] Replace force unwraps with XCTUnwrap in tests --- Tests/GRPCCoreTests/TimeoutTests.swift | 48 +++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index 7f98d481e..a2f958e0b 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -43,52 +43,52 @@ final class TimeoutTests: XCTestCase { XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) } - func testDecodeValidTimeout_Hours() { + func testDecodeValidTimeout_Hours() throws { let timeoutHeader = "123H" let timeout = Timeout(stringLiteral: timeoutHeader) - XCTAssertNotNil(timeout) - XCTAssertEqual(timeout!.duration, Duration.hours(123)) - XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.hours(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) } - func testDecodeValidTimeout_Minutes() { + func testDecodeValidTimeout_Minutes() throws { let timeoutHeader = "123M" let timeout = Timeout(stringLiteral: timeoutHeader) - XCTAssertNotNil(timeout) - XCTAssertEqual(timeout!.duration, Duration.minutes(123)) - XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.minutes(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) } - func testDecodeValidTimeout_Seconds() { + func testDecodeValidTimeout_Seconds() throws { let timeoutHeader = "123S" let timeout = Timeout(stringLiteral: timeoutHeader) - XCTAssertNotNil(timeout) - XCTAssertEqual(timeout!.duration, Duration.seconds(123)) - XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.seconds(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) } - func testDecodeValidTimeout_Milliseconds() { + func testDecodeValidTimeout_Milliseconds() throws { let timeoutHeader = "123m" let timeout = Timeout(stringLiteral: timeoutHeader) - XCTAssertNotNil(timeout) - XCTAssertEqual(timeout!.duration, Duration.milliseconds(123)) - XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.milliseconds(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) } - func testDecodeValidTimeout_Microseconds() { + func testDecodeValidTimeout_Microseconds() throws { let timeoutHeader = "123u" let timeout = Timeout(stringLiteral: timeoutHeader) - XCTAssertNotNil(timeout) - XCTAssertEqual(timeout!.duration, Duration.microseconds(123)) - XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.microseconds(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) } - func testDecodeValidTimeout_Nanoseconds() { + func testDecodeValidTimeout_Nanoseconds() throws { let timeoutHeader = "123n" let timeout = Timeout(stringLiteral: timeoutHeader) - XCTAssertNotNil(timeout) - XCTAssertEqual(timeout!.duration, Duration.nanoseconds(123)) - XCTAssertEqual(timeout!.wireEncoding, timeoutHeader) + let unwrappedTimeout = try XCTUnwrap(timeout) + XCTAssertEqual(unwrappedTimeout.duration, Duration.nanoseconds(123)) + XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) } func testEncodeValidTimeout_Hours() { From 7287bc6fa07ccc50934c6db7c80d3f4820292496 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 27 Nov 2023 10:56:19 +0000 Subject: [PATCH 09/14] Rename init parameter --- Sources/GRPCCore/Internal/Metadata+GRPC.swift | 2 +- Sources/GRPCCore/Timeout.swift | 2 +- Tests/GRPCCoreTests/TimeoutTests.swift | 22 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/GRPCCore/Internal/Metadata+GRPC.swift b/Sources/GRPCCore/Internal/Metadata+GRPC.swift index 0cfe67558..9bff423e3 100644 --- a/Sources/GRPCCore/Internal/Metadata+GRPC.swift +++ b/Sources/GRPCCore/Internal/Metadata+GRPC.swift @@ -40,7 +40,7 @@ extension Metadata { @inlinable var timeout: Duration? { get { - self.firstString(forKey: .timeout).flatMap { Timeout(stringLiteral: $0)?.duration } + self.firstString(forKey: .timeout).flatMap { Timeout(decoding: $0)?.duration } } set { if let newValue { diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index a34c1c7b5..1f65aeced 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -56,7 +56,7 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { } @usableFromInline - init?(stringLiteral value: String) { + init?(decoding value: String) { guard (2 ... 8).contains(value.count) else { return nil } diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index a2f958e0b..b83f5afa1 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -20,32 +20,32 @@ import XCTest final class TimeoutTests: XCTestCase { func testDecodeInvalidTimeout_Empty() { let timeoutHeader = "" - XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + XCTAssertNil(Timeout(decoding: timeoutHeader)) } func testDecodeInvalidTimeout_NoAmount() { let timeoutHeader = "H" - XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + XCTAssertNil(Timeout(decoding: timeoutHeader)) } func testDecodeInvalidTimeout_NoUnit() { let timeoutHeader = "123" - XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + XCTAssertNil(Timeout(decoding: timeoutHeader)) } func testDecodeInvalidTimeout_TooLongAmount() { let timeoutHeader = "100000000S" - XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + XCTAssertNil(Timeout(decoding: timeoutHeader)) } func testDecodeInvalidTimeout_InvalidUnit() { let timeoutHeader = "123j" - XCTAssertNil(Timeout(stringLiteral: timeoutHeader)) + XCTAssertNil(Timeout(decoding: timeoutHeader)) } func testDecodeValidTimeout_Hours() throws { let timeoutHeader = "123H" - let timeout = Timeout(stringLiteral: timeoutHeader) + let timeout = Timeout(decoding: timeoutHeader) let unwrappedTimeout = try XCTUnwrap(timeout) XCTAssertEqual(unwrappedTimeout.duration, Duration.hours(123)) XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) @@ -53,7 +53,7 @@ final class TimeoutTests: XCTestCase { func testDecodeValidTimeout_Minutes() throws { let timeoutHeader = "123M" - let timeout = Timeout(stringLiteral: timeoutHeader) + let timeout = Timeout(decoding: timeoutHeader) let unwrappedTimeout = try XCTUnwrap(timeout) XCTAssertEqual(unwrappedTimeout.duration, Duration.minutes(123)) XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) @@ -61,7 +61,7 @@ final class TimeoutTests: XCTestCase { func testDecodeValidTimeout_Seconds() throws { let timeoutHeader = "123S" - let timeout = Timeout(stringLiteral: timeoutHeader) + let timeout = Timeout(decoding: timeoutHeader) let unwrappedTimeout = try XCTUnwrap(timeout) XCTAssertEqual(unwrappedTimeout.duration, Duration.seconds(123)) XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) @@ -69,7 +69,7 @@ final class TimeoutTests: XCTestCase { func testDecodeValidTimeout_Milliseconds() throws { let timeoutHeader = "123m" - let timeout = Timeout(stringLiteral: timeoutHeader) + let timeout = Timeout(decoding: timeoutHeader) let unwrappedTimeout = try XCTUnwrap(timeout) XCTAssertEqual(unwrappedTimeout.duration, Duration.milliseconds(123)) XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) @@ -77,7 +77,7 @@ final class TimeoutTests: XCTestCase { func testDecodeValidTimeout_Microseconds() throws { let timeoutHeader = "123u" - let timeout = Timeout(stringLiteral: timeoutHeader) + let timeout = Timeout(decoding: timeoutHeader) let unwrappedTimeout = try XCTUnwrap(timeout) XCTAssertEqual(unwrappedTimeout.duration, Duration.microseconds(123)) XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) @@ -85,7 +85,7 @@ final class TimeoutTests: XCTestCase { func testDecodeValidTimeout_Nanoseconds() throws { let timeoutHeader = "123n" - let timeout = Timeout(stringLiteral: timeoutHeader) + let timeout = Timeout(decoding: timeoutHeader) let unwrappedTimeout = try XCTUnwrap(timeout) XCTAssertEqual(unwrappedTimeout.duration, Duration.nanoseconds(123)) XCTAssertEqual(unwrappedTimeout.wireEncoding, timeoutHeader) From a107eaa4f611418719b6d3d5a22c8d0cf8498015 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 27 Nov 2023 11:06:56 +0000 Subject: [PATCH 10/14] Change duration and wireFormat into computed properties --- Sources/GRPCCore/Timeout.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index 1f65aeced..ecc65f030 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -42,13 +42,20 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { /// 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 - let wireEncoding: String - - @usableFromInline - let duration: Duration + var wireEncoding: String { + "\(amount)\(unit.rawValue)" + } @usableFromInline var description: String { @@ -163,8 +170,8 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { internal init(amount: Int64, unit: Unit) { precondition((0 ... Timeout.maxAmount).contains(amount)) - self.duration = Duration(amount: amount, unit: unit) - self.wireEncoding = "\(amount)\(unit.rawValue)" + self.amount = amount + self.unit = unit } } From 3a9615136580a25a38fe58807a5047d1f5792895 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 27 Nov 2023 11:34:27 +0000 Subject: [PATCH 11/14] Correct docs --- Sources/GRPCCore/Timeout.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index ecc65f030..7af186854 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -83,9 +83,14 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { /// 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. + /// 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 From f73bd21ae1f0cdbbc64858c12ac59939180e770f Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 27 Nov 2023 14:26:04 +0000 Subject: [PATCH 12/14] Refactor rounding logic --- Sources/GRPCCore/Timeout.swift | 127 +++++++++++++------------ Tests/GRPCCoreTests/TimeoutTests.swift | 12 +-- 2 files changed, 73 insertions(+), 66 deletions(-) diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index 7af186854..afc11163e 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -94,78 +94,74 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { @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... + // Try converting to nanoseconds first, and continue rounding up if the + // max amount of digits is exceeded. let nanoseconds = Int64(round(Double(attoseconds) / 1e+9)) - if Self.exceedsDigitLimit(nanoseconds) { - // If the number of digits exceeds the max (8), try microseconds. - let microseconds = nanoseconds / 1000 - - // The max value for attoseconds, as per `Duration`'s docs is 1e+18. - // This means that, after dividing by 1e+9 and later by 1000 (1e+3), - // the max number of digits will be those in 1e+18 / 1e+12 = 1e+6 = 1.000.000 - // -> 7 digits. We don't need to check anymore and can represent this - // value as microseconds. - self.init(amount: microseconds, unit: .microseconds) + 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 { - self.init(amount: nanoseconds, unit: .nanoseconds) + 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 { - 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. - - // Try with minutes first... - let minutes = seconds / 60 - if Self.exceedsDigitLimit(minutes) { - // We don't have enough digits to represent the amount using minutes. - // Try hours... - let hours = minutes / 60 - if Self.exceedsDigitLimit(hours) { - // We don't have enough digits to represent the amount using hours. - // Then initialize the timeout to the maximum possible value. - self.init(amount: Timeout.maxAmount, unit: .hours) - } else { - self.init(amount: hours, unit: .hours) - } - } else { - self.init(amount: minutes, unit: .minutes) - } - } 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 - if Self.exceedsDigitLimit(totalMicroseconds) { - let totalMilliseconds = totalMicroseconds / 1000 - if Self.exceedsDigitLimit(totalMilliseconds) { - let totalSeconds = totalMicroseconds / 1000 - self.init(amount: totalSeconds, unit: .seconds) - } else { - self.init(amount: totalMilliseconds, unit: .milliseconds) - } - } else { - self.init(amount: totalMicroseconds, unit: .microseconds) - } + 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 == 0 ? 1 : floor(log10(Double(value))) + 1) > 8 + value > Timeout.maxAmount } /// Creates a `GRPCTimeout`. @@ -180,6 +176,17 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { } } +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`. diff --git a/Tests/GRPCCoreTests/TimeoutTests.swift b/Tests/GRPCCoreTests/TimeoutTests.swift index b83f5afa1..ddb664bf2 100644 --- a/Tests/GRPCCoreTests/TimeoutTests.swift +++ b/Tests/GRPCCoreTests/TimeoutTests.swift @@ -116,8 +116,8 @@ final class TimeoutTests: XCTestCase { let duration = Duration.seconds(111_111_111) let timeout = Timeout(duration: duration) // The conversion from seconds to minutes results in a loss of precision. - // 111,111,111 seconds / 60 = 1,851,851.85 minutes -rounding-> 1,851,851 minutes * 60 = 111,111,060 seconds - let expectedRoundedDuration = Duration.minutes(1_851_851) + // 111,111,111 seconds / 60 = 1,851,851.85 minutes -rounding up-> 1,851,852 minutes * 60 = 111,111,120 seconds + let expectedRoundedDuration = Duration.minutes(1_851_852) XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) XCTAssertEqual( timeout.duration.components.attoseconds, @@ -129,10 +129,10 @@ final class TimeoutTests: XCTestCase { let duration = Duration.seconds(9_999_999_999) let timeout = Timeout(duration: duration) // The conversion from seconds to hours results in a loss of precision. - // 9,999,999,999 seconds / 60 = 166,666,666.65 minutes -rounding-> - // 166,666,666 minutes / 60 = 2,777,777.77 hours -rounding-> - // 2,777,777 hours * 60 -> 166,666,620 minutes * 60 = 9,999,997,200 seconds - let expectedRoundedDuration = Duration.hours(2_777_777) + // 9,999,999,999 seconds / 60 = 166,666,666.65 minutes -rounding up-> + // 166,666,667 minutes / 60 = 2,777,777.78 hours -rounding up-> + // 2,777,778 hours * 60 -> 166,666,680 minutes * 60 = 10,000,000,800 seconds + let expectedRoundedDuration = Duration.hours(2_777_778) XCTAssertEqual(timeout.duration.components.seconds, expectedRoundedDuration.components.seconds) XCTAssertEqual( timeout.duration.components.attoseconds, From 4dd40f91263e0e3744480010efcf0febe752bbb1 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 27 Nov 2023 14:28:35 +0000 Subject: [PATCH 13/14] Formatting --- Sources/GRPCCore/Timeout.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index afc11163e..9dcacce16 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -42,10 +42,10 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { /// 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) @@ -64,7 +64,7 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { @usableFromInline init?(decoding value: String) { - guard (2 ... 8).contains(value.count) else { + guard (2 ... 8).contains(value.count) else { return nil } @@ -94,7 +94,7 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { @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 @@ -123,7 +123,7 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { } } } - + /// 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) { From d0fc215565e7961d5bde7764c9695882d94f7020 Mon Sep 17 00:00:00 2001 From: Gus Cairo Date: Mon, 27 Nov 2023 14:49:04 +0000 Subject: [PATCH 14/14] Remove rounding of nanoseconds --- Sources/GRPCCore/Timeout.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Sources/GRPCCore/Timeout.swift b/Sources/GRPCCore/Timeout.swift index 9dcacce16..b350fdaea 100644 --- a/Sources/GRPCCore/Timeout.swift +++ b/Sources/GRPCCore/Timeout.swift @@ -15,12 +15,6 @@ */ import Dispatch -#if canImport(Darwin) -import Darwin -#elseif canImport(Glibc) -import Glibc -#endif - /// 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 @@ -99,7 +93,7 @@ struct Timeout: CustomStringConvertible, Hashable, Sendable { // 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(round(Double(attoseconds) / 1e+9)) + 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