fix: clear pending pings to avoid promise leaks on connection close/suspend
- Add cancel() method to RttCommand to fail promise on connection close - Add dequeueAll() method to ConcurrentQueue for batch cleanup - Call clearPendingPings() in close(), suspend(), and disconnect() methods - Prevents 'leaking promise' crash when connection is closed while pings are pending
This commit is contained in:
@@ -29,4 +29,13 @@ internal class ConcurrentQueue<T> {
|
||||
guard !elements.isEmpty else { return nil }
|
||||
return elements.removeFirst()
|
||||
}
|
||||
|
||||
/// Dequeue all elements at once (used for cleanup)
|
||||
func dequeueAll() -> [T] {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let all = elements
|
||||
elements.removeAll()
|
||||
return all
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,6 +712,7 @@ class ConnectionHandler: ChannelInboundHandler {
|
||||
guard let eventLoop = self.channel?.eventLoop else {
|
||||
self.state.withLockedValue { $0 = .closed }
|
||||
self.pingTask?.cancel()
|
||||
clearPendingPings() // Clear pending pings to avoid promise leaks
|
||||
self.fire(.closed)
|
||||
return
|
||||
}
|
||||
@@ -720,6 +721,7 @@ class ConnectionHandler: ChannelInboundHandler {
|
||||
eventLoop.execute {
|
||||
self.state.withLockedValue { $0 = .closed }
|
||||
self.pingTask?.cancel()
|
||||
self.clearPendingPings() // Clear pending pings to avoid promise leaks
|
||||
self.channel?.close(mode: .all, promise: promise)
|
||||
}
|
||||
|
||||
@@ -735,9 +737,21 @@ class ConnectionHandler: ChannelInboundHandler {
|
||||
|
||||
private func disconnect() async throws {
|
||||
self.pingTask?.cancel()
|
||||
clearPendingPings() // Clear pending pings to avoid promise leaks
|
||||
try await self.channel?.close().get()
|
||||
}
|
||||
|
||||
/// Clear all pending ping requests to avoid promise leaks
|
||||
private func clearPendingPings() {
|
||||
let pendingPings = pingQueue.dequeueAll()
|
||||
for ping in pendingPings {
|
||||
ping.cancel()
|
||||
}
|
||||
if !pendingPings.isEmpty {
|
||||
logger.debug("Cleared \(pendingPings.count) pending ping(s)")
|
||||
}
|
||||
}
|
||||
|
||||
func suspend() async throws {
|
||||
self.reconnectTask?.cancel()
|
||||
_ = try await self.reconnectTask?.value
|
||||
@@ -746,6 +760,7 @@ class ConnectionHandler: ChannelInboundHandler {
|
||||
guard let eventLoop = self.channel?.eventLoop else {
|
||||
// Set state to suspended even if channel is nil
|
||||
self.state.withLockedValue { $0 = .suspended }
|
||||
clearPendingPings() // Clear pending pings to avoid promise leaks
|
||||
return
|
||||
}
|
||||
let promise = eventLoop.makePromise(of: Void.self)
|
||||
@@ -759,8 +774,10 @@ class ConnectionHandler: ChannelInboundHandler {
|
||||
|
||||
if shouldClose {
|
||||
self.pingTask?.cancel()
|
||||
self.clearPendingPings() // Clear pending pings to avoid promise leaks
|
||||
self.channel?.close(mode: .all, promise: promise)
|
||||
} else {
|
||||
self.clearPendingPings() // Clear pending pings even if not closing
|
||||
promise.succeed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,9 @@ internal class RttCommand {
|
||||
func getRoundTripTime() async throws -> TimeInterval {
|
||||
try await promise?.futureResult.get() ?? 0
|
||||
}
|
||||
|
||||
/// Cancel the ping request to avoid promise leaks when connection is closed
|
||||
func cancel() {
|
||||
promise?.fail(NatsError.ClientError.connectionClosed)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user