mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Suppress stale command sets after successful posts
This commit is contained in:
@@ -88,10 +88,10 @@ namespace eagle {
|
||||
public Action<Notification> NoteRecipient { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback to check if there's a pending command with the specified token.
|
||||
/// Callback to check if a command with the specified token is pending or was acknowledged.
|
||||
/// Set by PersistentClientConnection. When this returns true, HandleAvailableCommands
|
||||
/// will set the token (for TryPendingCommands matching) but not load the commands,
|
||||
/// since the pending command will be posted and we'll get fresh commands after.
|
||||
/// since that command is already in flight or accepted and we'll get fresh commands after.
|
||||
/// </summary>
|
||||
public Func<long, bool> HasPendingCommandWithToken { get; set; }
|
||||
|
||||
@@ -863,10 +863,9 @@ namespace eagle {
|
||||
|
||||
_currentModel.CommandToken = availableCommands.Token;
|
||||
|
||||
// Check if there's a pending command with this token waiting to be posted.
|
||||
// If so, don't load the commands - they're stale (we already acted on them).
|
||||
// The pending command will be posted by TryPendingCommands, and we'll get
|
||||
// fresh commands after it's processed.
|
||||
// Check if a command with this token is pending or was already acknowledged.
|
||||
// If so, don't load the commands: this response is stale because we already acted
|
||||
// on this token. A fresh command set will arrive after the post is processed.
|
||||
if (HasPendingCommandWithToken != null &&
|
||||
HasPendingCommandWithToken(availableCommands.Token)) {
|
||||
_connectionLogger.LogLine(
|
||||
|
||||
+2
-3
@@ -10,9 +10,8 @@ namespace eagle {
|
||||
public Int64? CurrentShardokToken(string shardokGameId);
|
||||
|
||||
/// <summary>
|
||||
/// Callback to check if there's a pending command with the specified token.
|
||||
/// Set by PersistentClientConnection. Used to skip loading stale commands
|
||||
/// when a pending command is about to be posted.
|
||||
/// Callback to check if a command with the specified token is pending or was acknowledged.
|
||||
/// Set by PersistentClientConnection and used to suppress stale command-set responses.
|
||||
/// </summary>
|
||||
public Func<long, bool> HasPendingCommandWithToken { get; set; }
|
||||
|
||||
|
||||
+33
-7
@@ -216,6 +216,7 @@ namespace eagle {
|
||||
private CancellationToken _currentThreadToken;
|
||||
|
||||
private List<PostCommandRequest> _pendingCommands = new();
|
||||
private readonly Dictionary<EagleGameId, long> _acknowledgedEagleCommandTokens = new();
|
||||
|
||||
// Serializes TryPendingCommands invocations. Multiple MainQueue actions (one per
|
||||
// ActionResultResponse / ShardokActionResultResponse) each call TryPendingCommands,
|
||||
@@ -228,12 +229,22 @@ namespace eagle {
|
||||
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
|
||||
/// is about to be posted.
|
||||
/// Checks if an Eagle command with the specified token is pending or has already been
|
||||
/// acknowledged by the server. GameModelUpdater uses this to suppress a stale command-set
|
||||
/// response that can race with the result of a successful post.
|
||||
/// </summary>
|
||||
public bool HasPendingEagleCommandWithToken(long gameId, long token) {
|
||||
lock (this) {
|
||||
if (_acknowledgedEagleCommandTokens.TryGetValue(
|
||||
gameId,
|
||||
out var acknowledgedToken)) {
|
||||
if (acknowledgedToken == token) { return true; }
|
||||
|
||||
// A different server token proves that this acknowledgement fence is no
|
||||
// longer needed, whether the game advanced normally or was rewound.
|
||||
_acknowledgedEagleCommandTokens.Remove(gameId);
|
||||
}
|
||||
|
||||
foreach (var cmd in _pendingCommands) {
|
||||
if (cmd.GameId == gameId &&
|
||||
cmd.Command.SealedValueCase ==
|
||||
@@ -394,12 +405,24 @@ namespace eagle {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove all pending commands for a specific game.
|
||||
/// Called when the server confirms a command was processed (SUCCESS or BAD_TOKEN).
|
||||
/// Remove all pending commands for a specific game. Successful Eagle command tokens are
|
||||
/// retained as a short-lived acknowledgement fence so a stale stream update cannot
|
||||
/// restore commands for a command the server has already accepted.
|
||||
/// </summary>
|
||||
private void RemovePendingCommandsForGame(long gameId) {
|
||||
private void RemovePendingCommandsForGame(
|
||||
long gameId,
|
||||
bool rememberAcknowledgedEagleToken = false) {
|
||||
lock (this) {
|
||||
var toRemove = _pendingCommands.Where(cmd => cmd.GameId == gameId).ToList();
|
||||
if (rememberAcknowledgedEagleToken) {
|
||||
var acknowledgedEagleCommand = toRemove.LastOrDefault(
|
||||
cmd => cmd.Command.SealedValueCase ==
|
||||
UniversalCommand.SealedValueOneofCase.EagleCommand);
|
||||
if (acknowledgedEagleCommand != null) {
|
||||
_acknowledgedEagleCommandTokens[gameId] =
|
||||
acknowledgedEagleCommand.Command.EagleCommand.EagleToken;
|
||||
}
|
||||
}
|
||||
foreach (var cmd in toRemove) { _pendingCommands.Remove(cmd); }
|
||||
if (toRemove.Count > 0) {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
@@ -811,6 +834,7 @@ namespace eagle {
|
||||
|
||||
// Clear pending commands
|
||||
_pendingCommands.Clear();
|
||||
_acknowledgedEagleCommandTokens.Clear();
|
||||
|
||||
OnChannelDead = null;
|
||||
OnCommandError = null;
|
||||
@@ -1446,7 +1470,9 @@ namespace eagle {
|
||||
_remoteEagleClientLogger.LogLine(
|
||||
$"[POST] Server confirmed command for game {postResponse.GameId}");
|
||||
// Command was successfully processed. Remove from pending.
|
||||
RemovePendingCommandsForGame(postResponse.GameId);
|
||||
RemovePendingCommandsForGame(
|
||||
postResponse.GameId,
|
||||
rememberAcknowledgedEagleToken: true);
|
||||
}
|
||||
// UNKNOWN is benign (old servers that don't set status)
|
||||
|
||||
|
||||
+29
@@ -1,8 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using eagle;
|
||||
using Grpc.Core;
|
||||
using Net.Eagle0.Eagle.Api;
|
||||
using NUnit.Framework;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
@@ -61,6 +63,33 @@ namespace eagle0.Tests {
|
||||
connection.Dispose();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SuccessfulPostSuppressesItsStaleCommandTokenUntilServerAdvances() {
|
||||
const long gameId = 7126;
|
||||
const long postedToken = 799;
|
||||
var connection =
|
||||
new PersistentClientConnection(null, new Metadata(), CancellationToken.None);
|
||||
var pending = new List<PostCommandRequest> { new() {
|
||||
GameId = gameId,
|
||||
Command =
|
||||
new UniversalCommand {
|
||||
EagleCommand = new EagleCommand { EagleToken = postedToken }
|
||||
}
|
||||
} };
|
||||
SetPrivateField(connection, "_pendingCommands", pending);
|
||||
|
||||
InvokePrivate(connection, "RemovePendingCommandsForGame", gameId, true);
|
||||
|
||||
Assert.IsEmpty(
|
||||
GetPrivateField<List<PostCommandRequest>>(connection, "_pendingCommands"));
|
||||
Assert.IsTrue(connection.HasPendingEagleCommandWithToken(gameId, postedToken));
|
||||
Assert.IsTrue(connection.HasPendingEagleCommandWithToken(gameId, postedToken));
|
||||
Assert.IsFalse(connection.HasPendingEagleCommandWithToken(gameId, postedToken + 1));
|
||||
Assert.IsFalse(connection.HasPendingEagleCommandWithToken(gameId, postedToken));
|
||||
|
||||
connection.Dispose();
|
||||
}
|
||||
|
||||
private static T GetPrivateField<T>(object target, string fieldName) {
|
||||
var field = target.GetType().GetField(
|
||||
fieldName,
|
||||
|
||||
Reference in New Issue
Block a user