using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using game.Account;
namespace game.PlayerDataStorage
{
public enum PlayerKVGameOpenError { None, OwnerRejected, Cancelled, Stale, CloseFailed }
public sealed class PlayerKVGameOpenResult
{
public bool Success => Error == PlayerKVGameOpenError.None;
public PlayerKVGameOpenError Error { get; }
public PlayerKVSessionOpenError OwnerError { get; }
public PlayerKVError TransportError { get; }
public PlayerKVGameSessionHandle Handle { get; }
internal PlayerKVGameOpenResult(PlayerKVGameOpenError error,
PlayerKVSessionOpenError ownerError = PlayerKVSessionOpenError.None,
PlayerKVError transportError = PlayerKVError.None, PlayerKVGameSessionHandle handle = null)
{ Error = error; OwnerError = ownerError; TransportError = transportError; Handle = handle; }
}
/// Owns the KV owner, but never disposes the supplied login session.
public sealed class PlayerKVGameSessionBridge
{
// A fixed slot is also the handle's identity. It can never be rebound to another owner.
internal sealed class Slot
{
internal readonly CancellationTokenSource Lifetime;
internal readonly Func ExternalCurrent;
internal readonly long Epoch;
readonly S101AccountSession session; readonly CookieContainer cookies;
internal PlayerKVSessionOwner Owner;
internal Task Creation;
internal Task Closing;
readonly object cancellationGate = new object();
bool lifetimeDisposed; Exception cancellationFailure;
internal Exception CancellationFailure { get { lock (cancellationGate) return cancellationFailure; } }
int revoked;
internal Slot(S101AccountSession session, long epoch, Func current, CancellationToken token)
{ this.session = session; cookies = session?.Cookies; Epoch = epoch; ExternalCurrent = current; Lifetime = CancellationTokenSource.CreateLinkedTokenSource(token); }
internal void DisposeLifetime()
{
lock (cancellationGate)
{
if (lifetimeDisposed) return;
Lifetime.Dispose(); lifetimeDisposed = true;
}
}
internal bool Revoked => Volatile.Read(ref revoked) != 0;
internal void Revoke() => Interlocked.Exchange(ref revoked, 1);
internal bool IsCurrent(long epoch) =>
!Revoked && !Lifetime.IsCancellationRequested && session != null && session.IsAuthenticated &&
ReferenceEquals(session.Cookies, cookies) && epoch == Epoch && ExternalCurrent(epoch);
internal void Cancel()
{
Revoke();
// Close can revoke an opening slot while Create is completing its cleanup.
// Serialize CTS disposal with cancellation, without holding the bridge monitor.
lock (cancellationGate)
{
if (lifetimeDisposed) return;
try { Lifetime.Cancel(); }
catch (Exception ex) { cancellationFailure = ex; }
}
}
}
sealed class OpenRequest
{
internal bool Entered;
internal CancellationTokenRegistration Registration;
internal readonly TaskCompletionSource Result =
new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
}
readonly object gate = new object();
readonly Func handlerFactory;
Task tail = Task.CompletedTask;
Task lastClose;
Slot active;
long closeGeneration;
bool closeFailed;
public PlayerKVGameSessionBridge() : this(jar => new HttpClientHandler
{ CookieContainer = jar, UseCookies = true, AllowAutoRedirect = false }) { }
internal PlayerKVGameSessionBridge(Func handlerFactory)
{ this.handlerFactory = handlerFactory ?? throw new ArgumentNullException(nameof(handlerFactory)); }
public Task OpenAsync(S101AccountSession session, string cacheRoot,
string playerId, Uri api, long epoch, Func isCurrent, CancellationToken lifetimeToken = default)
{
if (isCurrent == null)
return Task.FromResult(new PlayerKVGameOpenResult(PlayerKVGameOpenError.OwnerRejected,
PlayerKVSessionOpenError.Validation));
var request = new OpenRequest();
var done = NewCompletion();
Task predecessor; long generation;
lock (gate)
{
predecessor = tail; tail = done.Task;
generation = closeGeneration;
lastClose = null;
}
request.Registration = lifetimeToken.Register(() =>
{
lock (gate)
if (!request.Entered)
request.Result.TrySetResult(new PlayerKVGameOpenResult(PlayerKVGameOpenError.Cancelled));
});
// The reserved tail stays pending even if the caller's queued wait is cancelled.
_ = OpenCoreAsync(predecessor, done, request, generation, session, cacheRoot,
playerId, api, epoch, isCurrent, lifetimeToken);
return request.Result.Task;
}
async Task OpenCoreAsync(Task predecessor, TaskCompletionSource done, OpenRequest request,
long generation, S101AccountSession session, string cacheRoot, string playerId,
Uri api, long epoch, Func isCurrent, CancellationToken token)
{
try
{
await predecessor.ConfigureAwait(false);
Slot previous;
lock (gate)
{
request.Entered = true;
if (closeFailed) { Complete(request, PlayerKVGameOpenError.CloseFailed); return; }
if (token.IsCancellationRequested) { Complete(request, PlayerKVGameOpenError.Cancelled); return; }
if (generation != closeGeneration) { Complete(request, PlayerKVGameOpenError.Stale); return; }
previous = active;
previous?.Revoke();
}
if (previous != null) await CloseSlotAsync(previous).ConfigureAwait(false);
Slot slot;
lock (gate)
{
if (token.IsCancellationRequested) { Complete(request, PlayerKVGameOpenError.Cancelled); return; }
if (generation != closeGeneration) { Complete(request, PlayerKVGameOpenError.Stale); return; }
slot = new Slot(session, epoch, isCurrent, token);
active = slot;
}
slot.Creation = PlayerKVSessionOwner.CreateAsync(session, cacheRoot, playerId, api,
epoch, slot.IsCurrent, slot.Lifetime.Token, handlerFactory);
var opened = await slot.Creation.ConfigureAwait(false);
if (!opened.Success)
{
// CreateAsync returns failure only after its failed owner has drained.
if (slot.CancellationFailure != null) throw slot.CancellationFailure;
lock (gate) if (ReferenceEquals(active, slot)) active = null;
slot.Revoke(); slot.DisposeLifetime();
request.Result.TrySetResult(new PlayerKVGameOpenResult(PlayerKVGameOpenError.OwnerRejected,
opened.Error, opened.TransportError));
return;
}
slot.Owner = opened.Owner;
var valid = slot.IsCurrent(epoch);
lock (gate)
{
if (valid && generation == closeGeneration && !slot.Revoked && !token.IsCancellationRequested)
{
request.Result.TrySetResult(new PlayerKVGameOpenResult(PlayerKVGameOpenError.None,
handle: new PlayerKVGameSessionHandle(slot)));
return;
}
slot.Revoke();
}
await CloseSlotAsync(slot).ConfigureAwait(false);
Complete(request, token.IsCancellationRequested ? PlayerKVGameOpenError.Cancelled : PlayerKVGameOpenError.Stale);
}
catch (Exception)
{
// Includes failed Create cleanup. Retain the slot, Creation/Closing task and owner;
// no later Open is allowed to pretend the cache is reusable.
lock (gate) { closeFailed = true; active?.Revoke(); }
Complete(request, PlayerKVGameOpenError.CloseFailed);
}
finally
{
request.Registration.Dispose();
done.TrySetResult(true);
}
}
/// Immediately revokes admission, then drains every preceding lifecycle operation.
public Task CloseAsync()
{
Task predecessor; Slot observed;
var done = NewCompletion();
var result = NewCompletion();
lock (gate)
{
if (lastClose != null) return lastClose;
closeGeneration++;
observed = active; observed?.Revoke();
predecessor = tail; tail = done.Task; lastClose = result.Task;
}
// Cancellation callbacks must never run while the bridge monitor is held.
observed?.Cancel();
_ = CloseCoreAsync(predecessor, done, result);
return result.Task;
}
async Task CloseCoreAsync(Task predecessor, TaskCompletionSource done, TaskCompletionSource result)
{
try
{
await predecessor.ConfigureAwait(false);
Slot slot;
lock (gate)
{
if (closeFailed) throw new InvalidOperationException("KV owner cleanup previously failed.");
slot = active;
}
if (slot != null) await CloseSlotAsync(slot).ConfigureAwait(false);
result.TrySetResult(true);
}
catch (Exception ex)
{
lock (gate) { closeFailed = true; active?.Revoke(); }
result.TrySetException(ex);
}
finally { done.TrySetResult(true); }
}
async Task CloseSlotAsync(Slot slot)
{
slot.Cancel();
if (slot.Owner != null)
{
slot.Closing = slot.Closing ?? slot.Owner.CloseAsync();
await slot.Closing.ConfigureAwait(false);
}
if (slot.CancellationFailure != null) throw slot.CancellationFailure;
lock (gate) if (ReferenceEquals(active, slot)) active = null;
slot.DisposeLifetime();
}
static TaskCompletionSource NewCompletion() =>
new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
static void Complete(OpenRequest request, PlayerKVGameOpenError error) =>
request.Result.TrySetResult(new PlayerKVGameOpenResult(error));
}
/// Captured by delayed callbacks; all operations target this original owner only.
public sealed class PlayerKVGameSessionHandle
{
readonly PlayerKVGameSessionBridge.Slot slot;
internal PlayerKVGameSessionHandle(PlayerKVGameSessionBridge.Slot slot) { this.slot = slot; }
PlayerKVSessionOwner Owner => slot.Owner;
public string OwnerInstanceId => Owner.OwnerInstanceId;
public string PlayerId => Owner.PlayerId;
public long Epoch => Owner.Epoch;
public PlayerKVSyncStatus Status => Owner.Status;
public PlayerKVLocalReadResult ReadLocalSnapshot() => slot.Revoked
? new PlayerKVLocalReadResult(PlayerKVLocalReadError.StaleOrClosing, OwnerInstanceId, PlayerId, Epoch)
: Owner.ReadLocalSnapshot();
public PlayerKVEditResult Update(string key, string value, bool remoteStore = true) =>
slot.Revoked ? BlockedEdit() : Owner.Update(key, value, remoteStore);
public PlayerKVEditResult Delete(string key) => slot.Revoked ? BlockedEdit() : Owner.Delete(key);
public Task FlushAsync(CancellationToken waitToken = default) => slot.Revoked
? Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled, Owner.Status))
: Owner.FlushAsync(waitToken);
public Task WaitForTicketAsync(PlayerKVSyncTicket ticket, CancellationToken token = default) =>
!slot.IsCurrent(Epoch) ? Task.FromResult(new PlayerKVTicketResult(
PlayerKVTicketStatus.Blocked, PlayerKVError.Cancelled, Owner.Status)) : Owner.WaitForTicketAsync(ticket, token);
static PlayerKVEditResult BlockedEdit() =>
new PlayerKVEditResult(new PlayerKVStateResult(PlayerKVChangeError.Blocked));
}
}