Fix: make AutoRelog retries single-owner (#3186)

Login rejections could schedule and execute multiple restarts for one failure while leaving no usable console route during reconnect delays.

Changes:
- Bind failure and restart work to immutable connection attempts
- Coalesce automatic retries while allowing explicit settings replacement until commit
- Preserve held bots and offline command routing across failed logins
- Add deterministic retry, ownership, and routing regression tests

Fixes #3186
This commit is contained in:
Anon 2026-07-27 16:12:10 +02:00
parent 92212d2b95
commit 456a548cbc
11 changed files with 645 additions and 114 deletions

View file

@ -78,6 +78,7 @@ namespace MinecraftClient.ChatBots
}
private static readonly AutoRelogRetryPolicy s_retryPolicy = new(TimeProvider.System);
private readonly long? sourceConnectionAttempt;
/// <summary>
/// This bot automatically re-join the server if kick message contains predefined string
@ -85,8 +86,13 @@ namespace MinecraftClient.ChatBots
/// <param name="DelayBeforeRelogMin">Minimum delay before re-joining the server (in seconds)</param>
/// <param name="DelayBeforeRelogMax">Maximum delay before re-joining the server (in seconds)</param>
/// <param name="retries">Number of retries if connection fails (-1 = infinite)</param>
public AutoRelog()
public AutoRelog() : this(null)
{
}
private AutoRelog(long? sourceConnectionAttempt)
{
this.sourceConnectionAttempt = sourceConnectionAttempt;
LogDebugToConsole(string.Format(Translations.bot_autoRelog_launch, Config.Retries));
}
@ -163,21 +169,27 @@ namespace MinecraftClient.ChatBots
: retriesLeft.ToString();
McClient.ReconnectionAttemptsLeft = retriesLeft;
if (Program.TryRestart(TimeSpan.FromSeconds(delay), true))
long connectionAttempt = sourceConnectionAttempt ?? Handler.ConnectionAttempt;
if (Program.TryRestart(connectionAttempt, TimeSpan.FromSeconds(delay), true))
{
LogToConsole(string.Format(Translations.bot_autoRelog_wait_with_retries, delay, retriesDisplay));
return true;
}
s_retryPolicy.RollBackReservedAttempt();
return Program.HasRestartPending;
return Program.HasRestartPending(connectionAttempt);
}
public static bool OnDisconnectStatic(DisconnectReason reason, string message)
{
return OnDisconnectStatic(reason, message, Program.CurrentConnectionAttempt);
}
internal static bool OnDisconnectStatic(DisconnectReason reason, string message, long sourceConnectionAttempt)
{
if (Config.Enabled)
{
AutoRelog bot = new();
AutoRelog bot = new(sourceConnectionAttempt);
bot.Initialize();
return bot.OnDisconnect(reason, message);
}

View file

@ -1,4 +1,5 @@
using Brigadier.NET;
using System;
using Brigadier.NET;
using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler;
using static MinecraftClient.CommandHandler.CmdResult;
@ -42,35 +43,55 @@ namespace MinecraftClient.Commands
private int DoConnect(CmdResult r, string server, string account)
{
RestartSettingsSnapshot previousSettings = Program.CaptureRestartSettings();
if (!string.IsNullOrWhiteSpace(account) && !Settings.Config.Main.Advanced.SetAccount(account))
return r.SetAndReturn(Status.Fail, string.Format(Translations.cmd_connect_unknown, account));
if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHelper.MainConfig.ServerInfoConfig(server), true))
{
return r.SetAndReturn(Program.TryRestart(keepAccountAndServerSettings: true)
? Status.Done
: Status.Fail);
if (Program.TryRestart(
Program.CurrentConnectionAttempt,
TimeSpan.Zero,
keepAccountAndServerSettings: true,
replaceUntilCommit: true))
{
return r.SetAndReturn(Status.Done);
}
Program.RestoreRestartSettings(previousSettings);
return r.SetAndReturn(Status.Fail);
}
else
{
Program.RestoreRestartSettings(previousSettings);
return r.SetAndReturn(Status.Fail, string.Format(Translations.cmd_connect_invalid_ip, server));
}
}
internal static string DoConnect(string command)
{
RestartSettingsSnapshot previousSettings = Program.CaptureRestartSettings();
string[] args = GetArgs(command);
if (args.Length > 1 && !Settings.Config.Main.Advanced.SetAccount(args[1]))
return string.Format(Translations.cmd_connect_unknown, args[1]);
if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHelper.MainConfig.ServerInfoConfig(args[0]), true))
{
return Program.TryRestart(keepAccountAndServerSettings: true)
? string.Empty
: Translations.general_fail;
if (Program.TryRestart(
Program.CurrentConnectionAttempt,
TimeSpan.Zero,
keepAccountAndServerSettings: true,
replaceUntilCommit: true))
{
return string.Empty;
}
Program.RestoreRestartSettings(previousSettings);
return Translations.general_fail;
}
else
{
Program.RestoreRestartSettings(previousSettings);
return string.Format(Translations.cmd_connect_invalid_ip, args[0]);
}
}

