ably-js icon indicating copy to clipboard operation
ably-js copied to clipboard

React Native: distinguish disconnected state and suspended state for app goes to background

Open yibo-long opened this issue 1 year ago • 16 comments

We are trying to handle the state difference for a Native React app that requires going to background frequently. The difference state would lead us to different behaviors as:

  • if the app is only disconnected for a short time within 2mins, the app doesn't need to ask server to publish messages for the latest server state, but rely on Ably to buffer based on https://faqs.ably.com/connection-state-recovery
  • if the app is disconnected for a longer time that Ably treats it as connection closed, we will have to ask the server to publish the latest server state to the app's Ably channel again

Our current implementation is like this atm:

this.ably?.connection.on('disconnected', () => {
  // ask server to send a latest state
});

This is however triggered with logs like this for the app put into background for 15s:

Ably: Transport.onIdleTimerExpire(): No activity seen from realtime in 33964ms; assuming connection has dropped

and triggered disconnected callback regardless.

For 2mins longer, the behavior is exactly the same as above with disconnected event, without a suspended event.

Can we distinguish the difference in the SDK for client going to background longer than 2mins with a suspended event as the fact that the connection is closed by Ably server?

┆Issue is synchronized with this Jira Bug by Unito

yibo-long avatar Dec 19 '23 23:12 yibo-long

➤ Automation for Jira commented:

The link to the corresponding Jira issue is https://ably.atlassian.net/browse/SDK-4006

sync-by-unito[bot] avatar Dec 19 '23 23:12 sync-by-unito[bot]

Hi @yibo-long,

Thanks for bringing this to our attention! Let me clarify: you mentioned that you don't receive the suspended state when the client goes to the background for longer than 2 minutes. Can you double-check that you are subscribed to it, for example, using ably.connection.on('suspended', ...);?

ttypic avatar Dec 21 '23 15:12 ttypic

I did with:

      this.ably?.connection.on('suspended', () => {
        console.log('connection suspended');
      });
      this.ably?.connection.on('closed', () => {
        console.log('connection closed');
      });
      this.ably?.connection.on('disconnected', () => {
        console.log('connection disconnected');
      });
      this.ably?.connection.on('connected', () => {
        console.log('connection connected');
      });

I strictly checked Ably Dev console to wait for Connection Closed event to bring back app to foreground.

Both suspended and closed were not popped up from logs, and disconnected connected events were logged in sequence in milliseconds gap:

 WARN  08:59:38.385 Ably: Transport.onIdleTimerExpire(): No activity seen from realtime in 155098ms; assuming connection has dropped
 LOG  XXX: connection disconnected
 LOG  XXX: connection connected

yibo-long avatar Dec 21 '23 17:12 yibo-long

Thank you @yibo-long! We will investigate this internally. Could you, by any chance, provide a reproducible example?

ttypic avatar Dec 21 '23 21:12 ttypic

Hello @yibo-long, could you please provide more details? Specifically, in which environment can you reproduce the bug? Is it on an iPhone or Android device? Additionally, could you clarify whether it occurs on an actual device or simulator? Are there any other steps that can help us reproduce the issue?

ttypic avatar Jan 02 '24 19:01 ttypic

Hi sorry, I don't have a public reproducible example yet.

I only tested it on ios simulator and real ios debug app this moment, but can definitely try it in more environments.

Let me clarify the steps to reproduce:

  1. app setup: Have a singleton AblyService class that wraps Ably.Realtime initialization with authCallback and channel subscription. It's initialized in React Native's root app;
  2. Open up app and wait for AblyService to create connection, channel subscription and subscribe to disconnected, suspended, closed events;
  3. Checked Ably dev console that there is a log Channel Opened event;
  4. Bring app to background;
  5. In ~20s or a bit longer, checked Ably dev console that there is a log Transport Closed event for the app going offline.
  6. Wait for another exact 2min after the Transport Closed event, to check the Connection Closed event for the connection is forced closed by Ably. Between 3.-6., there is no log from the app as it's completely in background without activity.
  7. Bring app back to foreground, only disconnected event is triggered. No 'suspend' event to indicate that any messages after the 2mins are lost.

yibo-long avatar Jan 02 '24 19:01 yibo-long

Thank you very much @yibo-long. Could you provide insights into the urgency of this issue? We are currently in the middle of the JS v2 release, and our resources are limited at the moment. Would it be ok if we come back to it in 2 weeks?

ttypic avatar Jan 03 '24 14:01 ttypic

it appears that Android has completely different behaviors. The app was not suspended, and the connection was still connected when app is in the background. Still it only received 'disconnected' event when putting to background for too long.

yibo-long avatar Jan 03 '24 16:01 yibo-long

Thank you @ttypic for providing the context! This is not an urgent request but more a question at this moment for clarification.

We want to know how to distinguish the difference Ably mentioned in terms of Transport Closed vs Connection Closed in the client.

yibo-long avatar Jan 03 '24 16:01 yibo-long

@yibo-long The recommended approach is to listen for the 'suspended' event. However, it appears that this method may not function as intended when the app is in the background. As a workaround, you can consider the following options:

  • Subscribe to the React Native's AppState and, when the app transitions to the background, treat it as if the connection has been suspended, prompting the need to refetch data.

  • Run Ably in Headless JS. Unfortunately, please note that this solution is applicable only for Android.

