204 lines
11 KiB
C#
204 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
|
|
namespace game.PlayerDataStorage
|
|
{
|
|
/// <summary>
|
|
/// Synchronous, fixed-identity cache state. No HTTP, gameplay initialization or timers.
|
|
/// All transitions and disk writes share a gate. The phase-3B owner must drain its
|
|
/// admitted network operations before disposing this state and its filesystem lease.
|
|
/// </summary>
|
|
public sealed class PlayerKVSyncState : IDisposable
|
|
{
|
|
readonly object gate = new object();
|
|
readonly PlayerKVCacheStore store;
|
|
KVSnapshot snapshot;
|
|
PlayerKVSyncStatus status;
|
|
public PlayerKVSyncStatus Status { get { lock (gate) return status; } }
|
|
public string ApiBaseUri { get; }
|
|
public string PlayerId { get; }
|
|
|
|
PlayerKVSyncState(KVIdentity identity, PlayerKVCacheStore store, KVSnapshot snapshot)
|
|
{
|
|
ApiBaseUri = identity.Api; PlayerId = identity.PlayerId; this.store = store; this.snapshot = snapshot;
|
|
// Authentication belongs to the old owner. A new owner must obtain a fresh
|
|
// successful Get before Ready; this never clears a persisted unknown batch.
|
|
snapshot.AuthenticationInvalid = false;
|
|
// A recovered active batch might have reached the server. Never acknowledge it by guessing.
|
|
if (snapshot.Active != null) { snapshot.Unknown = snapshot.Active; snapshot.Active = null; }
|
|
status = snapshot.Unknown != null ? PlayerKVSyncStatus.UnknownOutcome : PlayerKVSyncStatus.NotLoaded;
|
|
}
|
|
|
|
public static PlayerKVOpenResult Open(string root, Uri api, string playerId) => Open(root, api, playerId, null);
|
|
internal static PlayerKVOpenResult Open(string root, Uri api, string playerId, Action<KVCacheWriteStage> fault)
|
|
{
|
|
KVIdentity identity;
|
|
try { identity = new KVIdentity(root, api, playerId); }
|
|
catch (Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PathTooLongException)
|
|
{ return new PlayerKVOpenResult(PlayerKVOpenError.Validation); }
|
|
var error = PlayerKVCacheStore.Open(identity, fault, out var store, out var snapshot);
|
|
return error == PlayerKVOpenError.None ? new PlayerKVOpenResult(error, new PlayerKVSyncState(identity, store, snapshot)) : new PlayerKVOpenResult(error);
|
|
}
|
|
|
|
public PlayerKVDiagnosticSnapshot Inspect() { lock (gate) return new PlayerKVDiagnosticSnapshot(snapshot); }
|
|
|
|
public IReadOnlyDictionary<string, string> ReadLocal()
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (status != PlayerKVSyncStatus.Ready) throw new InvalidOperationException("KV data is not ready.");
|
|
var result = new Dictionary<string, string>(snapshot.Baseline, StringComparer.Ordinal);
|
|
foreach (var item in snapshot.Pending.Values) Apply(result, item.Key, item.Target);
|
|
foreach (var pair in snapshot.LocalOnly) result[pair.Key] = pair.Value;
|
|
return new ReadOnlyDictionary<string, string>(result);
|
|
}
|
|
}
|
|
|
|
// The caller must supply a successful complete Get. Failed reads must use RecordReadFailure.
|
|
public PlayerKVStateResult Reconcile(IReadOnlyDictionary<string, string> remote)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (Terminal || snapshot.Active != null || snapshot.AuthenticationInvalid) return Blocked();
|
|
if (remote == null || remote.Any(p => !KVSnapshot.ValidKey(p.Key) || p.Value == null)) return Invalid();
|
|
var copy = snapshot.Clone();
|
|
copy.Conflicts.Clear();
|
|
if (copy.Unknown != null)
|
|
{
|
|
foreach (var item in copy.Pending.Values) copy.Conflicts[item.Key] = PlayerKVValue.At(remote, item.Key);
|
|
return Commit(copy, PlayerKVSyncStatus.UnknownOutcome);
|
|
}
|
|
foreach (var item in copy.Pending.Values.ToArray())
|
|
{
|
|
var observed = PlayerKVValue.At(remote, item.Key);
|
|
if (observed.Equals(item.Target)) copy.Pending.Remove(item.Key);
|
|
else if (!observed.Equals(item.BaseValue)) copy.Conflicts[item.Key] = observed;
|
|
}
|
|
copy.Baseline = remote.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal);
|
|
copy.HasBaseline = true;
|
|
return Commit(copy, copy.Conflicts.Count == 0 ? PlayerKVSyncStatus.Ready : PlayerKVSyncStatus.Conflict);
|
|
}
|
|
}
|
|
|
|
public PlayerKVStateResult RecordReadFailure(bool unauthorized = false)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (Terminal || snapshot.Active != null) return Blocked();
|
|
if (unauthorized)
|
|
{
|
|
var copy = snapshot.Clone(); copy.AuthenticationInvalid = true;
|
|
return Commit(copy, copy.Unknown == null ? PlayerKVSyncStatus.Unauthorized : PlayerKVSyncStatus.UnknownOutcome);
|
|
}
|
|
status = snapshot.Unknown != null ? PlayerKVSyncStatus.UnknownOutcome : PlayerKVSyncStatus.ReadFailed;
|
|
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
|
|
}
|
|
}
|
|
|
|
public PlayerKVStateResult Update(string key, string value, bool remoteStore = true) =>
|
|
Edit(key, value == null ? null : PlayerKVValue.Present(value), remoteStore);
|
|
public PlayerKVStateResult Delete(string key) => Edit(key, PlayerKVValue.Missing, true);
|
|
|
|
PlayerKVStateResult Edit(string key, PlayerKVValue target, bool remote)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid) return Blocked();
|
|
if (!KVSnapshot.ValidKey(key) || target == null || snapshot.NextSequence == long.MaxValue) return Invalid();
|
|
var copy = snapshot.Clone();
|
|
var sequence = copy.NextSequence++;
|
|
if (remote)
|
|
{
|
|
var baseline = copy.Pending.TryGetValue(key, out var previous) ? previous.BaseValue : PlayerKVValue.At(copy.Baseline, key);
|
|
copy.Pending[key] = new PlayerKVChange(key, sequence, baseline, target);
|
|
copy.LocalOnly.Remove(key);
|
|
}
|
|
else copy.LocalOnly[key] = target.Value;
|
|
return Commit(copy, PlayerKVSyncStatus.Ready, sequence);
|
|
}
|
|
}
|
|
|
|
public PlayerKVStateResult BeginBatch(long throughSequence, out PlayerKVBatch batch)
|
|
{
|
|
lock (gate)
|
|
{
|
|
batch = null;
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid) return Blocked();
|
|
if (throughSequence < 0) return Invalid();
|
|
var ordered = snapshot.Pending.Values.Where(p => p.Sequence <= throughSequence).OrderBy(p => p.Sequence).ToArray();
|
|
if (ordered.Length == 0) return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
|
|
var copy = snapshot.Clone();
|
|
copy.Active = new PlayerKVBatch(Guid.NewGuid().ToString("N"), ordered.TakeWhile(p => p.Target.Exists == ordered[0].Target.Exists).Take(10));
|
|
var result = Commit(copy, PlayerKVSyncStatus.Ready);
|
|
if (result.Success) batch = snapshot.Active; // Never expose a batch until its write-ahead record is durable.
|
|
return result;
|
|
}
|
|
}
|
|
|
|
public PlayerKVStateResult ConfirmBatch(string batchId)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.Active == null || snapshot.Active.Id != batchId) return Blocked();
|
|
var copy = snapshot.Clone();
|
|
foreach (var item in copy.Active.Items)
|
|
{
|
|
Apply(copy.Baseline, item.Key, item.Target);
|
|
var current = copy.Pending[item.Key];
|
|
if (current.Sequence == item.Sequence) copy.Pending.Remove(item.Key);
|
|
else copy.Pending[item.Key] = current.WithBase(item.Target);
|
|
}
|
|
copy.Active = null;
|
|
return Commit(copy, PlayerKVSyncStatus.Ready);
|
|
}
|
|
}
|
|
|
|
// mayHaveCommitted=false is only valid with positive evidence of no dispatch.
|
|
public PlayerKVStateResult FailBatch(string batchId, bool mayHaveCommitted, bool unauthorized = false)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (Terminal || snapshot.Active == null || snapshot.Active.Id != batchId) return Blocked();
|
|
var copy = snapshot.Clone();
|
|
copy.AuthenticationInvalid |= unauthorized;
|
|
if (mayHaveCommitted) copy.Unknown = copy.Active;
|
|
copy.Active = null;
|
|
return Commit(copy, mayHaveCommitted ? PlayerKVSyncStatus.UnknownOutcome :
|
|
copy.AuthenticationInvalid ? PlayerKVSyncStatus.Unauthorized : PlayerKVSyncStatus.ReadFailed);
|
|
}
|
|
}
|
|
|
|
bool Terminal => status == PlayerKVSyncStatus.Closed || status == PlayerKVSyncStatus.StorageError;
|
|
PlayerKVStateResult Commit(KVSnapshot copy, PlayerKVSyncStatus nextStatus, long sequence = 0)
|
|
{
|
|
if (snapshot.Revision == long.MaxValue)
|
|
{ status = PlayerKVSyncStatus.StorageError; return new PlayerKVStateResult(PlayerKVChangeError.StorageError, sequence, snapshot.Revision); }
|
|
copy.Revision = snapshot.Revision + 1;
|
|
if (!store.Save(copy, out var uncertain))
|
|
{
|
|
status = PlayerKVSyncStatus.StorageError;
|
|
return new PlayerKVStateResult(PlayerKVChangeError.StorageError, sequence, copy.Revision, uncertain);
|
|
}
|
|
snapshot = copy; status = nextStatus;
|
|
return new PlayerKVStateResult(PlayerKVChangeError.None, sequence, copy.Revision);
|
|
}
|
|
static void Apply(Dictionary<string, string> data, string key, PlayerKVValue value)
|
|
{ if (value.Exists) data[key] = value.Value; else data.Remove(key); }
|
|
PlayerKVStateResult Blocked() => new PlayerKVStateResult(PlayerKVChangeError.Blocked, revision: snapshot.Revision);
|
|
PlayerKVStateResult Invalid() => new PlayerKVStateResult(PlayerKVChangeError.Validation, revision: snapshot.Revision);
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (status == PlayerKVSyncStatus.Closed) return;
|
|
status = PlayerKVSyncStatus.Closed;
|
|
store.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|