View file

@ -41,19 +41,30 @@ namespace MinecraftClient.Commands
private int DoReconnect(CmdResult r, string account)
{
RestartSettingsSnapshot previousSettings = Program.CaptureRestartSettings();
if (!string.IsNullOrWhiteSpace(account))
{
account = account.Trim();
if (!Settings.Config.Main.Advanced.SetAccount(account))
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_connect_unknown, account));
}
return r.SetAndReturn(Program.TryRestart(keepAccountAndServerSettings: true)
? CmdResult.Status.Done
: CmdResult.Status.Fail);
if (Program.TryRestart(
Program.CurrentConnectionAttempt,
TimeSpan.Zero,
keepAccountAndServerSettings: true,
replaceUntilCommit: true))
{
return r.SetAndReturn(CmdResult.Status.Done);
}
Program.RestoreRestartSettings(previousSettings);
return r.SetAndReturn(CmdResult.Status.Fail);
}
internal static string DoReconnect(string command)
{
RestartSettingsSnapshot previousSettings = Program.CaptureRestartSettings();
string[] args = GetArgs(command);
if (args.Length > 0)
{
@ -63,9 +74,18 @@ namespace MinecraftClient.Commands
return string.Format(Translations.cmd_connect_unknown, account);
}
}
return Program.TryRestart(keepAccountAndServerSettings: true)
? String.Empty
: Translations.general_fail;
if (Program.TryRestart(
Program.CurrentConnectionAttempt,
TimeSpan.Zero,
keepAccountAndServerSettings: true,
replaceUntilCommit: true))
{
return string.Empty;
}
Program.RestoreRestartSettings(previousSettings);
return Translations.general_fail;
}
}
}

View file

@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.Threading;
using MinecraftClient.Scripting;
namespace MinecraftClient
{
internal sealed class ConnectionAttemptLifecycle
{
private int disconnectState;
internal bool IsFailureClaimed => Volatile.Read(ref disconnectState) != 0;
internal bool TryBeginDisconnect()
{
return Interlocked.CompareExchange(ref disconnectState, 1, 0) == 0;
}
internal void CompleteDisconnect()
{
Volatile.Write(ref disconnectState, 2);
}
internal static void RestoreHeldBots(ICollection<ChatBot> heldBots, Action<ChatBot> loadBot)
{
ArgumentNullException.ThrowIfNull(heldBots);
ArgumentNullException.ThrowIfNull(loadBot);
foreach (ChatBot bot in heldBots)
loadBot(bot);
heldBots.Clear();
}
}
internal sealed class AttemptOwnedRoute
{
private const long NoOwner = -1;
private readonly Lock stateLock = new();
private long ownerAttempt = NoOwner;
internal long OwnerAttempt
{
get
{
lock (stateLock)
return ownerAttempt;
}
}
internal bool TryActivate(long connectionAttempt, Action activate)
{
ArgumentNullException.ThrowIfNull(activate);
lock (stateLock)
{
if (ownerAttempt >= connectionAttempt)
return false;
ownerAttempt = connectionAttempt;
activate();
return true;
}
}
internal bool TryDeactivate(long connectionAttempt, Action deactivate)
{
ArgumentNullException.ThrowIfNull(deactivate);
lock (stateLock)
{
if (ownerAttempt != connectionAttempt)
return false;
ownerAttempt = NoOwner;
deactivate();
return true;
}
}
internal bool TryTransfer(long sourceConnectionAttempt, long targetConnectionAttempt)
{
lock (stateLock)
{
if (ownerAttempt != sourceConnectionAttempt)
return false;
ownerAttempt = targetConnectionAttempt;
return true;
}
}
internal bool TryDeactivate(Action deactivate)
{
ArgumentNullException.ThrowIfNull(deactivate);
lock (stateLock)
{
if (ownerAttempt == NoOwner)
return false;
ownerAttempt = NoOwner;
deactivate();
return true;
}
}
}
}

