Backendless iOS SDK RTClient.swift potential deadlock bug

Dear Backendless Support Team,

I’m using Backendless iOS SDK to implement the real-time messaging function in my app. I noticed there is something wrong with message listener when user presses the home button and moves the app to the background. Hope you could take a look. Detailed information are as follows.

Problem Description

In RTClient.swift, in the subscribe method, there is self._lock.lock() action. The condition of unlock is either self.socketConnected or execute self.connectSocket and unlock in the connected handler. Notice the self.connectSocket is an async method and usually it’ll take time.

However, when user press home button and the app goes to background, there is a chance that the connection is lost. Therefore the self.needResubscribe will be set to true. App will call subscribe once and onConnectionHandlers will call subscribe again because self.needResubscribe is true. Thus the deadlock.

Backendless Version (3.x / 5.x, Online / Managed / Pro )

5.x

Client SDK (REST / Android / Objective-C / Swift / JS )

Swift

Application ID

3B8C2D2E-B7D6-27F9-FF3F-ABEE20934E00

Expected Behavior

  1. I start a message listener in my ‘ChatRoom’ view controller. Then I press the home button and the app goes to background. After a while when I re-enter the app, the listener should work, or at least I can add a new listener to make the real-time chat continue.

Actual Behavior

  1. After re-entering the app, the listener won’t receive new messages anymore. I didn’t close the app for the entire process.
  2. I can’t add new listener either. When I call addCustomObjectMessageListener, nothing happened. No error code, response handled isn’t called, error handler isn’t called.
  3. I tried removing all listeners before the app goes to background and add listener when app reopens, still not work.
  4. This not just happens to MessageListener, also happens to realtime database listener.

Reproducible Test Case

The Real time chat sample code provided in the code generation section is a good example. It has the same problem. When the app goes to background and re-enters into foreground, the chat is dead. Publish still works but the message listener is dead.

Hello @Evan_Cui,

I’ve created an internal ticket BKNDLSS-22000 to investigate this issue.

Regards,
Olha

Hi Olha, Thanks for replying. My two cents, in onConnectionHandlers method, the lock is only released when socket is on connect and self.needResubscribe is false. For all other cases such as connect error cases, the lock is still held by the thread. Especially when self.needResubscribe is true, subscribe is called again and deadlock occurs.
Not sure if this makes sense. Happy to discuss further.

Thank you for clarification.
Could you please attаch a simple code snippet that reproduces it as you described?
This problem will be investigated as soon as possible.

Regards,
Olha

minimum_demo.zip (429.0 KB)

Hi Olha, thanks for your reply. The sample is very simple. Substitute the API key and app Key before running.

To reproduce the bug: There are a couple of ways to reproduce the bugs. I recommend to run this sample on a real device so it’s easier to reproduce.
Option 1. press any button to start a channel. Then press the home button to make the app to the background. Just open whatever another app, say Google, do some search request. Then switch back to this sample app, the listener will be dead. Error could be connection closed by server. code=1000, type=protocolError. Then nothing you can do to restart the channel, even if you want to start a different new channel, it’s not gonna work.
Option 2. Press Chat One button or Chat Two button really quick, e.g. 10 times in a row. the listener will be dead too. Error could be Tried emitting when not connected. Still, there’s nothing we could do to restart the channel except close the app and reopen.

The point is, whenever something error with the socket happens, The whole real-time system is dead. It’s really common and easy to run into those errors in practice.

Looking forward to hearing from you soon. Thanks!

Thanks,
I’ll look into it.

Regards,
Olha

Well, okay.

Option 1. Unfortuanately Socket.IO we use for our RT doesn’t work from the background mode on iOS: https://github.com/socketio/socket.io-client-swift/issues/1248
I assume this answer can be useful: https://github.com/socketio/socket.io-client-swift/issues/1151 in this case.

Option 2. This answer could be useful: https://github.com/nuclearace/Socket.IO-Client-Swift/issues/154
Wait till you get the "connect" event before emitting anything.

I understand that’s not very perfect but this is how Socket.IO works.

Regards,
Olha

Hi Olha,
For scenario one, I can leave the channel and stop the subscription once app goes to background.

However for scenario 2, in Backendless API, there is no easy way to check the Socket Status. Simply channel.isJoined isn’t enough.

Again, once there’s something wrong with Socket.IO, is there any way we could recover with Backendless API without quit the app?

Thanks!

Hello Evan,

I assume the rt connection listeners are exactly what you need:

func addConnectionListener() {
    let channelConnectListener = channel?.addConnectListener(responseHandler: {
        print("Channel \(self.channel?.channelName ?? "...") is connected")
    }, errorHandler: { fault in
        print("Connection fault: \(fault.message ?? "")")
	})
    
    let connectListener = Backendless.shared.rt.addConnectEventListener(responseHandler: {
        print("Socket connected")

        // wait until socket is connected and then do smth
    })
        
    let connectErrorListener = Backendless.shared.rt.addConnectErrorEventListener(responseHandler: { reason in
        print("Socket cannot be connected: \(reason)")
    })
    
    let disconnectListener = Backendless.shared.rt.addDisсonnectEventListener(responseHandler: { reason in
        print("Socket disconnected: \(reason)")
    })
    
    let reconnectListener = Backendless.shared.rt.addReconnectAttemptEventListener(responseHandler: { reconnectAttempt in
            print("Trying to reconnect...")
        })
}

E.g. when you press the Chat one button, socket tries to connect. It may take 1-2 seconds or so, so you shouldn’t trigger any other socket methods until it is connected.
After your application enters background, socket is disconnected. But it tries to reconnect after app enters foreground, and you’ll see the connectListener’s response right after it is connected again (or error, if not).

I’ve recorded a video that shows how it works: https://monosnap.com/file/ZUsJgijyStuQo1jW7meqWcLvYxVgsk

Regards,
Olha

Hi @olhadanylova,
Thanks for your detailed explanation and sample code. I tried this and also dug into the source code, it worked.

I really didn’t think adding listeners would actually change the program behavior. It seems only by adding connectErrorListener and disconnectListener can this program trigger the reconnect. Otherwise the socket is just dead. I’d suggest making that clear in the documentation, which can save users much trouble.

This is quite a tricky problem for me. Thanks for solving this for me.
Best!
Evan

1 Like