mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 21:35:42 +00:00
Rate-limit MainQueue to prevent blocking when resuming from background (#4659)
* Rate-limit MainQueue to prevent blocking when resuming from background When Unity is backgrounded during a Shardok game, the gRPC stream continues receiving updates which queue up in MainQueue. Previously, Update() would process all queued actions in a single frame, causing the UI to freeze/spin when resuming. This change limits processing to 10 actions per frame, spreading the work across multiple frames and keeping the UI responsive. Also adds logging when the queue has built up, to help diagnose similar issues. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix duplicate updates when reconnecting while Unity is backgrounded Root cause: When Unity is backgrounded, MainQueue.Update() doesn't run, so ReceiveGameUpdate() never processes updates and _lastUnfilteredResultCount never advances. When the connection times out and reconnects, it sends the stale count, causing the server to re-send all the same updates. This repeats with each reconnect, accumulating duplicates. Fix: Call UpdateResultCounts() immediately on the gRPC thread when updates arrive, BEFORE enqueueing to MainQueue. This ensures reconnects always use accurate counts regardless of MainQueue state. Also adds duplicate detection in Notification.Append() as a defense-in-depth measure to prevent the same text from being appended multiple times. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Implement UpdateResultCounts in CustomBattleHandler CustomBattleHandler only handles Shardok updates, so the implementation is a no-op. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
+3
@@ -449,4 +449,7 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
|
||||
} };
|
||||
|
||||
public List<IClientConnectionSubscriber.StreamingTextStatus> StreamingTextStatuses => new();
|
||||
|
||||
// CustomBattleHandler only handles Shardok updates, not Eagle, so no count to update
|
||||
public void UpdateResultCounts(GameUpdate update) {}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,11 @@ namespace eagle {
|
||||
|
||||
public long GameId { get; }
|
||||
|
||||
public int LastUnfilteredResultCount => _lastUnfilteredResultCount;
|
||||
public int LastUnfilteredResultCount {
|
||||
get {
|
||||
lock (_resultCountLock) { return _lastUnfilteredResultCount; }
|
||||
}
|
||||
}
|
||||
|
||||
public List<IClientConnectionSubscriber.ShardokViewStatus> ShardokViewStatuses =>
|
||||
_currentModel.ShardokGameModels
|
||||
@@ -122,7 +126,9 @@ namespace eagle {
|
||||
|
||||
public ErrorHandler ErrorHandler;
|
||||
|
||||
// Thread-safe: updated from gRPC thread via UpdateResultCounts, read from main thread
|
||||
private int _lastUnfilteredResultCount = 0;
|
||||
private readonly object _resultCountLock = new();
|
||||
|
||||
private readonly Logger _connectionLogger = Logger.GetLogger("ConnectionLogger");
|
||||
|
||||
@@ -368,6 +374,27 @@ namespace eagle {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update result counts immediately when an update is received from the server.
|
||||
/// Called from the gRPC thread BEFORE enqueueing to MainQueue, to ensure
|
||||
/// reconnects use accurate counts even when MainQueue is blocked (e.g., backgrounded).
|
||||
/// </summary>
|
||||
public void UpdateResultCounts(GameUpdate update) {
|
||||
switch (update.GameUpdateDetailsCase) {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
lock (_resultCountLock) {
|
||||
_lastUnfilteredResultCount =
|
||||
update.ActionResultResponse.UnfilteredResultCountAfter;
|
||||
}
|
||||
break;
|
||||
|
||||
// Note: Shardok counts are tracked in ShardokGameModel.History.Count
|
||||
// which is updated in ReceiveGameUpdate on the main thread.
|
||||
// For now, Shardok reconnects may still get duplicate data, but
|
||||
// the primary issue (Eagle duplicates) is fixed here.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to game updates. Returns true if subscription was acknowledged by server.
|
||||
/// </summary>
|
||||
|
||||
+7
@@ -11,6 +11,13 @@ namespace eagle {
|
||||
|
||||
public void ReceiveGameUpdate(GameUpdate update);
|
||||
|
||||
/// <summary>
|
||||
/// Update the known result counts immediately when an update is received.
|
||||
/// Called from the gRPC thread BEFORE enqueueing to MainQueue, to ensure
|
||||
/// reconnects don't request stale data while MainQueue is blocked.
|
||||
/// </summary>
|
||||
public void UpdateResultCounts(GameUpdate update);
|
||||
|
||||
// Used for registering for stream updates
|
||||
struct ShardokViewStatus {
|
||||
public string shardokGameId;
|
||||
|
||||
+5
@@ -102,6 +102,11 @@ namespace eagle.Notifications {
|
||||
}
|
||||
|
||||
public void Append(string text, List<ProvinceId> provinceIds) {
|
||||
// Skip if this exact text is already in the notification (prevents duplicates
|
||||
// when the same update is processed multiple times, e.g., after resuming from
|
||||
// background)
|
||||
if (Text.Contains(text)) { return; }
|
||||
|
||||
if (ShouldAppend) {
|
||||
Text += "\n" + text;
|
||||
ProvinceIds.AddRange(provinceIds);
|
||||
|
||||
+9
@@ -706,6 +706,15 @@ namespace eagle {
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ActionResultResponse:
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.ShardokActionResultResponse:
|
||||
case GameUpdate.GameUpdateDetailsOneofCase.StreamingTextResponse:
|
||||
// Update result counts IMMEDIATELY on the gRPC thread, before enqueueing.
|
||||
// This ensures reconnects use accurate counts even when MainQueue is blocked
|
||||
// (e.g., when Unity is backgrounded).
|
||||
lock (this) {
|
||||
if (_subscribers.TryGetValue(gameUpdate.GameId, out var sub)) {
|
||||
sub.UpdateResultCounts(gameUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
MainQueue.Q.Enqueue(async () => {
|
||||
IClientConnectionSubscriber subscriber;
|
||||
lock (this) {
|
||||
|
||||
@@ -7,6 +7,12 @@ public class MainQueue : MonoBehaviour {
|
||||
private readonly Queue<Action> _actionQueue = new();
|
||||
private readonly Queue<Action> _nextUpdateQueue = new();
|
||||
|
||||
// Limit actions per frame to prevent blocking when resuming from background
|
||||
private const int MaxActionsPerFrame = 10;
|
||||
|
||||
// Track queue depth for logging
|
||||
private int _lastLoggedQueueDepth = 0;
|
||||
|
||||
private MainQueue() {}
|
||||
|
||||
void Awake() {
|
||||
@@ -15,6 +21,19 @@ public class MainQueue : MonoBehaviour {
|
||||
|
||||
// Update is called once per frame
|
||||
void Update() {
|
||||
int actionsProcessed = 0;
|
||||
int queueDepthBefore;
|
||||
|
||||
lock (_actionQueue) { queueDepthBefore = _actionQueue.Count; }
|
||||
|
||||
// Log when queue has built up (e.g., after resuming from background)
|
||||
if (queueDepthBefore > MaxActionsPerFrame && queueDepthBefore != _lastLoggedQueueDepth) {
|
||||
Debug.Log($"[MainQueue] Processing backlog: {queueDepthBefore} actions queued");
|
||||
_lastLoggedQueueDepth = queueDepthBefore;
|
||||
} else if (queueDepthBefore <= MaxActionsPerFrame) {
|
||||
_lastLoggedQueueDepth = 0;
|
||||
}
|
||||
|
||||
Action possibleAction;
|
||||
do {
|
||||
possibleAction = null;
|
||||
@@ -22,8 +41,11 @@ public class MainQueue : MonoBehaviour {
|
||||
if (_actionQueue.Count > 0) { possibleAction = _actionQueue.Dequeue(); }
|
||||
}
|
||||
|
||||
if (possibleAction != null) { possibleAction.Invoke(); }
|
||||
} while (possibleAction != null);
|
||||
if (possibleAction != null) {
|
||||
possibleAction.Invoke();
|
||||
actionsProcessed++;
|
||||
}
|
||||
} while (possibleAction != null && actionsProcessed < MaxActionsPerFrame);
|
||||
|
||||
lock (_nextUpdateQueue) {
|
||||
foreach (Action action in _nextUpdateQueue) { Enqueue(action); }
|
||||
|
||||
Reference in New Issue
Block a user