Skip to content

Commit d740ca9

Browse files
authored
fix: prevent crash when disconnecting etcd connection (#456)
* fix: prevent crash when disconnecting etcd connection * docs: add CHANGELOG entry for etcd disconnect crash fix
1 parent 97912e4 commit d740ca9

File tree

2 files changed

+30
-8
lines changed

2 files changed

+30
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
### Fixed
1919

2020
- Saved connections disappearing after normal app quit (Cmd+Q) while persisting after force quit (#452)
21+
- Crash when disconnecting an etcd connection while requests are in-flight
2122
- Detail pane showing truncated values for LONGTEXT/MEDIUMTEXT/CLOB columns, preventing correct editing
2223
- Redis hash/list/set/zset/stream views showing empty or misaligned rows when values contained binary, null, or integer types
2324

Plugins/EtcdDriverPlugin/EtcdHttpClient.swift

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,7 @@ internal final class EtcdHttpClient: @unchecked Sendable {
310310
private let config: DriverConnectionConfig
311311
private let lock = NSLock()
312312
private var session: URLSession?
313+
private var sessionGeneration: UInt64 = 0
313314
private var currentTask: URLSessionDataTask?
314315
private var authToken: String?
315316
private var _isAuthenticating = false
@@ -408,6 +409,7 @@ internal final class EtcdHttpClient: @unchecked Sendable {
408409

409410
func disconnect() {
410411
lock.lock()
412+
sessionGeneration &+= 1
411413
currentTask?.cancel()
412414
currentTask = nil
413415
session?.invalidateAndCancel()
@@ -539,11 +541,12 @@ internal final class EtcdHttpClient: @unchecked Sendable {
539541

540542
func watch(key: String, prefix: Bool, timeout: TimeInterval) async throws -> [EtcdWatchEvent] {
541543
lock.lock()
542-
guard let session else {
544+
guard session != nil else {
543545
lock.unlock()
544546
throw EtcdError.notConnected
545547
}
546548
let token = authToken
549+
let generation = sessionGeneration
547550
lock.unlock()
548551

549552
let b64Key = Self.base64Encode(key)
@@ -571,7 +574,13 @@ internal final class EtcdHttpClient: @unchecked Sendable {
571574

572575
group.addTask {
573576
let data: Data = try await withCheckedThrowingContinuation { continuation in
574-
let task = session.dataTask(with: request) { data, _, error in
577+
self.lock.lock()
578+
guard self.sessionGeneration == generation, let currentSession = self.session else {
579+
self.lock.unlock()
580+
continuation.resume(throwing: EtcdError.notConnected)
581+
return
582+
}
583+
let task = currentSession.dataTask(with: request) { data, _, error in
575584
if let error {
576585
// URLError.cancelled is expected when we cancel after timeout
577586
if (error as? URLError)?.code == .cancelled {
@@ -583,7 +592,6 @@ internal final class EtcdHttpClient: @unchecked Sendable {
583592
}
584593
continuation.resume(returning: data ?? Data())
585594
}
586-
self.lock.lock()
587595
self.currentTask = task
588596
self.lock.unlock()
589597
collectedData.setTask(task)
@@ -698,6 +706,7 @@ internal final class EtcdHttpClient: @unchecked Sendable {
698706
throw EtcdError.notConnected
699707
}
700708
let token = authToken
709+
let generation = sessionGeneration
701710
lock.unlock()
702711

703712
guard let url = URL(string: "\(baseUrl)/\(path)") else {
@@ -714,7 +723,13 @@ internal final class EtcdHttpClient: @unchecked Sendable {
714723

715724
let (data, response) = try await withTaskCancellationHandler {
716725
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(Data, URLResponse), Error>) in
717-
let task = session.dataTask(with: request) { data, response, error in
726+
self.lock.lock()
727+
guard self.sessionGeneration == generation, let currentSession = self.session else {
728+
self.lock.unlock()
729+
continuation.resume(throwing: EtcdError.notConnected)
730+
return
731+
}
732+
let task = currentSession.dataTask(with: request) { data, response, error in
718733
if let error {
719734
continuation.resume(throwing: error)
720735
return
@@ -725,8 +740,6 @@ internal final class EtcdHttpClient: @unchecked Sendable {
725740
}
726741
continuation.resume(returning: (data, response))
727742
}
728-
729-
self.lock.lock()
730743
self.currentTask = task
731744
self.lock.unlock()
732745

@@ -806,15 +819,22 @@ internal final class EtcdHttpClient: @unchecked Sendable {
806819
request.httpBody = try JSONEncoder().encode(authReq)
807820

808821
lock.lock()
809-
guard let session else {
822+
guard session != nil else {
810823
lock.unlock()
811824
throw EtcdError.notConnected
812825
}
826+
let generation = sessionGeneration
813827
lock.unlock()
814828

815829
let (data, response) = try await withCheckedThrowingContinuation {
816830
(continuation: CheckedContinuation<(Data, URLResponse), Error>) in
817-
let task = session.dataTask(with: request) { data, response, error in
831+
self.lock.lock()
832+
guard self.sessionGeneration == generation, let currentSession = self.session else {
833+
self.lock.unlock()
834+
continuation.resume(throwing: EtcdError.notConnected)
835+
return
836+
}
837+
let task = currentSession.dataTask(with: request) { data, response, error in
818838
if let error {
819839
continuation.resume(throwing: error)
820840
return
@@ -825,6 +845,7 @@ internal final class EtcdHttpClient: @unchecked Sendable {
825845
}
826846
continuation.resume(returning: (data, response))
827847
}
848+
self.lock.unlock()
828849
task.resume()
829850
}
830851

0 commit comments

Comments
 (0)