Skip to content

Commit 4c98c24

Browse files
authored
Merge pull request #1149 from cobrowseio/development
Add configurable exponential backoff
2 parents f407bea + a17a66a commit 4c98c24

File tree

7 files changed

+79
-5
lines changed

7 files changed

+79
-5
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# v14.0.0
2+
3+
- Add exponential backoff for reconnects, with `reconnectWaitMax` and `randomizationFactor` options [#1149](https://github.com/socketio/socket.io-client-swift/pull/1149)
4+
15
# v13.4.0
26

37
- Add emits with write completion handlers. [#1096](https://github.com/socketio/socket.io-client-swift/issues/1096)
@@ -69,4 +73,3 @@ Important API changes
6973
- Adds `.sentPing` and `.gotPong` client events for tracking ping/pongs.
7074
- Makes the framework a single target.
7175
- Updates Starscream to 3.0
72-

Source/SocketIO/Client/SocketIOClientOption.swift

+15-1
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,14 @@ public enum SocketIOClientOption : ClientOption {
7575
/// The number of times to try and reconnect before giving up. Pass `-1` to [never give up](https://www.youtube.com/watch?v=dQw4w9WgXcQ).
7676
case reconnectAttempts(Int)
7777

78-
/// The number of seconds to wait before reconnect attempts.
78+
/// The minimum number of seconds to wait before reconnect attempts.
7979
case reconnectWait(Int)
80+
81+
/// The maximum number of seconds to wait before reconnect attempts.
82+
case reconnectWaitMax(Int)
83+
84+
/// The randomization factor for calculating reconnect jitter.
85+
case randomizationFactor(Double)
8086

8187
/// Set `true` if your server is using secure transports.
8288
case secure(Bool)
@@ -125,6 +131,10 @@ public enum SocketIOClientOption : ClientOption {
125131
description = "reconnectAttempts"
126132
case .reconnectWait:
127133
description = "reconnectWait"
134+
case .reconnectWaitMax:
135+
description = "reconnectWaitMax"
136+
case .randomizationFactor:
137+
description = "randomizationFactor"
128138
case .secure:
129139
description = "secure"
130140
case .selfSigned:
@@ -170,6 +180,10 @@ public enum SocketIOClientOption : ClientOption {
170180
value = attempts
171181
case let .reconnectWait(wait):
172182
value = wait
183+
case let .reconnectWaitMax(wait):
184+
value = wait
185+
case let .randomizationFactor(factor):
186+
value = factor
173187
case let .secure(secure):
174188
value = secure
175189
case let .security(security):

Source/SocketIO/Manager/SocketManager.swift

+26-2
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,15 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa
9797
/// If `true`, this client will try and reconnect on any disconnects.
9898
public var reconnects = true
9999

100-
/// The number of seconds to wait before attempting to reconnect.
100+
/// The minimum number of seconds to wait before attempting to reconnect.
101101
public var reconnectWait = 10
102102

103+
/// The maximum number of seconds to wait before attempting to reconnect.
104+
public var reconnectWaitMax = 30
105+
106+
/// The randomization factor for calculating reconnect jitter.
107+
public var randomizationFactor = 0.5
108+
103109
/// The status of this manager.
104110
public private(set) var status: SocketIOStatus = .notConnected {
105111
didSet {
@@ -474,7 +480,21 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa
474480
currentReconnectAttempt += 1
475481
connect()
476482

477-
handleQueue.asyncAfter(deadline: DispatchTime.now() + Double(reconnectWait), execute: _tryReconnect)
483+
let interval = reconnectInterval(attempts: currentReconnectAttempt)
484+
DefaultSocketLogger.Logger.log("Scheduling reconnect in \(interval)s", type: SocketManager.logType)
485+
handleQueue.asyncAfter(deadline: DispatchTime.now() + interval, execute: _tryReconnect)
486+
}
487+
488+
func reconnectInterval(attempts: Int) -> Double {
489+
// apply exponential factor
490+
let backoffFactor = pow(1.5, attempts)
491+
let interval = Double(reconnectWait) * Double(truncating: backoffFactor as NSNumber)
492+
// add in a random factor smooth thundering herds
493+
let rand = Double.random(in: 0 ..< 1)
494+
let randomFactor = rand * randomizationFactor * Double(truncating: interval as NSNumber)
495+
// add in random factor, and clamp to min and max values
496+
let combined = interval + randomFactor
497+
return Double(fmax(Double(reconnectWait), fmin(combined, Double(reconnectWaitMax))))
478498
}
479499

480500
/// Sets manager specific configs.
@@ -493,6 +513,10 @@ open class SocketManager : NSObject, SocketManagerSpec, SocketParsable, SocketDa
493513
self.reconnectAttempts = attempts
494514
case let .reconnectWait(wait):
495515
reconnectWait = abs(wait)
516+
case let .reconnectWaitMax(wait):
517+
reconnectWaitMax = abs(wait)
518+
case let .randomizationFactor(factor):
519+
randomizationFactor = factor
496520
case let .log(log):
497521
DefaultSocketLogger.Logger.log = log
498522
case let .logger(logger):

Source/SocketIO/Manager/SocketManagerSpec.swift

+7-1
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,14 @@ public protocol SocketManagerSpec : AnyObject, SocketEngineClient {
6969
/// If `true`, this manager will try and reconnect on any disconnects.
7070
var reconnects: Bool { get set }
7171

72-
/// The number of seconds to wait before attempting to reconnect.
72+
/// The minimum number of seconds to wait before attempting to reconnect.
7373
var reconnectWait: Int { get set }
74+
75+
/// The maximum number of seconds to wait before attempting to reconnect.
76+
var reconnectWaitMax: Int { get set }
77+
78+
/// The randomization factor for calculating reconnect jitter.
79+
var randomizationFactor: Double { get set }
7480

7581
/// The URL of the socket.io server.
7682
var socketURL: URL { get }

Source/SocketIO/Util/SocketExtensions.swift

+4
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ extension Dictionary where Key == String, Value == Any {
7171
return .reconnectAttempts(attempts)
7272
case let ("reconnectWait", wait as Int):
7373
return .reconnectWait(wait)
74+
case let ("reconnectWaitMax", wait as Int):
75+
return .reconnectWaitMax(wait)
76+
case let ("randomizationFactor", factor as Double):
77+
return .randomizationFactor(factor)
7478
case let ("secure", secure as Bool):
7579
return .secure(secure)
7680
case let ("security", security as SSLSecurity):

Tests/TestSocketIO/SocketMangerTest.swift

+21
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ class SocketMangerTest : XCTestCase {
1515
XCTAssertEqual(manager.handleQueue, DispatchQueue.main)
1616
XCTAssertTrue(manager.reconnects)
1717
XCTAssertEqual(manager.reconnectWait, 10)
18+
XCTAssertEqual(manager.reconnectWaitMax, 30)
19+
XCTAssertEqual(manager.randomizationFactor, 0.5)
1820
XCTAssertEqual(manager.status, .notConnected)
1921
}
2022

@@ -27,6 +29,21 @@ class SocketMangerTest : XCTestCase {
2729

2830
XCTAssertEqual(manager.config.first!, .secure(true))
2931
}
32+
33+
func testBackoffIntervalCalulation() {
34+
XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: -1), Double(manager.reconnectWaitMax))
35+
XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 0), 15)
36+
XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 1), 22.5)
37+
XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 2), 33.75)
38+
XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 50), Double(manager.reconnectWaitMax))
39+
XCTAssertLessThanOrEqual(manager.reconnectInterval(attempts: 10000), Double(manager.reconnectWaitMax))
40+
41+
XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: -1), Double(manager.reconnectWait))
42+
XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 0), Double(manager.reconnectWait))
43+
XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 1), 15)
44+
XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 2), 22.5)
45+
XCTAssertGreaterThanOrEqual(manager.reconnectInterval(attempts: 10000), Double(manager.reconnectWait))
46+
}
3047