View file

@ -231,12 +231,13 @@ namespace MinecraftClient
SessionToken _sessionToken;
Tuple<Thread, CancellationTokenSource>? timeoutdetector = null;
private int transferInProgress = 0;
private int disconnectState;
private readonly ConnectionAttemptLifecycle connectionLifecycle = new();
private int disconnectOwnerThreadId;
private readonly TaskCompletionSource<bool> disconnectCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously);
public ILogger Log;
public DialogManager Dialogs { get; }
internal long ConnectionAttempt { get; }
private static IMinecraftComHandler? instance;
public static IMinecraftComHandler? Instance => instance;
@ -251,9 +252,22 @@ namespace MinecraftClient
/// <param name="protocolversion">Minecraft protocol version to use</param>
/// <param name="forgeInfo">ForgeInfo item stating that Forge is enabled</param>
public McClient(SessionToken session, PlayerKeyPair? playerKeyPair, string server_ip, ushort port, int protocolversion, ForgeInfo? forgeInfo)
: this(session, playerKeyPair, server_ip, port, protocolversion, forgeInfo, Program.CurrentConnectionAttempt)
{
}
internal McClient(
SessionToken session,
PlayerKeyPair? playerKeyPair,
string server_ip,
ushort port,
int protocolversion,
ForgeInfo? forgeInfo,
long connectionAttempt)
{
CmdResult.currentHandler = this;
instance = this;
ConnectionAttempt = connectionAttempt;
terrainAndMovementsEnabled = Config.Main.Advanced.TerrainAndMovements;
inventoryHandlingEnabled = Config.Main.Advanced.InventoryHandling;
@ -311,7 +325,13 @@ namespace MinecraftClient
LoadCommands();
if (botsOnHold.Count == 0)
{
RegisterBots();
}
else
{
ConnectionAttemptLifecycle.RestoreHeldBots(botsOnHold, bot => BotLoad(bot, false));
}
try
{
@ -331,10 +351,6 @@ namespace MinecraftClient
{
if (handler.Login(this.playerKeyPair, session))
{
foreach (ChatBot bot in botsOnHold)
BotLoad(bot, false);
botsOnHold.Clear();
Log.Info(string.Format(Translations.mcc_joined, Config.Main.Advanced.InternalCmdChar.ToLogString()));
StartConsoleSession();
@ -368,6 +384,9 @@ namespace MinecraftClient
timeoutdetector = null;
}
if (connectionLifecycle.IsFailureClaimed)
return;
if (!InternalConfig.InteractiveMode)
{
StopConsoleSession();
@ -391,14 +410,8 @@ namespace MinecraftClient
return;
}
// AutoRelog is enabled - invoke its static handler to trigger reconnection.
// Use the same "Connection has been lost" message that OnConnectionLost uses
// for ConnectionLost, so it matches the default Kick_Messages.
if (AutoRelog.OnDisconnectStatic(ChatBot.DisconnectReason.ConnectionLost, Translations.mcc_disconnect_lost))
return;
StopConsoleSession();
Program.HandleFailure();
OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.mcc_disconnect_lost);
return;
}
public void Transfer(string newHost, int newPort)
@ -530,6 +543,7 @@ namespace MinecraftClient
private void StartConsoleSession()
{
Program.EndOfflinePrompt(ConnectionAttempt);
ConsoleInputRouter.RouteToClient(this);
}
@ -875,7 +889,7 @@ namespace MinecraftClient
}
}
restartScheduled = !exitOnFailure && Program.HasRestartPending;
restartScheduled = !exitOnFailure && Program.HasRestartPending(ConnectionAttempt);
}
finally
{
@ -888,7 +902,7 @@ namespace MinecraftClient
private bool TryBeginDisconnect()
{
if (Interlocked.CompareExchange(ref disconnectState, 1, 0) != 0)
if (!connectionLifecycle.TryBeginDisconnect())
return false;
Volatile.Write(ref disconnectOwnerThreadId, Environment.CurrentManagedThreadId);
@ -937,7 +951,7 @@ namespace MinecraftClient
}
finally
{
Volatile.Write(ref disconnectState, 2);
connectionLifecycle.CompleteDisconnect();
Volatile.Write(ref disconnectOwnerThreadId, 0);
disconnectCompletion.TrySetResult(true);
}

View file

@ -55,7 +55,7 @@ namespace MinecraftClient
private static bool useMcVersionOnce = false;
private static readonly RestartCoordinator restartCoordinator = new(ExecuteRestartAsync, ReportRestartFailure);
private static long connectionAttempt;
private static int offlinePromptActive;
private static readonly AttemptOwnedRoute offlinePromptRoute = new();
private static int exitOnFailurePending;
private static string settingsIniPath = "MinecraftClient.ini";
private static AuthenticationSelection? pendingAuthenticationSelection;
@ -747,12 +747,14 @@ namespace MinecraftClient
private sealed record AuthenticationSelection(LoginType AccountType, LoginMethod Method, string AuthServerUrl);
internal static long CurrentConnectionAttempt => Volatile.Read(ref connectionAttempt);
/// <summary>
/// Start a new Client
/// </summary>
private static void InitializeClient()
{
Interlocked.Increment(ref connectionAttempt);
long attempt = Interlocked.Increment(ref connectionAttempt);
// Ensure that we use the provided Minecraft version if we can't connect automatically.
//
@ -984,7 +986,7 @@ namespace MinecraftClient
try
{
//Start the main TCP client
client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo);
client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo, attempt);
//Update console title
if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle))
@ -1062,7 +1064,22 @@ namespace MinecraftClient
TryRestart(TimeSpan.FromSeconds(Math.Max(0, delaySeconds)), keepAccountAndServerSettings);
}
internal static bool HasRestartPending => restartCoordinator.HasScheduledRestart(Volatile.Read(ref connectionAttempt));
internal static bool HasRestartPending(long sourceConnectionAttempt)
{
return restartCoordinator.HasScheduledRestart(sourceConnectionAttempt);
}
internal static RestartSettingsSnapshot CaptureRestartSettings()
{
return new RestartSettingsSnapshot(InternalConfig.Account, InternalConfig.ServerIP, InternalConfig.ServerPort);
}
internal static void RestoreRestartSettings(RestartSettingsSnapshot settingsSnapshot)
{
InternalConfig.Account = settingsSnapshot.Account;
InternalConfig.ServerIP = settingsSnapshot.ServerIP;
InternalConfig.ServerPort = settingsSnapshot.ServerPort;
}
internal static bool TryRestart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
{
@ -1070,6 +1087,15 @@ namespace MinecraftClient
}
internal static bool TryRestart(TimeSpan delay, bool keepAccountAndServerSettings = false)
{
return TryRestart(CurrentConnectionAttempt, delay, keepAccountAndServerSettings);
}
internal static bool TryRestart(
long sourceConnectionAttempt,
TimeSpan delay,
bool keepAccountAndServerSettings = false,
bool replaceUntilCommit = false)
{
if (Volatile.Read(ref exitOnFailurePending) != 0)
return false;
@ -1078,18 +1104,25 @@ namespace MinecraftClient
delay = TimeSpan.Zero;
RestartSettingsSnapshot? settingsSnapshot = keepAccountAndServerSettings
? new RestartSettingsSnapshot(InternalConfig.Account, InternalConfig.ServerIP, InternalConfig.ServerPort)
? CaptureRestartSettings()
: null;
return restartCoordinator.TrySchedule(new RestartRequest(
Volatile.Read(ref connectionAttempt),
bool scheduled = restartCoordinator.TrySchedule(new RestartRequest(
sourceConnectionAttempt,
delay,
keepAccountAndServerSettings,
settingsSnapshot));
settingsSnapshot,
replaceUntilCommit));
if (scheduled)
BeginOfflinePrompt(sourceConnectionAttempt);
return scheduled;
}
private static async Task ExecuteRestartAsync(RestartRequest request, CancellationToken cancellationToken)
{
if (request.ConnectionAttempt != CurrentConnectionAttempt)
return;
McClient? disconnectedClient = client;
if (disconnectedClient is not null)
{
@ -1098,7 +1131,6 @@ namespace MinecraftClient
client = null;
}
EndOfflinePrompt();
ConsoleIO.Reset();
if (request.Delay > TimeSpan.Zero)
@ -1108,14 +1140,22 @@ namespace MinecraftClient
}
cancellationToken.ThrowIfCancellationRequested();
if (request.ConnectionAttempt != CurrentConnectionAttempt
|| !restartCoordinator.TryBeginCommit(request, out RestartRequest latestRequest)
|| latestRequest.ConnectionAttempt != CurrentConnectionAttempt)
{
return;
}
ConsoleIO.WriteLine(Translations.mcc_restart);
ReloadSettings(request.KeepAccountAndServerSettings);
if (request.SettingsSnapshot is RestartSettingsSnapshot settingsSnapshot)
ReloadSettings(latestRequest.KeepAccountAndServerSettings);
if (latestRequest.SettingsSnapshot is RestartSettingsSnapshot settingsSnapshot)
{
InternalConfig.Account = settingsSnapshot.Account;
InternalConfig.ServerIP = settingsSnapshot.ServerIP;
InternalConfig.ServerPort = settingsSnapshot.ServerPort;
}
TransferOfflinePrompt(latestRequest.ConnectionAttempt, latestRequest.ConnectionAttempt + 1);
InitializeClient();
}
@ -1197,7 +1237,7 @@ namespace MinecraftClient
if (!string.IsNullOrEmpty(errorMessage) && disconnectReason.HasValue)
{
autoRelogHandled = true;
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage))
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage, CurrentConnectionAttempt))
return;
}
@ -1217,35 +1257,49 @@ namespace MinecraftClient
if (!autoRelogHandled && disconnectReason.HasValue)
{
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!))
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!, CurrentConnectionAttempt))
return;
}
BeginOfflinePrompt();
BeginOfflinePrompt(CurrentConnectionAttempt);
}
}
private static void BeginOfflinePrompt()
private static void BeginOfflinePrompt(long connectionAttempt)
{
if (Interlocked.CompareExchange(ref offlinePromptActive, 1, 0) != 0)
return;
ConsoleInputRouter.RouteOffline(HandleOfflineCommand);
ConsoleIO.WriteLine(string.Empty);
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString()));
if (ConsoleIO.Backend is Tui.TuiConsoleBackend)
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString()));
else
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
offlinePromptRoute.TryActivate(connectionAttempt, () =>
{
ConsoleInputRouter.RouteOffline(HandleOfflineCommand);
ConsoleIO.WriteLine(string.Empty);
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString()));
if (ConsoleIO.Backend is Tui.TuiConsoleBackend)
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString()));
else
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
});
}
private static void EndOfflinePrompt()
{
if (Interlocked.Exchange(ref offlinePromptActive, 0) == 0)
return;
offlinePromptRoute.TryDeactivate(() =>
{
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
ConsoleIO.Reset();
});
}
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
ConsoleIO.Reset();
internal static void EndOfflinePrompt(long connectionAttempt)
{
offlinePromptRoute.TryDeactivate(connectionAttempt, () =>
{
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
ConsoleIO.Reset();
});
}
private static void TransferOfflinePrompt(long sourceConnectionAttempt, long targetConnectionAttempt)
{
offlinePromptRoute.TryTransfer(sourceConnectionAttempt, targetConnectionAttempt);
}
private static void HandleOfflineCommand(string input)
@ -1269,19 +1323,13 @@ namespace MinecraftClient
{
message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command));
if (message.Length == 0)
{
EndOfflinePrompt();
return;
}
}
else if (command.StartsWith("connect", StringComparison.Ordinal))
{
message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command));
if (message.Length == 0)
{
EndOfflinePrompt();
return;
}
}
else if (command.StartsWith("exit", StringComparison.Ordinal)
|| command.StartsWith("quit", StringComparison.Ordinal))

