Files
ft/Client/Assets/Scripts/Services/PlayerData/PlayerKVShutdownRegistry.cs

93 lines
3.9 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace game.PlayerDataStorage
{
internal enum PlayerKVShutdownStatus { Ready, Pending, Failed, Cancelled, Input }
/// <summary>Process-lifetime barrier. Faulted and unfinished work cannot be reset by replacing a scene/container.</summary>
internal sealed class PlayerKVShutdownRegistry
{
internal static PlayerKVShutdownRegistry Process { get; } = new PlayerKVShutdownRegistry();
readonly object gate = new object();
readonly Dictionary<Task, PlayerKVLoginContext> pending = new Dictionary<Task, PlayerKVLoginContext>();
TaskCompletionSource<bool> changed = Signal();
internal void Register(Task task, PlayerKVLoginContext owner = null)
{
if (task == null) throw new ArgumentNullException(nameof(task));
lock (gate)
{
if (!pending.ContainsKey(task) || owner != null) pending[task] = owner;
NotifyChanged();
}
_ = task.ContinueWith(t =>
{
if (t.IsFaulted) { _ = t.Exception; }
lock (gate)
{
if (t.Status == TaskStatus.RanToCompletion) pending.Remove(t);
NotifyChanged();
}
// Cancelled work does not prove resource cleanup; retain it as a failure barrier.
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
internal PlayerKVShutdownStatus Status
{
get
{
lock (gate)
{
if (pending.Keys.Any(t => t.IsFaulted || t.IsCanceled)) return PlayerKVShutdownStatus.Failed;
return pending.Keys.Any(t => !t.IsCompleted) ? PlayerKVShutdownStatus.Pending : PlayerKVShutdownStatus.Ready;
}
}
}
// Injection is an observation clock only, never a force-close or permission to reopen.
internal async Task<PlayerKVShutdownStatus> ObserveAsync(int timeoutMs, CancellationToken token = default,
Func<int, CancellationToken, Task> delay = null)
{
if (timeoutMs < 1000 || timeoutMs > 15000) return PlayerKVShutdownStatus.Input;
if (token.IsCancellationRequested) return PlayerKVShutdownStatus.Cancelled;
using (var clock = CancellationTokenSource.CreateLinkedTokenSource(token))
{
var deadline = (delay ?? Task.Delay)(timeoutMs, clock.Token);
try
{
while (true)
{
if (token.IsCancellationRequested) return PlayerKVShutdownStatus.Cancelled;
Task notification;
lock (gate)
{
var status = Status;
if (status != PlayerKVShutdownStatus.Pending) return status;
notification = changed.Task;
}
if (await Task.WhenAny(notification, deadline).ConfigureAwait(false) == deadline)
{
if (token.IsCancellationRequested) return PlayerKVShutdownStatus.Cancelled;
return Status;
}
// Recheck the live collection; another Root may have registered work during the await.
}
}
finally { clock.Cancel(); }
}
}
void NotifyChanged()
{
var previous = changed; changed = Signal(); previous.TrySetResult(true);
}
static TaskCompletionSource<bool> Signal() =>
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
}
}