perf(inventory): jitter the flush interval (2-5 min, re-rolled per tick)

The 60s fixed flush made the whole fleet flush in lockstep after an auto-update
relog wave (timers phase-aligned), producing a synchronized burst that starved
backend telemetry → player-count flapping. Now each client flushes on a random
2-5 min interval (2 min base + up to 3 min jitter), re-rolled after every tick,
so timers continuously drift apart and never re-synchronize — even when many
clients hot-reload at the same instant. Inventory is up to 5 min stale by
design. Pairs with the backend concurrency cap as defense-in-depth.

Built on master (placeholder secret, works via SHARED_SECRET_LEGACY); the
websocket_secret.txt change stays parked on secret-rollout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-23 22:19:04 +02:00
parent 92a51b205c
commit 4fdec3bbcd
2 changed files with 22 additions and 2 deletions

View file

@ -24,13 +24,27 @@ namespace MosswartMassacre
// so the backend sees ~1 update per item per flush instead of many per // so the backend sees ~1 update per item per flush instead of many per
// second. Adds and removes stay immediate (they're rare and meaningful). // second. Adds and removes stay immediate (they're rare and meaningful).
private readonly HashSet<int> _dirtyItemIds = new HashSet<int>(); private readonly HashSet<int> _dirtyItemIds = new HashSet<int>();
private const int FlushIntervalMs = 60000; // 60s — tune here if needed // Flush on a RANDOMIZED interval — 2 minutes plus up to 3 more (so
// 25 min) — re-rolled after every tick. This de-synchronizes the
// fleet: even when many clients hot-reload at once (an auto-update
// wave) and start their timers at the same instant, each picks a
// different interval, so their flushes spread out instead of arriving
// at the backend as one synchronized burst (which starved telemetry
// and made the player count flap).
private const int FlushBaseMs = 120000; // 2 minutes
private const int FlushJitterMs = 180000; // + up to 3 minutes → 25 min
private readonly Random _rng = new Random();
private readonly System.Windows.Forms.Timer _flushTimer; private readonly System.Windows.Forms.Timer _flushTimer;
private int NextFlushIntervalMs()
{
return FlushBaseMs + _rng.Next(0, FlushJitterMs + 1); // 120000300000 ms
}
internal LiveInventoryTracker(IPluginLogger logger) internal LiveInventoryTracker(IPluginLogger logger)
{ {
_logger = logger; _logger = logger;
_flushTimer = new System.Windows.Forms.Timer { Interval = FlushIntervalMs }; _flushTimer = new System.Windows.Forms.Timer { Interval = NextFlushIntervalMs() };
_flushTimer.Tick += FlushDirtyItems; _flushTimer.Tick += FlushDirtyItems;
} }
@ -53,6 +67,7 @@ namespace MosswartMassacre
{ {
_logger?.Log($"[LiveInv] Error initializing: {ex.Message}"); _logger?.Log($"[LiveInv] Error initializing: {ex.Message}");
} }
_flushTimer.Interval = NextFlushIntervalMs(); // fresh random phase per login
_flushTimer.Start(); _flushTimer.Start();
} }
@ -148,6 +163,11 @@ namespace MosswartMassacre
/// </summary> /// </summary>
private void FlushDirtyItems(object sender, EventArgs e) private void FlushDirtyItems(object sender, EventArgs e)
{ {
// Re-roll the interval every tick so the fleet's timers keep
// drifting apart and never re-synchronize. (Setting Interval on a
// WinForms timer restarts its countdown — desired here.)
_flushTimer.Interval = NextFlushIntervalMs();
if (_dirtyItemIds.Count == 0) return; if (_dirtyItemIds.Count == 0) return;
// Snapshot then clear, so changes arriving during the flush are // Snapshot then clear, so changes arriving during the flush are