ttypic avatar Jan 03 '24 18:01 ttypic

Hi @yibo-long, I'm taking a look into this issue. I can confirm that I've reproduced the behaviour you're seeing, when running in the iOS Simulator.

It's worth mentioning here that the behaviour you're seeing is not exclusive to React Native. The same behaviour can be observed by running ably-js in a browser, and putting your computer to sleep for ~2.5 minutes and then waking it up again. You'll observe the same connection state transition sequence (connecteddisconnected (after waking up) → connectingconnected).

I believe that not seeing a state transition to suspended is an expected behaviour here. Our recommended way to discover that channel continuity has been lost is not to listen for the connection becoming suspended, but to listen for a channel attached event with resumed == false. To quote from the support article that you linked to (emphasis mine):

Once the connection is reestablished, the client library will reattach the suspended channels automatically and emit an attached event with the resumed flag set to false. This ensures that as a developer, you can listen for attached events and check the resumed flag to see if a channel resumed fully and no messages were lost (when resumed is true), or the channel attached but could not resume (when resumed is false).

I started writing an example for you of what this would look like in React Native, and wished to demonstrate how to fetch the messages that were not delivered (using the history API), but have run into an issue which I am currently seeking help with internally. I will get back to you once I have an update on this.

lawrence-forooghian avatar Jan 24 '24 16:01 lawrence-forooghian

Thanks @lawrence-forooghian I wanted to give it a try with attached event, but so far failed to receive any channel level callbacks like this:

ably?.connection.on('disconnected', () => {
        console.log('AblyService: disconnected');
        channel.whenState('attached', (changeStateChange) => {
          console.log('AblyService: attached to channel', this.channelName, changeStateChange);
        });
      });

There seems like a .on callback function at channel level like the one at connection level, so I am not sure if it's subscribing at after connection disconnected already too late.

For 25s Transport Closed, I got logs from above code as:

 LOG  AblyService: disconnected
 LOG  AblyService: attached to channel c:19523ec5747b400281f1d0c2dee2a391:5A989E2A-2179-4EC0-A120-E794D267ACE4 undefined

For ~2.5min Connection Closed, I got logs from above code as:

 WARN  14:19:07.275 Ably: Transport.onIdleTimerExpire(): No activity seen from realtime in 186150ms; assuming connection has dropped
 LOG  AblyService: disconnected
 LOG  AblyService: attached to channel c:19523ec5747b400281f1d0c2dee2a391:5A989E2A-2179-4EC0-A120-E794D267ACE4 undefined

yibo-long avatar Jan 24 '24 22:01 yibo-long

found .on function at channel level, and I think I made it work with following code:

      this.ably.connection.whenState('connected', () => {
        const channel = this.ably?.channels.get(this.channelName ?? '');
        channel?.on('attached', (changeStateChange) => {
          console.log('AblyService: attached to channel', this.channelName, changeStateChange);
        });
      });

yibo-long avatar Jan 24 '24 23:01 yibo-long

found .on function at channel level, and I think I made it work with following code:

      this.ably.connection.whenState('connected', () => {
        const channel = this.ably?.channels.get(this.channelName ?? '');
        channel?.on('attached', (changeStateChange) => {
          console.log('AblyService: attached to channel', this.channelName, changeStateChange);
        });
      });

Yes, channel.on is the right thing to use. To detect a loss in channel continuity, you should then check the value of changeStateChange.resumed. If it‘s false, then channel continuity has been lost and if you need to access these messages then you'll have to fetch them from history (this fetching from history is the thing that I mentioned I was having issues with in https://github.com/ably/ably-js/issues/1559#issuecomment-1908548893).

On another note, I am wondering what was your motivation for wrapping your attached listener inside .whenState('connected')? That shouldn’t be necessary.

lawrence-forooghian avatar Jan 26 '24 16:01 lawrence-forooghian

For 25s Transport Closed, I got logs from above code as:

 LOG  AblyService: disconnected
 LOG  AblyService: attached to channel c:19523ec5747b400281f1d0c2dee2a391:5A989E2A-2179-4EC0-A120-E794D267ACE4 undefined

I've also noticed from your log that the channel’s whenState method emitted a state change of undefined. This is a bug, and I've raised https://github.com/ably/ably-js/issues/1598 for it.

lawrence-forooghian avatar Jan 26 '24 17:01 lawrence-forooghian

Thanks @lawrence-forooghian ! I verified it fixed our issue.

On another note, I am wondering what was your motivation for wrapping your attached listener inside .whenState('connected')? That shouldn’t be necessary.

We are currently using authCallback with backend sending us the authenticated channelName and token, so can only get a valid channel name to hook with until that. We are refactoring some of those to be based on Ably hooks so hopefully won't need to maintain the callbacks in such way later.

yibo-long avatar Jan 26 '24 17:01 yibo-long

Hi @yibo-long, do you still need help with this issue? I can post the example code that I was referring to in https://github.com/ably/ably-js/issues/1559#issuecomment-1908548893 if that would help (the problem that I encountered was due to a backend bug that's now fixed).

lawrence-forooghian avatar May 24 '24 17:05 lawrence-forooghian

@lawrence-forooghian we are all good now, thanks so much for the help!

yibo-long avatar May 24 '24 18:05 yibo-long