Files
SVSimServer/SVSim.BattleNode/Sessions/InMemoryBattleSessionStore.cs
gamer147 564b1d678f fix(battle-node): collision-safe battle-id registration + viewer eviction
RegisterPending → TryRegisterPending (TryAdd instead of indexer) so
battle-id collisions return false instead of silently evicting a live
battle. MatchingBridge retries with fresh IDs on collision (max 5).

Before registering, EvictStaleForViewer removes any stale pending
battle the viewer left behind, enforcing the one-pending-per-viewer
invariant that was previously comment-asserted.

Store tests switched to per-test local stores to fix a race under
the assembly-wide ParallelScope.All.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-04 22:13:20 -04:00

33 lines
1.2 KiB
C#

using System.Collections.Concurrent;
namespace SVSim.BattleNode.Sessions;
public sealed class InMemoryBattleSessionStore : IBattleSessionStore
{
private readonly ConcurrentDictionary<string, PendingBattle> _pending = new();
public bool TryRegisterPending(PendingBattle battle) =>
_pending.TryAdd(battle.BattleId, battle);
public PendingBattle? TryGetPending(string battleId) =>
_pending.TryGetValue(battleId, out var b) ? b : null;
public PendingBattle? TryFindPendingForViewer(long viewerId)
{
// Linear scan — _pending is bounded by concurrent in-flight matches (low
// double digits at most), so this stays cheap. Returns whichever match the
// dictionary's enumerator yields first; in practice a viewer has at most one
// pending battle since each /do_matching either pairs/falls-back the existing
// slot or parks without registering.
foreach (var b in _pending.Values)
{
if (b.P1.ViewerId == viewerId) return b;
if (b.P2 is not null && b.P2.ViewerId == viewerId) return b;
}
return null;
}
public bool RemovePending(string battleId) =>
_pending.TryRemove(battleId, out _);
}