Reconnecting player crashes lobby

Hi folks,

There is a strange situation when player reconnect to lobby. And I think this is bug.

Step 1: Host player creates a lobby.
Step 2: Only the Host player joins the lobby, then the host disconnects by closing the application. (doesn’t use RemovePlayerAsync or DeleteLobbyAsync methods)
Step 3: The host reconnects to the lobby with “LobbyService.Instance.ReconnectToLobbyAsync(lobbyId)” before the “Disconnect Removal Time” expires.

Until this point everything works well but, if host close the app again then reconnect the Lobby second time. After this point there is 2 error appear.

1- Player can reconnect the Lobby even “Active Lifespan” expires.
2 - If host close app again the Lobby will not destroy itself even “Active Lifespan” expires.

But, if I close the app then try to reconnect after “Disconnect Removal Time” expires, I got LobbyNotFound error. This is expected behavior but after join lobby with “LobbyService.Instance.ReconnectToLobbyAsync” method. The lobby is not behaving properly.

I use Multiplay Services package with “LobbyService.Instance” methods. Is this effect anything? Except this behavior everything works well.

Lobby config:
Minimum players slots : 1
Maximum players slots: 2
Active Lifespan: 1m
Disconnect Removal Time: 20s
Disconnect Host Migration Time: 10s

Multiplay Services package : 1.0.2
Unity : 6000.0.61f1

Also, I forgot to mention something important.
If 2 players join the lobby and one of them has reconnected once, the reconnected player cannot be disconnected if they close the game , which prevents new players from entering the lobby.

That’s the error. You should not leave the Lobby running when the Lobby host quits the application. Be sure to delete the Lobby in OnApplicationQuit for instance.

However, the host can disconnect or leave the lobby while other players are in the lobby. Because of this there is a host switch, right?
Also, if I close the lobby while other players are in the lobby, it will be a very bad experience for other players.

Actually the problem is, when a player reconnected the lobby, after that point is the player will never disconnect the lobby, and that slot always be occupied.
It think this is the bug.

Also, this situation is not only for the host but also for other players who are not hosts.

Check the wording in the “fine print”:

In the scenarios where the host does not specify a new host, the new host is selected randomly from the other players who are currently in the lobby.

This implies that if the host leaves the Lobby with no one else still in the Lobby, there is no host migration and presumably the Lobby timeout counter starts ticking irreversibly.

If you do have a client in the Lobby and the host leaves, check that the host migration did succeed. You may have to implement a callback in order to catch this situation because your code may need to respond to the host switch appropriately (eg provide the new host the ability to change game settings).

This is not unexpected! Every game that I know of where a host starts a private Lobby will kick all players if the Lobby host leaves.

This is true at least for client-hosted games where it wouldn’t make sense that the Lobby host would not actually host the game session, this may provide a bad experience if the Lobby randomly happens to choose the player with the lowest upstream bandwidth and/or the highest ping - then every other client will suffer in that session. Therefore it’s best to leave the decision who is going to host the game to the players in a client-hosted game.

Lobby host switching IMO ought to be left exclusively to dedicated server sessions.

I don’t want to get off-topic.

The problem is:

The host or another player can disconnect from the lobby and then reconnect to the lobby within the “Disconnect Timeout” period using the “LobbyService.Instance.ReconnectToLobbyAsync(lobbyId)” method.

However, if that player disconnects a second time, the player will not disconnect from the lobby. So the player can reconnect to the lobby even after the “Disconnect Timeout” period has expired.

I think this is a bug and not appropriate behavior.

That may be a bug. If you can create a minimal project where this behaviour is reproducible you should report this as a bug.

However, in my experience, most of these “bugs” are due to a programming error. Commonly a resource like the Lobby has not been properly shut down, or there’s a timing issue with an async method such as not awaiting a call or not handling an exception but rather ignoring it.

So feel free to post your code if you want other eyes to look over it for such issues. So far we’ve only spoken hypothetically about an issue that sounds like a bug, but neither of us can say for sure whether that’s on you or on Unity. :wink:

