819 lines
36 KiB
C#
819 lines
36 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
|
|
{
|
|
private readonly object gate = new object();
|
|
private readonly PlayerKVCacheStore store;
|
|
private KVSnapshot snapshot;
|
|
private PlayerKVSyncStatus status;
|
|
private readonly HashSet<long> recoveredImmediate;
|
|
private bool recoverImmediateView = true;
|
|
private bool recoveredOrdinaryUnknown;
|
|
private KVSnapshot comparisonLocal;
|
|
private Dictionary<string, string> comparisonRemote;
|
|
private Dictionary<string, DateTime> comparisonTimes;
|
|
private Dictionary<string, string> comparisonView;
|
|
private HashSet<string> comparisonImmediateKeys;
|
|
internal bool HasPendingComparison
|
|
{
|
|
get
|
|
{
|
|
lock (gate)
|
|
{
|
|
return comparisonLocal != null;
|
|
}
|
|
}
|
|
}
|
|
internal bool AuthenticationInvalid
|
|
{
|
|
get
|
|
{
|
|
lock (gate)
|
|
{
|
|
return snapshot.AuthenticationInvalid;
|
|
}
|
|
}
|
|
}
|
|
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;
|
|
recoveredImmediate = new HashSet<long>(snapshot.RemoteOnlyWrites.Keys);
|
|
// 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)
|
|
{
|
|
if (snapshot.Active.IsOrdinary)
|
|
{
|
|
snapshot.OrdinaryUnknownBatches.Add(snapshot.Active);
|
|
}
|
|
else
|
|
{
|
|
snapshot.Unknown = snapshot.Active;
|
|
}
|
|
snapshot.Active = null;
|
|
}
|
|
recoveredOrdinaryUnknown = snapshot.OrdinaryUnknownBatches.Count != 0;
|
|
status = snapshot.Unknown != null || recoveredOrdinaryUnknown
|
|
? 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.");
|
|
return new ReadOnlyDictionary<string, string>(ReadView());
|
|
}
|
|
}
|
|
|
|
// The caller must supply a successful complete Get. Failed reads must use RecordReadFailure.
|
|
public PlayerKVStateResult Reconcile(IReadOnlyDictionary<string, string> remote)
|
|
{
|
|
return Reconcile(remote, null, false);
|
|
}
|
|
|
|
internal PlayerKVStateResult Reconcile(IReadOnlyDictionary<string, string> remote,
|
|
IReadOnlyDictionary<string, DateTime> updatedAtUtc, bool prepareOriginalComparison)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (Terminal || snapshot.Active != null || snapshot.AuthenticationInvalid || comparisonLocal != null)
|
|
{
|
|
return Blocked();
|
|
}
|
|
if (remote == null || remote.Any(pair => !KVSnapshot.ValidKey(pair.Key) || pair.Value == null))
|
|
{
|
|
return Invalid();
|
|
}
|
|
if (prepareOriginalComparison && (!recoverImmediateView || updatedAtUtc == null ||
|
|
updatedAtUtc.Count != remote.Count || remote.Keys.Any(key => !updatedAtUtc.ContainsKey(key)) ||
|
|
updatedAtUtc.Values.Any(time => time.Kind != DateTimeKind.Utc)))
|
|
{
|
|
return Invalid();
|
|
}
|
|
var original = snapshot;
|
|
var copy = snapshot.Clone();
|
|
copy.Conflicts.Clear();
|
|
if (copy.Unknown != null || (recoveredOrdinaryUnknown && !prepareOriginalComparison))
|
|
{
|
|
foreach (var item in copy.Unsent())
|
|
{
|
|
copy.Conflicts[item.Key] = PlayerKVValue.At(remote, item.Key);
|
|
}
|
|
return Commit(copy, PlayerKVSyncStatus.UnknownOutcome);
|
|
}
|
|
var recoveringKeys = recoverImmediateView
|
|
? new HashSet<string>(copy.ImmediatePreservedKeys, StringComparer.Ordinal)
|
|
: new HashSet<string>(StringComparer.Ordinal);
|
|
if (recoverImmediateView)
|
|
{
|
|
foreach (var sequence in recoveredImmediate)
|
|
{
|
|
var source = copy.RemoteOnlyWrites[sequence];
|
|
Rebase(copy, source.Key, PlayerKVValue.At(remote, source.Key));
|
|
if (!prepareOriginalComparison)
|
|
{
|
|
copy.RemoteOnlyWrites.Remove(sequence);
|
|
}
|
|
}
|
|
if (!prepareOriginalComparison)
|
|
{
|
|
copy.ImmediatePreservedKeys.Clear();
|
|
}
|
|
}
|
|
foreach (var source in Sources(copy))
|
|
{
|
|
if (prepareOriginalComparison && copy.OrdinaryBuffers.Contains(source))
|
|
{
|
|
continue;
|
|
}
|
|
foreach (var item in source.Values.ToArray())
|
|
{
|
|
var observed = PlayerKVValue.At(remote, item.Key);
|
|
if (observed.Equals(item.Target))
|
|
{
|
|
source.Remove(item.Key);
|
|
}
|
|
else if (!observed.Equals(item.BaseValue))
|
|
{
|
|
copy.Conflicts[item.Key] = observed;
|
|
}
|
|
}
|
|
}
|
|
foreach (var item in copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
|
|
{
|
|
var observed = PlayerKVValue.At(remote, item.Key);
|
|
if (!observed.Equals(item.Target) && !observed.Equals(item.BaseValue))
|
|
{
|
|
copy.Conflicts[item.Key] = observed;
|
|
}
|
|
}
|
|
var visible = new Dictionary<string, string>(remote, StringComparer.Ordinal);
|
|
foreach (var pair in copy.LocalOrigins.ToArray())
|
|
{
|
|
if (pair.Value.LocalOnly)
|
|
{
|
|
if (recoverImmediateView && copy.LegacyLocalOnlyUnderImmediate.TryGetValue(pair.Key, out var lower))
|
|
{
|
|
visible[pair.Key] = lower;
|
|
}
|
|
else
|
|
{
|
|
Apply(visible, pair.Key, PlayerKVValue.At(copy.LocalData, pair.Key));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var source = copy.Unsent().Concat(copy.PreparedOrdinaryBatches.SelectMany(batch => batch.Items))
|
|
.Concat(copy.OrdinaryUnknownBatches.SelectMany(batch => batch.Items))
|
|
.FirstOrDefault(item => item.Sequence == pair.Value.Sequence);
|
|
if (!recoverImmediateView)
|
|
{
|
|
Apply(visible, pair.Key, PlayerKVValue.At(copy.LocalData, pair.Key));
|
|
}
|
|
else if (source != null)
|
|
{
|
|
Apply(visible, pair.Key, recoveringKeys.Contains(pair.Key)
|
|
? source.Target : PlayerKVValue.At(copy.LocalData, pair.Key));
|
|
}
|
|
else
|
|
{
|
|
copy.LocalOrigins.Remove(pair.Key);
|
|
}
|
|
}
|
|
}
|
|
// An immediate write preserves its view until a new owner performs its initial Get.
|
|
foreach (var key in copy.ImmediatePreservedKeys)
|
|
{
|
|
if (prepareOriginalComparison && recoveringKeys.Contains(key))
|
|
{
|
|
continue;
|
|
}
|
|
Apply(visible, key, PlayerKVValue.At(copy.LocalData, key));
|
|
}
|
|
if (recoverImmediateView && !prepareOriginalComparison)
|
|
{
|
|
copy.LegacyLocalOnlyUnderImmediate.Clear();
|
|
}
|
|
if (!prepareOriginalComparison)
|
|
{
|
|
copy.LocalData = visible;
|
|
}
|
|
copy.Baseline = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
|
copy.HasBaseline = true;
|
|
var reconciled = Commit(copy, copy.Conflicts.Count == 0 ? PlayerKVSyncStatus.Ready : PlayerKVSyncStatus.Conflict);
|
|
if (reconciled.Success)
|
|
{
|
|
recoverImmediateView = false;
|
|
if (prepareOriginalComparison)
|
|
{
|
|
comparisonLocal = original;
|
|
comparisonRemote = remote.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
|
comparisonTimes = updatedAtUtc.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
|
comparisonView = visible;
|
|
comparisonImmediateKeys = new HashSet<string>(StringComparer.Ordinal);
|
|
}
|
|
else
|
|
{
|
|
recoveredImmediate.Clear();
|
|
}
|
|
}
|
|
return reconciled;
|
|
}
|
|
}
|
|
|
|
private Dictionary<string, string> ReadView()
|
|
{
|
|
if (comparisonLocal == null)
|
|
{
|
|
return LocalView(snapshot);
|
|
}
|
|
var view = new Dictionary<string, string>(comparisonView, StringComparer.Ordinal);
|
|
foreach (var pair in snapshot.LocalOrigins)
|
|
{
|
|
if (pair.Value.Sequence >= comparisonLocal.NextSequence)
|
|
{
|
|
Apply(view, pair.Key, PlayerKVValue.At(snapshot.LocalData, pair.Key));
|
|
}
|
|
}
|
|
return view;
|
|
}
|
|
|
|
internal PlayerKVStateResult CompareDatas(long nowTicks)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
|
|
snapshot.Unknown != null || comparisonLocal == null)
|
|
{
|
|
return Blocked();
|
|
}
|
|
if ((comparisonLocal.OrdinaryUnknownBatches.Count != 0 && !recoveredOrdinaryUnknown) ||
|
|
snapshot.OrdinaryUnknownBatches.Any(batch => !comparisonLocal.OrdinaryUnknownBatches.Any(initial => initial.Id == batch.Id)))
|
|
{
|
|
return Blocked();
|
|
}
|
|
if (nowTicks < 0 || nowTicks > DateTime.MaxValue.Ticks)
|
|
{
|
|
return Invalid();
|
|
}
|
|
var copy = snapshot.Clone();
|
|
// A new login uses the original time comparison, not the old request's outcome.
|
|
// Retire only its initial ordinary evidence in the same commit as that comparison.
|
|
foreach (var batch in comparisonLocal.OrdinaryUnknownBatches)
|
|
{
|
|
copy.OrdinaryUnknownBatches.RemoveAll(current => current.Id == batch.Id);
|
|
foreach (var item in batch.Items)
|
|
{
|
|
if (copy.LocalOrigins.TryGetValue(item.Key, out var origin) && origin.Sequence == item.Sequence)
|
|
{
|
|
copy.LocalOrigins.Remove(item.Key);
|
|
}
|
|
}
|
|
}
|
|
var protectedKeys = new HashSet<string>(StringComparer.Ordinal);
|
|
foreach (var source in new[] { comparisonLocal, snapshot })
|
|
{
|
|
protectedKeys.UnionWith(source.PreparedOrdinaryBatches.SelectMany(batch => batch.Items).Select(item => item.Key));
|
|
protectedKeys.UnionWith(source.LegacyPending.Keys);
|
|
protectedKeys.UnionWith(source.ExplicitDeletes.Keys);
|
|
protectedKeys.UnionWith(source.ImmediatePreservedKeys);
|
|
protectedKeys.UnionWith(source.RemoteOnlyWrites.Values.Select(item => item.Key));
|
|
}
|
|
protectedKeys.UnionWith(snapshot.LocalOrigins.Where(pair => pair.Value.Sequence >= comparisonLocal.NextSequence).Select(pair => pair.Key));
|
|
var visible = ReadView();
|
|
foreach (var key in protectedKeys)
|
|
{
|
|
Apply(copy.LocalData, key, PlayerKVValue.At(visible, key));
|
|
}
|
|
foreach (var key in comparisonLocal.LocalData.Keys.Concat(comparisonRemote.Keys).Distinct(StringComparer.Ordinal))
|
|
{
|
|
if (protectedKeys.Contains(key))
|
|
{
|
|
continue;
|
|
}
|
|
var localExists = comparisonLocal.HasBaseline && comparisonLocal.LocalData.TryGetValue(key, out _);
|
|
comparisonLocal.LocalData.TryGetValue(key, out var localValue);
|
|
var remoteExists = comparisonRemote.TryGetValue(key, out var remoteValue);
|
|
var localTime = comparisonLocal.LocalUpdateTicks.TryGetValue(key, out var ticks) ? ticks : 0;
|
|
var remoteTime = remoteExists ? comparisonTimes[key].Ticks : 0;
|
|
if (!localExists || (remoteExists && remoteTime - localTime > TimeSpan.TicksPerSecond * 60))
|
|
{
|
|
copy.LocalData[key] = remoteValue;
|
|
copy.LocalUpdateTicks[key] = remoteTime;
|
|
foreach (var bucket in copy.OrdinaryBuffers)
|
|
{
|
|
if (bucket.TryGetValue(key, out var current) && comparisonLocal.OrdinaryBuffers.Any(old =>
|
|
old.TryGetValue(key, out var item) && item.Sequence == current.Sequence))
|
|
{
|
|
bucket.Remove(key);
|
|
if (copy.LocalOrigins.TryGetValue(key, out var origin) && origin.Sequence == current.Sequence)
|
|
{
|
|
copy.LocalOrigins.Remove(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (!remoteExists || localTime - remoteTime > TimeSpan.TicksPerSecond * 60)
|
|
{
|
|
if (copy.NextSequence == long.MaxValue)
|
|
{
|
|
return Invalid();
|
|
}
|
|
ApplyUpdate(copy, key, localValue, true, nowTicks);
|
|
}
|
|
else
|
|
{
|
|
copy.LocalData[key] = localValue;
|
|
copy.LocalUpdateTicks[key] = localTime;
|
|
}
|
|
}
|
|
foreach (var sequence in comparisonLocal.RemoteOnlyWrites.Keys)
|
|
{
|
|
copy.RemoteOnlyWrites.Remove(sequence);
|
|
}
|
|
foreach (var key in comparisonLocal.LegacyLocalOnlyUnderImmediate.Keys)
|
|
{
|
|
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
|
|
}
|
|
foreach (var key in comparisonLocal.ImmediatePreservedKeys)
|
|
{
|
|
if (!comparisonImmediateKeys.Contains(key))
|
|
{
|
|
copy.ImmediatePreservedKeys.Remove(key);
|
|
}
|
|
}
|
|
var result = Commit(copy, PlayerKVSyncStatus.Ready);
|
|
if (result.Success)
|
|
{
|
|
recoveredOrdinaryUnknown = false;
|
|
ClearComparison();
|
|
recoveredImmediate.Clear();
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private void ClearComparison()
|
|
{
|
|
comparisonLocal = null;
|
|
comparisonRemote = null;
|
|
comparisonTimes = null;
|
|
comparisonView = null;
|
|
comparisonImmediateKeys = null;
|
|
}
|
|
|
|
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)
|
|
{
|
|
return Update(key, value, remoteStore, null);
|
|
}
|
|
|
|
internal PlayerKVStateResult Update(string key, string value, bool remoteStore, long? updateTicks)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
|
{
|
|
return Blocked();
|
|
}
|
|
if (!KVSnapshot.ValidKey(key) || value == null || snapshot.NextSequence == long.MaxValue ||
|
|
(updateTicks.HasValue && (updateTicks.Value < 0 || updateTicks.Value > DateTime.MaxValue.Ticks)))
|
|
{
|
|
return Invalid();
|
|
}
|
|
var copy = snapshot.Clone();
|
|
var sequence = ApplyUpdate(copy, key, value, remoteStore, updateTicks);
|
|
return Commit(copy, PlayerKVSyncStatus.Ready, sequence);
|
|
}
|
|
}
|
|
|
|
private static long ApplyUpdate(KVSnapshot copy, string key, string value, bool remoteStore, long? updateTicks)
|
|
{
|
|
var sequence = copy.NextSequence++;
|
|
copy.LocalData[key] = value;
|
|
copy.ImmediatePreservedKeys.Remove(key);
|
|
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
|
|
if (updateTicks.HasValue)
|
|
{
|
|
copy.LocalUpdateTicks[key] = updateTicks.Value;
|
|
}
|
|
else
|
|
{
|
|
copy.LocalUpdateTicks.Remove(key);
|
|
}
|
|
var acceptedRemote = false;
|
|
if (remoteStore)
|
|
{
|
|
foreach (var bucket in copy.OrdinaryBuffers)
|
|
{
|
|
if (!bucket.ContainsKey(key) && bucket.Count >= 10)
|
|
{
|
|
continue;
|
|
}
|
|
var baseline = bucket.TryGetValue(key, out var previous)
|
|
? previous.BaseValue : PlayerKVValue.At(copy.Baseline, key);
|
|
bucket[key] = new PlayerKVChange(key, sequence, baseline, PlayerKVValue.Present(value));
|
|
copy.LegacyPending.Remove(key);
|
|
copy.ExplicitDeletes.Remove(key);
|
|
acceptedRemote = true;
|
|
break;
|
|
}
|
|
}
|
|
copy.LocalOrigins[key] = (!acceptedRemote, sequence);
|
|
return sequence;
|
|
}
|
|
|
|
public PlayerKVStateResult Delete(string key)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
|
{
|
|
return Blocked();
|
|
}
|
|
if (!KVSnapshot.ValidKey(key) || snapshot.NextSequence == long.MaxValue)
|
|
{
|
|
return Invalid();
|
|
}
|
|
var copy = snapshot.Clone();
|
|
var sequence = copy.NextSequence++;
|
|
var prior = copy.ExplicitDeletes.TryGetValue(key, out var previous) ? previous.BaseValue : PlayerKVValue.At(copy.Baseline, key);
|
|
RemoveSources(copy, key);
|
|
copy.ExplicitDeletes[key] = new PlayerKVChange(key, sequence, prior, PlayerKVValue.Missing);
|
|
copy.LocalData.Remove(key);
|
|
copy.LocalUpdateTicks.Remove(key);
|
|
copy.LocalOrigins[key] = (false, sequence);
|
|
copy.ImmediatePreservedKeys.Remove(key);
|
|
copy.LegacyLocalOnlyUnderImmediate.Remove(key);
|
|
return Commit(copy, PlayerKVSyncStatus.Ready, sequence);
|
|
}
|
|
}
|
|
|
|
internal PlayerKVStateResult CaptureOrdinaryBatch(out PlayerKVBatch batch)
|
|
{
|
|
lock (gate)
|
|
{
|
|
batch = null;
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
|
{
|
|
return Blocked();
|
|
}
|
|
if (snapshot.OrdinaryBuffers[0].Count == 0)
|
|
{
|
|
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
|
|
}
|
|
var copy = snapshot.Clone();
|
|
var first = copy.OrdinaryBuffers[0];
|
|
var captured = new PlayerKVBatch(Guid.NewGuid().ToString("N"), first.Values.OrderBy(item => item.Sequence), true);
|
|
copy.PreparedOrdinaryBatches.Add(captured);
|
|
copy.OrdinaryBuffers.RemoveAt(0);
|
|
copy.OrdinaryBuffers.Add(new Dictionary<string, PlayerKVChange>(StringComparer.Ordinal));
|
|
var result = Commit(copy, PlayerKVSyncStatus.Ready);
|
|
if (result.Success)
|
|
{
|
|
batch = captured;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
internal PlayerKVStateResult BeginOrdinaryBatch(out PlayerKVBatch batch)
|
|
{
|
|
lock (gate)
|
|
{
|
|
batch = null;
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.Active != null || snapshot.Unknown != null || snapshot.AuthenticationInvalid)
|
|
{
|
|
return Blocked();
|
|
}
|
|
if (snapshot.PreparedOrdinaryBatches.Count == 0)
|
|
{
|
|
return new PlayerKVStateResult(PlayerKVChangeError.None, revision: snapshot.Revision);
|
|
}
|
|
var copy = snapshot.Clone();
|
|
copy.Active = copy.PreparedOrdinaryBatches[0];
|
|
copy.PreparedOrdinaryBatches.RemoveAt(0);
|
|
var result = Commit(copy, PlayerKVSyncStatus.Ready);
|
|
if (result.Success)
|
|
{
|
|
batch = snapshot.Active;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Explicit requests retain their original source until the outcome is known. Ordinary buckets are never selected here.
|
|
public PlayerKVStateResult BeginBatch(long throughSequence, out PlayerKVBatch batch)
|
|
{
|
|
return BeginBatch(throughSequence, false, out batch);
|
|
}
|
|
|
|
internal PlayerKVStateResult BeginBatch(long throughSequence, bool legacyOnly, 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 source = legacyOnly ? snapshot.LegacyPending.Values : snapshot.LegacyPending.Values.Concat(snapshot.ExplicitDeletes.Values);
|
|
var ordered = source.Where(item => item.Sequence <= throughSequence).OrderBy(item => item.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(item => item.Target.Exists == ordered[0].Target.Exists).Take(10));
|
|
var result = Commit(copy, PlayerKVSyncStatus.Ready);
|
|
if (result.Success)
|
|
{
|
|
batch = snapshot.Active;
|
|
}
|
|
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);
|
|
if (!copy.Active.IsOrdinary)
|
|
{
|
|
foreach (var source in new[] { copy.LegacyPending, copy.ExplicitDeletes })
|
|
{
|
|
if (source.TryGetValue(item.Key, out var current) && current.Sequence == item.Sequence)
|
|
{
|
|
source.Remove(item.Key);
|
|
}
|
|
}
|
|
}
|
|
Rebase(copy, item.Key, item.Target);
|
|
}
|
|
copy.Active = null;
|
|
return Commit(copy, PlayerKVSyncStatus.Ready);
|
|
}
|
|
}
|
|
|
|
// A definitely undispatched ordinary batch retires, as the original SDK call did; explicit source references stay.
|
|
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();
|
|
var ordinary = copy.Active.IsOrdinary;
|
|
copy.AuthenticationInvalid |= unauthorized;
|
|
if (mayHaveCommitted)
|
|
{
|
|
if (ordinary)
|
|
{
|
|
copy.OrdinaryUnknownBatches.Add(copy.Active);
|
|
}
|
|
else
|
|
{
|
|
copy.Unknown = copy.Active;
|
|
}
|
|
}
|
|
copy.Active = null;
|
|
var next = copy.AuthenticationInvalid ? PlayerKVSyncStatus.Unauthorized : ordinary ? PlayerKVSyncStatus.Ready : PlayerKVSyncStatus.ReadFailed;
|
|
if (copy.Unknown != null)
|
|
{
|
|
next = PlayerKVSyncStatus.UnknownOutcome;
|
|
}
|
|
return Commit(copy, next);
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<Dictionary<string, PlayerKVChange>> Sources(KVSnapshot value)
|
|
{
|
|
return value.OrdinaryBuffers.Concat(new[] { value.LegacyPending, value.ExplicitDeletes });
|
|
}
|
|
private static void RemoveSources(KVSnapshot value, string key)
|
|
{
|
|
foreach (var source in Sources(value))
|
|
{
|
|
source.Remove(key);
|
|
}
|
|
}
|
|
private static void Rebase(KVSnapshot value, string key, PlayerKVValue baseline)
|
|
{
|
|
foreach (var source in Sources(value))
|
|
{
|
|
if (source.TryGetValue(key, out var pending))
|
|
{
|
|
source[key] = pending.WithBase(baseline);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal PlayerKVStateResult PrepareImmediate(string key, string value, out PlayerKVChange operation)
|
|
{
|
|
operation = null;
|
|
if (!KVSnapshot.ValidKey(key) || value == null)
|
|
{
|
|
// Preserve the single-key state admission order and validation result.
|
|
lock (gate)
|
|
{
|
|
return status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid ? Blocked() : Invalid();
|
|
}
|
|
}
|
|
var result = PrepareImmediate(new Dictionary<string, string> { [key] = value }, out var operations);
|
|
if (result.Success)
|
|
{
|
|
operation = operations[0];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
internal PlayerKVStateResult PrepareImmediate(IReadOnlyDictionary<string, string> data,
|
|
out IReadOnlyList<PlayerKVChange> operations)
|
|
{
|
|
lock (gate)
|
|
{
|
|
operations = null;
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid)
|
|
{
|
|
return Blocked();
|
|
}
|
|
if (data == null || data.Count == 0 || data.Count > long.MaxValue - snapshot.NextSequence ||
|
|
data.Any(pair => !KVSnapshot.ValidKey(pair.Key) || pair.Value == null))
|
|
{
|
|
return Invalid();
|
|
}
|
|
var copy = snapshot.Clone();
|
|
var prepared = new List<PlayerKVChange>();
|
|
foreach (var pair in data)
|
|
{
|
|
copy.ImmediatePreservedKeys.Add(pair.Key);
|
|
var operation = new PlayerKVChange(pair.Key, copy.NextSequence++,
|
|
PlayerKVValue.At(copy.Baseline, pair.Key), PlayerKVValue.Present(pair.Value));
|
|
copy.RemoteOnlyWrites.Add(operation.Sequence, operation);
|
|
RemoveSources(copy, pair.Key);
|
|
prepared.Add(operation);
|
|
}
|
|
var result = Commit(copy, PlayerKVSyncStatus.Ready, prepared[0].Sequence);
|
|
if (result.Success)
|
|
{
|
|
operations = prepared.AsReadOnly();
|
|
if (comparisonLocal != null)
|
|
{
|
|
comparisonImmediateKeys.UnionWith(data.Keys);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
internal PlayerKVStateResult CompleteImmediate(PlayerKVChange operation)
|
|
{
|
|
return CompleteImmediate(new[] { operation });
|
|
}
|
|
|
|
internal PlayerKVStateResult CompleteImmediate(IReadOnlyList<PlayerKVChange> operations)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (status != PlayerKVSyncStatus.Ready || snapshot.AuthenticationInvalid || snapshot.Active != null ||
|
|
snapshot.Unknown != null || operations.Any(operation => !snapshot.RemoteOnlyWrites.ContainsKey(operation.Sequence)))
|
|
{
|
|
return Blocked();
|
|
}
|
|
var copy = snapshot.Clone();
|
|
foreach (var operation in operations)
|
|
{
|
|
Apply(copy.Baseline, operation.Key, operation.Target);
|
|
Rebase(copy, operation.Key, operation.Target);
|
|
copy.RemoteOnlyWrites.Remove(operation.Sequence);
|
|
copy.ImmediatePreservedKeys.Add(operation.Key);
|
|
}
|
|
return Commit(copy, PlayerKVSyncStatus.Ready);
|
|
}
|
|
}
|
|
|
|
internal void CancelUndispatchedImmediate(PlayerKVChange operation)
|
|
{
|
|
CancelUndispatchedImmediate(new[] { operation });
|
|
}
|
|
|
|
internal void CancelUndispatchedImmediate(IReadOnlyList<PlayerKVChange> operations)
|
|
{
|
|
lock (gate)
|
|
{
|
|
if (Terminal)
|
|
{
|
|
return;
|
|
}
|
|
var protectedBatches = snapshot.OrdinaryUnknownBatches.Concat(new[] { snapshot.Active, snapshot.Unknown }).Where(batch => batch != null);
|
|
var removable = operations.Where(operation => snapshot.RemoteOnlyWrites.ContainsKey(operation.Sequence) &&
|
|
!protectedBatches.Any(batch => batch.Items.Any(item => item.Key == operation.Key && item.Sequence < operation.Sequence))).ToArray();
|
|
if (removable.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
var copy = snapshot.Clone();
|
|
foreach (var operation in removable)
|
|
{
|
|
copy.RemoteOnlyWrites.Remove(operation.Sequence);
|
|
}
|
|
Commit(copy, status);
|
|
}
|
|
}
|
|
|
|
private static Dictionary<string, string> LocalView(KVSnapshot source)
|
|
{
|
|
return new Dictionary<string, string>(source.LocalData, StringComparer.Ordinal);
|
|
}
|
|
|
|
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;
|
|
ClearComparison();
|
|
store.Dispose();
|
|
}
|
|
}
|
|
}
|
|
}
|