Files
ft/Client/Assets/Scripts/Services/PlayerData/PlayerKVSyncState.cs

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();
}
}
}
}