fix(session): bound the SID→UDID dict with FIFO eviction

The map used to grow unbounded over the process's lifetime — every fresh
signup added an entry that was never reclaimed. Long-running dev hosts
(or any future emulator deployment that doesn't restart often) would
gradually leak memory. Cap at 10k entries by default with a simple FIFO
eviction queue; re-stores of the same SID don't grow the queue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-05-28 21:13:47 -04:00
parent 91412ff821
commit 177b4925a1
2 changed files with 89 additions and 3 deletions

View File

@@ -14,11 +14,27 @@ public class ShadowverseSessionService
/// </summary>
private const string MakeMd5Salt = "r!I@ws8e5i=";
private readonly ConcurrentDictionary<string, Guid> _sessionIdToUdid;
/// <summary>
/// Default cap for the in-memory SID→UDID map. Each entry is roughly 32B SID + 16B Guid
/// plus dict + queue overhead — 10k entries ≈ 1 MB of process memory. Sized for the
/// emulator's expected ceiling, not prod scale. Long-running dev hosts that keep
/// accumulating signups would otherwise grow this dict unboundedly.
/// </summary>
public const int DefaultMaxEntries = 10_000;
public ShadowverseSessionService()
private readonly int _maxEntries;
private readonly ConcurrentDictionary<string, Guid> _sessionIdToUdid;
private readonly ConcurrentQueue<string> _insertionOrder;
public ShadowverseSessionService() : this(DefaultMaxEntries) { }
public ShadowverseSessionService(int maxEntries)
{
if (maxEntries <= 0)
throw new ArgumentOutOfRangeException(nameof(maxEntries), "Cap must be positive.");
_maxEntries = maxEntries;
_sessionIdToUdid = new();
_insertionOrder = new();
}
public Guid? GetUdidFromSessionId(string sid)
@@ -33,7 +49,27 @@ public class ShadowverseSessionService
public void StoreUdidForSessionId(string sid, Guid udid)
{
_sessionIdToUdid.AddOrUpdate(sid, _ => udid, (_, _) => udid);
// FIFO eviction: only enqueue on first insertion so the queue doesn't grow when
// an existing SID is re-stored (the only realistic "update" — same SID always
// resolves to the same UDID by construction of ComputeClientSessionId, so this
// path is effectively a no-op semantically).
if (_sessionIdToUdid.TryAdd(sid, udid))
{
_insertionOrder.Enqueue(sid);
EvictIfOverCap();
}
else
{
_sessionIdToUdid[sid] = udid;
}
}
private void EvictIfOverCap()
{
while (_sessionIdToUdid.Count > _maxEntries && _insertionOrder.TryDequeue(out var oldest))
{
_sessionIdToUdid.TryRemove(oldest, out _);
}
}
/// <summary>