fix(dashboard): alarms feed doc row + monitor-contract comment + mid-truncation attach test
The In-process page feeds table still described the alarms page's subscription as
provider status only, contradicting the three passages updated in 7b6dfba.
The loop's catch-all comment (and the cadence bullet that repeated it) claimed the
monitor completes a subscriber's stream on restart. It does not: ClearCache pushes
snapshot_status(false) through the still-open channel, and a subscriber is only
completed-with-error on a failed TryWrite, or removed by its own disposal.
The SnapshotStatus arm's priming claim had no test behind it — every push test
attached to an untruncated feed and pushed the edge itself. ScriptedAlarmFeed now
replays an optional open sequence, and a new test attaches to a feed already
primed provider_status then snapshot_status(truncated) and asserts the banner
comes up with no edge pushed after render.
This commit is contained in:
@@ -186,7 +186,7 @@ hubs stay for the audience that genuinely needs a wire.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| every page deriving from `DashboardPageBase` | `IDashboardSnapshotFeed.WatchAsync` | `DashboardSnapshotFeed` (singleton) multicasting one `IDashboardSnapshotService.WatchSnapshotsAsync` enumeration |
|
| every page deriving from `DashboardPageBase` | `IDashboardSnapshotFeed.WatchAsync` | `DashboardSnapshotFeed` (singleton) multicasting one `IDashboardSnapshotService.WatchSnapshotsAsync` enumeration |
|
||||||
| `SessionDetailsPage` | `IDashboardSessionEventSubscriber.Subscribe(sessionId)` | `DashboardEventBroadcaster` — the same singleton the session mirror publishes to, registered behind both interfaces |
|
| `SessionDetailsPage` | `IDashboardSessionEventSubscriber.Subscribe(sessionId)` | `DashboardEventBroadcaster` — the same singleton the session mirror publishes to, registered behind both interfaces |
|
||||||
| `AlarmsPage` | `IGatewayAlarmService.StreamAsync` | the central alarm monitor, **provider status only**; the alarm rows still come from the 3 s `QueryAlarmsAsync` poll |
|
| `AlarmsPage` | `IGatewayAlarmService.StreamAsync` | the central alarm monitor, **gateway-status frames only** — `provider_status` for the badge and `snapshot_status` for the truncated-snapshot banner; the alarm rows still come from the 3 s `QueryAlarmsAsync` poll |
|
||||||
|
|
||||||
The snapshot feed multicasts rather than handing each page its own enumeration:
|
The snapshot feed multicasts rather than handing each page its own enumeration:
|
||||||
`WatchSnapshotsAsync` is not multicast on its own — each enumeration owns a timer
|
`WatchSnapshotsAsync` is not multicast on its own — each enumeration owns a timer
|
||||||
@@ -288,9 +288,9 @@ Both seams consume the same producing services, so they share these cadences:
|
|||||||
to its internal dashboard-mirror subscriber (independent of any gRPC `StreamEvents`);
|
to its internal dashboard-mirror subscriber (independent of any gRPC `StreamEvents`);
|
||||||
- the alarms page's status feed resubscribes one second after its
|
- the alarms page's status feed resubscribes one second after its
|
||||||
`IGatewayAlarmService.StreamAsync` enumeration ends — the monitor completes a
|
`IGatewayAlarmService.StreamAsync` enumeration ends — the monitor completes a
|
||||||
subscriber's stream when it falls behind and again when it restarts, both
|
subscriber's stream only when that subscriber has fallen behind, which resubscribing
|
||||||
recoverable by resubscribing — and the badge and banner hold their last values in
|
recovers (a monitor restart keeps the channel and pushes cleared status frames through
|
||||||
between. That feed carries both gateway-status frames: `provider_status` drives the
|
it) — and the badge and banner hold their last values in between. That feed carries both gateway-status frames: `provider_status` drives the
|
||||||
badge, and `snapshot_status` drives the truncated-snapshot banner, so the caveat
|
badge, and `snapshot_status` drives the truncated-snapshot banner, so the caveat
|
||||||
appears on the monitor's verdict change rather than up to three seconds later. Every
|
appears on the monitor's verdict change rather than up to three seconds later. Every
|
||||||
subscriber is primed with a `snapshot_status` frame at open, so a page attaching
|
subscriber is primed with a `snapshot_status` frame at open, so a page attaching
|
||||||
|
|||||||
@@ -258,10 +258,13 @@
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// The monitor completes a subscriber's stream when it falls behind, and
|
// The monitor drops a subscriber whose queue it cannot write to, completing
|
||||||
// again when the monitor restarts. Both are recoverable by resubscribing;
|
// that stream with an error; short of cancellation or disposal that is the
|
||||||
// the badge and banner hold their last values in the meantime, and the
|
// only way this enumeration ends. A monitor restart is NOT one of them — it
|
||||||
// resubscribe is primed with the current ones.
|
// keeps the channel and pushes the cleared status frames through it — so this
|
||||||
|
// catch is the fell-behind case, recoverable by resubscribing. The badge and
|
||||||
|
// banner hold their last values meanwhile, and the resubscribe is primed with
|
||||||
|
// the current ones.
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -134,6 +134,44 @@ public sealed class AlarmsPageTruncationBannerTests
|
|||||||
"banner to clear after a complete snapshot_status frame");
|
"banner to clear after a complete snapshot_status frame");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attach while the verdict is already truncated. The page carries no
|
||||||
|
/// priming logic of its own — it relies on <c>StreamAsync</c> opening every
|
||||||
|
/// subscription with a <c>snapshot_status</c> baseline — so the caveat has
|
||||||
|
/// to come up off the open sequence alone, with no edge pushed afterwards.
|
||||||
|
/// A page that only handled the edge would show an un-caveated alarm list
|
||||||
|
/// to every operator who opened it after the truncation began.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
|
[Fact]
|
||||||
|
public async Task AlarmsPage_AttachingToAnAlreadyTruncatedFeed_RaisesTheBannerFromThePriming()
|
||||||
|
{
|
||||||
|
// The monitor's open sequence: provider status, then the unconditional
|
||||||
|
// completeness baseline. Nothing is pushed after this.
|
||||||
|
ScriptedAlarmFeed feed = new()
|
||||||
|
{
|
||||||
|
Priming =
|
||||||
|
[
|
||||||
|
new AlarmFeedMessage { ProviderStatus = new AlarmProviderStatus() },
|
||||||
|
SnapshotStatusFrame(truncated: true),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await using ServiceProvider provider = BuildPushServices(feed);
|
||||||
|
await using HtmlRenderer renderer = new(
|
||||||
|
provider,
|
||||||
|
provider.GetRequiredService<ILoggerFactory>());
|
||||||
|
|
||||||
|
HtmlRootComponent page = await renderer.Dispatcher.InvokeAsync(
|
||||||
|
() => renderer.RenderComponentAsync<AlarmsPage>());
|
||||||
|
|
||||||
|
await WaitForHtmlAsync(
|
||||||
|
renderer,
|
||||||
|
page,
|
||||||
|
html => html.Contains(BannerMarker, StringComparison.Ordinal),
|
||||||
|
"banner to appear from the feed's open-time snapshot_status baseline");
|
||||||
|
}
|
||||||
|
|
||||||
private static AlarmFeedMessage SnapshotStatusFrame(bool truncated)
|
private static AlarmFeedMessage SnapshotStatusFrame(bool truncated)
|
||||||
{
|
{
|
||||||
return new AlarmFeedMessage
|
return new AlarmFeedMessage
|
||||||
@@ -280,6 +318,14 @@ public sealed class AlarmsPageTruncationBannerTests
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool SnapshotTruncated { get; set; }
|
public bool SnapshotTruncated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Frames replayed at the head of every subscription, standing in for the
|
||||||
|
/// monitor's open sequence (provider status, then the unconditional
|
||||||
|
/// completeness baseline). Empty means the subscriber sees only what the
|
||||||
|
/// test pushes.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<AlarmFeedMessage> Priming { get; init; } = [];
|
||||||
|
|
||||||
/// <summary>Pushes one frame onto the feed the page is subscribed to.</summary>
|
/// <summary>Pushes one frame onto the feed the page is subscribed to.</summary>
|
||||||
/// <param name="message">The feed frame to deliver.</param>
|
/// <param name="message">The feed frame to deliver.</param>
|
||||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
@@ -290,6 +336,12 @@ public sealed class AlarmsPageTruncationBannerTests
|
|||||||
string? alarmFilterPrefix,
|
string? alarmFilterPrefix,
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
foreach (AlarmFeedMessage primed in Priming)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
yield return primed;
|
||||||
|
}
|
||||||
|
|
||||||
await foreach (AlarmFeedMessage message in _frames.Reader
|
await foreach (AlarmFeedMessage message in _frames.Reader
|
||||||
.ReadAllAsync(cancellationToken)
|
.ReadAllAsync(cancellationToken)
|
||||||
.ConfigureAwait(false))
|
.ConfigureAwait(false))
|
||||||
|
|||||||
Reference in New Issue
Block a user