Skip to content

Add configurable exponential backoff #1149

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

Merged
merged 3 commits into from
Jan 14, 2019
Merged
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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

16 changes: 15 additions & 1 deletion Source/SocketIO/Client/SocketIOClientOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
28 changes: 26 additions & 2 deletions Source/SocketIO/Manager/SocketManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand All @@ -493,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):
Expand Down
8 changes: 7 additions & 1 deletion Source/SocketIO/Manager/SocketManagerSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
4 changes: 4 additions & 0 deletions Source/SocketIO/Util/SocketExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
21 changes: 21 additions & 0 deletions Tests/TestSocketIO/SocketMangerTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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()
Expand Down Expand Up @@ -90,13 +107,17 @@ class SocketMangerTest : XCTestCase {
.forceNew(true),
.reconnects(false),
.reconnectWait(5),
.reconnectWaitMax(5),
.randomizationFactor(0.7),
.reconnectAttempts(5)
])

XCTAssertEqual(manager.handleQueue, queue)
XCTAssertTrue(manager.forceNew)
XCTAssertFalse(manager.reconnects)
XCTAssertEqual(manager.reconnectWait, 5)
XCTAssertEqual(manager.reconnectWaitMax, 5)
XCTAssertEqual(manager.randomizationFactor, 0.7)
XCTAssertEqual(manager.reconnectAttempts, 5)
}

Expand Down
2 changes: 2 additions & 0 deletions Tests/TestSocketIOObjc/ManagerObjectiveCTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down