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:
@@ -14,11 +14,27 @@ public class ShadowverseSessionService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const string MakeMd5Salt = "r!I@ws8e5i=";
|
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();
|
_sessionIdToUdid = new();
|
||||||
|
_insertionOrder = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid? GetUdidFromSessionId(string sid)
|
public Guid? GetUdidFromSessionId(string sid)
|
||||||
@@ -33,7 +49,27 @@ public class ShadowverseSessionService
|
|||||||
|
|
||||||
public void StoreUdidForSessionId(string sid, Guid udid)
|
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>
|
/// <summary>
|
||||||
|
|||||||
@@ -35,4 +35,54 @@ public class ShadowverseSessionServiceTests
|
|||||||
|
|
||||||
Assert.That(svc.GetUdidFromSessionId("dc4aac79d35fe15dfb6262e0071bb03c"), Is.EqualTo(udid));
|
Assert.That(svc.GetUdidFromSessionId("dc4aac79d35fe15dfb6262e0071bb03c"), Is.EqualTo(udid));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void StoreUdidForSessionId_evicts_oldest_when_cap_exceeded()
|
||||||
|
{
|
||||||
|
// Cap=3, insert 5 distinct SIDs; the two earliest must be evicted.
|
||||||
|
var svc = new ShadowverseSessionService(maxEntries: 3);
|
||||||
|
var udid = new System.Guid("62747917-93bc-454c-abb4-ef423b3c9317");
|
||||||
|
|
||||||
|
svc.StoreUdidForSessionId("sid-1", udid);
|
||||||
|
svc.StoreUdidForSessionId("sid-2", udid);
|
||||||
|
svc.StoreUdidForSessionId("sid-3", udid);
|
||||||
|
svc.StoreUdidForSessionId("sid-4", udid);
|
||||||
|
svc.StoreUdidForSessionId("sid-5", udid);
|
||||||
|
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-1"), Is.Null, "Oldest entry must be evicted.");
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-2"), Is.Null, "Second-oldest entry must be evicted.");
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-3"), Is.EqualTo(udid));
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-4"), Is.EqualTo(udid));
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-5"), Is.EqualTo(udid));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void StoreUdidForSessionId_re_storing_same_sid_does_not_grow_queue()
|
||||||
|
{
|
||||||
|
// Cap=2. Store sid-A, then re-store sid-A many times, then store sid-B and sid-C.
|
||||||
|
// The re-stores must NOT count toward the cap — sid-A should still resolve after
|
||||||
|
// sid-B and sid-C land, because only two distinct SIDs are tracked.
|
||||||
|
var svc = new ShadowverseSessionService(maxEntries: 2);
|
||||||
|
var udid = new System.Guid("62747917-93bc-454c-abb4-ef423b3c9317");
|
||||||
|
|
||||||
|
svc.StoreUdidForSessionId("sid-A", udid);
|
||||||
|
for (int i = 0; i < 20; i++) svc.StoreUdidForSessionId("sid-A", udid);
|
||||||
|
svc.StoreUdidForSessionId("sid-B", udid);
|
||||||
|
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-A"), Is.EqualTo(udid), "sid-A must still resolve after re-stores.");
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-B"), Is.EqualTo(udid));
|
||||||
|
|
||||||
|
// sid-C pushes us over the cap → sid-A (oldest) evicted.
|
||||||
|
svc.StoreUdidForSessionId("sid-C", udid);
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-A"), Is.Null);
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-B"), Is.EqualTo(udid));
|
||||||
|
Assert.That(svc.GetUdidFromSessionId("sid-C"), Is.EqualTo(udid));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Constructor_rejects_non_positive_cap()
|
||||||
|
{
|
||||||
|
Assert.Throws<System.ArgumentOutOfRangeException>(() => new ShadowverseSessionService(maxEntries: 0));
|
||||||
|
Assert.Throws<System.ArgumentOutOfRangeException>(() => new ShadowverseSessionService(maxEntries: -1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user