View file

@ -11,6 +11,7 @@ namespace MinecraftClient
TimeSpan Delay,
bool KeepAccountAndServerSettings,
RestartSettingsSnapshot? SettingsSnapshot = null,
bool ReplaceUntilCommit = false,
long RequestId = 0);
internal readonly record struct RestartSettingsSnapshot(
@ -18,6 +19,17 @@ namespace MinecraftClient
string ServerIP,
ushort ServerPort);
internal enum RestartRequestState
{
Replaceable,
Committing,
}
internal readonly record struct PendingRestart(
long RequestId,
RestartRequest Request,
RestartRequestState State);
internal sealed class RestartCoordinator : IDisposable
{
private readonly Lock stateLock = new();
@ -26,7 +38,7 @@ namespace MinecraftClient
private readonly Func<RestartRequest, CancellationToken, Task> restart;
private readonly Action<Exception> reportFailure;
private readonly Task worker;
private readonly Dictionary<long, long> pendingAttempts = [];
private readonly Dictionary<long, PendingRestart> pendingAttempts = [];
private long highestScheduledAttempt = -1;
private long nextRequestId;
private bool stopped;
@ -59,25 +71,58 @@ namespace MinecraftClient
{
lock (stateLock)
{
bool hasPendingRequest = pendingAttempts.TryGetValue(request.ConnectionAttempt, out long previousRequestId);
if (stopped || request.ConnectionAttempt < highestScheduledAttempt
|| (request.ConnectionAttempt == highestScheduledAttempt && !hasPendingRequest))
if (stopped)
return false;
if (pendingAttempts.TryGetValue(request.ConnectionAttempt, out PendingRestart pendingRequest))
{
if (pendingRequest.State != RestartRequestState.Replaceable || !request.ReplaceUntilCommit)
return false;
request = request with { RequestId = pendingRequest.RequestId };
pendingAttempts[request.ConnectionAttempt] = pendingRequest with { Request = request };
return true;
}
if (request.ConnectionAttempt <= highestScheduledAttempt)
return false;
highestScheduledAttempt = Math.Max(highestScheduledAttempt, request.ConnectionAttempt);
request = request with { RequestId = ++nextRequestId };
pendingAttempts[request.ConnectionAttempt] = request.RequestId;
pendingAttempts[request.ConnectionAttempt] = new PendingRestart(
request.RequestId,
request,
RestartRequestState.Replaceable);
if (requests.Writer.TryWrite(request))
return true;
if (hasPendingRequest)
pendingAttempts[request.ConnectionAttempt] = previousRequestId;
else
pendingAttempts.Remove(request.ConnectionAttempt);
pendingAttempts.Remove(request.ConnectionAttempt);
return false;
}
}
internal bool TryBeginCommit(RestartRequest scheduledRequest, out RestartRequest latestRequest)
{
lock (stateLock)
{
if (stopped
|| !pendingAttempts.TryGetValue(scheduledRequest.ConnectionAttempt, out PendingRestart pendingRequest)
|| pendingRequest.RequestId != scheduledRequest.RequestId
|| pendingRequest.State != RestartRequestState.Replaceable)
{
latestRequest = default;
return false;
}
latestRequest = pendingRequest.Request;
pendingAttempts[scheduledRequest.ConnectionAttempt] = pendingRequest with
{
State = RestartRequestState.Committing,
};
return true;
}
}
internal void Stop()
{
lock (stateLock)
@ -100,8 +145,8 @@ namespace MinecraftClient
{
lock (stateLock)
{
if (!pendingAttempts.TryGetValue(request.ConnectionAttempt, out long requestId)
|| requestId != request.RequestId)
if (!pendingAttempts.TryGetValue(request.ConnectionAttempt, out PendingRestart pendingRequest)
|| pendingRequest.RequestId != request.RequestId)
continue;
}
@ -121,8 +166,8 @@ namespace MinecraftClient
{
lock (stateLock)
{
if (pendingAttempts.TryGetValue(request.ConnectionAttempt, out long requestId)
&& requestId == request.RequestId)
if (pendingAttempts.TryGetValue(request.ConnectionAttempt, out PendingRestart pendingRequest)
&& pendingRequest.RequestId == request.RequestId)
pendingAttempts.Remove(request.ConnectionAttempt);
}
}