Skip to content

Commit 2dff80b

Browse files
authored
Improve disconnect vs reconnect reliability (#761)
Resolves #757 - Adds cancellation to reconnect, making sure that it cannot continue after `disconnect()` and bring the room back to life - ⚠️ Optionally, we can introduce a breaking `.disconnecting` state to prevent new tasks from being started (vs cancelling ongoing tasks - a little _less elegant_) - Theoretically it should not affect other state transitions, as `disconnect()` is basically "destructive" - the only valid path leads to `.disconnected` during `cleanup()`, however that's not statically enforced in any way - On the other hand, it does not provide any added value for other parts of the SDK, just covering this tiny time window
1 parent 8b44810 commit 2dff80b

6 files changed

Lines changed: 51 additions & 14 deletions

File tree

.changes/disconnect-reconnect

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="fixed" "Fixed race condition between reconnect and disconnect leading to failed disconnects"

.changes/disconnected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
patch type="added" "Added .disconnected connection state"

Sources/LiveKit/Core/Room+Engine.swift

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -357,16 +357,16 @@ extension Room {
357357
}
358358

359359
do {
360-
try await Task.retrying(totalAttempts: _state.connectOptions.reconnectAttempts,
361-
retryDelay: { @Sendable attempt in
362-
let delay = TimeInterval.computeReconnectDelay(forAttempt: attempt,
363-
baseDelay: self._state.connectOptions.reconnectAttemptDelay,
364-
maxDelay: self._state.connectOptions.reconnectMaxDelay,
365-
totalAttempts: self._state.connectOptions.reconnectAttempts,
366-
addJitter: true)
367-
self.log("[Connect] Retry cycle waiting for \(String(format: "%.2f", delay)) seconds before attempt \(attempt + 1)")
368-
return delay
369-
}) { currentAttempt, totalAttempts in
360+
let reconnectTask = Task.retrying(totalAttempts: _state.connectOptions.reconnectAttempts,
361+
retryDelay: { @Sendable attempt in
362+
let delay = TimeInterval.computeReconnectDelay(forAttempt: attempt,
363+
baseDelay: self._state.connectOptions.reconnectAttemptDelay,
364+
maxDelay: self._state.connectOptions.reconnectMaxDelay,
365+
totalAttempts: self._state.connectOptions.reconnectAttempts,
366+
addJitter: true)
367+
self.log("[Connect] Retry cycle waiting for \(String(format: "%.2f", delay)) seconds before attempt \(attempt + 1)")
368+
return delay
369+
}) { currentAttempt, totalAttempts in
370370
// Not reconnecting state anymore
371371
guard let currentMode = self._state.isReconnectingWithMode else {
372372
self.log("[Connect] Not in reconnect state anymore, exiting retry cycle.")
@@ -403,12 +403,19 @@ extension Room {
403403
// Re-throw
404404
throw error
405405
}
406-
}.value
406+
}
407+
408+
_state.mutate {
409+
$0.reconnectTask = reconnectTask
410+
}
411+
412+
try await reconnectTask.value
407413

408414
// Re-connect sequence successful
409415
log("[Connect] Sequence completed")
410416
_state.mutate {
411417
$0.connectionState = .connected
418+
$0.reconnectTask = nil
412419
$0.isReconnectingWithMode = nil
413420
$0.nextReconnectMode = nil
414421
}

Sources/LiveKit/Core/Room.swift

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ public class Room: NSObject, @unchecked Sendable, ObservableObject, Loggable {
167167
var nextReconnectMode: ReconnectMode?
168168
var isReconnectingWithMode: ReconnectMode?
169169
var connectionState: ConnectionState = .disconnected
170+
var reconnectTask: Task<Void, Error>?
170171
var disconnectError: LiveKitError?
171172
var connectStopwatch = Stopwatch(label: "connect")
172173
var hasPublished: Bool = false
@@ -410,16 +411,38 @@ public class Room: NSObject, @unchecked Sendable, ObservableObject, Loggable {
410411

411412
@objc
412413
public func disconnect() async {
413-
// Return if already disconnected state
414-
if case .disconnected = connectionState { return }
414+
let shouldDisconnect = _state.mutate {
415+
switch $0.connectionState {
416+
case .disconnecting, .disconnected:
417+
return false
418+
default:
419+
$0.connectionState = .disconnecting
420+
return true
421+
}
422+
}
423+
guard shouldDisconnect else { return }
424+
425+
cancelReconnect()
415426

416427
do {
417428
try await signalClient.sendLeave()
418429
} catch {
419430
log("Failed to send leave with error: \(error)")
420431
}
421432

433+
cancelReconnect()
434+
422435
await cleanUp()
436+
437+
cancelReconnect()
438+
}
439+
440+
private func cancelReconnect() {
441+
_state.mutate {
442+
log("Cancelling reconnect task: \(String(describing: $0.reconnectTask))")
443+
$0.reconnectTask?.cancel()
444+
$0.reconnectTask = nil
445+
}
423446
}
424447
}
425448

@@ -430,6 +453,7 @@ extension Room {
430453
func cleanUp(withError disconnectError: Error? = nil,
431454
isFullReconnect: Bool = false) async
432455
{
456+
guard !Task.isCancelled else { return }
433457
log("withError: \(String(describing: disconnectError)), isFullReconnect: \(isFullReconnect)")
434458

435459
// Reset completers
@@ -456,11 +480,13 @@ extension Room {
456480
token: $0.token,
457481
nextReconnectMode: $0.nextReconnectMode,
458482
isReconnectingWithMode: $0.isReconnectingWithMode,
459-
connectionState: $0.connectionState
483+
connectionState: $0.connectionState,
484+
reconnectTask: $0.reconnectTask
460485
) : State(
461486
connectOptions: $0.connectOptions,
462487
roomOptions: $0.roomOptions,
463488
connectionState: .disconnected,
489+
reconnectTask: $0.reconnectTask,
464490
disconnectError: LiveKitError.from(error: disconnectError)
465491
)
466492
}

Sources/LiveKit/Extensions/CustomStringConvertible.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ extension ConnectionState: CustomStringConvertible {
134134
case .connecting: ".connecting"
135135
case .reconnecting: ".reconnecting"
136136
case .connected: ".connected"
137+
case .disconnecting: ".disconnecting"
137138
}
138139
}
139140
}

Sources/LiveKit/Types/ConnectionState.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public enum ConnectionState: Int, Sendable {
3535
case connecting
3636
case reconnecting
3737
case connected
38+
case disconnecting
3839
}
3940

4041
extension ConnectionState: Identifiable {

0 commit comments

Comments
 (0)