Coverage Report

Created: 2026-04-29 07:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/grpc-swift/Sources/GRPC/ConnectionBackoff.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 Foundation
17
18
/// Provides backoff timeouts for making a connection.
19
///
20
/// This algorithm and defaults are determined by the gRPC connection backoff
21
/// [documentation](https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md).
22
public struct ConnectionBackoff: Sequence, Sendable {
23
  public typealias Iterator = ConnectionBackoffIterator
24
25
  /// The initial backoff in seconds.
26
  public var initialBackoff: TimeInterval
27
28
  /// The maximum backoff in seconds. Note that the backoff is _before_ jitter has been applied,
29
  /// this means that in practice the maximum backoff can be larger than this value.
30
  public var maximumBackoff: TimeInterval
31
32
  /// The backoff multiplier.
33
  public var multiplier: Double
34
35
  /// Backoff jitter; should be between 0 and 1.
36
  public var jitter: Double
37
38
  /// The minimum amount of time in seconds to try connecting.
39
  public var minimumConnectionTimeout: TimeInterval
40
41
  /// A limit on the number of times to attempt reconnection.
42
  public var retries: Retries
43
44
  public struct Retries: Hashable, Sendable {
45
    fileprivate enum Limit: Hashable, Sendable {
46
      case limited(Int)
47
      case unlimited
48
    }
49
50
    fileprivate var limit: Limit
51
0
    private init(_ limit: Limit) {
52
0
      self.limit = limit
53
0
    }
54
55
    /// An unlimited number of retry attempts.
56
    public static let unlimited = Retries(.unlimited)
57
58
    /// No retry attempts will be made.
59
    public static let none = Retries(.limited(0))
60
61
    /// A limited number of retry attempts. `limit` must be positive. Note that a limit of zero is
62
    /// identical to `.none`.
63
0
    public static func upTo(_ limit: Int) -> Retries {
64
0
      precondition(limit >= 0)
65
0
      return Retries(.limited(limit))
66
0
    }
67
  }
68
69
  /// Creates a ``ConnectionBackoff``.
70
  ///
71
  /// - Parameters:
72
  ///   - initialBackoff: Initial backoff in seconds, defaults to 1.0.
73
  ///   - maximumBackoff: Maximum backoff in seconds (prior to adding jitter), defaults to 120.0.
74
  ///   - multiplier: Backoff multiplier, defaults to 1.6.
75
  ///   - jitter: Backoff jitter, defaults to 0.2.
76
  ///   - minimumConnectionTimeout: Minimum connection timeout in seconds, defaults to 20.0.
77
  ///   - retries: A limit on the number of times to retry establishing a connection.
78
  ///       Defaults to `.unlimited`.
79
  public init(
80
    initialBackoff: TimeInterval = 1.0,
81
    maximumBackoff: TimeInterval = 120.0,
82
    multiplier: Double = 1.6,
83
    jitter: Double = 0.2,
84
    minimumConnectionTimeout: TimeInterval = 20.0,
85
    retries: Retries = .unlimited
86
0
  ) {
87
0
    self.initialBackoff = initialBackoff
88
0
    self.maximumBackoff = maximumBackoff
89
0
    self.multiplier = multiplier
90
0
    self.jitter = jitter
91
0
    self.minimumConnectionTimeout = minimumConnectionTimeout
92
0
    self.retries = retries
93
0
  }
94
95
0
  public func makeIterator() -> ConnectionBackoff.Iterator {
96
0
    return Iterator(connectionBackoff: self)
97
0
  }
98
}
99
100
/// An iterator for ``ConnectionBackoff``.
101
public class ConnectionBackoffIterator: IteratorProtocol {
102
  public typealias Element = (timeout: TimeInterval, backoff: TimeInterval)
103
104
  /// Creates a new connection backoff iterator with the given configuration.
105
0
  public init(connectionBackoff: ConnectionBackoff) {
106
0
    self.connectionBackoff = connectionBackoff
107
0
    self.unjitteredBackoff = connectionBackoff.initialBackoff
108
0
109
0
    // Since the first backoff is `initialBackoff` it must be generated here instead of
110
0
    // by `makeNextElement`.
111
0
    let backoff = min(connectionBackoff.initialBackoff, connectionBackoff.maximumBackoff)
112
0
    self.initialElement = self.makeElement(backoff: backoff)
113
0
  }
114
115
  /// The configuration being used.
116
  private var connectionBackoff: ConnectionBackoff
117
118
  /// The backoff in seconds, without jitter.
119
  private var unjitteredBackoff: TimeInterval
120
121
  /// The first element to return. Since the first backoff is defined as `initialBackoff` we can't
122
  /// compute it on-the-fly.
123
  private var initialElement: Element?
124
125
  /// Returns the next pair of connection timeout and backoff (in that order) to use should the
126
  /// connection attempt fail.
127
0
  public func next() -> Element? {
128
0
    // Should we make another element?
129
0
    switch self.connectionBackoff.retries.limit {
130
0
    // Always make a new element.
131
0
    case .unlimited:
132
0
      ()
133
0
134
0
    // Use up one from our remaining limit.
135
0
    case let .limited(limit) where limit > 0:
136
0
      self.connectionBackoff.retries.limit = .limited(limit - 1)
137
0
138
0
    // limit must be <= 0, no new element.
139
0
    case .limited:
140
0
      return nil
141
0
    }
142
0
143
0
    if let initial = self.initialElement {
144
0
      self.initialElement = nil
145
0
      return initial
146
0
    } else {
147
0
      return self.makeNextElement()
148
0
    }
149
0
  }
150
151
  /// Produces the next element to return.
152
0
  private func makeNextElement() -> Element {
153
0
    let unjittered = self.unjitteredBackoff * self.connectionBackoff.multiplier
154
0
    self.unjitteredBackoff = min(unjittered, self.connectionBackoff.maximumBackoff)
155
0
156
0
    let backoff = self.jittered(value: self.unjitteredBackoff)
157
0
    return self.makeElement(backoff: backoff)
158
0
  }
159
160
  /// Make a timeout-backoff pair from the given backoff. The timeout is the `max` of the backoff
161
  /// and `connectionBackoff.minimumConnectionTimeout`.
162
0
  private func makeElement(backoff: TimeInterval) -> Element {
163
0
    let timeout = max(backoff, self.connectionBackoff.minimumConnectionTimeout)
164
0
    return (timeout, backoff)
165
0
  }
166
167
  /// Adds 'jitter' to the given value.
168
0
  private func jittered(value: TimeInterval) -> TimeInterval {
169
0
    let lower = -self.connectionBackoff.jitter * value
170
0
    let upper = self.connectionBackoff.jitter * value
171
0
    return value + TimeInterval.random(in: lower ... upper)
172
0
  }
173
}