chore(m2): 保存功能验收及后续清理前检查点
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -8,11 +8,10 @@ namespace game.PlayerDataStorage
|
||||
{
|
||||
public sealed class KVPlayerSaveStore : IPlayerSaveStore, IDisposable
|
||||
{
|
||||
PlayerKVGameSessionHandle handle;
|
||||
CancellationToken lifetime;
|
||||
bool bound, disposed;
|
||||
readonly object receiptsGate = new object();
|
||||
readonly Dictionary<string, PlayerKVSyncTicket> pendingReceipts = new Dictionary<string, PlayerKVSyncTicket>(StringComparer.Ordinal);
|
||||
private PlayerKVGameSessionHandle handle;
|
||||
private CancellationToken lifetime;
|
||||
private bool bound, disposed;
|
||||
private readonly object receiptsGate = new object();
|
||||
public bool IsCurrent => bound && !disposed && !lifetime.IsCancellationRequested && handle.Status != PlayerKVSyncStatus.Closed;
|
||||
|
||||
internal void Bind(PlayerKVGameSessionHandle original, CancellationToken loginLifetime)
|
||||
@@ -27,6 +26,23 @@ namespace game.PlayerDataStorage
|
||||
bound = true;
|
||||
}
|
||||
|
||||
internal void ReleaseFailedBinding(PlayerKVGameSessionHandle original)
|
||||
{
|
||||
lock (receiptsGate)
|
||||
{
|
||||
if (!bound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (disposed || !ReferenceEquals(handle, original) || original.Status != PlayerKVSyncStatus.Closed)
|
||||
{
|
||||
throw new InvalidOperationException("Failed save binding has not closed.");
|
||||
}
|
||||
handle = null;
|
||||
bound = false;
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerSaveReadResult Read(string key)
|
||||
{
|
||||
if (!IsCurrent) return new PlayerSaveReadResult("StaleOrNotReady");
|
||||
@@ -45,94 +61,92 @@ namespace game.PlayerDataStorage
|
||||
return rejected;
|
||||
}
|
||||
var result = KVMgr.WriteKV(handle, key, value, remoteStore);
|
||||
if (result.Accepted && result.Ticket != null) pendingReceipts[key] = result.Ticket;
|
||||
var write = result.Accepted ? new PlayerSaveWriteResult(new PlayerSaveReceipt(this, result.Ticket)) :
|
||||
new PlayerSaveWriteResult(result.StateResult.Error.ToString());
|
||||
return write;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PlayerSaveConfirmation> WriteAndConfirmAsync(string key, string value, CancellationToken token = default)
|
||||
public async Task<bool> SyncDataImmidiate(string key, string value)
|
||||
{
|
||||
if (token.IsCancellationRequested) return Cancelled();
|
||||
var write = Write(key, value);
|
||||
if (!write.Accepted) return new PlayerSaveConfirmation(PlayerSaveStatus.Rejected, write.Error);
|
||||
return await ConfirmAsync(write.Receipt, token);
|
||||
}
|
||||
|
||||
public async Task<PlayerSaveConfirmation> ConfirmAsync(PlayerSaveReceipt receipt, CancellationToken token = default)
|
||||
{
|
||||
if (!IsCurrent || token.IsCancellationRequested) return Cancelled();
|
||||
if (receipt == null || !ReferenceEquals(receipt.Store, this))
|
||||
return new PlayerSaveConfirmation(PlayerSaveStatus.Rejected, "ForeignReceipt");
|
||||
if (receipt.Ticket == null) return new PlayerSaveConfirmation(PlayerSaveStatus.LocalOnly);
|
||||
using (var wait = CancellationTokenSource.CreateLinkedTokenSource(lifetime, token))
|
||||
PlayerKVGameSessionHandle original;
|
||||
CancellationToken originalLifetime;
|
||||
Task<bool> save;
|
||||
lock (receiptsGate)
|
||||
{
|
||||
try
|
||||
if (!IsCurrent)
|
||||
{
|
||||
await KVMgr.FlushKVAsync(handle, wait.Token);
|
||||
var ticket = await KVMgr.WaitForKVTicketAsync(handle, (PlayerKVSyncTicket)receipt.Ticket, wait.Token);
|
||||
if (!IsCurrent || wait.IsCancellationRequested) return Cancelled();
|
||||
if (ticket.Status == PlayerKVTicketStatus.Confirmed)
|
||||
{
|
||||
ForgetConfirmed((PlayerKVSyncTicket)receipt.Ticket);
|
||||
return new PlayerSaveConfirmation(PlayerSaveStatus.Confirmed);
|
||||
}
|
||||
return new PlayerSaveConfirmation(ticket.Status == PlayerKVTicketStatus.Unknown ? PlayerSaveStatus.Unknown :
|
||||
ticket.Status == PlayerKVTicketStatus.Superseded ? PlayerSaveStatus.Superseded :
|
||||
ticket.Status == PlayerKVTicketStatus.Cancelled ? PlayerSaveStatus.Cancelled : PlayerSaveStatus.Rejected,
|
||||
ticket.TransportError.ToString());
|
||||
return false;
|
||||
}
|
||||
catch (OperationCanceledException) { return Cancelled(); }
|
||||
// Preserve the former current-store dictionary lookup's null-key behavior.
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
original = handle;
|
||||
originalLifetime = lifetime;
|
||||
save = KVMgr.SyncDataImmidiate(original, key, value);
|
||||
}
|
||||
var success = await save;
|
||||
lock (receiptsGate)
|
||||
{
|
||||
return success && IsCurrent && !originalLifetime.IsCancellationRequested && ReferenceEquals(handle, original);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PlayerSaveConfirmation> FlushAsync(CancellationToken token = default)
|
||||
internal async Task<bool> SyncDataImmidiate(IReadOnlyDictionary<string, string> values)
|
||||
{
|
||||
if (!IsCurrent || token.IsCancellationRequested) return Cancelled();
|
||||
PlayerKVSyncTicket[] captured;
|
||||
lock (receiptsGate) captured = pendingReceipts.Values.ToArray();
|
||||
using (var wait = CancellationTokenSource.CreateLinkedTokenSource(lifetime, token))
|
||||
PlayerKVGameSessionHandle original;
|
||||
CancellationToken originalLifetime;
|
||||
Task<bool> save;
|
||||
lock (receiptsGate)
|
||||
{
|
||||
try
|
||||
if (!IsCurrent || values == null || values.Count == 0)
|
||||
{
|
||||
var result = await KVMgr.FlushKVAsync(handle, wait.Token);
|
||||
if (!IsCurrent || wait.IsCancellationRequested) return Cancelled();
|
||||
var status = result.Unknown > 0 || result.CacheStatus == PlayerKVSyncStatus.UnknownOutcome ?
|
||||
PlayerSaveStatus.Unknown : result.Blocked > 0 || result.TransportError != PlayerKVError.None ||
|
||||
result.CacheStatus != PlayerKVSyncStatus.Ready ? PlayerSaveStatus.Rejected : PlayerSaveStatus.Confirmed;
|
||||
if (status != PlayerSaveStatus.Confirmed) return new PlayerSaveConfirmation(status, result.TransportError.ToString());
|
||||
// Multiple writes before this flush are covered by the captured final receipt.
|
||||
// A replacement during the await belongs to a later flush and cannot confirm it.
|
||||
foreach (var receipt in captured)
|
||||
{
|
||||
var ticket = await KVMgr.WaitForKVTicketAsync(handle, receipt, wait.Token);
|
||||
if (!IsCurrent || wait.IsCancellationRequested) return Cancelled();
|
||||
if (ticket.Status != PlayerKVTicketStatus.Confirmed)
|
||||
return new PlayerSaveConfirmation(ticket.Status == PlayerKVTicketStatus.Superseded ? PlayerSaveStatus.Superseded :
|
||||
ticket.Status == PlayerKVTicketStatus.Unknown ? PlayerSaveStatus.Unknown :
|
||||
ticket.Status == PlayerKVTicketStatus.Cancelled ? PlayerSaveStatus.Cancelled : PlayerSaveStatus.Rejected,
|
||||
ticket.TransportError.ToString());
|
||||
ForgetConfirmed(receipt);
|
||||
}
|
||||
return new PlayerSaveConfirmation(PlayerSaveStatus.Confirmed);
|
||||
return false;
|
||||
}
|
||||
catch (OperationCanceledException) { return Cancelled(); }
|
||||
var data = values.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal);
|
||||
original = handle;
|
||||
originalLifetime = lifetime;
|
||||
save = KVMgr.SyncDataImmidiate(original, data);
|
||||
}
|
||||
var success = await save;
|
||||
lock (receiptsGate)
|
||||
{
|
||||
return success && IsCurrent && !originalLifetime.IsCancellationRequested && ReferenceEquals(handle, original);
|
||||
}
|
||||
}
|
||||
|
||||
static PlayerSaveConfirmation Cancelled() => new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled);
|
||||
void ForgetConfirmed(PlayerKVSyncTicket receipt)
|
||||
internal Task<PlayerKVFlushResult> SyncDataAsync()
|
||||
{
|
||||
lock (receiptsGate)
|
||||
if (pendingReceipts.TryGetValue(receipt.Key, out var latest) && ReferenceEquals(latest, receipt))
|
||||
pendingReceipts.Remove(receipt.Key);
|
||||
{
|
||||
var original = handle;
|
||||
if (!IsCurrent)
|
||||
{
|
||||
return Task.FromResult(new PlayerKVFlushResult(0, 0, 1, 0, PlayerKVError.Cancelled,
|
||||
original == null ? PlayerKVSyncStatus.NotLoaded : original.Status));
|
||||
}
|
||||
return KVMgr.SyncKVAsync(original);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (receiptsGate)
|
||||
{
|
||||
disposed = true;
|
||||
}
|
||||
}
|
||||
public void Dispose() { disposed = true; lock (receiptsGate) { pendingReceipts.Clear(); } }
|
||||
}
|
||||
|
||||
public sealed class PlayFabPlayerSaveStore : IPlayerSaveStore
|
||||
{
|
||||
public Task<bool> SyncDataImmidiate(string key, string value)
|
||||
{
|
||||
return manager.SyncDataImmidiate(key, value);
|
||||
}
|
||||
|
||||
readonly PlayFabMgr manager;
|
||||
readonly CancellationToken lifetime;
|
||||
public PlayFabPlayerSaveStore(IUserService user) : this(PlayFabMgr.Instance, user.LoginLifetime) { }
|
||||
@@ -162,38 +176,5 @@ namespace game.PlayerDataStorage
|
||||
catch (Exception ex) { return new PlayerSaveWriteResult(ex.GetType().Name); }
|
||||
}
|
||||
|
||||
public async Task<PlayerSaveConfirmation> WriteAndConfirmAsync(string key, string value, CancellationToken token = default)
|
||||
{
|
||||
if (!IsCurrent || token.IsCancellationRequested) return new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled);
|
||||
using (var wait = CancellationTokenSource.CreateLinkedTokenSource(lifetime, token))
|
||||
{
|
||||
var cancelled = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
using (wait.Token.Register(() => cancelled.TrySetResult(false)))
|
||||
{
|
||||
try
|
||||
{
|
||||
var save = manager.SyncDataImmidiate(key, value);
|
||||
var finished = await Task.WhenAny(save, cancelled.Task);
|
||||
if (!IsCurrent || wait.IsCancellationRequested) return new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled);
|
||||
return new PlayerSaveConfirmation(await finished ? PlayerSaveStatus.Confirmed : PlayerSaveStatus.Rejected);
|
||||
}
|
||||
catch (Exception ex) { return new PlayerSaveConfirmation(PlayerSaveStatus.Rejected, ex.GetType().Name); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<PlayerSaveConfirmation> ConfirmAsync(PlayerSaveReceipt receipt, CancellationToken token = default) =>
|
||||
Task.FromResult(new PlayerSaveConfirmation(!IsCurrent || token.IsCancellationRequested ? PlayerSaveStatus.Cancelled :
|
||||
receipt == null || !ReferenceEquals(receipt.Store, this) ? PlayerSaveStatus.Rejected :
|
||||
receipt.Ticket == null ? PlayerSaveStatus.LocalOnly : PlayerSaveStatus.Buffered));
|
||||
|
||||
public Task<PlayerSaveConfirmation> FlushAsync(CancellationToken token = default)
|
||||
{
|
||||
if (!IsCurrent || token.IsCancellationRequested)
|
||||
return Task.FromResult(new PlayerSaveConfirmation(PlayerSaveStatus.Cancelled));
|
||||
// The original manager owns its timer and callback-based buffered dispatch.
|
||||
manager.FlushBufferedData();
|
||||
return Task.FromResult(new PlayerSaveConfirmation(PlayerSaveStatus.Buffered));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user