Skip to content

Commit ad633b2

Browse files
fix: SceneEventProgress does not handle NetworkManager shut down during scene event (#2254)
* fix This resolves the issue where SceneEventProgress was not checking if the NetworkManager was no longer listening or a shutdown was in progress. * update Added additional NetworkLog warning when a client times out during connection approval. * test Added a mock test to verify SceneEventProgress can handle the NetworkManager shutting down during a scene event.
1 parent ac8ce24 commit ad633b2

File tree

3 files changed

+140
-11
lines changed

3 files changed

+140
-11
lines changed

com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,13 +1678,18 @@ private IEnumerator ApprovalTimeout(ulong clientId)
16781678
else
16791679
{
16801680
float timeStarted = Time.realtimeSinceStartup;
1681-
1682-
//We yield every frame incase a pending client disconnects and someone else gets its connection id
1681+
//We yield every frame in case a pending client disconnects and someone else gets its connection id
16831682
while (IsListening && (Time.realtimeSinceStartup - timeStarted) < NetworkConfig.ClientConnectionBufferTimeout && !IsConnectedClient)
16841683
{
16851684
yield return null;
16861685
}
16871686

1687+
if (!IsConnectedClient && NetworkLog.CurrentLogLevel <= LogLevel.Normal)
1688+
{
1689+
// TODO: Possibly add a callback for users to be notified of this condition here?
1690+
NetworkLog.LogWarning($"[ApprovalTimeout] Client timed out! You might need to increase the {nameof(NetworkConfig.ClientConnectionBufferTimeout)} duration. Approval Check Start: {timeStarted} | Approval Check Stopped: {Time.realtimeSinceStartup}");
1691+
}
1692+
16881693
if (!IsListening)
16891694
{
16901695
yield break;

com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventProgress.cs

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ internal void ClientFinishedSceneEvent(ulong clientId)
201201
/// </remarks>
202202
private bool HasFinished()
203203
{
204+
// If the network session is terminated/terminating then finish tracking
205+
// this scene event
206+
if (!IsNetworkSessionActive())
207+
{
208+
return true;
209+
}
210+
204211
// Clients skip over this
205212
foreach (var clientStatus in ClientsProcessingSceneEvent)
206213
{
@@ -222,11 +229,25 @@ internal void SetAsyncOperation(AsyncOperation asyncOperation)
222229
m_AsyncOperation = asyncOperation;
223230
m_AsyncOperation.completed += new Action<AsyncOperation>(asyncOp2 =>
224231
{
225-
OnSceneEventCompleted?.Invoke(SceneEventId);
232+
// Don't invoke the callback if the network session is disconnected
233+
// during a SceneEventProgress
234+
if (IsNetworkSessionActive())
235+
{
236+
OnSceneEventCompleted?.Invoke(SceneEventId);
237+
}
238+
239+
// Go ahead and try finishing even if the network session is terminated/terminating
240+
// as we might need to stop the coroutine
226241
TryFinishingSceneEventProgress();
227242
});
228243
}
229244

245+
246+
internal bool IsNetworkSessionActive()
247+
{
248+
return m_NetworkManager != null && m_NetworkManager.IsListening && !m_NetworkManager.ShutdownInProgress;
249+
}
250+
230251
/// <summary>
231252
/// Will try to finish the current scene event in progress as long as
232253
/// all conditions are met.
@@ -235,10 +256,15 @@ internal void TryFinishingSceneEventProgress()
235256
{
236257
if (HasFinished() || HasTimedOut())
237258
{
238-
OnComplete?.Invoke(this);
239-
m_NetworkManager.SceneManager.SceneEventProgressTracking.Remove(Guid);
240-
m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnectCallback;
241-
if (m_NetworkManager.IsServer)
259+
// Don't attempt to finalize this scene event if we are no longer listening or a shutdown is in progress
260+
if (IsNetworkSessionActive())
261+
{
262+
OnComplete?.Invoke(this);
263+
m_NetworkManager.SceneManager.SceneEventProgressTracking.Remove(Guid);
264+
m_NetworkManager.OnClientDisconnectCallback -= OnClientDisconnectCallback;
265+
}
266+
267+
if (m_TimeOutCoroutine != null)
242268
{
243269
m_NetworkManager.StopCoroutine(m_TimeOutCoroutine);
244270
}

testproject/Assets/Tests/Runtime/NetworkSceneManager/SceneEventProgressTests.cs

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ namespace TestProject.RuntimeTests
1313
public class SceneEventProgressTests : NetcodeIntegrationTest
1414
{
1515
private const string k_SceneUsedToGetAsyncOperation = "EmptyScene";
16+
private const string k_SceneUsedToGetClientAsyncOperation = "UnitTestBaseScene";
17+
1618
protected override int NumberOfClients => 4;
1719

1820
private bool m_SceneEventProgressCompleted;
@@ -51,6 +53,7 @@ private void StartNewSceneEventProgress()
5153
m_CurrentSceneEventProgress.SetAsyncOperation(SceneManager.LoadSceneAsync(k_SceneUsedToGetAsyncOperation, LoadSceneMode.Additive));
5254
}
5355

56+
5457
private void MockServerLoadedSene(Scene scene, LoadSceneMode loadSceneMode)
5558
{
5659
if (scene.name == k_SceneUsedToGetAsyncOperation)
@@ -155,6 +158,31 @@ public IEnumerator ClientsDisconnectDuring()
155158
VerifyClientsThatCompleted();
156159
}
157160

161+
[UnityTest]
162+
public IEnumerator ClientsShutdownDuring()
163+
{
164+
StartNewSceneEventProgress();
165+
166+
for (int i = 0; i < NumberOfClients; i++)
167+
{
168+
var currentClientNetworkManager = m_ClientNetworkManagers[i];
169+
// Two clients will shutdown
170+
var clientFinished = i % 2 == 0;
171+
SetClientFinished(currentClientNetworkManager.LocalClientId, clientFinished);
172+
173+
if (!clientFinished)
174+
{
175+
currentClientNetworkManager.Shutdown();
176+
}
177+
// wait anywhere from 100-500ms until processing next client
178+
var randomWaitPeriod = Random.Range(0.1f, 0.5f);
179+
yield return new WaitForSeconds(randomWaitPeriod);
180+
}
181+
yield return WaitForConditionOrTimeOut(() => m_SceneEventProgressCompleted);
182+
AssertOnTimeout($"Timed out waiting for SceneEventProgress to time out!");
183+
VerifyClientsThatCompleted();
184+
}
185+
158186
/// <summary>
159187
/// This verifies that SceneEventProgress will still complete
160188
/// even when clients late join.
@@ -163,16 +191,13 @@ public IEnumerator ClientsDisconnectDuring()
163191
public IEnumerator ClientsLateJoinDuring()
164192
{
165193
StartNewSceneEventProgress();
166-
167194
for (int i = 0; i < NumberOfClients; i++)
168195
{
169196
// Two clients will connect during a SceneEventProgress
170197
var shouldNewClientJoin = i % 2 == 0;
171198
var currentClientNetworkManager = m_ClientNetworkManagers[i];
172-
173199
// All connected clients will finish their SceneEventProgress
174200
SetClientFinished(currentClientNetworkManager.LocalClientId, true);
175-
176201
if (shouldNewClientJoin)
177202
{
178203
yield return CreateAndStartNewClient();
@@ -181,11 +206,84 @@ public IEnumerator ClientsLateJoinDuring()
181206
var randomWaitPeriod = Random.Range(0.1f, 0.5f);
182207
yield return new WaitForSeconds(randomWaitPeriod);
183208
}
184-
185209
yield return WaitForConditionOrTimeOut(() => m_SceneEventProgressCompleted);
186210
AssertOnTimeout($"Timed out waiting for SceneEventProgress to finish!");
211+
VerifyClientsThatCompleted();
212+
}
213+
214+
private List<ulong> m_ClientsThatTimedOutAndFinished = new List<ulong>();
215+
216+
private SceneEventProgress StartClientSceneEventProgress(NetworkManager networkManager)
217+
{
218+
var sceneEventProgress = new SceneEventProgress(networkManager, SceneEventProgressStatus.Started);
187219

220+
// Mock Scene Loading Event for mocking timed out client
221+
m_CurrentSceneEventProgress.SceneEventId = (uint)networkManager.LocalClientId;
222+
var asyncOperation = SceneManager.LoadSceneAsync(k_SceneUsedToGetClientAsyncOperation, LoadSceneMode.Additive);
223+
asyncOperation.completed += new System.Action<AsyncOperation>(asyncOp2 =>
224+
{
225+
m_ClientsThatTimedOutAndFinished.Add(networkManager.LocalClientId);
226+
});
227+
228+
m_CurrentSceneEventProgress.SetAsyncOperation(asyncOperation);
229+
return sceneEventProgress;
230+
}
231+
232+
private Dictionary<NetworkManager, SceneEventProgress> m_ClientsToFinishAfterDisconnecting = new Dictionary<NetworkManager, SceneEventProgress>();
233+
private bool TimedOutClientsFinishedSceneEventProgress()
234+
{
235+
// Now, verify all "mock timed out" clients still finished their SceneEventProgress
236+
foreach (var entry in m_ClientsToFinishAfterDisconnecting)
237+
{
238+
if (!m_ClientsThatTimedOutAndFinished.Contains(entry.Key.LocalClientId))
239+
{
240+
return false;
241+
}
242+
}
243+
return true;
244+
}
245+
246+
/// <summary>
247+
/// This mocks a client timing out during a SceneEventProgress
248+
/// </summary>
249+
[UnityTest]
250+
public IEnumerator ClientsMockTimeOutDuring()
251+
{
252+
m_ClientsThatTimedOutAndFinished.Clear();
253+
m_ClientsToFinishAfterDisconnecting.Clear();
254+
StartNewSceneEventProgress();
255+
256+
for (int i = 0; i < NumberOfClients; i++)
257+
{
258+
// Two clients will mock timing out during a SceneEventProgress
259+
var shouldClientFinish = i % 2 == 0;
260+
var currentClientNetworkManager = m_ClientNetworkManagers[i];
261+
262+
// Set whether the client should or should not have finished
263+
SetClientFinished(currentClientNetworkManager.LocalClientId, shouldClientFinish);
264+
if (!shouldClientFinish)
265+
{
266+
var sceneEventProgress = StartClientSceneEventProgress(currentClientNetworkManager);
267+
m_ClientsToFinishAfterDisconnecting.Add(currentClientNetworkManager, sceneEventProgress);
268+
currentClientNetworkManager.Shutdown();
269+
}
270+
}
271+
272+
yield return WaitForConditionOrTimeOut(() => m_SceneEventProgressCompleted);
273+
AssertOnTimeout($"Timed out waiting for SceneEventProgress to finish!");
188274
VerifyClientsThatCompleted();
275+
276+
yield return WaitForConditionOrTimeOut(TimedOutClientsFinishedSceneEventProgress);
277+
if (s_GlobalTimeoutHelper.TimedOut)
278+
{
279+
foreach (var entry in m_ClientsToFinishAfterDisconnecting)
280+
{
281+
Assert.IsTrue(m_ClientsThatTimedOutAndFinished.Contains(entry.Key.LocalClientId), $"Client-{entry.Key.LocalClientId} did not complete its {nameof(SceneEventProgress)}!");
282+
// Now, as a final check we try to finish the "mock" timed out client's scene event progress
283+
entry.Value.TryFinishingSceneEventProgress();
284+
}
285+
}
286+
m_ClientsToFinishAfterDisconnecting.Clear();
189287
}
190288
}
191289
}

0 commit comments

Comments
 (0)