Skip to content

Commit 603b0dc

Browse files
authored
Try to fix some lifetime related bugs (#53)
* Some update to improve lifetime * Another update * Upate
1 parent 510533a commit 603b0dc

File tree

2 files changed

+70
-42
lines changed

2 files changed

+70
-42
lines changed

Sources/SignalRClient/HttpConnection.swift

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ actor HttpConnection: ConnectionProtocol {
8484
private let negotiationRedirectionLimit = 100
8585

8686
private var connectionState: ConnectionState = .disconnected
87-
private var connectionStarted: Bool = false
87+
private var connectionStartedSuccessfully: Bool = false
8888
private let httpClient: AccessTokenHttpClient
8989
private let logger: Logger
9090
private var options: HttpConnectionOptions
@@ -107,6 +107,7 @@ actor HttpConnection: ConnectionProtocol {
107107
private var onReceive: Transport.OnReceiveHandler?
108108
private var onClose: Transport.OnCloseHander?
109109
private let negotiateVersion = 1
110+
private var closeDuringStartError: Error? = nil
110111

111112
// MARK: - Initialization
112113

@@ -142,39 +143,27 @@ actor HttpConnection: ConnectionProtocol {
142143
// - If startInternalTask is nil, start will directly stop
143144
// - If startInternalTask is not nil, wait it finish and then call the stop
144145
guard connectionState == .disconnected else {
145-
throw SignalRError.invalidOperation("Cannot start an HttpConnection that is not in the 'Disconnected' state.")
146+
throw SignalRError.invalidOperation("Cannot start an HttpConnection that is not in the 'Disconnected' state. Currently it's \(connectionState)")
146147
}
147148

148149
connectionState = .connecting
149150

150151
startInternalTask = Task {
151-
try await self.startInternal(transferFormat: transferFormat)
152-
}
153-
154-
do {
155-
try await startInternalTask?.value
156-
} catch {
157-
connectionState = .disconnected
158-
throw error
159-
}
160-
161-
if connectionState == .disconnecting {
162-
let message = "Failed to start the HttpConnection before stop() was called."
163-
logger.log(level: .error, message: "\(message)")
164-
await stopTask?.value
165-
throw SignalRError.failedToStartConnection(message)
166-
} else if connectionState != .connected {
167-
let message = "HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!"
168-
logger.log(level: .error, message: "\(message)")
169-
throw SignalRError.failedToStartConnection(message)
152+
do {
153+
try await self.startInternal(transferFormat: transferFormat)
154+
connectionStartedSuccessfully = true
155+
} catch {
156+
connectionState = .disconnected
157+
throw error
158+
}
170159
}
171160

172-
connectionStarted = true
161+
try await startInternalTask?.value
173162
}
174163

175164
func send(_ data: StringOrData) async throws {
176165
guard connectionState == .connected else {
177-
throw SignalRError.invalidOperation("Cannot send data if the connection is not in the 'Connected' State.")
166+
throw SignalRError.invalidOperation("Cannot send data if the connection is not in the 'Connected' State. Currently it's \(connectionState)")
178167
}
179168

180169
try await transport?.send(data)
@@ -204,6 +193,11 @@ actor HttpConnection: ConnectionProtocol {
204193
// MARK: - Private Methods
205194

206195
private func startInternal(transferFormat: TransferFormat) async throws {
196+
guard connectionState == .connecting else {
197+
throw SignalRError.connectionAborted
198+
}
199+
closeDuringStartError = nil
200+
207201
var url = baseUrl
208202
await httpClient.setAccessTokenFactory(factory: accessTokenFactory)
209203

@@ -252,10 +246,14 @@ actor HttpConnection: ConnectionProtocol {
252246
inherentKeepAlivePrivate = true
253247
}
254248

255-
if connectionState == .connecting {
256-
logger.log(level: .debug, message: "The HttpConnection connected successfully.")
257-
connectionState = .connected
249+
guard closeDuringStartError == nil else {
250+
throw closeDuringStartError!
258251
}
252+
253+
// IMPORTANT: There should be no async code start from here. Otherwise, we may lost the control of the connection lifecycle
254+
255+
connectionState = .connected
256+
logger.log(level: .debug, message: "The HttpConnection connected successfully.")
259257
} catch {
260258
logger.log(level: .error, message: "Failed to start the connection: \(error)")
261259
connectionState = .disconnected
@@ -265,9 +263,17 @@ actor HttpConnection: ConnectionProtocol {
265263
}
266264

267265
private func stopInternal(error: Error?) async {
266+
guard connectionState != .disconnected else {
267+
return
268+
}
269+
268270
stopError = error
271+
closeDuringStartError = error ?? SignalRError.connectionAborted
269272

270273
do {
274+
// startInternalTask may have several cases:
275+
// 1. Already finished. Just return immediately
276+
// 2. Still in progress. Caused by closeDuringStartError, it will throw and set transport to nil
271277
try await startInternalTask?.value
272278
} catch {
273279
// Ignore errors from startInternal
@@ -278,9 +284,8 @@ actor HttpConnection: ConnectionProtocol {
278284
try await transport?.stop(error: nil)
279285
} catch {
280286
logger.log(level: .error, message: "HttpConnection.transport.stop() threw error '\(error)'.")
281-
await stopConnection(error: error)
287+
await handleConnectionClose(error: error)
282288
}
283-
transport = nil
284289
} else {
285290
logger.log(level: .debug, message: "HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.")
286291
}
@@ -367,46 +372,60 @@ actor HttpConnection: ConnectionProtocol {
367372
await transport!.onReceive(self.onReceive)
368373
await transport!.onClose { [weak self] error in
369374
guard let self = self else { return }
370-
await self.stopConnection(error: error)
375+
await self.handleConnectionClose(error: error)
371376
}
372377

373-
try await transport!.connect(url: url, transferFormat: transferFormat)
378+
do {
379+
try await transport!.connect(url: url, transferFormat: transferFormat)
380+
} catch {
381+
await transport!.onReceive(nil)
382+
await transport!.onClose(nil)
383+
throw error
384+
}
374385
}
375386

376-
private func stopConnection(error: Error?) async {
387+
private func handleConnectionClose(error: Error?) async {
377388
logger.log(level: .debug, message: "HttpConnection.stopConnection(\(String(describing: error))) called while in state \(connectionState).")
378389

379390
transport = nil
380391

381392
let finalError = stopError ?? error
382393
stopError = nil
394+
closeDuringStartError = finalError ?? SignalRError.connectionAborted
383395

384396
if connectionState == .disconnected {
385397
logger.log(level: .debug, message: "Call to HttpConnection.stopConnection(\(String(describing: finalError))) was ignored because the connection is already in the disconnected state.")
386398
return
387399
}
388400

389-
if connectionState == .connecting {
390-
logger.log(level: .warning, message: "Call to HttpConnection.stopConnection(\(String(describing: finalError))) was ignored because the connection is still in the connecting state.")
401+
if (connectionState == .connecting) {
402+
// connecting means start still control the lifetime. As we set closeDuringStartError, it throws there.
403+
logger.log(level: .debug, message: "Call to HttpConnection.stopConnection(\(String(describing: finalError))) was ignored because the connection is already in the connecting state.")
391404
return
392405
}
393406

394-
if connectionState == .disconnecting {
395-
// Any stop() awaiters will be scheduled to continue after the onClose callback fires.
396-
}
397-
398407
if let error = finalError {
399408
logger.log(level: .error, message: "Connection disconnected with error '\(error)'.")
400409
} else {
401410
logger.log(level: .information, message: "Connection disconnected.")
402411
}
403412

404413
connectionId = nil
414+
await completeConnectionClose(error: finalError)
415+
}
416+
417+
// Should be called whenever connection is started (start() doesn't throw and connection is closed)
418+
private func completeConnectionClose(error: Error?) async {
405419
connectionState = .disconnected
406420

407-
if connectionStarted {
408-
connectionStarted = false
409-
await onClose?(finalError)
421+
// There's a chance that we call close() and status changed to disconnecting and startinteral throws.
422+
// We should not call onclose again
423+
if connectionStartedSuccessfully {
424+
connectionStartedSuccessfully = false
425+
Task { [weak self] () in
426+
guard let self = self else { return }
427+
await self.onClose?(error)
428+
}
410429
}
411430
}
412431

Sources/SignalRClient/HubConnection.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public actor HubConnection {
2929

3030
private var stopTask: Task<Void, Never>?
3131
private var startTask: Task<Void, Error>?
32+
private var startSuccessfully = false
3233

3334
internal init(connection: ConnectionProtocol,
3435
logger: Logger,
@@ -64,6 +65,7 @@ public actor HubConnection {
6465

6566
try await startInternal()
6667
logger.log(level: .debug, message: "HubConnection started")
68+
startSuccessfully = true
6769
} catch {
6870
connectionStatus = .Stopped
6971
stopping = false
@@ -252,8 +254,10 @@ public actor HubConnection {
252254
logger.log(level: .information, message: "Connection closed")
253255
stopDuringStartError = error ?? SignalRError.connectionAborted
254256

257+
// Should not happen? It should either changed to stopped in another complete (which has called close handler) or in start() via throw
255258
if (connectionStatus == .Stopped) {
256-
await completeClose(error: error)
259+
// await completeClose(error: error)
260+
logger.log(level: .warning, message: "Connection is stopped during connection close. It won't trigger close handlers.")
257261
return
258262
}
259263

@@ -438,7 +442,12 @@ public actor HubConnection {
438442
stopping = false
439443
await keepAliveScheduler.stop()
440444
await serverTimeoutScheduler.stop()
441-
await triggerClosedHandlers(error: error)
445+
446+
// Either throw from start(), either call close handlers
447+
if (startSuccessfully) {
448+
startSuccessfully = false
449+
await triggerClosedHandlers(error: error)
450+
}
442451
}
443452

444453
private func startInternal() async throws {

0 commit comments

Comments
 (0)