How to reconnect and rejoin when mobile app goes to background. Best practice?

Hi guys, we are using PUN for almost a year and are very happy with it. The only problem we have is handling the case where the mobile app is going to the background and to trigger a successful reconnect and rejoin. We can’t seem to find some best practices or code snippets so we cooked up something ourselves. The following implementation seems to work most of the times but it’s not 100% reliable. We still have cases where the app is not successfully reconnecting and rejoining.

We hope you could take the time and spot some problems with this script. We also hope you could provide us with a best practice for reconnecting and rejoining.

https://pastebin.com/X83mubxf

FYI:
- The room we create has a PlayerTTL of 300000.
- We are only targeting iOS.
- PUN v2.7
- Unity 2018.3.5

Thanks!

Comments

  • JohnTubeJohnTube mod
    edited May 27
    Hi @Burny123,

    Try this UtilityScript.
    Do you have the same issue described here?
  • @JohnTube Thanks for helping out. The link you provided leads to an empty page on hastebin, I guess something went wrong?

    The issue from the link is not the same. We can successfully reconnect and rejoin most of the times, but not always.. It seems random and is hard for us to debug.

    I love to see the UtilityScript.

    Thanks!
  • Hey @Burny123,

    I have checked again and I think the link works fine now.
    Maybe there was an issue yesterday.
    Anyways find the code also here:
    using System;
    using Photon.Realtime;
    using UnityEngine;

    namespace Photon.Pun.UtilityScripts
    {
    ///
    /// Unexpected disconnects recovery
    ///
    public class DisconnectsRecovery : MonoBehaviourPunCallbacks { [Tooltip("Whether or not attempt a rejoin without doing any checks.")] [SerializeField] private bool skipRejoinChecks; [Tooltip("Whether or not realtime webhooks are configured with persistence enabled")] [SerializeField] private bool persistenceEnabled; private bool rejoinCalled; private int minTimeRequiredToRejoin = 0; // TODO: set dynamically based on PhotonNetwork.NetworkingClient.LoadBalancingPeer.RoundTripTime private DisconnectCause lastDisconnectCause; private bool wasInRoom; private bool reconnectCalled; public override void OnEnable() { base.OnEnable(); PhotonNetwork.NetworkingClient.StateChanged += this.OnStateChanged; } public override void OnDisable() { base.OnDisable(); PhotonNetwork.NetworkingClient.StateChanged -= this.OnStateChanged; } private void OnStateChanged(ClientState fromState, ClientState toState) { if (toState == ClientState.Disconnected) { Debug.LogFormat("OnStateChanged from {0} to {1}, PeerState={2}", fromState, toState, PhotonNetwork.NetworkingClient.LoadBalancingPeer.PeerState); this.HandleDisconnect(); } } public override void OnDisconnected(DisconnectCause cause) { Debug.LogFormat("OnDisconnected(cause={0}) ClientState={1} PeerState={2}", cause, PhotonNetwork.NetworkingClient.State, PhotonNetwork.NetworkingClient.LoadBalancingPeer.PeerState); if (rejoinCalled) { Debug.LogError("Rejoin failed, client disconnected"); rejoinCalled = false; return; } if (reconnectCalled) { Debug.LogError("Reconnect failed, client disconnected"); reconnectCalled = false; return; } lastDisconnectCause = cause; wasInRoom = PhotonNetwork.CurrentRoom != null; if (PhotonNetwork.NetworkingClient.State == ClientState.Disconnected) { this.HandleDisconnect(); } } private void HandleDisconnect() { switch (lastDisconnectCause) { case DisconnectCause.Exception: case DisconnectCause.ServerTimeout: case DisconnectCause.ClientTimeout: case DisconnectCause.DisconnectByServerLogic: case DisconnectCause.AuthenticationTicketExpired: case DisconnectCause.DisconnectByServerReasonUnknown: if (wasInRoom) { this.CheckAndRejoin(); } else { Debug.Log("PhotonNetwork.Reconnect called"); reconnectCalled = PhotonNetwork.Reconnect(); } break; case DisconnectCause.OperationNotAllowedInCurrentState: case DisconnectCause.CustomAuthenticationFailed: case DisconnectCause.DisconnectByClientLogic: case DisconnectCause.InvalidAuthentication: case DisconnectCause.ExceptionOnConnect: case DisconnectCause.MaxCcuReached: case DisconnectCause.InvalidRegion: case DisconnectCause.None: break; default: throw new ArgumentOutOfRangeException("cause", lastDisconnectCause, null); } lastDisconnectCause = DisconnectCause.None; wasInRoom = false; } public override void OnJoinRoomFailed(short returnCode, string message) { if (!rejoinCalled) { return; } rejoinCalled = false; Debug.LogErrorFormat("Quick rejoin failed with error code: {0} & error message: {1}", returnCode, message); } public override void OnJoinedRoom() { if (rejoinCalled) { Debug.Log("Rejoin successful"); rejoinCalled = false; } } private void CheckAndRejoin() { if (skipRejoinChecks) { Debug.Log("PhotonNetwork.ReconnectAndRejoin called"); rejoinCalled = PhotonNetwork.ReconnectAndRejoin(); } else { bool wasLastActivePlayer = true; if (!persistenceEnabled) { for (int i = 0; i < PhotonNetwork.PlayerListOthers.Length; i++) { if (!PhotonNetwork.PlayerListOthers[i].IsInactive) { wasLastActivePlayer = false; break; } } } if ((PhotonNetwork.CurrentRoom.PlayerTtl < 0 || PhotonNetwork.CurrentRoom.PlayerTtl > minTimeRequiredToRejoin) // PlayerTTL checks && (!wasLastActivePlayer || PhotonNetwork.CurrentRoom.EmptyRoomTtl > minTimeRequiredToRejoin || persistenceEnabled)) // EmptyRoomTTL checks { Debug.Log("PhotonNetwork.ReconnectAndRejoin called"); rejoinCalled = PhotonNetwork.ReconnectAndRejoin(); } else { Debug.Log("PhotonNetwork.ReconnectAndRejoin not called, PhotonNetwork.Reconnect is called instead."); reconnectCalled = PhotonNetwork.Reconnect(); } } } public override void OnConnectedToMaster() { if (reconnectCalled) { Debug.Log("Reconnect successful"); reconnectCalled = false; } } }
    }
  • @JohnTube The hastebin still shows an empty page for me. But I'm going to try out the snippet you provided.

    I'll report the results, thanks!
  • @JohnTube I tried the script but it not works on my iPad. The main issue seems to be:

    ’Reconnect() failed. Can only connect while in state 'Disconnected'. Current state: Connected’

    Full log: https://pastebin.com/xZCZJibz

    We had this issue too and added a coroutine which waits for the player to be disconnected fully.

    What do you suggest to do from here?

    Thanks!
  • Hi @Burny123,

    I can only think of adding a coroutine to detect when a client + peer are fully properly and disconnected before attempting a reconnect or a rejoin. So it's like your solution, however, you said;
    implementation seems to work most of the times but it’s not 100% reliable. We still have cases where the app is not successfully reconnecting and rejoining.
    can we try to find out how to reproduce those cases? Let's try to narrow it down.
  • @JohnTube Sure thing, one of these days I'm going to create a full test environment to try and narrow it down.

    To be continued, thanks!
  • Hi @Burny123,

    In order to prioritize this you can send an email to [email protected] with all the delails.
    Thanks.
  • ok,

    just for the record should other face this issue while we discuss this internally:

    when you get a disconnect call back, raise a boolean provate variable, and during an update call, try to reconnect if that bool is true, this is the shortest way to reconnect and avoid the issue explained above.

    It's going to be tricky, because it could be a case where it's a race condition situation, if we change the order of property sets and callbacks, it could break other projects.

    Bye,

    Jean
  • @jeanfabre

    Is the Utility Script you provided still valid, or does it need updating?
  • Hi,

    yes, it needs updating. here's the code featuring the update for the reconnecting to occur,

    https://pastebin.com/wk39tgzA

    Let me know how it goes.

    Bye,

    Jean


  • JohnTubeJohnTube mod
    edited June 7
    I want to clarify/rectify something:
    the script shared by @jeanfabre has some extra lines we used for testing internally:

    remove them from OnEnable:

    before:
            public override void OnEnable()
            {
                base.OnEnable();
                PhotonNetwork.NetworkingClient.StateChanged += this.OnStateChanged;
                PhotonNetwork.KeepAliveInBackground = 0f;
                PhotonNetwork.PhotonServerSettings.RunInBackground = false;
                Application.runInBackground = false;
            }
    after:
            public override void OnEnable()
            {
                base.OnEnable();
                PhotonNetwork.NetworkingClient.StateChanged += this.OnStateChanged;
            }
    @jeanfabre
    so if I understood correctly, we need to wait 1 frame before trying to reconnect and rejoin?
    we could do this using a coroutine and WaitForEndOfFrame?
    hmm will test this.
  • Hi,

    yes. That's what I am witnessing here. We are currently investigating as to what exactly happens during that frame and if we can remove this and be able to reconnect straight from a disconnect callback.

    Bye,

    Jean
  • a better, updated version:
    using System;
    using System.Collections;
    using ExitGames.Client.Photon;
    using Photon.Realtime;
    using UnityEngine;

    namespace Photon.Pun.UtilityScripts
    {
    ///
    /// Unexpected disconnects recovery
    ///
    public class DisconnectsRecovery : MonoBehaviourPunCallbacks { [Tooltip("Whether or not attempt a rejoin without doing any checks.")] [SerializeField] private bool skipRejoinChecks; [Tooltip("Whether or not realtime webhooks are configured with persistence enabled")] [SerializeField] private bool persistenceEnabled; private bool rejoinCalled; private int minTimeRequiredToRejoin = 0; // TODO: set dynamically based on PhotonNetwork.NetworkingClient.LoadBalancingPeer.RoundTripTime private bool wasInRoom { get { return roomToRejoin != null; } } private bool reconnectCalled; private Room roomToRejoin; private Coroutine reconnectDelayCoroutine; public override void OnDisable() { base.OnDisable(); if (reconnectDelayCoroutine != null) { StopCoroutine(reconnectDelayCoroutine); reconnectDelayCoroutine = null; } } public override void OnDisconnected(DisconnectCause cause) { Debug.LogFormat("OnDisconnected(cause={0}) ClientState={1} PeerState={2}", cause, // TODO: filter obsolete enum values PhotonNetwork.NetworkingClient.State, PhotonNetwork.NetworkingClient.LoadBalancingPeer.PeerState); if (rejoinCalled) { Debug.LogErrorFormat("Rejoin failed, client disconnected due to {0}", cause); rejoinCalled = false; } else if (reconnectCalled) { Debug.LogErrorFormat("Reconnect failed, client disconnected due to {0}", cause); reconnectCalled = false; } else { HandleDisconnect(cause); } } private void HandleDisconnect(DisconnectCause cause) { switch (cause) { case DisconnectCause.ClientTimeout: case DisconnectCause.Exception: case DisconnectCause.ServerTimeout: case DisconnectCause.DisconnectByServerLogic: case DisconnectCause.AuthenticationTicketExpired: case DisconnectCause.DisconnectByServerReasonUnknown: reconnectDelayCoroutine = StartCoroutine(HandleDisconnectDelayed()); return; } } private IEnumerator HandleDisconnectDelayed() { WaitForEndOfFrame wait = new WaitForEndOfFrame(); yield return wait; HandleDisconnect(); } private void HandleDisconnect() { if (wasInRoom) { CheckAndTryQuickRejoin(); } else { Debug.Log("PhotonNetwork.Reconnect called"); reconnectCalled = PhotonNetwork.Reconnect(); } } public override void OnJoinRoomFailed(short returnCode, string message) { if (!rejoinCalled) { return; } rejoinCalled = false; Debug.LogErrorFormat("Rejoin failed with error code: {0} & error message: {1}", returnCode, message); } public override void OnJoinedRoom() { if (rejoinCalled) { Debug.Log("Rejoin successful"); rejoinCalled = false; } roomToRejoin = PhotonNetwork.CurrentRoom; } private void CheckAndTryQuickRejoin() { if (ShouldTryRejoin(roomToRejoin)) { rejoinCalled = PhotonNetwork.ReconnectAndRejoin(); if (rejoinCalled) { Debug.Log("PhotonNetwork.ReconnectAndRejoin called"); return; } } Debug.LogWarning("PhotonNetwork.ReconnectAndRejoin not called, PhotonNetwork.Reconnect is called instead."); reconnectCalled = PhotonNetwork.Reconnect(); } private bool ShouldTryRejoin(Room room) { if (room == null) { Debug.LogError("Room is null"); return false; } if (skipRejoinChecks) { return true; } bool wasLastActivePlayer = true; if (!persistenceEnabled) { foreach (Player player in room.Players.Values) { if (!player.IsInactive && !player.IsLocal) { wasLastActivePlayer = false; break; } } } if ((room.PlayerTtl < 0 || room.PlayerTtl > minTimeRequiredToRejoin) // PlayerTTL checks && (!wasLastActivePlayer || room.EmptyRoomTtl > minTimeRequiredToRejoin || persistenceEnabled)) // EmptyRoomTTL checks { return true; } return false; } public override void OnConnectedToMaster() { if (reconnectCalled) { Debug.Log("Reconnect successful"); reconnectCalled = false; } if (wasInRoom && !rejoinCalled && ShouldTryRejoin(roomToRejoin)) // should not happen { rejoinCalled = PhotonNetwork.RejoinRoom(roomToRejoin.Name); } roomToRejoin = null; } } }
Sign In or Register to comment.