refactor(engine-ambient): wrap residual UnitTests + delete EngineSessionGate
Step 7 of multi-instancing migration. Residual SVSim.UnitTests that touch engine code directly are wrapped in TestBattleScope. EngineSessionGate is deleted along with the _engineOwned bookkeeping in BattleSession; engine setup is unconditional now that per-battle state is isolated on the ambient. Gate-specific fallback branches in BattleSession.ShadowIngest are simplified. Suite fully green (SVSim.UnitTests, SVSim.BattleEngine.Tests). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,11 +52,6 @@ public sealed class BattleSession
|
||||
/// was already processed during the Swap feed.</summary>
|
||||
private bool _engineReadyFed;
|
||||
|
||||
/// <summary>True once this session has acquired the process-wide <see cref="Engine.EngineSessionGate"/>
|
||||
/// (and is therefore the single active engine owner). Drives the matching <c>Release</c> at battle
|
||||
/// end so the next session can take the engine.</summary>
|
||||
private bool _engineOwned;
|
||||
|
||||
/// <summary>Serializes dispatch. Both participants' read loops raise FrameEmitted on their own
|
||||
/// threads, and a dispatch (<see cref="ComputeFrames"/> + the relay <c>PushAsync</c> calls) mutates
|
||||
/// shared, non-thread-safe state — the <see cref="BattleSessionState"/> dictionaries and each
|
||||
@@ -191,24 +186,18 @@ public sealed class BattleSession
|
||||
if (A is RealParticipant rpA) rpA.Outbound.Clear();
|
||||
if (B is RealParticipant rpB) rpB.Outbound.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(
|
||||
A.TerminateAsync(BattleFinishReason.NormalFinish),
|
||||
B.TerminateAsync(BattleFinishReason.NormalFinish))
|
||||
.ConfigureAwait(false);
|
||||
// Per-session BattleAmbientContext on the engine isolates per-battle state across concurrent
|
||||
// sessions (Task 7 of multi-instancing migration), so the historical single-active-engine gate
|
||||
// (and its matching try/finally Release) is gone — engine setup is unconditional per session,
|
||||
// and there is no teardown obligation that must run on a throw from the participant tear-down.
|
||||
await Task.WhenAll(
|
||||
A.TerminateAsync(BattleFinishReason.NormalFinish),
|
||||
B.TerminateAsync(BattleFinishReason.NormalFinish))
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await A.DisposeAsync().ConfigureAwait(false);
|
||||
await B.DisposeAsync().ConfigureAwait(false);
|
||||
_dispatchGate.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Release the single-active-engine gate exactly once, in a finally so a throw from the
|
||||
// terminate/dispose teardown above can never leak it to the next session (the gate is a
|
||||
// process-global static shared across all sessions, incl. across tests in one process).
|
||||
if (_engineOwned) Engine.EngineSessionGate.Release();
|
||||
}
|
||||
await A.DisposeAsync().ConfigureAwait(false);
|
||||
await B.DisposeAsync().ConfigureAwait(false);
|
||||
_dispatchGate.Dispose();
|
||||
}
|
||||
|
||||
private Task OnFrameFromA(MsgEnvelope env, CancellationToken ct) => HandleFrameAsync(A, env, ct);
|
||||
@@ -349,18 +338,13 @@ public sealed class BattleSession
|
||||
if (_engineSetupAttempted) return;
|
||||
_engineSetupAttempted = true;
|
||||
|
||||
// Single-active-engine gate: the engine's process-global turn state can't back two concurrent
|
||||
// battles, so only one session may own it (carried-risk B). On failure we DON'T set the engine
|
||||
// up — it stays not-ready and ShadowIngest no-ops on !IsReady — and log the limitation loudly
|
||||
// (not a silent fallback). Per-session isolation (dropping the gate) is the tracked follow-up.
|
||||
if (!Engine.EngineSessionGate.TryAcquire())
|
||||
{
|
||||
_log.LogWarning("BattleSession {Bid}: another battle owns the engine; this battle runs " +
|
||||
"WITHOUT engine-sourced fields (single-active-engine limitation — per-session isolation pending)",
|
||||
BattleId);
|
||||
return;
|
||||
}
|
||||
_engineOwned = true;
|
||||
// Per-session BattleAmbientContext on the engine isolates per-battle state across concurrent
|
||||
// sessions (Task 7 of multi-instancing migration), so the historical single-active-engine gate
|
||||
// (EngineSessionGate.TryAcquire) is gone and engine setup is unconditional. A genuine setup
|
||||
// failure still surfaces via ComputeFrames' shadow-engine try/catch (it logs + swallows so the
|
||||
// relay never sees an engine exception, ND6), and IsReady stays false in that case so
|
||||
// ShadowIngest/ShadowFeedServerFrames no-op for the rest of the battle.
|
||||
//
|
||||
// Seed the engine's StableRandom with BattleSeeds.Stable(MasterSeed) — the SAME value the
|
||||
// Matched frame ships to both clients (InitBattleHandler.cs:28). The clients seed their
|
||||
// System.Random with Matched.seed (BattleManagerBase.cs:721), so the engine's stream must
|
||||
|
||||
Reference in New Issue
Block a user