diff --git a/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift new file mode 100644 index 000000000..7a356ce2b --- /dev/null +++ b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+Backoff.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the AsyncHTTPClient open source project +// +// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import NIOCore +#if canImport(Darwin) + import func Darwin.pow +#else + import func Glibc.pow +#endif + +extension HTTPConnectionPool { + /// Calculates the delay for the next connection attempt after the given number of failed `attempts`. + /// + /// Our backoff formula is: 100ms * 1.25^(attempts - 1) that is capped of at 1 minute. + /// This means for: + /// - 1 failed attempt : 100ms + /// - 5 failed attempts: ~300ms + /// - 10 failed attempts: ~930ms + /// - 15 failed attempts: ~2.84s + /// - 20 failed attempts: ~8.67s + /// - 25 failed attempts: ~26s + /// - 29 failed attempts: ~60s (max out) + /// + /// - Parameter attempts: number of failed attempts in a row + /// - Returns: time to wait until trying to establishing a new connection + static func calculateBackoff(failedAttempt attempts: Int) -> TimeAmount { + // Our backoff formula is: 100ms * 1.25^(attempts - 1) that is capped of at 1minute + // This means for: + // - 1 failed attempt : 100ms + // - 5 failed attempts: ~300ms + // - 10 failed attempts: ~930ms + // - 15 failed attempts: ~2.84s + // - 20 failed attempts: ~8.67s + // - 25 failed attempts: ~26s + // - 29 failed attempts: ~60s (max out) + + let start = Double(TimeAmount.milliseconds(100).nanoseconds) + let backoffNanoseconds = Int64(start * pow(1.25, Double(attempts - 1))) + + let backoff: TimeAmount = min(.nanoseconds(backoffNanoseconds), .seconds(60)) + + // Calculate a 3% jitter range + let jitterRange = (backoff.nanoseconds / 100) * 3 + // Pick a random element from the range +/- jitter range. + let jitter: TimeAmount = .nanoseconds((-jitterRange...jitterRange).randomElement()!) + let jitteredBackoff = backoff + jitter + return jitteredBackoff + } +} diff --git a/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1StateMachine.swift b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1StateMachine.swift index 3695b3c90..f73521bfb 100644 --- a/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1StateMachine.swift +++ b/Sources/AsyncHTTPClient/ConnectionPool/State Machine/HTTPConnectionPool+HTTP1StateMachine.swift @@ -154,7 +154,7 @@ extension HTTPConnectionPool { // decision about the retry will be made in `connectionCreationBackoffDone(_:)` let eventLoop = self.connections.backoffNextConnectionAttempt(connectionID) - let backoff = self.calculateBackoff(failedAttempt: self.failedConsecutiveConnectionAttempts) + let backoff = calculateBackoff(failedAttempt: self.failedConsecutiveConnectionAttempts) return .init( request: .none, connection: .scheduleBackoffTimer(connectionID, backoff: backoff, on: eventLoop) @@ -444,30 +444,6 @@ extension HTTPConnectionPool { self.connections.removeConnection(at: index) return .none } - - private func calculateBackoff(failedAttempt attempts: Int) -> TimeAmount { - // Our backoff formula is: 100ms * 1.25^(attempts - 1) that is capped of at 1minute - // This means for: - // - 1 failed attempt : 100ms - // - 5 failed attempts: ~300ms - // - 10 failed attempts: ~930ms - // - 15 failed attempts: ~2.84s - // - 20 failed attempts: ~8.67s - // - 25 failed attempts: ~26s - // - 29 failed attempts: ~60s (max out) - - let start = Double(TimeAmount.milliseconds(100).nanoseconds) - let backoffNanoseconds = Int64(start * pow(1.25, Double(attempts - 1))) - - let backoff: TimeAmount = min(.nanoseconds(backoffNanoseconds), .seconds(60)) - - // Calculate a 3% jitter range - let jitterRange = (backoff.nanoseconds / 100) * 3 - // Pick a random element from the range +/- jitter range. - let jitter: TimeAmount = .nanoseconds((-jitterRange...jitterRange).randomElement()!) - let jitteredBackoff = backoff + jitter - return jitteredBackoff - } } }