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 1 commit
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
Next Next commit
add configurable exponential backoff
  • Loading branch information
headlessme committed Jan 12, 2019
commit 49b9a07a95a1837091155622d29c8b0b22085f3e
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
24 changes: 22 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 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
17 changes: 17 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