保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
193 lines
13 KiB
Diff
193 lines
13 KiB
Diff
diff --git a/Client/Assets/Scripts/Services/PlayerData/PlayerKVGameLoadCoordinator.cs b/Client/Assets/Scripts/Services/PlayerData/PlayerKVGameLoadCoordinator.cs
|
|
index 2ce900a10..dc6fdc43b 100644
|
|
--- a/Client/Assets/Scripts/Services/PlayerData/PlayerKVGameLoadCoordinator.cs
|
|
+++ b/Client/Assets/Scripts/Services/PlayerData/PlayerKVGameLoadCoordinator.cs
|
|
@@ -6,7 +6,7 @@ namespace game.PlayerDataStorage
|
|
{
|
|
internal enum PlayerKVGameLoadError
|
|
{ None, BackendNotEnabled, BackendUnsupported, Input, Prepare, Schema, Apply, Stale, SideEffect, GateBusy, PreviousClosePending, PreviousCloseFailed, ClosePending, CloseFailed }
|
|
- internal enum PlayerKVGameLoadState { Rejected, LoadedForValidation }
|
|
+ internal enum PlayerKVGameLoadState { Rejected, LoadedForValidation, GameLoaded }
|
|
// Preserve stage evidence without retaining a successful Prepare payload on a later load failure.
|
|
internal sealed class PlayerKVPrepareEvidence
|
|
{
|
|
@@ -27,7 +27,7 @@ namespace game.PlayerDataStorage
|
|
}
|
|
internal sealed class PlayerKVGameLoadResult
|
|
{
|
|
- internal PlayerKVGameLoadState State => Error==PlayerKVGameLoadError.None ? PlayerKVGameLoadState.LoadedForValidation : PlayerKVGameLoadState.Rejected;
|
|
+ internal PlayerKVGameLoadState State { get; }
|
|
internal PlayerKVGameLoadError Error { get; }
|
|
internal PlayerKVGameLoadError OriginalError { get; }
|
|
internal bool CleanupFailed => Error==PlayerKVGameLoadError.CloseFailed;
|
|
@@ -37,8 +37,9 @@ namespace game.PlayerDataStorage
|
|
internal int ModuleCount { get; }
|
|
internal int IntentCount { get; }
|
|
internal PlayerKVGameLoadResult(PlayerKVGameLoadError error,long epoch,PlayerKVGameLoadError? original=null,
|
|
- PlayerKVSchemaVerdict? verdict=null,PlayerKVLoginPrepareResult prepare=null,int modules=0,int intents=0)
|
|
- { Error=error; OriginalError=original ?? error; SchemaVerdict=verdict; Prepare=prepare==null ? null : new PlayerKVPrepareEvidence(prepare); Epoch=epoch; ModuleCount=modules; IntentCount=intents; }
|
|
+ PlayerKVSchemaVerdict? verdict=null,PlayerKVLoginPrepareResult prepare=null,int modules=0,int intents=0,bool forGame=false)
|
|
+ { Error=error; OriginalError=original ?? error; SchemaVerdict=verdict; Prepare=prepare==null ? null : new PlayerKVPrepareEvidence(prepare); Epoch=epoch; ModuleCount=modules; IntentCount=intents;
|
|
+ State=error!=PlayerKVGameLoadError.None ? PlayerKVGameLoadState.Rejected : forGame ? PlayerKVGameLoadState.GameLoaded : PlayerKVGameLoadState.LoadedForValidation; }
|
|
}
|
|
internal sealed class PlayerKVGameLoadCoordinator
|
|
{
|
|
@@ -61,6 +62,7 @@ namespace game.PlayerDataStorage
|
|
Action clearAfterApply;
|
|
long epoch;
|
|
bool loaded;
|
|
+ PlayerKVGameSessionHandle gameHandle;
|
|
internal bool LoadedForValidation => loaded && context!=null && context.IsCurrent && guard!=null && guard.IsCurrent && guard.Violations==0;
|
|
internal int QuitObserveMs => capability?.QuitObserveMs ?? 5000;
|
|
internal PlayerKVGameLoadCoordinator(PlayerKVGameLoadTransaction transaction,Func<bool> unstarted,Action notify,Func<int> intentCount,
|
|
@@ -72,7 +74,29 @@ namespace game.PlayerDataStorage
|
|
this.unbind=unbind ?? throw new ArgumentNullException(nameof(unbind)); this.registry=registry ?? PlayerKVShutdownRegistry.Process;
|
|
mainThread=Thread.CurrentThread.ManagedThreadId; mainContext=SynchronizationContext.Current;
|
|
}
|
|
- internal async Task<PlayerKVGameLoadResult> LoadAsync(string id,PlayerKVGameLoadCapability next,CancellationToken token=default,Func<bool> identityCurrent=null)
|
|
+ internal Task<PlayerKVGameLoadResult> LoadAsync(string id,PlayerKVGameLoadCapability next,CancellationToken token=default,Func<bool> identityCurrent=null)
|
|
+ => LoadCoreAsync(id,next,token,identityCurrent,false);
|
|
+ internal Task<PlayerKVGameLoadResult> LoadForGameAsync(string id,PlayerKVGameLoadCapability next,CancellationToken token=default,Func<bool> identityCurrent=null)
|
|
+ => LoadCoreAsync(id,next,token,identityCurrent,true);
|
|
+ internal bool TryGetCurrentGameHandle(long expectedEpoch,out PlayerKVGameSessionHandle handle)
|
|
+ {
|
|
+ handle=null;
|
|
+ PlayerKVLoginContext owner; PlayerKVGameSessionHandle published;
|
|
+ lock(closeGate)
|
|
+ {
|
|
+ if(closing!=null || epoch!=expectedEpoch || gameHandle==null) return false;
|
|
+ owner=context; published=gameHandle;
|
|
+ }
|
|
+ // Never call the external identity predicate while holding a publication monitor.
|
|
+ if(!owner.TryAcquireGameHandle(expectedEpoch,out var original) || !ReferenceEquals(original,published)) return false;
|
|
+ lock(closeGate)
|
|
+ {
|
|
+ if(closing!=null || epoch!=expectedEpoch || !ReferenceEquals(context,owner) || !ReferenceEquals(gameHandle,published)) return false;
|
|
+ handle=published;
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+ async Task<PlayerKVGameLoadResult> LoadCoreAsync(string id,PlayerKVGameLoadCapability next,CancellationToken token,Func<bool> identityCurrent,bool forGame)
|
|
{
|
|
if(next==null || Thread.CurrentThread.ManagedThreadId!=mainThread || string.IsNullOrEmpty(id))
|
|
return Result(PlayerKVGameLoadError.Input);
|
|
@@ -89,9 +113,13 @@ namespace game.PlayerDataStorage
|
|
if(token.IsCancellationRequested || !unstarted() || transaction.Poisoned) return Result(PlayerKVGameLoadError.Input);
|
|
if(!PlayerKVGameLoadGuard.TryEnter(out var enteredGuard)) return Result(PlayerKVGameLoadError.GateBusy);
|
|
lifetimeRegistration.Dispose();
|
|
- guard=enteredGuard; capability=next; closing=null; loaded=false;
|
|
- attempt=Interlocked.Increment(ref epoch);
|
|
- context=next.CreateContext(id,attempt,e=>Interlocked.Read(ref epoch)==e && !token.IsCancellationRequested && (identityCurrent==null || identityCurrent()));
|
|
+ lock(closeGate)
|
|
+ {
|
|
+ guard=enteredGuard; capability=next; closing=null; loaded=false; gameHandle=null;
|
|
+ attempt=Interlocked.Increment(ref epoch);
|
|
+ context=next.CreateContext(id,attempt,e=>Interlocked.Read(ref epoch)==e && !token.IsCancellationRequested && (identityCurrent==null || identityCurrent()));
|
|
+ }
|
|
+ var owner=context;
|
|
lifetimeRegistration=token.Register(()=> { _=Revoke(attempt); });
|
|
var work=context.PrepareAsync(token);
|
|
var cancelled=new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
@@ -113,9 +141,26 @@ namespace game.PlayerDataStorage
|
|
finally { var pending=clearAfterApply; clearAfterApply=null; pending?.Invoke(); }
|
|
if(applied!=PlayerKVApplyError.None) return await Reject(applied==PlayerKVApplyError.Stale ? PlayerKVGameLoadError.Stale :
|
|
applied==PlayerKVApplyError.SideEffect ? PlayerKVGameLoadError.SideEffect : PlayerKVGameLoadError.Apply,attempt,next,prepared,verdict);
|
|
- if(token.IsCancellationRequested || !context.IsCurrent || guard.Violations!=0) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict);
|
|
- loaded=true;
|
|
- return new PlayerKVGameLoadResult(PlayerKVGameLoadError.None,attempt,verdict:verdict,modules:31,intents:intentCount());
|
|
+ if(token.IsCancellationRequested || !owner.IsCurrent || enteredGuard.Violations!=0) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict);
|
|
+ var intents=intentCount();
|
|
+ PlayerKVGameSessionHandle original=null;
|
|
+ if(forGame && !owner.TryAcquireGameHandle(attempt,out original)) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict);
|
|
+ bool published=false;
|
|
+ lock(closeGate)
|
|
+ {
|
|
+ if(closing==null && epoch==attempt && !token.IsCancellationRequested && ReferenceEquals(context,owner) && enteredGuard.IsCurrent && enteredGuard.Violations==0)
|
|
+ {
|
|
+ if(forGame)
|
|
+ {
|
|
+ enteredGuard.Dispose();
|
|
+ // Dispose serializes with Reject; its final count cannot change afterwards.
|
|
+ if(enteredGuard.Violations==0) { gameHandle=original; published=true; }
|
|
+ }
|
|
+ else { loaded=true; published=true; }
|
|
+ }
|
|
+ }
|
|
+ if(!published) return await Reject(PlayerKVGameLoadError.Stale,attempt,next,prepared,verdict);
|
|
+ return new PlayerKVGameLoadResult(PlayerKVGameLoadError.None,attempt,verdict:verdict,modules:31,intents:intents,forGame:forGame);
|
|
}
|
|
catch { return await Reject(PlayerKVGameLoadError.Apply,attempt,next,prepared,verdict); }
|
|
finally { loginGate.Release(); }
|
|
@@ -148,7 +193,7 @@ namespace game.PlayerDataStorage
|
|
{
|
|
if(expectedEpoch.HasValue && expectedEpoch.Value!=Interlocked.Read(ref epoch)) return Task.CompletedTask;
|
|
if(closing!=null) return closing;
|
|
- loaded=false; Interlocked.Increment(ref epoch);
|
|
+ loaded=false; gameHandle=null; Interlocked.Increment(ref epoch);
|
|
if(context==null && guard==null) return Task.CompletedTask;
|
|
var completion=new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
closing=completion.Task;
|
|
diff --git a/Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginContext.cs b/Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginContext.cs
|
|
index a1bb78c83..8b779f45c 100644
|
|
--- a/Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginContext.cs
|
|
+++ b/Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginContext.cs
|
|
@@ -58,6 +58,21 @@ namespace game.PlayerDataStorage
|
|
internal PlayerKVAccountDescriptor AccountDescriptor =>
|
|
Volatile.Read(ref prepared) != 0 && IsCurrent ? accountDescriptor : null;
|
|
|
|
+ // Only the coordinator may publish this original handle after Apply and Guard release.
|
|
+ internal bool TryAcquireGameHandle(long expectedEpoch, out PlayerKVGameSessionHandle gameHandle)
|
|
+ {
|
|
+ gameHandle = null;
|
|
+ // IsCurrent can invoke external code and synchronously reenter CloseAsync.
|
|
+ if (expectedEpoch != epoch || !IsCurrent) return false;
|
|
+ lock (gate)
|
|
+ {
|
|
+ if (closing != null || revoked != 0 || prepared != 1 || handle == null || handle.Epoch != expectedEpoch)
|
|
+ return false;
|
|
+ gameHandle = handle;
|
|
+ return true;
|
|
+ }
|
|
+ }
|
|
+
|
|
// All issuance and Close admission share gate. The editor factory may not block on Close.
|
|
internal bool TryAcquireAccountLease(PlayerKVControlledLoadCapability capability,
|
|
out PlayerKVAccountLease lease)
|
|
diff --git a/CodexOutput/upserver/KVGameLoadTests/CoordinatorTests.cs b/CodexOutput/upserver/KVGameLoadTests/CoordinatorTests.cs
|
|
index 46efb0c55..d53aa274c 100644
|
|
--- a/CodexOutput/upserver/KVGameLoadTests/CoordinatorTests.cs
|
|
+++ b/CodexOutput/upserver/KVGameLoadTests/CoordinatorTests.cs
|
|
@@ -59,6 +59,19 @@ static partial class Tests
|
|
}
|
|
static async Task RunCoordinatorTests()
|
|
{
|
|
+ await Run("M2-CP1-game-load-releases-guard-after-notification",()=>OnMain(async()=>
|
|
+ {
|
|
+ var f=new LoadFixture();
|
|
+ try
|
|
+ {
|
|
+ var result=await f.Coordinator.LoadForGameAsync("101",f.Capability());
|
|
+ Check(result.Error==PlayerKVGameLoadError.None && f.Notify==1,"complete apply and notification");
|
|
+ Check(!PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.ReadLocal),"game success must release its load guard");
|
|
+ Check(result.State.ToString()=="GameLoaded" && !f.Coordinator.LoadedForValidation,"game success differs from validation");
|
|
+ }
|
|
+ finally { await f.Coordinator.Revoke(); }
|
|
+ }));
|
|
+ await RunGameHandoffTests();
|
|
await Run("coordinator-success-reload-revoke-and-one-auth",()=>OnMain(async()=>
|
|
{
|
|
var f=new LoadFixture();
|
|
diff --git a/CodexOutput/upserver/KVGameLoadTests/KVGameLoadTests.csproj b/CodexOutput/upserver/KVGameLoadTests/KVGameLoadTests.csproj
|
|
index 423e444ba..eaf11bc49 100644
|
|
--- a/CodexOutput/upserver/KVGameLoadTests/KVGameLoadTests.csproj
|
|
+++ b/CodexOutput/upserver/KVGameLoadTests/KVGameLoadTests.csproj
|
|
@@ -5,6 +5,8 @@
|
|
<RestoreConfigFile>$(MSBuildThisFileDirectory)../MinimalLoginTests/NuGet.Config</RestoreConfigFile>
|
|
</PropertyGroup>
|
|
<ItemGroup>
|
|
+ <Compile Include="../../../Client/Assets/Scripts/Core/KVMgr.cs" Link="Production/KVMgr.cs" />
|
|
+ <Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVKeyReadResult.cs" Link="Production/PlayerKVKeyReadResult.cs" />
|
|
<Compile Remove="ServiceHost/**/*.cs" />
|
|
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVInitializationIntents.cs" Link="Production/PlayerKVInitializationIntents.cs" />
|
|
<Compile Include="../../../Client/Assets/Scripts/Services/PlayerData/PlayerKVGameLoadTransaction.cs" Link="Production/PlayerKVGameLoadTransaction.cs" />
|