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

@ -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;
}
}
}
}