Skip to content

Commit 77194a4

Browse files
authored
Add a retry limit to ConnectionBackoff (#784)
Motivation: See #783 Modifications: - Add a `retries` option to `ConnectionBackoff` Result: Connection attempts may now be made a limited number of times. By default the behaviour is unchanged. Resolves #783
1 parent 1b18470 commit 77194a4

File tree

5 files changed

+97
-4
lines changed

5 files changed

+97
-4
lines changed

Sources/GRPC/ConnectionBackoff.swift

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ public struct ConnectionBackoff: Sequence {
3838
/// The minimum amount of time in seconds to try connecting.
3939
public var minimumConnectionTimeout: TimeInterval
4040

41+
/// A limit on the number of times to attempt reconnection.
42+
public var retries: Retries
43+
44+
public struct Retries: Hashable {
45+
fileprivate enum Limit: Hashable {
46+
case limited(Int)
47+
case unlimited
48+
}
49+
50+
fileprivate var limit: Limit
51+
private init(_ limit: Limit) {
52+
self.limit = limit
53+
}
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+
public static func upTo(_ limit: Int) -> Retries {
64+
precondition(limit >= 0)
65+
return Retries(.limited(limit))
66+
}
67+
}
68+
4169
/// Creates a `ConnectionBackoff`.
4270
///
4371
/// - Parameters:
@@ -46,18 +74,22 @@ public struct ConnectionBackoff: Sequence {
4674
/// - multiplier: Backoff multiplier, defaults to 1.6.
4775
/// - jitter: Backoff jitter, defaults to 0.2.
4876
/// - 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`.
4979
public init(
5080
initialBackoff: TimeInterval = 1.0,
5181
maximumBackoff: TimeInterval = 120.0,
5282
multiplier: Double = 1.6,
5383
jitter: Double = 0.2,
54-
minimumConnectionTimeout: TimeInterval = 20.0
84+
minimumConnectionTimeout: TimeInterval = 20.0,
85+
retries: Retries = .unlimited
5586
) {
5687
self.initialBackoff = initialBackoff
5788
self.maximumBackoff = maximumBackoff
5889
self.multiplier = multiplier
5990
self.jitter = jitter
6091
self.minimumConnectionTimeout = minimumConnectionTimeout
92+
self.retries = retries
6193
}
6294

6395
public func makeIterator() -> ConnectionBackoff.Iterator {
@@ -81,7 +113,7 @@ public class ConnectionBackoffIterator: IteratorProtocol {
81113
}
82114

83115
/// The configuration being used.
84-
private let connectionBackoff: ConnectionBackoff
116+
private var connectionBackoff: ConnectionBackoff
85117

86118
/// The backoff in seconds, without jitter.
87119
private var unjitteredBackoff: TimeInterval
@@ -93,6 +125,21 @@ public class ConnectionBackoffIterator: IteratorProtocol {
93125
/// Returns the next pair of connection timeout and backoff (in that order) to use should the
94126
/// connection attempt fail.
95127
public func next() -> Element? {
128+
// Should we make another element?
129+
switch self.connectionBackoff.retries.limit {
130+
// Always make a new element.
131+
case .unlimited:
132+
()
133+
134+
// Use up one from our remaining limit.
135+
case .limited(let limit) where limit > 0:
136+
self.connectionBackoff.retries.limit = .limited(limit - 1)
137+
138+
// limit must be <= 0, no new element.
139+
case .limited:
140+
return nil
141+
}
142+
96143
if let initial = self.initialElement {
97144
self.initialElement = nil
98145
return initial

Sources/GRPC/GRPCChannel/GRPCChannelBuilder.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ extension ClientConnection.Builder {
123123
return self
124124
}
125125

126+
/// Sets the limit on the number of times to attempt to re-establish a connection. Defaults
127+
/// to `.unlimited` if not set.
128+
@discardableResult
129+
public func withConnectionBackoff(retries: ConnectionBackoff.Retries) -> Self {
130+
self.connectionBackoff.retries = retries
131+
return self
132+
}
133+
126134
/// Sets whether the connection should be re-established automatically if it is dropped. Defaults
127135
/// to `true` if not set.
128136
@discardableResult

Tests/GRPCTests/ClientConnectionBackoffTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,20 @@ class ClientConnectionBackoffTests: GRPCTestCase {
139139
XCTAssertEqual(self.stateDelegate.states, [.connecting, .shutdown])
140140
}
141141

142+
func testClientConnectionFailureIsLimited() throws {
143+
let connectionShutdown = self.expectation(description: "client shutdown")
144+
let failures = self.expectation(description: "connection failed")
145+
self.stateDelegate.expectations[.shutdown] = connectionShutdown
146+
self.stateDelegate.expectations[.transientFailure] = failures
147+
148+
self.client = self.connectionBuilder()
149+
.withConnectionBackoff(retries: .upTo(1))
150+
.connect(host: "localhost", port: self.port)
151+
152+
self.wait(for: [connectionShutdown, failures], timeout: 1.0)
153+
XCTAssertEqual(self.stateDelegate.states, [.connecting, .transientFailure, .connecting, .shutdown])
154+
}
155+
142156
func testClientEventuallyConnects() throws {
143157
let transientFailure = self.expectation(description: "connection transientFailure")
144158
let connectionReady = self.expectation(description: "connection ready")

Tests/GRPCTests/ConnectionBackoffTests.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,29 @@ class ConnectionBackoffTests: GRPCTestCase {
6565
}
6666
}
6767

68-
func testConnectionTimeoutAlwaysGreatherThanOrEqualToMinimum() {
68+
func testConnectionTimeoutAlwaysGreaterThanOrEqualToMinimum() {
6969
for connectionTimeout in self.backoff.prefix(100).map({ $0.timeout }) {
7070
XCTAssertGreaterThanOrEqual(connectionTimeout, self.backoff.minimumConnectionTimeout)
7171
}
7272
}
73+
74+
func testConnectionBackoffHasLimitedRetries() {
75+
for limit in [1, 3, 5] {
76+
let backoff = ConnectionBackoff(retries: .upTo(limit))
77+
let values = Array(backoff)
78+
XCTAssertEqual(values.count, limit)
79+
}
80+
}
81+
82+
func testConnectionBackoffWhenLimitedToZeroRetries() {
83+
let backoff = ConnectionBackoff(retries: .upTo(0))
84+
let values = Array(backoff)
85+
XCTAssertTrue(values.isEmpty)
86+
}
87+
88+
func testConnectionBackoffWithNoRetries() {
89+
let backoff = ConnectionBackoff(retries: .none)
90+
let values = Array(backoff)
91+
XCTAssertTrue(values.isEmpty)
92+
}
7393
}

Tests/GRPCTests/XCTestManifests.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ extension ClientConnectionBackoffTests {
4646
// to regenerate.
4747
static let __allTests__ClientConnectionBackoffTests = [
4848
("testClientConnectionFailsWithNoBackoff", testClientConnectionFailsWithNoBackoff),
49+
("testClientConnectionFailureIsLimited", testClientConnectionFailureIsLimited),
4950
("testClientEventuallyConnects", testClientEventuallyConnects),
5051
("testClientReconnectsAutomatically", testClientReconnectsAutomatically),
5152
]
@@ -105,7 +106,10 @@ extension ConnectionBackoffTests {
105106
("testBackoffDoesNotExceedMaximum", testBackoffDoesNotExceedMaximum),
106107
("testBackoffWithJitter", testBackoffWithJitter),
107108
("testBackoffWithNoJitter", testBackoffWithNoJitter),
108-
("testConnectionTimeoutAlwaysGreatherThanOrEqualToMinimum", testConnectionTimeoutAlwaysGreatherThanOrEqualToMinimum),
109+
("testConnectionBackoffHasLimitedRetries", testConnectionBackoffHasLimitedRetries),
110+
("testConnectionBackoffWhenLimitedToZeroRetries", testConnectionBackoffWhenLimitedToZeroRetries),
111+
("testConnectionBackoffWithNoRetries", testConnectionBackoffWithNoRetries),
112+
("testConnectionTimeoutAlwaysGreaterThanOrEqualToMinimum", testConnectionTimeoutAlwaysGreaterThanOrEqualToMinimum),
109113
("testExpectedValuesWithNoJitter", testExpectedValuesWithNoJitter),
110114
]
111115
}

0 commit comments

Comments
 (0)