diff --git a/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift index 7a356ce2b..ad0ef81a4 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift @@ -46,9 +46,12 @@ extension HTTPConnectionPool { // - 29 failed attempts: ~60s (max out) let start = Double(TimeAmount.milliseconds(100).nanoseconds) - let backoffNanoseconds = Int64(start * pow(1.25, Double(attempts - 1))) + let backoffNanosecondsDouble = start * pow(1.25, Double(attempts - 1)) - let backoff: TimeAmount = min(.nanoseconds(backoffNanoseconds), .seconds(60)) + // Cap to 60s _before_ we convert to Int64, to avoid trapping in the Int64 initializer. + let backoffNanoseconds = Int64(min(backoffNanosecondsDouble, Double(TimeAmount.seconds(60).nanoseconds))) + + let backoff = TimeAmount.nanoseconds(backoffNanoseconds) // Calculate a 3% jitter range let jitterRange = (backoff.nanoseconds / 100) * 3 diff --git a/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests+XCTest.swift b/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests+XCTest.swift index d780de597..acdc0ab26 100644 --- a/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests+XCTest.swift +++ b/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests+XCTest.swift @@ -33,6 +33,7 @@ extension HTTPConnectionPoolTests { ("testConnectionCreationIsRetriedUntilRequestIsCancelled", testConnectionCreationIsRetriedUntilRequestIsCancelled), ("testConnectionShutdownIsCalledOnActiveConnections", testConnectionShutdownIsCalledOnActiveConnections), ("testConnectionPoolStressResistanceHTTP1", testConnectionPoolStressResistanceHTTP1), + ("testBackoffBehavesSensibly", testBackoffBehavesSensibly), ] } } diff --git a/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift b/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift index dac679eb6..60e5077ee 100644 --- a/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift +++ b/Tests/AsyncHTTPClientTests/HTTPConnectionPoolTests.swift @@ -453,6 +453,32 @@ class HTTPConnectionPoolTests: XCTestCase { pool.shutdown() XCTAssertNoThrow(try poolDelegate.future.wait()) } + + func testBackoffBehavesSensibly() throws { + var backoff = HTTPConnectionPool.calculateBackoff(failedAttempt: 1) + + // The value should be 100msĀ±3ms + XCTAssertLessThanOrEqual((backoff - .milliseconds(100)).nanoseconds.magnitude, TimeAmount.milliseconds(3).nanoseconds.magnitude) + + // Should always increase + // We stop when we get within the jitter of 60s, which is 1.8s + var attempt = 1 + while backoff < (.seconds(60) - .milliseconds(1800)) { + attempt += 1 + let newBackoff = HTTPConnectionPool.calculateBackoff(failedAttempt: attempt) + + XCTAssertGreaterThan(newBackoff, backoff) + backoff = newBackoff + } + + // Ok, now we should be able to do a hundred increments, and always hit 60s, plus or minus 1.8s of jitter. + for offset in 0..<100 { + XCTAssertLessThanOrEqual( + (HTTPConnectionPool.calculateBackoff(failedAttempt: attempt + offset) - .seconds(60)).nanoseconds.magnitude, + TimeAmount.milliseconds(1800).nanoseconds.magnitude + ) + } + } } class TestDelegate: HTTPConnectionPoolDelegate {