mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Fix restart publication races (#3186)
Prepare offline routing before restart requests become observable, reject stale route owners, and wait for source-client cleanup before executing automatic retries. Add deterministic publication-order, cleanup-gate, and stale-route regression tests.
This commit is contained in:
parent
456a548cbc
commit
5655e3fc89
7 changed files with 164 additions and 25 deletions
|
|
@ -170,7 +170,11 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
McClient.ReconnectionAttemptsLeft = retriesLeft;
|
||||
long connectionAttempt = sourceConnectionAttempt ?? Handler.ConnectionAttempt;
|
||||
if (Program.TryRestart(connectionAttempt, TimeSpan.FromSeconds(delay), true))
|
||||
if (Program.TryRestart(
|
||||
connectionAttempt,
|
||||
TimeSpan.FromSeconds(delay),
|
||||
keepAccountAndServerSettings: true,
|
||||
sourceCleanupCompletion: sourceConnectionAttempt.HasValue ? null : Handler.DisconnectCompletion))
|
||||
{
|
||||
LogToConsole(string.Format(Translations.bot_autoRelog_wait_with_retries, delay, retriesDisplay));
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
internal bool TryActivate(long connectionAttempt, Action activate)
|
||||
internal bool TryActivate(long connectionAttempt, long currentConnectionAttempt, Action activate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(activate);
|
||||
|
||||
lock (stateLock)
|
||||
{
|
||||
if (ownerAttempt >= connectionAttempt)
|
||||
if (connectionAttempt < currentConnectionAttempt || ownerAttempt >= connectionAttempt)
|
||||
return false;
|
||||
|
||||
ownerAttempt = connectionAttempt;
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ namespace MinecraftClient
|
|||
public ILogger Log;
|
||||
public DialogManager Dialogs { get; }
|
||||
internal long ConnectionAttempt { get; }
|
||||
internal Task DisconnectCompletion => disconnectCompletion.Task;
|
||||
|
||||
private static IMinecraftComHandler? instance;
|
||||
public static IMinecraftComHandler? Instance => instance;
|
||||
|
|
|
|||
|
|
@ -1095,11 +1095,15 @@ namespace MinecraftClient
|
|||
long sourceConnectionAttempt,
|
||||
TimeSpan delay,
|
||||
bool keepAccountAndServerSettings = false,
|
||||
bool replaceUntilCommit = false)
|
||||
bool replaceUntilCommit = false,
|
||||
Task? sourceCleanupCompletion = null)
|
||||
{
|
||||
if (Volatile.Read(ref exitOnFailurePending) != 0)
|
||||
return false;
|
||||
|
||||
if (sourceConnectionAttempt != CurrentConnectionAttempt)
|
||||
return false;
|
||||
|
||||
if (delay < TimeSpan.Zero)
|
||||
delay = TimeSpan.Zero;
|
||||
|
||||
|
|
@ -1107,14 +1111,15 @@ namespace MinecraftClient
|
|||
? CaptureRestartSettings()
|
||||
: null;
|
||||
|
||||
bool scheduled = restartCoordinator.TrySchedule(new RestartRequest(
|
||||
sourceConnectionAttempt,
|
||||
delay,
|
||||
keepAccountAndServerSettings,
|
||||
settingsSnapshot,
|
||||
replaceUntilCommit));
|
||||
if (scheduled)
|
||||
BeginOfflinePrompt(sourceConnectionAttempt);
|
||||
bool scheduled = restartCoordinator.TrySchedule(
|
||||
new RestartRequest(
|
||||
sourceConnectionAttempt,
|
||||
delay,
|
||||
keepAccountAndServerSettings,
|
||||
settingsSnapshot,
|
||||
replaceUntilCommit,
|
||||
sourceCleanupCompletion),
|
||||
() => BeginOfflinePrompt(sourceConnectionAttempt));
|
||||
return scheduled;
|
||||
}
|
||||
|
||||
|
|
@ -1265,9 +1270,13 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
private static void BeginOfflinePrompt(long connectionAttempt)
|
||||
private static bool BeginOfflinePrompt(long connectionAttempt)
|
||||
{
|
||||
offlinePromptRoute.TryActivate(connectionAttempt, () =>
|
||||
long currentConnectionAttempt = CurrentConnectionAttempt;
|
||||
if (connectionAttempt != currentConnectionAttempt)
|
||||
return false;
|
||||
|
||||
if (offlinePromptRoute.TryActivate(connectionAttempt, currentConnectionAttempt, () =>
|
||||
{
|
||||
ConsoleInputRouter.RouteOffline(HandleOfflineCommand);
|
||||
ConsoleIO.WriteLine(string.Empty);
|
||||
|
|
@ -1276,7 +1285,12 @@ namespace MinecraftClient
|
|||
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString()));
|
||||
else
|
||||
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
|
||||
});
|
||||
}))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return offlinePromptRoute.OwnerAttempt == connectionAttempt;
|
||||
}
|
||||
|
||||
private static void EndOfflinePrompt()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace MinecraftClient
|
|||
bool KeepAccountAndServerSettings,
|
||||
RestartSettingsSnapshot? SettingsSnapshot = null,
|
||||
bool ReplaceUntilCommit = false,
|
||||
Task? SourceCleanupCompletion = null,
|
||||
long RequestId = 0);
|
||||
|
||||
internal readonly record struct RestartSettingsSnapshot(
|
||||
|
|
@ -67,7 +68,7 @@ namespace MinecraftClient
|
|||
return !stopped && pendingAttempts.ContainsKey(connectionAttempt);
|
||||
}
|
||||
|
||||
internal bool TrySchedule(RestartRequest request)
|
||||
internal bool TrySchedule(RestartRequest request, Func<bool>? beforePublish = null)
|
||||
{
|
||||
lock (stateLock)
|
||||
{
|
||||
|
|
@ -79,7 +80,11 @@ namespace MinecraftClient
|
|||
if (pendingRequest.State != RestartRequestState.Replaceable || !request.ReplaceUntilCommit)
|
||||
return false;
|
||||
|
||||
request = request with { RequestId = pendingRequest.RequestId };
|
||||
request = request with
|
||||
{
|
||||
RequestId = pendingRequest.RequestId,
|
||||
SourceCleanupCompletion = pendingRequest.Request.SourceCleanupCompletion,
|
||||
};
|
||||
pendingAttempts[request.ConnectionAttempt] = pendingRequest with { Request = request };
|
||||
return true;
|
||||
}
|
||||
|
|
@ -87,14 +92,30 @@ namespace MinecraftClient
|
|||
if (request.ConnectionAttempt <= highestScheduledAttempt)
|
||||
return false;
|
||||
|
||||
highestScheduledAttempt = Math.Max(highestScheduledAttempt, request.ConnectionAttempt);
|
||||
request = request with { RequestId = ++nextRequestId };
|
||||
pendingAttempts[request.ConnectionAttempt] = new PendingRestart(
|
||||
request.RequestId,
|
||||
request,
|
||||
RestartRequestState.Replaceable);
|
||||
if (requests.Writer.TryWrite(request))
|
||||
return true;
|
||||
try
|
||||
{
|
||||
if (beforePublish is not null && !beforePublish())
|
||||
{
|
||||
pendingAttempts.Remove(request.ConnectionAttempt);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requests.Writer.TryWrite(request))
|
||||
{
|
||||
highestScheduledAttempt = Math.Max(highestScheduledAttempt, request.ConnectionAttempt);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
pendingAttempts.Remove(request.ConnectionAttempt);
|
||||
throw;
|
||||
}
|
||||
|
||||
pendingAttempts.Remove(request.ConnectionAttempt);
|
||||
return false;
|
||||
|
|
@ -152,6 +173,9 @@ namespace MinecraftClient
|
|||
|
||||
try
|
||||
{
|
||||
if (request.SourceCleanupCompletion is Task sourceCleanupCompletion)
|
||||
await sourceCleanupCompletion.WaitAsync(shutdown.Token).ConfigureAwait(false);
|
||||
|
||||
await restart(request, shutdown.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (shutdown.IsCancellationRequested)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue