From 49b9a07a95a1837091155622d29c8b0b22085f3e Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 12 Jan 2019 17:08:02 +0000 Subject: [PATCH 1/3] 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 02d88989..1d687e0a 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 e348eed3..eb9e4663 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 4440193f..35d5afc2 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 80419482..b0aa00ef 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() From 9433050588f4f1f941d78b5619841be5a9e771ba Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 12 Jan 2019 22:05:01 +0000 Subject: [PATCH 2/3] config setting and test updates for reconnectWaitMax and randomizationFactor --- Source/SocketIO/Manager/SocketManager.swift | 4 ++++ Source/SocketIO/Util/SocketExtensions.swift | 4 ++++ Tests/TestSocketIO/SocketMangerTest.swift | 4 ++++ Tests/TestSocketIOObjc/ManagerObjectiveCTest.m | 2 ++ 4 files changed, 14 insertions(+) diff --git a/Source/SocketIO/Manager/SocketManager.swift b/Source/SocketIO/Manager/SocketManager.swift index eb9e4663..ce3e9013 100644 --- a/Source/SocketIO/Manager/SocketManager.swift +++ b/Source/SocketIO/Manager/SocketManager.swift @@ -513,6 +513,10 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa self.reconnectAttempts = attempts case let .reconnectWait(wait): reconnectWait = abs(wait) + case let .reconnectWaitMax(wait): + reconnectWaitMax = abs(wait) + case let .randomizationFactor(factor): + randomizationFactor = factor case let .log(log): DefaultSocketLogger.Logger.log = log case let .logger(logger): diff --git a/Source/SocketIO/Util/SocketExtensions.swift b/Source/SocketIO/Util/SocketExtensions.swift index fe9086fd..46363270 100644 --- a/Source/SocketIO/Util/SocketExtensions.swift +++ b/Source/SocketIO/Util/SocketExtensions.swift @@ -71,6 +71,10 @@ extension Dictionary where Key == String, Value == Any { return .reconnectAttempts(attempts) case let ("reconnectWait", wait as Int): return .reconnectWait(wait) + case let ("reconnectWaitMax", wait as Int): + return .reconnectWaitMax(wait) + case let ("randomizationFactor", factor as Double): + return .randomizationFactor(factor) case let ("secure", secure as Bool): return .secure(secure) case let ("security", security as SSLSecurity): diff --git a/Tests/TestSocketIO/SocketMangerTest.swift b/Tests/TestSocketIO/SocketMangerTest.swift index b0aa00ef..453af2ea 100644 --- a/Tests/TestSocketIO/SocketMangerTest.swift +++ b/Tests/TestSocketIO/SocketMangerTest.swift @@ -107,6 +107,8 @@ class SocketMangerTest : XCTestCase { .forceNew(true), .reconnects(false), .reconnectWait(5), + .reconnectWaitMax(5), + .randomizationFactor(0.7), .reconnectAttempts(5) ]) @@ -114,6 +116,8 @@ class SocketMangerTest : XCTestCase { XCTAssertTrue(manager.forceNew) XCTAssertFalse(manager.reconnects) XCTAssertEqual(manager.reconnectWait, 5) + XCTAssertEqual(manager.reconnectWaitMax, 5) + XCTAssertEqual(manager.randomizationFactor, 0.7) XCTAssertEqual(manager.reconnectAttempts, 5) } diff --git a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m index 393da4f2..a807eedd 100644 --- a/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m +++ b/Tests/TestSocketIOObjc/ManagerObjectiveCTest.m @@ -34,6 +34,8 @@ - (void)testManagerProperties { XCTAssertEqual(self.manager.handleQueue, dispatch_get_main_queue()); XCTAssertTrue(self.manager.reconnects); XCTAssertEqual(self.manager.reconnectWait, 10); + XCTAssertEqual(self.manager.reconnectWaitMax, 30); + XCTAssertEqual(self.manager.randomizationFactor, 0.5); XCTAssertEqual(self.manager.status, SocketIOStatusNotConnected); } From a17a66af00fb1e074133e852709468b872c6e605 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 14 Jan 2019 19:16:58 +0000 Subject: [PATCH 3/3] add changelog entry for exponential backoff --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f795a08..d6f548ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v14.0.0 + +- Add exponential backoff for reconnects, with `reconnectWaitMax` and `randomizationFactor` options [#1149](https://github.com/socketio/socket.io-client-swift/pull/1149) + # v13.4.0 - Add emits with write completion handlers. [#1096](https://github.com/socketio/socket.io-client-swift/issues/1096) @@ -69,4 +73,3 @@ Important API changes - Adds `.sentPing` and `.gotPong` client events for tracking ping/pongs. - Makes the framework a single target. - Updates Starscream to 3.0 -