462 lines
15 KiB
C#
462 lines
15 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|