119 lines
4.8 KiB
C#
119 lines
4.8 KiB
C#
using Microsoft.AspNetCore.Components;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Components;
|
|
|
|
/// <summary>
|
|
/// Base class for Blazor dashboard pages that watch gateway metrics snapshots.
|
|
/// Pages subscribe to the in-process <see cref="IDashboardSnapshotFeed"/>, which
|
|
/// multicasts a single <see cref="IDashboardSnapshotService.WatchSnapshotsAsync"/>
|
|
/// enumeration to every circuit. An earlier implementation had each page open its
|
|
/// own SignalR connection to <c>/hubs/snapshot</c> — a loopback WebSocket back into
|
|
/// this same process, per page. The snapshot hub and its publisher remain for
|
|
/// external (non-circuit) clients; server-rendered pages no longer use them.
|
|
/// </summary>
|
|
public abstract class DashboardPageBase : ComponentBase, IAsyncDisposable
|
|
{
|
|
/// <summary>
|
|
/// Upper bound on waiting for the watch loop while disposing. The loop marshals
|
|
/// renders through the renderer's dispatcher and disposal can run on that same
|
|
/// dispatcher, so the wait is bounded rather than unconditional.
|
|
/// </summary>
|
|
private static readonly TimeSpan WatchDrainTimeout = TimeSpan.FromSeconds(5);
|
|
|
|
private readonly CancellationTokenSource _watchCancellation = new();
|
|
private Task? _watchTask;
|
|
|
|
/// <summary>Snapshot service used to seed the initial render before the first feed update.</summary>
|
|
[Inject]
|
|
protected IDashboardSnapshotService SnapshotService { get; set; } = null!;
|
|
|
|
/// <summary>Shared in-process snapshot feed this page renders from.</summary>
|
|
[Inject]
|
|
protected IDashboardSnapshotFeed SnapshotFeed { get; set; } = null!;
|
|
|
|
/// <summary>Logger used to report a snapshot subscription that ended or would not drain.</summary>
|
|
[Inject]
|
|
protected ILogger<DashboardPageBase>? Logger { get; set; }
|
|
|
|
/// <summary>
|
|
/// The most recent gateway metric snapshot. Synchronously seeded from
|
|
/// <see cref="IDashboardSnapshotService.GetSnapshot"/> for the very first
|
|
/// render, then refreshed from the feed.
|
|
/// </summary>
|
|
protected DashboardSnapshot? Snapshot { get; private set; }
|
|
|
|
/// <inheritdoc />
|
|
protected override Task OnInitializedAsync()
|
|
{
|
|
Snapshot = SnapshotService.GetSnapshot();
|
|
|
|
// Deliberately not awaited: the watch loop runs for the lifetime of the page
|
|
// and is cancelled and drained by DisposeAsync.
|
|
_watchTask = WatchSnapshotsAsync(_watchCancellation.Token);
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>Cancels the snapshot subscription created for this page, tolerating disposal-time errors.</summary>
|
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
try
|
|
{
|
|
await _watchCancellation.CancelAsync().ConfigureAwait(false);
|
|
|
|
if (_watchTask is not null)
|
|
{
|
|
await _watchTask.WaitAsync(WatchDrainTimeout).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (TimeoutException)
|
|
{
|
|
// Accepted limitation: the abandoned loop still holds its feed subscription, so
|
|
// the feed's idle gate stays open until it does unwind. There is no way to force
|
|
// a detach — the loop is parked on a dispatcher that is not draining — so the
|
|
// warning is the operator's only signal that a circuit teardown wedged.
|
|
Logger?.LogWarning(
|
|
"Dashboard page {Page} did not release its snapshot subscription within {Timeout}; "
|
|
+ "the shared snapshot feed stays active until it unwinds.",
|
|
GetType().Name,
|
|
WatchDrainTimeout);
|
|
}
|
|
catch
|
|
{
|
|
// Other disposal-time errors are best-effort.
|
|
}
|
|
|
|
_watchCancellation.Dispose();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
private async Task WatchSnapshotsAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
await foreach (DashboardSnapshot snapshot in SnapshotFeed
|
|
.WatchAsync(cancellationToken)
|
|
.ConfigureAwait(false))
|
|
{
|
|
Snapshot = snapshot;
|
|
await InvokeAsync(StateHasChanged).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// The page is going away.
|
|
}
|
|
catch (Exception error)
|
|
{
|
|
// The feed is best-effort: the last rendered snapshot stays on screen and the
|
|
// snapshot service keeps serving GetSnapshot() for the next page load. Logged
|
|
// once here, on the way out of the loop — never per snapshot.
|
|
Logger?.LogWarning(
|
|
error,
|
|
"Live snapshot updates ended for dashboard page {Page}; it keeps the last rendered snapshot.",
|
|
GetType().Name);
|
|
}
|
|
}
|
|
}
|