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