From 49b9a07a95a1837091155622d29c8b0b22085f3e Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 12 Jan 2019 17:08:02 +0000 Subject: [PATCH] add configurable exponential backoff --- .../Client/SocketIOClientOption.swift | 16 ++++++++++++- Source/SocketIO/Manager/SocketManager.swift | 24 +++++++++++++++++-- .../SocketIO/Manager/SocketManagerSpec.swift | 8 ++++++- Tests/TestSocketIO/SocketMangerTest.swift | 17 +++++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/Source/SocketIO/Client/SocketIOClientOption.swift b/Source/SocketIO/Client/SocketIOClientOption.swift index 02d8898..1d687e0 100644 --- a/Source/SocketIO/Client/SocketIOClientOption.swift +++ b/Source/SocketIO/Client/SocketIOClientOption.swift @@ -75,8 +75,14 @@ public enum SocketIOClientOption : ClientOption { /// The number of times to try and reconnect before giving up. Pass `-1` to [never give up](https://www.youtube.com/watch?v=dQw4w9WgXcQ). case reconnectAttempts(Int) - /// The number of seconds to wait before reconnect attempts. + /// The minimum number of seconds to wait before reconnect attempts. case reconnectWait(Int) + + /// The maximum number of seconds to wait before reconnect attempts. + case reconnectWaitMax(Int) + + /// The randomization factor for calculating reconnect jitter. + case randomizationFactor(Double) /// Set `true` if your server is using secure transports. case secure(Bool) @@ -125,6 +131,10 @@ public enum SocketIOClientOption : ClientOption { description = "reconnectAttempts" case .reconnectWait: description = "reconnectWait" + case .reconnectWaitMax: + description = "reconnectWaitMax" + case .randomizationFactor: + description = "randomizationFactor" case .secure: description = "secure" case .selfSigned: @@ -170,6 +180,10 @@ public enum SocketIOClientOption : ClientOption { value = attempts case let .reconnectWait(wait): value = wait + case let .reconnectWaitMax(wait): + value = wait + case let .randomizationFactor(factor): + value = factor case let .secure(secure): value = secure case let .security(security): diff --git a/Source/SocketIO/Manager/SocketManager.swift b/Source/SocketIO/Manager/SocketManager.swift index e348eed..eb9e466 100644 --- a/Source/SocketIO/Manager/SocketManager.swift +++ b/Source/SocketIO/Manager/SocketManager.swift @@ -97,9 +97,15 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa /// If `true`, this client will try and reconnect on any disconnects. public var reconnects = true - /// The number of seconds to wait before attempting to reconnect. + /// The minimum number of seconds to wait before attempting to reconnect. public var reconnectWait = 10 + /// The maximum number of seconds to wait before attempting to reconnect. + public var reconnectWaitMax = 30 + + /// The randomization factor for calculating reconnect jitter. + public var randomizationFactor = 0.5 + /// The status of this manager. public private(set) var status: SocketIOStatus = .notConnected { didSet { @@ -474,7 +480,21 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa currentReconnectAttempt += 1 connect() - handleQueue.asyncAfter(deadline: DispatchTime.now() + Double(reconnectWait), execute: _tryReconnect) + let interval = reconnectInterval(attempts: currentReconnectAttempt) + DefaultSocketLogger.Logger.log("Scheduling reconnect in \(interval)s", type: SocketManager.logType) + handleQueue.asyncAfter(deadline: DispatchTime.now() + interval, execute: _tryReconnect) + } + + func reconnectInterval(attempts: Int) -> Double { + // apply exponential factor + let backoffFactor = pow(1.5, attempts) + let interval = Double(reconnectWait) * Double(truncating: backoffFactor as NSNumber) + // add in a random factor smooth thundering herds + let rand = Double.random(in: 0 ..< 1) + let randomFactor = rand * randomizationFactor * Double(truncating: interval as NSNumber) + // add in random factor, and clamp to min and max values + let combined = interval + randomFactor + return Double(fmax(Double(reconnectWait), fmin(combined, Double(reconnectWaitMax)))) } /// Sets manager specific configs. diff --git a/Source/SocketIO/Manager/SocketManagerSpec.swift b/Source/SocketIO/Manager/SocketManagerSpec.swift index 4440193..35d5afc 100644 --- a/Source/SocketIO/Manager/SocketManagerSpec.swift +++ b/Source/SocketIO/Manager/SocketManagerSpec.swift @@ -69,8 +69,14 @@ public protocol SocketManagerSpec : AnyObject, SocketEngineClient { /// If `true`, this manager will try and reconnect on any disconnects. var reconnects: Bool { get set } - /// The number of seconds to wait before attempting to reconnect. + /// The minimum number of seconds to wait before attempting to reconnect. var reconnectWait: Int { get set } + + /// The maximum number of seconds to wait before attempting to reconnect. + var reconnectWaitMax: Int { get set } + + /// The randomization factor for calculating reconnect jitter. + var randomizationFactor: Double { get set } /// The URL of the socket.io server. var socketURL: URL { get } diff --git a/Tests/TestSocketIO/SocketMangerTest.swift b/Tests/TestSocketIO/SocketMangerTest.swift index 8041948..b0aa00e 100644 --- a/Tests/TestSocketIO/SocketMangerTest.swift +++ b/Tests/TestSocketIO/SocketMangerTest.swift @@ -15,6 +15,8 @@ class SocketMangerTest : XCTestCase { XCTAssertEqual(manager.handleQueue, DispatchQueue.main) XCTAssertTrue(manager.reconnects) XCTAssertEqual(manager.reconnectWait, 10) + XCTAssertEqual(manager.reconnectWaitMax, 30) + XCTAssertEqual(manager.randomizationFactor, 0.5) XCTAssertEqual(manager.status, .notConnected) } @@ -27,6 +29,21 @@ class SocketMangerTest : XCTestCase { XCTAssertEqual(manager.config.first!, .secure(true)) } + + func testBackoffIntervalCalulation() { + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: -1), Double(manager.reconnectWaitMax)) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 0), 15) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 1), 22.5) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 2), 33.75) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 50), Double(manager.reconnectWaitMax)) + XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 10000), Double(manager.reconnectWaitMax)) + + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: -1), Double(manager.reconnectWait)) + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 0), Double(manager.reconnectWait)) + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 1), 15) + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 2), 22.5) + XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 10000), Double(manager.reconnectWait)) + } func testManagerCallsConnect() { setUpSockets()