mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
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
218 lines
8.9 KiB
C#
218 lines
8.9 KiB
C#
namespace MinecraftClient.Tests;
|
|
|
|
public sealed class RestartCoordinatorTests
|
|
{
|
|
[Fact]
|
|
public async Task AutomaticSameAttemptIsCoalescedWhileQueued()
|
|
{
|
|
var blockerStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var releaseBlocker = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var queuedCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
int queuedExecutions = 0;
|
|
|
|
RestartCoordinator coordinator = null!;
|
|
coordinator = new RestartCoordinator(
|
|
async (request, cancellationToken) =>
|
|
{
|
|
if (request.ConnectionAttempt == 9)
|
|
{
|
|
blockerStarted.SetResult();
|
|
await releaseBlocker.Task.WaitAsync(cancellationToken);
|
|
return;
|
|
}
|
|
|
|
Interlocked.Increment(ref queuedExecutions);
|
|
Assert.True(coordinator.TryBeginCommit(request, out _));
|
|
queuedCompleted.SetResult();
|
|
},
|
|
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
|
using var cleanup = coordinator;
|
|
|
|
Assert.True(coordinator.TrySchedule(new RestartRequest(9, TimeSpan.Zero, true)));
|
|
await blockerStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
Assert.True(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true)));
|
|
Assert.False(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true)));
|
|
Assert.True(coordinator.HasScheduledRestart(10));
|
|
|
|
releaseBlocker.SetResult();
|
|
await queuedCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
Assert.Equal(1, queuedExecutions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AutomaticSameAttemptIsCoalescedDuringCallback()
|
|
{
|
|
var callbackStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var releaseCallback = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var callbackCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
int executions = 0;
|
|
|
|
RestartCoordinator coordinator = null!;
|
|
coordinator = new RestartCoordinator(
|
|
async (request, cancellationToken) =>
|
|
{
|
|
Interlocked.Increment(ref executions);
|
|
callbackStarted.SetResult();
|
|
await releaseCallback.Task.WaitAsync(cancellationToken);
|
|
Assert.True(coordinator.TryBeginCommit(request, out _));
|
|
callbackCompleted.SetResult();
|
|
},
|
|
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
|
using var cleanup = coordinator;
|
|
|
|
Assert.True(coordinator.TrySchedule(new RestartRequest(42, TimeSpan.Zero, true)));
|
|
await callbackStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
Assert.False(coordinator.TrySchedule(new RestartRequest(42, TimeSpan.Zero, true)));
|
|
|
|
releaseCallback.SetResult();
|
|
await callbackCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
Assert.Equal(1, executions);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ExplicitReplacementDuringDelayUsesLatestSnapshotWithoutAnotherExecution()
|
|
{
|
|
var callbackStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var allowCommit = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var callbackCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
RestartRequest committedRequest = default;
|
|
int executions = 0;
|
|
|
|
RestartCoordinator coordinator = null!;
|
|
coordinator = new RestartCoordinator(
|
|
async (request, cancellationToken) =>
|
|
{
|
|
Interlocked.Increment(ref executions);
|
|
callbackStarted.SetResult();
|
|
await allowCommit.Task.WaitAsync(cancellationToken);
|
|
Assert.True(coordinator.TryBeginCommit(request, out committedRequest));
|
|
callbackCompleted.SetResult();
|
|
},
|
|
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
|
using var cleanup = coordinator;
|
|
|
|
Assert.True(coordinator.TrySchedule(new RestartRequest(
|
|
50,
|
|
TimeSpan.FromSeconds(10),
|
|
true,
|
|
CreateSettingsSnapshot("first"))));
|
|
await callbackStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
Assert.True(coordinator.TrySchedule(new RestartRequest(
|
|
50,
|
|
TimeSpan.Zero,
|
|
true,
|
|
CreateSettingsSnapshot("replacement"),
|
|
ReplaceUntilCommit: true)));
|
|
|
|
allowCommit.SetResult();
|
|
await callbackCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
Assert.Equal(1, executions);
|
|
Assert.Equal("replacement", committedRequest.SettingsSnapshot?.Account.Login);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RejectsSameAttemptReplacementAfterCommit()
|
|
{
|
|
var commitStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var releaseCommit = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
RestartRequest committedRequest = default;
|
|
|
|
RestartCoordinator coordinator = null!;
|
|
coordinator = new RestartCoordinator(
|
|
async (request, cancellationToken) =>
|
|
{
|
|
Assert.True(coordinator.TryBeginCommit(request, out committedRequest));
|
|
commitStarted.SetResult();
|
|
await releaseCommit.Task.WaitAsync(cancellationToken);
|
|
},
|
|
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
|
using var cleanup = coordinator;
|
|
|
|
Assert.True(coordinator.TrySchedule(new RestartRequest(
|
|
60,
|
|
TimeSpan.Zero,
|
|
true,
|
|
CreateSettingsSnapshot("committed"))));
|
|
await commitStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
Assert.False(coordinator.TrySchedule(new RestartRequest(
|
|
60,
|
|
TimeSpan.Zero,
|
|
true,
|
|
CreateSettingsSnapshot("rejected"),
|
|
ReplaceUntilCommit: true)));
|
|
Assert.Equal("committed", committedRequest.SettingsSnapshot?.Account.Login);
|
|
|
|
releaseCommit.SetResult();
|
|
}
|
|
|
|
[Fact]
|
|
public void RejectsStaleAttempt()
|
|
{
|
|
RestartCoordinator coordinator = null!;
|
|
coordinator = new RestartCoordinator(
|
|
(_, _) => Task.CompletedTask,
|
|
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
|
using var cleanup = coordinator;
|
|
|
|
Assert.True(coordinator.TrySchedule(new RestartRequest(20, TimeSpan.Zero, true)));
|
|
Assert.False(coordinator.TrySchedule(new RestartRequest(19, TimeSpan.Zero, true)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RejectsCompletedAttempt()
|
|
{
|
|
var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
RestartCoordinator coordinator = null!;
|
|
coordinator = new RestartCoordinator(
|
|
(request, cancellationToken) =>
|
|
{
|
|
Assert.True(coordinator.TryBeginCommit(request, out _));
|
|
completed.SetResult();
|
|
return Task.CompletedTask;
|
|
},
|
|
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
|
using var cleanup = coordinator;
|
|
|
|
Assert.True(coordinator.TrySchedule(new RestartRequest(20, TimeSpan.Zero, true)));
|
|
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
Assert.True(SpinWait.SpinUntil(() => !coordinator.HasScheduledRestart(20), TimeSpan.FromSeconds(5)));
|
|
|
|
Assert.False(coordinator.TrySchedule(new RestartRequest(20, TimeSpan.Zero, true)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task TerminalStopCancelsInFlightWorkAndRejectsFurtherRestarts()
|
|
{
|
|
var callbackStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
using var coordinator = new RestartCoordinator(
|
|
async (_, cancellationToken) =>
|
|
{
|
|
callbackStarted.SetResult();
|
|
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
|
|
},
|
|
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
|
|
|
|
Assert.True(coordinator.TrySchedule(new RestartRequest(1, TimeSpan.Zero, true)));
|
|
await callbackStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
|
|
|
coordinator.Stop();
|
|
|
|
Assert.False(coordinator.TrySchedule(new RestartRequest(2, TimeSpan.Zero, true)));
|
|
Assert.False(coordinator.HasScheduledRestart(1));
|
|
}
|
|
|
|
private static RestartSettingsSnapshot CreateSettingsSnapshot(string account)
|
|
{
|
|
return new RestartSettingsSnapshot(
|
|
new Settings.MainConfigHelper.MainConfig.AccountInfoConfig(account, "-"),
|
|
"localhost",
|
|
25565);
|
|
}
|
|
}
|