Minecraft-Console-Client/MinecraftClient/RestartCoordinator.cs

190 lines
6.6 KiB
C#
Raw Normal View History

2026-07-19 14:42:10 +02:00
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace MinecraftClient
{
internal readonly record struct RestartRequest(
long ConnectionAttempt,
TimeSpan Delay,
2026-07-26 01:16:07 +02:00
bool KeepAccountAndServerSettings,
RestartSettingsSnapshot? SettingsSnapshot = null,
bool ReplaceUntilCommit = false,
2026-07-26 01:16:07 +02:00
long RequestId = 0);
internal readonly record struct RestartSettingsSnapshot(
Settings.MainConfigHelper.MainConfig.AccountInfoConfig Account,
string ServerIP,
ushort ServerPort);
2026-07-19 14:42:10 +02:00
internal enum RestartRequestState
{
Replaceable,
Committing,
}
internal readonly record struct PendingRestart(
long RequestId,
RestartRequest Request,
RestartRequestState State);
2026-07-19 14:42:10 +02:00
internal sealed class RestartCoordinator : IDisposable
{
private readonly Lock stateLock = new();
private readonly Channel<RestartRequest> requests;
private readonly CancellationTokenSource shutdown = new();
private readonly Func<RestartRequest, CancellationToken, Task> restart;
private readonly Action<Exception> reportFailure;
private readonly Task worker;
private readonly Dictionary<long, PendingRestart> pendingAttempts = [];
2026-07-19 14:42:10 +02:00
private long highestScheduledAttempt = -1;
2026-07-26 01:16:07 +02:00
private long nextRequestId;
2026-07-19 14:42:10 +02:00
private bool stopped;
internal RestartCoordinator(
Func<RestartRequest, CancellationToken, Task> restart,
Action<Exception> reportFailure)
{
ArgumentNullException.ThrowIfNull(restart);
ArgumentNullException.ThrowIfNull(reportFailure);
this.restart = restart;
this.reportFailure = reportFailure;
requests = Channel.CreateUnbounded<RestartRequest>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false,
AllowSynchronousContinuations = false,
});
worker = ProcessRequestsAsync();
}
internal bool HasScheduledRestart(long connectionAttempt)
{
lock (stateLock)
2026-07-26 01:16:07 +02:00
return !stopped && pendingAttempts.ContainsKey(connectionAttempt);
2026-07-19 14:42:10 +02:00
}
internal bool TrySchedule(RestartRequest request)
{
lock (stateLock)
{
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)
2026-07-19 14:42:10 +02:00
return false;
2026-07-26 01:16:07 +02:00
highestScheduledAttempt = Math.Max(highestScheduledAttempt, request.ConnectionAttempt);
request = request with { RequestId = ++nextRequestId };
pendingAttempts[request.ConnectionAttempt] = new PendingRestart(
request.RequestId,
request,
RestartRequestState.Replaceable);
2026-07-19 14:42:10 +02:00
if (requests.Writer.TryWrite(request))
return true;
pendingAttempts.Remove(request.ConnectionAttempt);
2026-07-19 14:42:10 +02:00
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;
}
}
2026-07-19 14:42:10 +02:00
internal void Stop()
{
lock (stateLock)
{
if (stopped)
return;
stopped = true;
pendingAttempts.Clear();
requests.Writer.TryComplete();
shutdown.Cancel();
}
}
private async Task ProcessRequestsAsync()
{
try
{
await foreach (RestartRequest request in requests.Reader.ReadAllAsync(shutdown.Token).ConfigureAwait(false))
{
2026-07-26 01:16:07 +02:00
lock (stateLock)
{
if (!pendingAttempts.TryGetValue(request.ConnectionAttempt, out PendingRestart pendingRequest)
|| pendingRequest.RequestId != request.RequestId)
2026-07-26 01:16:07 +02:00
continue;
}
2026-07-19 14:42:10 +02:00
try
{
await restart(request, shutdown.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (shutdown.IsCancellationRequested)
{
return;
}
catch (Exception exception)
{
reportFailure(exception);
}
finally
{
lock (stateLock)
2026-07-26 01:16:07 +02:00
{
if (pendingAttempts.TryGetValue(request.ConnectionAttempt, out PendingRestart pendingRequest)
&& pendingRequest.RequestId == request.RequestId)
2026-07-26 01:16:07 +02:00
pendingAttempts.Remove(request.ConnectionAttempt);
}
2026-07-19 14:42:10 +02:00
}
}
}
catch (OperationCanceledException) when (shutdown.IsCancellationRequested)
{
}
}
public void Dispose()
{
Stop();
worker.GetAwaiter().GetResult();
shutdown.Dispose();
GC.SuppressFinalize(this);
}
}
}