supabase / realtime

Broadcast, Presence, and Postgres Changes via WebSockets
https://supabase.com/realtime
Apache License 2.0
6.78k stars 308 forks source link

Realtime connection unable to reconnect after TIMED_OUT #1088

Open farzd opened 3 months ago

farzd commented 3 months ago

Bug report

Describe the bug

When the realtime connection is lost on a native iOS/Android device [using Expo], attempting to auto reconnect always results in a loop. Keeps disconnecting after a reconnect.

To Reproduce

After a successful SUBSCRIBED subscription status, minimise the mobile app and lock the phone for 3 seconds. After reverting back to the app you should get two CHANNEL_ERROR subscription statuses [If you stay in the app, supabase manages to reconnect most of the time automatically]

But if you repeat the process before this reconnection and minimise the mobile app and lock the phone for 3 seconds. Reverting back to the app results in two more CHANNEL_ERROR subscription statuses. Followed by a CLOSED. After 10 seconds this results in a TIMED_OUT. if you start the reconnection process within this 10 seconds - the subscription fluctuates between SUBSCRIBED and CLOSED in a loop

Expected behavior

After CLOSED, the user defined reconnection strategy should result in a successful SUBSCRIBED state. This works if i manually disconnect [the disconnect button at the top in the screenshot triggers a removeChannel call for the active subscription ]

Screenshots

[see status log in the black] IMG_2171

System information

Additional context

The basic version of my reconnection strategy, i have tested this with various methods [like reconnectingFlag] etc etc but still failing to avoid the loop described above. I've also used a flag for channel status and to only reconnect when status is not 'joined' but the TIMED_OUT forces a CLOSED after it and i'm getting strange race conditions.

IMG_2174

 const subscriptionRef = useRef(null)
 const reconnectAttemptsRef = useRef(0)

  useEffect(() => {
    if (user_id) {
      subscribeToChannel()
    }

    return () => {
      if (subscriptionRef.current) {
        supabase.removeChannel(subscriptionRef.current)
      }
    }
  }, [user_id])

  const subscribeToChannel = () => {
    subscriptionRef.current = supabase
      .channel(`XXX:${user_id}`)
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'table',
          filter: `user_id=eq.${user_id}`,
        },
        (payload) => {
          // Reset reconnect attempts on successful message
          reconnectAttemptsRef.current = 0;
        }
      )
      .subscribe((status) =>  {
        console.log({ status });

        if (status === "SUBSCRIBED") {
          console.log("Successfully subscribed");
          reconnectAttemptsRef.current = 0;

        } else if (status === "CLOSED") {
          console.log("Subscription closed");
          reconnectWithBackoff();
        }
      })
  }

  const reconnectWithBackoff = () => {
    if (
      reconnectAttemptsRef.current < 5
    ) {
      const delay = 3000 * Math.pow(2, reconnectAttemptsRef.current)
      console.log(`Reconnecting in ${delay}ms...`)

      setTimeout(() => {
        console.log(`Reconnect attempt ${reconnectAttemptsRef.current + 1}`)
        subscribeToChannel()
        reconnectAttemptsRef.current += 1
      }, delay)
    } else {
      console.log('Max reconnection attempts reached. Please try again later.')
    }
  }
farzd commented 3 months ago

i'm resorting to this for now, not sure if its gonna have any performance implications on the server

  const subscriptionRef = useRef(null)

  AppState.addEventListener('change', (state) => {
    if (state === 'active') {
      if (!subscriptionRef.current) {
        subscribeToChannel()
      }
    } else {
      if (subscriptionRef.current) {
        supabase.removeChannel(subscriptionRef.current)
        subscriptionRef.current = null
      }
    }
  })
flogy commented 2 months ago

@farzd I too have the issue that my customers tell me some values are not updated anymore, which for me is clearly because of broken realtime connections. When testing locally, it seem to always reconnect after the app was in background or the network connection was interrupted, thought. Were you able to fix your issue using the foreground change listener above?

farzd commented 2 months ago

@flogy i've raised a ticket and the team got back to me. [10th July]

Hi Farzad,

We've been seeing this as a common issue with mobile applications that user Expo or React Native. We're actively working on debugging what could be wrong on our side but it seems that we need to move some of the JWT refresh logic to background workers as that seems to be breaking when something is moved to be inactive.

Thank you for reporting the issue and we will inform in the issue when we have more information.

i'm finding that the reconnect on app foregrounding appears to be functioning faster / better than maintaining the same connection, mentioned above: https://github.com/supabase/realtime/issues/1088#issuecomment-2213064814 [of course be aware that upon initial subscription connection you get a full payload, AFAIK] And the server load seems to be fine with this strategy