mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 22:35:42 +00:00
Serialize TryPendingCommands to prevent BAD_TOKEN bursts (#6630)
Multiple MainQueue actions (one per ActionResultResponse and ShardokActionResultResponse) each call TryPendingCommands as async void lambdas. When one yields at an await, the MainQueue Update loop immediately invokes the next action, which re-enters TryPendingCommands. Because PostRequest synchronously re-adds the command to _pendingCommands before yielding for WriteAsync, each parallel invocation re-drains the same buffered command and writes it to the gRPC stream again. This produces bursts of 10+ identical writes in a single frame, all of which the server rejects with BAD_TOKEN. Wrap TryPendingCommands in a non-blocking SemaphoreSlim acquire. If another invocation is already running, skip and return — any items added to _pendingCommands while it runs will be picked up by the next ActionResultResponse handler that fires. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+24
-1
@@ -191,6 +191,16 @@ namespace eagle {
|
||||
|
||||
private List<PostCommandRequest> _pendingCommands = new();
|
||||
|
||||
// Serializes TryPendingCommands invocations. Multiple MainQueue actions (one per
|
||||
// ActionResultResponse / ShardokActionResultResponse) each call TryPendingCommands,
|
||||
// and they're enqueued as `async void` lambdas — when one yields at an await, the
|
||||
// MainQueue's per-frame Update loop immediately invokes the next action, which
|
||||
// re-enters TryPendingCommands. Without this guard, each invocation re-drains
|
||||
// _pendingCommands (which PostRequest synchronously re-adds to before yielding),
|
||||
// causing the same buffered command to be written to the wire many times in a
|
||||
// single frame and producing bursts of BAD_TOKEN responses from the server.
|
||||
private readonly SemaphoreSlim _tryPendingCommandsSemaphore = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if there's a pending Eagle command with the specified token for the given game.
|
||||
/// Used by GameModelUpdater to skip loading stale commands when a pending command
|
||||
@@ -542,6 +552,18 @@ namespace eagle {
|
||||
}
|
||||
|
||||
private async Task<bool> TryPendingCommands() {
|
||||
// Try-acquire-or-skip: if another invocation is already draining the queue,
|
||||
// bail out immediately. The running invocation will process whatever is in
|
||||
// _pendingCommands at the moment it copied the list; anything that arrives
|
||||
// later (from a fresh PostRequest, or from a server response triggering
|
||||
// RemovePendingCommandsForGame + RefreshGameSubscription) will be picked up
|
||||
// by the next ActionResultResponse handler that calls TryPendingCommands.
|
||||
if (!await _tryPendingCommandsSemaphore.WaitAsync(0)) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
"TryPendingCommands skipped - another invocation is in progress");
|
||||
return true;
|
||||
}
|
||||
|
||||
Queue<PostCommandRequest> pendingCommands = new();
|
||||
try {
|
||||
lock (this) {
|
||||
@@ -662,7 +684,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} finally { _tryPendingCommandsSemaphore.Release(); }
|
||||
}
|
||||
|
||||
public void Disconnect() {
|
||||
@@ -722,6 +744,7 @@ namespace eagle {
|
||||
}
|
||||
|
||||
tokenSourceToDispose?.Dispose();
|
||||
_tryPendingCommandsSemaphore.Dispose();
|
||||
}
|
||||
|
||||
public void SetLobbySubscriber(ILobbySubscriber subscriber) {
|
||||
|
||||
Reference in New Issue
Block a user