Coverage Report

Created: 2026-03-11 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/grpc-swift/Sources/GRPC/GRPCTimeout.swift
Line
Count
Source
1
/*
2
 * Copyright 2019, gRPC Authors All rights reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
import Dispatch
17
import NIOCore
18
19
/// A timeout for a gRPC call.
20
///
21
/// Timeouts must be positive and at most 8-digits long.
22
public struct GRPCTimeout: CustomStringConvertible, Equatable {
23
  /// Creates an infinite timeout. This is a sentinel value which must __not__ be sent to a gRPC service.
24
  public static let infinite = GRPCTimeout(
25
    nanoseconds: Int64.max,
26
    wireEncoding: "infinite"
27
  )
28
29
  /// The largest amount of any unit of time which may be represented by a gRPC timeout.
30
  internal static let maxAmount: Int64 = 99_999_999
31
32
  /// The wire encoding of this timeout as described in the gRPC protocol.
33
  /// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md.
34
  public let wireEncoding: String
35
  public let nanoseconds: Int64
36
37
0
  public var description: String {
38
0
    return self.wireEncoding
39
0
  }
40
41
  /// Creates a timeout from the given deadline.
42
  ///
43
  /// - Parameter deadline: The deadline to create a timeout from.
44
0
  internal init(deadline: NIODeadline, testingOnlyNow: NIODeadline? = nil) {
45
0
    switch deadline {
46
0
    case .distantFuture:
47
0
      self = .infinite
48
0
    default:
49
0
      let timeAmountUntilDeadline = deadline - (testingOnlyNow ?? .now())
50
0
      self.init(rounding: timeAmountUntilDeadline.nanoseconds, unit: .nanoseconds)
51
0
    }
52
0
  }
53
54
0
  private init(nanoseconds: Int64, wireEncoding: String) {
55
0
    self.nanoseconds = nanoseconds
56
0
    self.wireEncoding = wireEncoding
57
0
  }
58
59
  /// Creates a `GRPCTimeout`.
60
  ///
61
  /// - Precondition: The amount should be greater than or equal to zero and less than or equal
62
  ///   to `GRPCTimeout.maxAmount`.
63
0
  internal init(amount: Int64, unit: GRPCTimeoutUnit) {
64
0
    precondition(amount >= 0 && amount <= GRPCTimeout.maxAmount)
65
0
    // See "Timeout" in https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests
66
0
67
0
    // If we overflow at this point, which is certainly possible if `amount` is sufficiently large
68
0
    // and `unit` is `.hours`, clamp the nanosecond timeout to `Int64.max`. It's about 292 years so
69
0
    // it should be long enough for the user not to notice the difference should the rpc time out.
70
0
    let (partial, overflow) = amount.multipliedReportingOverflow(by: unit.asNanoseconds)
71
0
72
0
    self.init(
73
0
      nanoseconds: overflow ? Int64.max : partial,
74
0
      wireEncoding: "\(amount)\(unit.rawValue)"
75
0
    )
76
0
  }
77
78
  /// Create a timeout by rounding up the timeout so that it may be represented in the gRPC
79
  /// wire format.
80
0
  internal init(rounding amount: Int64, unit: GRPCTimeoutUnit) {
81
0
    var roundedAmount = amount
82
0
    var roundedUnit = unit
83
0
84
0
    if roundedAmount <= 0 {
85
0
      roundedAmount = 0
86
0
    } else {
87
0
      while roundedAmount > GRPCTimeout.maxAmount {
88
0
        switch roundedUnit {
89
0
        case .nanoseconds:
90
0
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
91
0
          roundedUnit = .microseconds
92
0
        case .microseconds:
93
0
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
94
0
          roundedUnit = .milliseconds
95
0
        case .milliseconds:
96
0
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 1000)
97
0
          roundedUnit = .seconds
98
0
        case .seconds:
99
0
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
100
0
          roundedUnit = .minutes
101
0
        case .minutes:
102
0
          roundedAmount = roundedAmount.quotientRoundedUp(dividingBy: 60)
103
0
          roundedUnit = .hours
104
0
        case .hours:
105
0
          roundedAmount = GRPCTimeout.maxAmount
106
0
          roundedUnit = .hours
107
0
        }
108
0
      }
109
0
    }
110
0
111
0
    self.init(amount: roundedAmount, unit: roundedUnit)
112
0
  }
113
}
114
115
extension Int64 {
116
  /// Returns the quotient of this value when divided by `divisor` rounded up to the nearest
117
  /// multiple of `divisor` if the remainder is non-zero.
118
  ///
119
  /// - Parameter divisor: The value to divide this value by.
120
0
  fileprivate func quotientRoundedUp(dividingBy divisor: Int64) -> Int64 {
121
0
    let (quotient, remainder) = self.quotientAndRemainder(dividingBy: divisor)
122
0
    return quotient + (remainder != 0 ? 1 : 0)
123
0
  }
124
}
125
126
internal enum GRPCTimeoutUnit: String {
127
  case hours = "H"
128
  case minutes = "M"
129
  case seconds = "S"
130
  case milliseconds = "m"
131
  case microseconds = "u"
132
  case nanoseconds = "n"
133
134
0
  internal var asNanoseconds: Int64 {
135
0
    switch self {
136
0
    case .hours:
137
0
      return 60 * 60 * 1000 * 1000 * 1000
138
0
139
0
    case .minutes:
140
0
      return 60 * 1000 * 1000 * 1000
141
0
142
0
    case .seconds:
143
0
      return 1000 * 1000 * 1000
144
0
145
0
    case .milliseconds:
146
0
      return 1000 * 1000
147
0
148
0
    case .microseconds:
149
0
      return 1000
150
0
151
0
    case .nanoseconds:
152
0
      return 1
153
0
    }
154
0
  }
155
}