93 lines
3.9 KiB
C#
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);
|
|
}
|
|
}
|