refactor(kv): 整理存档接线并清理迁移冗余实现
This commit is contained in:
461
Client/Assets/Scripts/Core/KV/KvSessionOwner.cs
Normal file
461
Client/Assets/Scripts/Core/KV/KvSessionOwner.cs
Normal file
@@ -0,0 +1,461 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using game.Account;
|
||||
|
||||
namespace Core.KV
|
||||
{
|
||||
public enum KVSessionOpenError
|
||||
{
|
||||
None,
|
||||
Validation,
|
||||
Unauthenticated,
|
||||
Busy,
|
||||
RecoveryRequired,
|
||||
StorageError,
|
||||
ReadFailed,
|
||||
Unauthorized,
|
||||
Stale,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
public sealed class KvSessionOpenResult
|
||||
{
|
||||
public bool Success => Error == KVSessionOpenError.None;
|
||||
public KVSessionOpenError Error
|
||||
{
|
||||
get;
|
||||
}
|
||||
public KvSessionOwner Owner
|
||||
{
|
||||
get;
|
||||
}
|
||||
internal KvSessionOpenResult(KVSessionOpenError error, KvSessionOwner owner = null)
|
||||
{
|
||||
Error = error;
|
||||
Owner = owner;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Owns one immutable account/epoch/cache/transport tuple.</summary>
|
||||
public sealed class KvSessionOwner
|
||||
{
|
||||
private sealed class OperationLease : IDisposable
|
||||
{
|
||||
private KvSessionOwner owner;
|
||||
internal OperationLease(KvSessionOwner owner)
|
||||
{
|
||||
this.owner = owner;
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
Interlocked.Exchange(ref owner, null)?.ExitOperation();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object admission = new object();
|
||||
private readonly GameServerAccountSession session;
|
||||
private readonly CookieContainer cookies;
|
||||
private readonly CancellationTokenSource lifetime;
|
||||
private readonly Func<long, bool> isCurrent;
|
||||
private readonly KvSyncState state;
|
||||
|
||||
private readonly KvSyncCoordinator coordinator;
|
||||
private readonly TaskCompletionSource<bool> drained =
|
||||
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
private readonly string ownerId = Guid.NewGuid().ToString("N");
|
||||
private int activeOperations;
|
||||
private bool closing, closed;
|
||||
private TaskCompletionSource<bool> closeCompletion;
|
||||
|
||||
public string OwnerInstanceId => ownerId;
|
||||
public Uri ApiBaseUri
|
||||
{
|
||||
get;
|
||||
}
|
||||
public string PlayerId
|
||||
{
|
||||
get;
|
||||
}
|
||||
public long Epoch
|
||||
{
|
||||
get;
|
||||
}
|
||||
public KVSyncStatus Status => state.Status;
|
||||
|
||||
private KvSessionOwner(GameServerAccountSession session, CookieContainer cookies, Uri api, string playerId,
|
||||
long epoch, Func<long, bool> isCurrent, CancellationTokenSource lifetime,
|
||||
KvSyncState state, HttpClient http)
|
||||
{
|
||||
this.session = session;
|
||||
this.cookies = cookies;
|
||||
ApiBaseUri = api;
|
||||
PlayerId = playerId;
|
||||
Epoch = epoch;
|
||||
this.isCurrent = isCurrent;
|
||||
this.lifetime = lifetime;
|
||||
this.state = state;
|
||||
coordinator = new KvSyncCoordinator(state, new KvClient(http, api), http,
|
||||
IsCapturedOwnerCurrent, lifetime.Token);
|
||||
}
|
||||
|
||||
internal static async Task<KvSessionOpenResult> CreateAsync(GameServerAccountSession session, string cacheRoot,
|
||||
string expectedPlayerId, Uri expectedApiBaseUri, long epoch, Func<long, bool> isCurrent,
|
||||
CancellationToken lifetimeToken, Func<CookieContainer, HttpMessageHandler> handlerFactory)
|
||||
{
|
||||
if (session == null || expectedApiBaseUri == null || isCurrent == null || handlerFactory == null || epoch < 0 ||
|
||||
!long.TryParse(expectedPlayerId, NumberStyles.None, CultureInfo.InvariantCulture, out var numericId) || numericId <= 0)
|
||||
{
|
||||
return new KvSessionOpenResult(KVSessionOpenError.Validation);
|
||||
}
|
||||
var playerId = numericId.ToString(CultureInfo.InvariantCulture);
|
||||
Uri expectedApi;
|
||||
try
|
||||
{
|
||||
expectedApi = NormalizeApi(expectedApiBaseUri);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return new KvSessionOpenResult(KVSessionOpenError.Validation);
|
||||
}
|
||||
if (lifetimeToken.IsCancellationRequested)
|
||||
{
|
||||
return new KvSessionOpenResult(KVSessionOpenError.Cancelled);
|
||||
}
|
||||
if (!session.IsAuthenticated || session.Cookies == null)
|
||||
{
|
||||
return new KvSessionOpenResult(KVSessionOpenError.Unauthenticated);
|
||||
}
|
||||
var capturedCookies = session.Cookies;
|
||||
var capturedApi = NormalizeApi(session.ApiBaseUri);
|
||||
if (session.PlayerId != playerId || capturedApi != expectedApi || !isCurrent(epoch))
|
||||
{
|
||||
return new KvSessionOpenResult(KVSessionOpenError.Stale);
|
||||
}
|
||||
|
||||
var opened = KvSyncState.Open(cacheRoot, capturedApi, playerId);
|
||||
if (!opened.Success)
|
||||
{
|
||||
return new KvSessionOpenResult(MapOpen(opened.Error));
|
||||
}
|
||||
HttpClient http = null;
|
||||
CancellationTokenSource lifetime = null;
|
||||
KvSessionOwner owner = null;
|
||||
try
|
||||
{
|
||||
var handler = handlerFactory(capturedCookies);
|
||||
if (handler == null)
|
||||
{
|
||||
throw new ArgumentException("Transport factory returned null.");
|
||||
}
|
||||
http = new HttpClient(handler)
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(15)
|
||||
};
|
||||
lifetime = CancellationTokenSource.CreateLinkedTokenSource(lifetimeToken);
|
||||
owner = new KvSessionOwner(session, capturedCookies, capturedApi, playerId, epoch,
|
||||
isCurrent, lifetime, opened.State, http);
|
||||
http = null;
|
||||
lifetime = null;
|
||||
KvResult read;
|
||||
using (var lease = owner.TryEnter())
|
||||
{
|
||||
if (lease == null)
|
||||
{
|
||||
return await owner.FailCreate(KVSessionOpenError.Cancelled);
|
||||
}
|
||||
read = await owner.coordinator.InitializeAsync();
|
||||
}
|
||||
if (!read.Success)
|
||||
{
|
||||
return await owner.FailCreate(read.Error == KVError.Unauthorized
|
||||
? KVSessionOpenError.Unauthorized
|
||||
: read.Error == KVError.Cancelled ? KVSessionOpenError.Cancelled
|
||||
: KVSessionOpenError.ReadFailed);
|
||||
}
|
||||
if (!owner.IsCapturedOwnerCurrent())
|
||||
{
|
||||
return await owner.FailCreate(KVSessionOpenError.Stale);
|
||||
}
|
||||
return new KvSessionOpenResult(KVSessionOpenError.None, owner: owner);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (owner != null)
|
||||
{
|
||||
return await owner.FailCreate(KVSessionOpenError.Cancelled);
|
||||
}
|
||||
opened.State.Dispose();
|
||||
return new KvSessionOpenResult(KVSessionOpenError.Cancelled);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (owner != null)
|
||||
{
|
||||
return await owner.FailCreate(KVSessionOpenError.Validation);
|
||||
}
|
||||
http?.Dispose();
|
||||
lifetime?.Dispose();
|
||||
opened.State.Dispose();
|
||||
return new KvSessionOpenResult(KVSessionOpenError.Validation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Reads the existing state under an operation lease; does not open another cache.</summary>
|
||||
public KvLocalReadResult ReadLocalSnapshot()
|
||||
{
|
||||
using (var lease = TryEnter())
|
||||
{
|
||||
if (lease == null)
|
||||
{
|
||||
return ReadFailure(KVLocalReadError.StaleOrClosing);
|
||||
}
|
||||
try
|
||||
{
|
||||
var data = state.ReadLocal();
|
||||
lock (admission)
|
||||
{
|
||||
if (!IsCapturedOwnerCurrent() || closing || closed)
|
||||
{
|
||||
return ReadFailure(KVLocalReadError.StaleOrClosing);
|
||||
}
|
||||
return new KvLocalReadResult(KVLocalReadError.None, data);
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
lock (admission)
|
||||
return ReadFailure(!IsCapturedOwnerCurrent() || closing || closed
|
||||
? KVLocalReadError.StaleOrClosing : KVLocalReadError.NotReady);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private KvLocalReadResult ReadFailure(KVLocalReadError error) =>
|
||||
new KvLocalReadResult(error);
|
||||
|
||||
internal KvStateResult CompareDatas(long nowTicks)
|
||||
{
|
||||
using (var lease = TryEnter())
|
||||
{
|
||||
return lease == null ? new KvStateResult(KVChangeError.Blocked) : coordinator.CompareDatas(nowTicks);
|
||||
}
|
||||
}
|
||||
|
||||
internal KvStateResult Update(string key, string value, bool remoteStore, long updateTicks)
|
||||
{
|
||||
using (var lease = TryEnter())
|
||||
{
|
||||
return lease == null ? ClosedEdit() : coordinator.Update(key, value, remoteStore, updateTicks);
|
||||
}
|
||||
}
|
||||
|
||||
public Task<KvFlushResult> SyncDataAsync()
|
||||
{
|
||||
var lease = TryEnter();
|
||||
if (lease == null)
|
||||
{
|
||||
return Task.FromResult(new KvFlushResult(1, 0, KVError.Cancelled));
|
||||
}
|
||||
return RunOrdinarySyncAsync(lease);
|
||||
}
|
||||
|
||||
private async Task<KvFlushResult> RunOrdinarySyncAsync(OperationLease lease)
|
||||
{
|
||||
using (lease)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await coordinator.SyncDataAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return new KvFlushResult(1, 0, KVError.Protocol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> SyncDataImmidiate(string key, string value)
|
||||
{
|
||||
using (var lease = TryEnter())
|
||||
{
|
||||
if (lease == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await coordinator.SyncDataImmidiate(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task<bool> SyncDataImmidiate(IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
using (var lease = TryEnter())
|
||||
{
|
||||
if (lease == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return await coordinator.SyncDataImmidiate(values);
|
||||
}
|
||||
}
|
||||
|
||||
private OperationLease TryEnter()
|
||||
{
|
||||
lock (admission)
|
||||
{
|
||||
if (closing || closed || !IsCapturedOwnerCurrent())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
activeOperations++;
|
||||
return new OperationLease(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExitOperation()
|
||||
{
|
||||
lock (admission)
|
||||
{
|
||||
activeOperations--;
|
||||
if (closing && activeOperations == 0)
|
||||
{
|
||||
drained.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task CloseAsync()
|
||||
{
|
||||
TaskCompletionSource<bool> completion;
|
||||
lock (admission)
|
||||
{
|
||||
if (closeCompletion != null)
|
||||
{
|
||||
return closeCompletion.Task;
|
||||
}
|
||||
closing = true;
|
||||
completion = closeCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
if (activeOperations == 0)
|
||||
{
|
||||
drained.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
_ = CloseCoreAsync(completion);
|
||||
return completion.Task;
|
||||
}
|
||||
|
||||
private async Task CloseCoreAsync(TaskCompletionSource<bool> completion)
|
||||
{
|
||||
try
|
||||
{
|
||||
lifetime.Cancel();
|
||||
coordinator.CloseTickets();
|
||||
await drained.Task;
|
||||
coordinator.Dispose();
|
||||
state.Dispose();
|
||||
lifetime.Dispose();
|
||||
lock (admission)
|
||||
closed = true;
|
||||
completion.TrySetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
completion.TrySetException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<KvSessionOpenResult> FailCreate(KVSessionOpenError error)
|
||||
{
|
||||
await CloseAsync();
|
||||
return new KvSessionOpenResult(error);
|
||||
}
|
||||
|
||||
private bool IsCapturedOwnerCurrent()
|
||||
{
|
||||
if (lifetime.IsCancellationRequested || !isCurrent(Epoch) || state.AuthenticationInvalid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return session.IsAuthenticated && ReferenceEquals(session.Cookies, cookies) &&
|
||||
session.PlayerId == PlayerId && NormalizeApi(session.ApiBaseUri) == ApiBaseUri;
|
||||
}
|
||||
|
||||
private static Uri NormalizeApi(Uri api)
|
||||
{
|
||||
if (api == null || !api.IsAbsoluteUri || (api.Scheme != "http" && api.Scheme != "https") ||
|
||||
api.UserInfo.Length != 0 || api.Query.Length != 0 || api.Fragment.Length != 0)
|
||||
{
|
||||
throw new ArgumentException("Invalid API base URI.");
|
||||
}
|
||||
return new Uri(api.AbsoluteUri.TrimEnd('/') + "/");
|
||||
}
|
||||
|
||||
private static KVSessionOpenError MapOpen(KVOpenError error)
|
||||
{
|
||||
if (error == KVOpenError.Busy)
|
||||
{
|
||||
return KVSessionOpenError.Busy;
|
||||
}
|
||||
if (error == KVOpenError.RecoveryRequired)
|
||||
{
|
||||
return KVSessionOpenError.RecoveryRequired;
|
||||
}
|
||||
if (error == KVOpenError.StorageError)
|
||||
{
|
||||
return KVSessionOpenError.StorageError;
|
||||
}
|
||||
return KVSessionOpenError.Validation;
|
||||
}
|
||||
|
||||
private KvStateResult ClosedEdit()
|
||||
{
|
||||
return new KvStateResult(KVChangeError.Blocked);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum KVLocalReadError
|
||||
{
|
||||
None,
|
||||
StaleOrClosing,
|
||||
NotReady
|
||||
}
|
||||
|
||||
/// <summary>A detached snapshot. Failed reads have no data, never a pretend empty save.</summary>
|
||||
public sealed class KvLocalReadResult
|
||||
{
|
||||
public bool Success => Error == KVLocalReadError.None;
|
||||
public KVLocalReadError Error
|
||||
{
|
||||
get;
|
||||
}
|
||||
public IReadOnlyDictionary<string, string> Data
|
||||
{
|
||||
get;
|
||||
}
|
||||
|
||||
internal KvLocalReadResult(KVLocalReadError error, IReadOnlyDictionary<string, string> data = null)
|
||||
{
|
||||
Error = error;
|
||||
if (error != KVLocalReadError.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (data == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(data));
|
||||
}
|
||||
var copy = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
foreach (var item in data)
|
||||
{
|
||||
copy.Add(item.Key, item.Value);
|
||||
}
|
||||
Data = new ReadOnlyDictionary<string, string>(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user