feat(upserver): 接入认证与KV存档并完成限定钓鱼闭环
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>Owns the KV owner, but never disposes the supplied login session.</summary>
|
||||
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<long, bool> ExternalCurrent;
|
||||
internal readonly long Epoch;
|
||||
readonly S101AccountSession session; readonly CookieContainer cookies;
|
||||
internal PlayerKVSessionOwner Owner;
|
||||
internal Task<PlayerKVSessionOpenResult> 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<long, bool> 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<PlayerKVGameOpenResult> Result =
|
||||
new TaskCompletionSource<PlayerKVGameOpenResult>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
}
|
||||
|
||||
readonly object gate = new object();
|
||||
readonly Func<CookieContainer, HttpMessageHandler> 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<CookieContainer, HttpMessageHandler> handlerFactory)
|
||||
{ this.handlerFactory = handlerFactory ?? throw new ArgumentNullException(nameof(handlerFactory)); }
|
||||
|
||||
public Task<PlayerKVGameOpenResult> OpenAsync(S101AccountSession session, string cacheRoot,
|
||||
string playerId, Uri api, long epoch, Func<long, bool> 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<bool> done, OpenRequest request,
|
||||
long generation, S101AccountSession session, string cacheRoot, string playerId,
|
||||
Uri api, long epoch, Func<long, bool> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Immediately revokes admission, then drains every preceding lifecycle operation.</summary>
|
||||
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<bool> done, TaskCompletionSource<bool> 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<bool> NewCompletion() =>
|
||||
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
static void Complete(OpenRequest request, PlayerKVGameOpenError error) =>
|
||||
request.Result.TrySetResult(new PlayerKVGameOpenResult(error));
|
||||
}
|
||||
|
||||
/// <summary>Captured by delayed callbacks; all operations target this original owner only.</summary>
|
||||
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<PlayerKVFlushResult> FlushAsync(CancellationToken waitToken = default) => slot.Revoked
|
||||
? Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled, Owner.Status))
|
||||
: Owner.FlushAsync(waitToken);
|
||||
public Task<PlayerKVTicketResult> 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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user