Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix connection timeout when retries are set to none #1650

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions Sources/GRPC/ConnectionBackoff.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public struct ConnectionBackoff: Sequence, Sendable {

/// An iterator for ``ConnectionBackoff``.
public class ConnectionBackoffIterator: IteratorProtocol {
public typealias Element = (timeout: TimeInterval, backoff: TimeInterval)
public typealias Element = (timeout: TimeInterval, backoff: TimeInterval?)

/// Creates a new connection backoff iterator with the given configuration.
public init(connectionBackoff: ConnectionBackoff) {
Expand Down Expand Up @@ -135,7 +135,12 @@ public class ConnectionBackoffIterator: IteratorProtocol {
case let .limited(limit) where limit > 0:
self.connectionBackoff.retries.limit = .limited(limit - 1)

// limit must be <= 0, no new element.
// limit is reached, return an element with only a timeout.
case let .limited(limit) where limit == 0:
self.connectionBackoff.retries.limit = .limited(limit - 1)
return self.makeElement(backoff: nil)
Comment on lines +138 to +141
Copy link
Author

@afeick afeick Oct 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glbrntt What do you think about using a nil Element.backoff as the sentinel for the iterator?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It changes the Element type which is an API breaking change so we can't do it unfortunately. Otherwise it'd be a good solution.


// limit must be < 0, no new element.
case .limited:
return nil
}
Expand All @@ -159,8 +164,8 @@ public class ConnectionBackoffIterator: IteratorProtocol {

/// Make a timeout-backoff pair from the given backoff. The timeout is the `max` of the backoff
/// and `connectionBackoff.minimumConnectionTimeout`.
private func makeElement(backoff: TimeInterval) -> Element {
let timeout = max(backoff, self.connectionBackoff.minimumConnectionTimeout)
private func makeElement(backoff: TimeInterval?) -> Element {
let timeout = max(backoff ?? self.unjitteredBackoff, self.connectionBackoff.minimumConnectionTimeout)
return (timeout, backoff)
}

Expand Down
5 changes: 4 additions & 1 deletion Sources/GRPC/ConnectionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -997,7 +997,10 @@ extension ConnectionManager {
}

// Should we reconnect if the candidate channel fails?
let reconnect: Reconnect = timeoutAndBackoff.map { .after($0.backoff) } ?? .none
var reconnect = Reconnect.none
if let backoff = timeoutAndBackoff?.backoff {
reconnect = Reconnect.after(backoff)
}
let connecting = ConnectingState(
backoffIterator: backoffIterator,
reconnect: reconnect,
Expand Down
12 changes: 6 additions & 6 deletions Tests/GRPCTests/ConnectionBackoffTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ConnectionBackoffTests: GRPCTestCase {
pow(self.backoff.initialBackoff * self.backoff.multiplier, Double(i)),
self.backoff.maximumBackoff
)
XCTAssertEqual(expected, backoff, accuracy: 1e-6)
XCTAssertEqual(expected, backoff!, accuracy: 1e-6)
}
}

Expand All @@ -55,7 +55,7 @@ class ConnectionBackoffTests: GRPCTestCase {
)
let halfJitterRange = self.backoff.jitter * unjittered
let jitteredRange = (unjittered - halfJitterRange) ... (unjittered + halfJitterRange)
XCTAssert(jitteredRange.contains(timeoutAndBackoff.backoff))
XCTAssert(jitteredRange.contains(timeoutAndBackoff.backoff!))
}
}

Expand All @@ -65,7 +65,7 @@ class ConnectionBackoffTests: GRPCTestCase {
self.backoff.jitter = 0.0

for backoff in self.backoff.prefix(100).map({ $0.backoff }) {
XCTAssertLessThanOrEqual(backoff, self.backoff.maximumBackoff)
XCTAssertLessThanOrEqual(backoff!, self.backoff.maximumBackoff)
}
}

Expand All @@ -79,19 +79,19 @@ class ConnectionBackoffTests: GRPCTestCase {
for limit in [1, 3, 5] {
let backoff = ConnectionBackoff(retries: .upTo(limit))
let values = Array(backoff)
XCTAssertEqual(values.count, limit)
XCTAssertEqual(values.count, limit+1)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value count would make more sense if the ConnectionBackoff took a value called tries or attempts instead of retries, but I didn't think it was a good idea to make a breaking change like that.

}
}

func testConnectionBackoffWhenLimitedToZeroRetries() {
let backoff = ConnectionBackoff(retries: .upTo(0))
let values = Array(backoff)
XCTAssertTrue(values.isEmpty)
XCTAssertEqual(values.count, 1)
}

func testConnectionBackoffWithNoRetries() {
let backoff = ConnectionBackoff(retries: .none)
let values = Array(backoff)
XCTAssertTrue(values.isEmpty)
XCTAssertEqual(values.count, 1)
}
}