Skip to content

Commit 7bb2506

Browse files
authored
Use configured connect timeout when retries is none (#1777)
Motivation: The connection manager uses an iterator to get values for the connect timeout and the backoff to use if the connection attempt fails. Backoff can also be configured to have a limited number of tries. This controls when the iterator returns `nil`. If the connection attempt retries are disabled then the iterator always returns `nil`. This is an issue because the connection manager doesn't respect the configured connect timeout as the iterator doesn't return a value. Modifications: Check when starting a connection attempt from the idle state whether there a connect timeout is configured if there are no retries. This Result: The connect timeout value is repected if the connection retries aren't enabled.
1 parent 9499327 commit 7bb2506

File tree

2 files changed

+69
-3
lines changed

2 files changed

+69
-3
lines changed

Sources/GRPC/ConnectionManager.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,9 +1011,22 @@ extension ConnectionManager {
10111011
switch self.state {
10121012
case .idle:
10131013
let iterator = self.connectionBackoff?.makeIterator()
1014+
1015+
// The iterator produces the connect timeout and the backoff to use for the next attempt. This
1016+
// is unfortunate if retries is set to none because we need to connect timeout but not the
1017+
// backoff yet the iterator will not return a value to us. To workaround this we grab the
1018+
// connect timeout and override it.
1019+
let connectTimeoutOverride: TimeAmount?
1020+
if let backoff = self.connectionBackoff, backoff.retries == .none {
1021+
connectTimeoutOverride = .seconds(timeInterval: backoff.minimumConnectionTimeout)
1022+
} else {
1023+
connectTimeoutOverride = nil
1024+
}
1025+
10141026
self.startConnecting(
10151027
backoffIterator: iterator,
1016-
muxPromise: self.eventLoop.makePromise()
1028+
muxPromise: self.eventLoop.makePromise(),
1029+
connectTimeoutOverride: connectTimeoutOverride
10171030
)
10181031

10191032
case let .transientFailure(pending):
@@ -1039,7 +1052,8 @@ extension ConnectionManager {
10391052

10401053
private func startConnecting(
10411054
backoffIterator: ConnectionBackoffIterator?,
1042-
muxPromise: EventLoopPromise<HTTP2StreamMultiplexer>
1055+
muxPromise: EventLoopPromise<HTTP2StreamMultiplexer>,
1056+
connectTimeoutOverride: TimeAmount? = nil
10431057
) {
10441058
let timeoutAndBackoff = backoffIterator?.next()
10451059

@@ -1048,10 +1062,17 @@ extension ConnectionManager {
10481062
self.eventLoop.assertInEventLoop()
10491063

10501064
let candidate: EventLoopFuture<Channel> = self.eventLoop.flatSubmit {
1065+
let connectTimeout: TimeAmount?
1066+
if let connectTimeoutOverride = connectTimeoutOverride {
1067+
connectTimeout = connectTimeoutOverride
1068+
} else {
1069+
connectTimeout = timeoutAndBackoff.map { TimeAmount.seconds(timeInterval: $0.timeout) }
1070+
}
1071+
10511072
let channel: EventLoopFuture<Channel> = self.channelProvider.makeChannel(
10521073
managedBy: self,
10531074
onEventLoop: self.eventLoop,
1054-
connectTimeout: timeoutAndBackoff.map { .seconds(timeInterval: $0.timeout) },
1075+
connectTimeout: connectTimeout,
10551076
logger: self.logger
10561077
)
10571078

Tests/GRPCTests/ConnectionManagerTests.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,6 +1357,51 @@ extension ConnectionManagerTests {
13571357
XCTAssertGreaterThan(keepalive.interval, .seconds(1))
13581358
XCTAssertLessThanOrEqual(keepalive.interval, .seconds(4))
13591359
}
1360+
1361+
func testConnectTimeoutIsRespectedWithNoRetries() {
1362+
// Setup a factory which makes channels. We'll use this as the point to check that the
1363+
// connect timeout is as expected.
1364+
struct Provider: ConnectionManagerChannelProvider {
1365+
func makeChannel(
1366+
managedBy connectionManager: ConnectionManager,
1367+
onEventLoop eventLoop: any EventLoop,
1368+
connectTimeout: TimeAmount?,
1369+
logger: Logger
1370+
) -> EventLoopFuture<Channel> {
1371+
XCTAssertEqual(connectTimeout, .seconds(314_159_265))
1372+
return eventLoop.makeFailedFuture(DoomedChannelError())
1373+
}
1374+
}
1375+
1376+
var configuration = self.defaultConfiguration
1377+
configuration.connectionBackoff = ConnectionBackoff(
1378+
minimumConnectionTimeout: 314_159_265,
1379+
retries: .none
1380+
)
1381+
1382+
let manager = ConnectionManager(
1383+
configuration: configuration,
1384+
channelProvider: Provider(),
1385+
connectivityDelegate: self.monitor,
1386+
logger: self.logger
1387+
)
1388+
1389+
// Setup the state change expectations and trigger them by asking for the multiplexer.
1390+
// We expect connecting to shutdown as no connect retries are configured and the factory
1391+
// always returns errors.
1392+
let multiplexer = self.waitForStateChanges([
1393+
Change(from: .idle, to: .connecting),
1394+
Change(from: .connecting, to: .shutdown),
1395+
]) {
1396+
let multiplexer = manager.getHTTP2Multiplexer()
1397+
self.loop.run()
1398+
return multiplexer
1399+
}
1400+
1401+
XCTAssertThrowsError(try multiplexer.wait()) { error in
1402+
XCTAssert(error is DoomedChannelError)
1403+
}
1404+
}
13601405
}
13611406

13621407
internal struct Change: Hashable, CustomStringConvertible {

0 commit comments

Comments
 (0)