socketio / socket.io-client-swift

Other
5.21k stars 839 forks source link

Potential Memory Management Issue with emitWithAck and timingOut(after:) #1470

Open mahyarq opened 8 months ago

mahyarq commented 8 months ago

Environment:

Library Version: 16.1.0 Xcode: tested on Xcode 14 and Xcode 15 Platform: iOS 16 and iOS 17

Description:

I've encountered an issue with the emitWithAck function in the Socket.IO-Client-Swift library, specifically when using the timingOut(after:) closure. The OnAckCallback object returned by emitWithAck seems to get deallocated prematurely if not explicitly retained in the client code.

In addition to the premature deallocation of the OnAckCallback instance, a critical part of the issue is that the callback code socket.ackHandlers.timeoutAck(self.ackNumber)within the timingOut(after:)function is never executed as expected. This is because self (referring to the OnAckCallback instance) becomes nil after the timeout period elapses. This behavior results in the failure of the timeout handling logic, as the necessary callback for a timeout situation is not called.

Steps to Reproduce:

  1. Call emitWithAck().timingOut(after:) on a SocketIOClient instance. Make sure server doesnt send any callbacks.
  2. Observe that the OnAckCallback instance is deallocated before the timeout closure is executed, resulting in self being nil within the closure.

Expected Behavior:

The OnAckCallback instance should be retained internally by the library until the timingOut(after:) closure is executed or the timeout occurs.

Actual Behavior:

The OnAckCallback instance is deallocated prematurely unless explicitly retained in the client code.

My hacky fix

In my current SocketHandler class, i've added a var emitAckCallBack: OnAckCallback? which i assign the value of socket.emitWithAck() and afterwards i call emitAckCallBack?.timingOut(after:) and thereafter set the emitAckCallBack to nil inside the completion block. This seems to hold a reference to OnAckCallback which prevents it from getting deallocated prematurely.

Code example:

var emitAckCallBack: OnAckCallback?

func send() {
    emitAckCallBack = self.socket.emitWithAck("send_event", "test")

    emitAckCallBack?.timingOut(after: 2) {  data in
        print("Ack received with data: \(data)")
    }
}
xmollv commented 6 months ago

I've been going crazy trying to figure out why the timingOut would never fire. And indeed, the issue is that without retaining it it's being deallocated. You can easily see the issue on OnAckCallback.swift:140. self is nil when the dispatch fires and therefore the callback is never called!

image

// @nuclearace