I made a simple tutorial for testing.
“JoinLobby()”, “ReconnectLobby()”, “CreateLobby()” and “SignIn()” method are triggered on UI.

I builded it out for the Mac version.
Editor is Host and doesn’t left or disconnect from the lobby.

Test steps :

1 - Mac build (Player2) joins the lobby.
2 - Player2 quits the application without to use any methods like “RemovePlayerAsync” etc.
3 - Player2 reconnects to lobby within the “ReconnectToLobbyAsync” method in “Disconnect Removal Time” period.

Note: If Player2 doesn’t reconnect to the lobby in “Disconnect Removal Time” period, “Disconnect Removal Time” will be disconnected after “Disconnect Removal Time” expired.

4 - Same as step 2.
5- Player2 can reconnect after “Disconnect Removal Time” expired.
In Host side, Player2 will never disconnect.
I think this behavior is a bug.

Unity : 6000.0.31f1
Multiplayer Services: 1.0.2

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using TMPro;
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.Services.Lobbies;
using Unity.Services.Lobbies.Models;
using UnityEngine;


public class LobbyTest : MonoBehaviour
{
    private Lobby _currentLobby;
    private string _playerId;
    private LobbyEventCallbacks _lobbyEventCallbacks;
    
    [SerializeField] private TMP_InputField _playerNameText;
    [SerializeField] private TMP_InputField _joinText;
    [SerializeField] private TMP_InputField _reconnectText;

    
    public void SignIn()
    {
        SignInAnonymouslyAsync(_playerNameText.text);
    }

    private async void SignInAnonymouslyAsync(string profile)
    {
        Debug.Log($"LobbyTest-SignInAnonymouslyAsync-profile:{profile}");
        try
        {
            InitializationOptions initializationOptions = new();
            initializationOptions.SetProfile(profile);
            await UnityServices.InitializeAsync(initializationOptions);

            await AuthenticationService.Instance.SignInAnonymouslyAsync();

            _playerId = AuthenticationService.Instance.PlayerId;
            Debug.Log($"LobbyTest-InitializeUnityServices-_playerId:{_playerId}");
        }
        catch (AuthenticationException ex)
        {
            Debug.Log($"LobbyTest-InitializeUnityServices-ex:{ex}");
        }
        catch (RequestFailedException ex)
        {
            Debug.Log($"LobbyTest-InitializeUnityServices-ex:{ex}");
        }
    }
    
    
    public void CreateLobby()
    {
        CreateLobbyAsync();
    }
    private async void CreateLobbyAsync()
    {
        try
        {
            CreateLobbyOptions createLobbyOptions = new CreateLobbyOptions
            {
                Player = new Player(_playerId)
            };

            _currentLobby = await LobbyService.Instance.CreateLobbyAsync("Lobby-name", 2, createLobbyOptions);
            Debug.Log($"LobbyTest-CreateLobbyAsync-LobbyId:{_currentLobby.Id}/{_currentLobby.LobbyCode}");
            StartHeartBeat();
            BindLobby();
        }
        catch (Exception ex)
        {
            Debug.Log($"LobbyTest-CreateLobbyAsync-ex:{ex}");
        }
    }
    
    
    public void JoinLobby()
    {
        JoinAsync();
    }
    private async void JoinAsync()
    {
        try
        {
            JoinLobbyByIdOptions options = new JoinLobbyByIdOptions
            {
                Player = new Player(_playerId)
            };
            
            _currentLobby = await LobbyService.Instance.JoinLobbyByIdAsync(_joinText.text, options);
            Debug.Log($"LobbyTest-JoinLobby:{_currentLobby.Id}");
            BindLobby();
        }
        catch (Exception e)
        {
            Debug.Log($"LobbyTest-QuickJoinLobby-ex: {e}");
        }
    }
        
    
    public void ReconnectLobby()
    {
        ReconnectToLobbyAsync(_reconnectText.text);
    }
    private async void ReconnectToLobbyAsync(string lobbyId)
    {
        try
        {
            Debug.Log($"LobbyTest-ReconnectToLobbyAsync-lobbyId:{lobbyId}");
            _currentLobby = await LobbyService.Instance.ReconnectToLobbyAsync(lobbyId);
            BindLobby();
        }
        catch (LobbyServiceException ex)
        {
            Debug.Log($"LobbyTest-ReconnectToLobbyAsync-Reason:{ex.Reason}");
        }
    }
    
    
    private async void BindLobby()
    {
        try
        {
            _lobbyEventCallbacks = new LobbyEventCallbacks();
            _lobbyEventCallbacks.LobbyChanged += OnLobbyChanged;
            _lobbyEventCallbacks.PlayerJoined += OnPlayerJoined;
            _lobbyEventCallbacks.PlayerLeft += OnPlayerLeft;
            _lobbyEventCallbacks.LobbyEventConnectionStateChanged += OnLobbyEventConnectionStateChanged;
            await LobbyService.Instance.SubscribeToLobbyEventsAsync(_currentLobby.Id, _lobbyEventCallbacks);
            Debug.Log("LobbyTest-BindLobby");
        }
        catch (Exception ex)
        {
            Debug.Log($"LobbyTest-BindLobby-ex:{ex}");
        }
    }
    