3148
func testManagerCallsConnect() {
3249
setUpSockets()
@@ -90,13 +107,17 @@ class SocketMangerTest : XCTestCase {
90107
.forceNew(true),
91108
.reconnects(false),
92109
.reconnectWait(5),
110+
.reconnectWaitMax(5),
111+
.randomizationFactor(0.7),
93112
.reconnectAttempts(5)
94113
])
95114

96115
XCTAssertEqual(manager.handleQueue, queue)
97116
XCTAssertTrue(manager.forceNew)
98117
XCTAssertFalse(manager.reconnects)
99118
XCTAssertEqual(manager.reconnectWait, 5)
119+
XCTAssertEqual(manager.reconnectWaitMax, 5)
120+
XCTAssertEqual(manager.randomizationFactor, 0.7)
100121
XCTAssertEqual(manager.reconnectAttempts, 5)
101122
}
102123

Tests/TestSocketIOObjc/ManagerObjectiveCTest.m

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ - (void)testManagerProperties {
3434
XCTAssertEqual(self.manager.handleQueue, dispatch_get_main_queue());
3535
XCTAssertTrue(self.manager.reconnects);
3636
XCTAssertEqual(self.manager.reconnectWait, 10);
37+
XCTAssertEqual(self.manager.reconnectWaitMax, 30);
38+
XCTAssertEqual(self.manager.randomizationFactor, 0.5);
3739
XCTAssertEqual(self.manager.status, SocketIOStatusNotConnected);
3840
}
3941

0 commit comments

Comments
 (0)