fix(dashboard): enforce/document the advised-set cap honestly; cover handle-0 and oversize-read paths
This commit is contained in:
@@ -19,6 +19,16 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
|
||||
// One browse page of tags plus headroom. Bounds the standing advise load the
|
||||
// single dashboard worker carries — and the event churn that advise set feeds —
|
||||
// however much of a galaxy an operator browses through in one sitting.
|
||||
//
|
||||
// The bound is per-read, not absolute: a read may never evict a tag it is itself
|
||||
// about to return, so a single read of more distinct tags than the cap leaves the
|
||||
// set that large. The invariant EvictForAsync actually maintains is
|
||||
//
|
||||
// |advise set| after a read <= max(MaxSubscribedTags, distinct tags in that read)
|
||||
//
|
||||
// and any overshoot is squeezed back out by the next read that subscribes a tag
|
||||
// (see EvictForAsync). A browse page requests far fewer tags than the cap, so in
|
||||
// practice the set settles at MaxSubscribedTags.
|
||||
private const int MaxSubscribedTags = 256;
|
||||
|
||||
private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(5);
|
||||
@@ -125,6 +135,11 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
|
||||
// order). `justReadCount` is how many distinct tags of this read were already
|
||||
// advised — they now occupy the front of the list and must never be evicted to
|
||||
// make room for the same read's new tags. Callers must hold _gate.
|
||||
//
|
||||
// Every tag of one read is equally recently read; the recency list needs a total
|
||||
// order anyway, so the whole service uses one tie-break: later in the request wins.
|
||||
// Promoting in request order gives that here, and TrackSubscribed inserts new tags
|
||||
// the same way.
|
||||
private string[] TouchAndCollectNewTags(IReadOnlyCollection<string> tagAddresses, out int justReadCount)
|
||||
{
|
||||
int touched = 0;
|
||||
@@ -161,6 +176,18 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
|
||||
// one batch. A failed unadvise must not fail the read: the tags are dropped
|
||||
// from tracking regardless, and the session-invalidation path already handles
|
||||
// gateway/worker drift. Callers must hold _gate.
|
||||
//
|
||||
// Eviction stops at the tags this read just touched (`justReadCount`), so a read
|
||||
// whose own distinct tags outnumber the cap ends over it — see MaxSubscribedTags
|
||||
// for the exact invariant. That overshoot is not sticky: the next read that
|
||||
// subscribes anything computes `overflow` against the oversized set and evicts the
|
||||
// whole excess in one pass (a 300-tag set plus one new tag evicts 45 and lands
|
||||
// back at the cap). A read that subscribes nothing new evicts nothing, but it also
|
||||
// cannot grow the set.
|
||||
//
|
||||
// Cancellation mid-eviction follows this file's policy: OperationCanceledException
|
||||
// is deliberately not caught here or in ReadAsync, so it propagates with the tags
|
||||
// already dropped from tracking — the same end state as a failed unadvise.
|
||||
private async Task EvictForAsync(
|
||||
GatewaySession session,
|
||||
int serverHandle,
|
||||
@@ -227,10 +254,10 @@ public sealed class DashboardLiveDataService : IDashboardLiveDataService, IAsync
|
||||
}
|
||||
}
|
||||
|
||||
// Inserted back-to-front so the read's first tag ends up most recent.
|
||||
for (int i = tagAddresses.Count - 1; i >= 0; i--)
|
||||
// Request order, so the read's last tag ends up most recent — the same
|
||||
// tie-break TouchAndCollectNewTags applies to the tags it promotes.
|
||||
foreach (string tag in tagAddresses)
|
||||
{
|
||||
string tag = tagAddresses[i];
|
||||
handles.TryGetValue(tag, out int itemHandle);
|
||||
_subscribed[tag] = _recency.AddFirst(new SubscribedTag(tag, itemHandle));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user