    private void OnLobbyChanged(ILobbyChanges lobbyChanges)
    {
        Debug.Log($"LobbyTest-OnLobbyChanged");
    }
    
    private void OnPlayerJoined(List<LobbyPlayerJoined> joinedPlayers)
    {
        Debug.Log($"LobbyTest-OnPlayerJoined-Count:{joinedPlayers.Count}");
        foreach (LobbyPlayerJoined item in joinedPlayers)
        {
            Debug.Log($"LobbyTest-OnPlayerJoined-Index:{item.PlayerIndex}=> Player:{item.Player.Id}");
        }
        Debug.Log($"LobbyTest-OnPlayerJoined-Players.Count:{_currentLobby.Players.Count}");
    }
    
    private void OnPlayerLeft(List<int> leftPlayerIds)
    {
        Debug.Log($"LobbyTest-OnPlayerLeft-Count:{leftPlayerIds.Count}");
    }
    
    private void OnLobbyEventConnectionStateChanged(LobbyEventConnectionState lobbyEventConnectionState)
    {
        Debug.Log($"LobbyTest-OnLobbyEventConnectionStateChanged:{lobbyEventConnectionState}");
    }

    private async void StartHeartBeat()
    {
        await HeartBeatLoop();
    }
    private async Task HeartBeatLoop()
    {
        while (_currentLobby != null)
        {
            try
            {
                await LobbyService.Instance.SendHeartbeatPingAsync(_currentLobby.Id);
                Debug.Log($"LobbyTest-HeartBeatLoop-Start:{_currentLobby.Id}");
                await Task.Delay(5000);//5 sc.
            }
            catch (Exception ex)
            {
                Debug.Log($"LobbyTest-HeartBeatLoop-ex:{ex}");
            }
        }
        Debug.Log("LobbyTest-HeartBeatLoop-Stopped!");
    }
}

You should await these calls!

What do you mean by “await”?
I am already awaiting them like this “_currentLobby = await LobbyService.Instance.ReconnectToLobbyAsync(lobbyId);”

Yes but it’s best practice to await the entire call chain of async methods. Your IDE will actually warn you of this potential source of issues.

If you were to follow up another line of code after JoinAsync() for example, then that second line will run before the async method completes, ie it does not await joining the Lobby even though the called method does await.

An async method not awaited will also not propagate exceptions. Although you enclosed the entire method body with try/catch handling, I would not rely on this pattern because it’s all too easy to forget that the caller is not awaiting.

More details here

Thanks for your advice but this is test project and I just wanted to show you the logs.
I use Actions inside the “JoinAsync” method, this way I log in to the lobby or show an error message.

Hi @ArthurAtUnity, Did you take a look at this topic?

Hi @Mj-Kkaya ,

Thanks for the ping ! It indeed looks like a bug for us to investigate. To help replicate, could you please confirm if you use any other multiplayer components on top of Lobby ? (Relay, NGO, …)

Hi,
I use NGO and Dedicated Server, but in this step the Client isn’t connected to the server.

Also I created a bug report (IN-91987)

1 Like