Enhance school management and protocol handling by introducing a new worker failure mechanism, ensuring proper cleanup of schools when a worker thread encounters an error. Update the map snapshot protocol to allow larger school data sizes, improving the handling of extensive map layouts. Revise documentation to reflect changes in save intervals and introduce a minimum save interval for better performance. Update tests to validate the new functionalities and ensure robustness in handling large data scenarios.
ci / server (push) Failing after 3m29s
ci / client (push) Successful in 16s

This commit is contained in:
Leonid Pershin
2026-08-18 17:02:28 +03:00
parent 6ca9ff9d03
commit f000b128f6
14 changed files with 325 additions and 74 deletions
+33 -26
View File
@@ -80,6 +80,13 @@ internal sealed class GameClient(uint playerId, WebSocket socket)
var reliable = _reliable.Reader;
var outbox = _outbox.Reader;
// The two waits live across iterations on purpose. Creating a fresh pair every idle pass
// left the loser of Task.WhenAny queued on its channel forever — and because clock frames
// arrive twenty times a second, the reliable channel collected one dead waiter per tick,
// each holding a cancellation registration, for as long as the player stayed in a school.
Task<bool>? waitReliable = null;
Task<bool>? waitOutbox = null;
while (Socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
{
if (reliable.TryRead(out var reliableFrame))
@@ -102,44 +109,44 @@ internal sealed class GameClient(uint playerId, WebSocket socket)
continue;
}
var waitReliable = reliable.WaitToReadAsync(cancellationToken).AsTask();
var waitOutbox = outbox.WaitToReadAsync(cancellationToken).AsTask();
// A completed channel is empty for good; waiting on it again would spin.
if (!reliable.Completion.IsCompleted)
{
waitReliable ??= reliable.WaitToReadAsync(cancellationToken).AsTask();
}
if (!outbox.Completion.IsCompleted)
{
waitOutbox ??= outbox.WaitToReadAsync(cancellationToken).AsTask();
}
if (waitReliable is null && waitOutbox is null)
{
return;
}
Task<bool> finished;
try
{
finished = await Task.WhenAny(waitReliable, waitOutbox).ConfigureAwait(false);
finished = waitReliable is null ? waitOutbox!
: waitOutbox is null ? waitReliable
: await Task.WhenAny(waitReliable, waitOutbox).ConfigureAwait(false);
await finished.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}
bool hasData;
try
// Only the wait that finished is dropped; the other one stays queued on its channel.
if (ReferenceEquals(finished, waitReliable))
{
hasData = await finished.ConfigureAwait(false);
waitReliable = null;
}
catch (OperationCanceledException)
else
{
return;
}
if (hasData)
{
continue;
}
var other = ReferenceEquals(finished, waitReliable) ? waitOutbox : waitReliable;
try
{
if (!await other.ConfigureAwait(false))
{
return;
}
}
catch (OperationCanceledException)
{
return;
waitOutbox = null;
}
}
}