perf(comms+audit): close phase-2 residuals — direct ingest path, monotonic timeouts, synthetic probe, not-reporting set, cursor-exact audit pull
This commit is contained in:
@@ -30,6 +30,7 @@ namespace ZB.MOM.WW.ScadaBridge.Communication;
|
||||
public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
|
||||
{
|
||||
private static readonly IReadOnlyList<AlarmStateChanged> Empty = Array.Empty<AlarmStateChanged>();
|
||||
private static readonly IReadOnlyList<string> EmptyNames = Array.Empty<string>();
|
||||
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly CommunicationService _communicationService;
|
||||
@@ -135,6 +136,13 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
|
||||
return _sites.TryGetValue(siteId, out var entry) && entry.HasPublished && entry.StreamLive;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> GetNotReportingInstances(int siteId)
|
||||
{
|
||||
lock (_lock)
|
||||
return _sites.TryGetValue(siteId, out var entry) ? entry.NotReporting : EmptyNames;
|
||||
}
|
||||
|
||||
// ── Subscriber teardown ─────────────────────────────────────────────────────
|
||||
|
||||
private void Unsubscribe(Subscription subscription)
|
||||
@@ -332,6 +340,15 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
|
||||
/// <see cref="DebugViewSnapshot.InstanceNotFound"/> contributes no rows rather than
|
||||
/// failing the whole seed (mirrors <c>AlarmSummaryService</c>). Re-enumerates instances
|
||||
/// each call so a reconcile picks up enable/disable/deploy/delete drift.
|
||||
/// <para>
|
||||
/// It also records WHICH instances failed to answer into
|
||||
/// <see cref="SiteEntry.NotReporting"/>, published through
|
||||
/// <see cref="ISiteAlarmLiveCache.GetNotReportingInstances"/>. That set was previously
|
||||
/// computed and thrown away here, forcing the Alarm Summary page to run a second, identical
|
||||
/// fan-out through <c>AlarmSummaryService</c> purely to rebuild it. Same classification as
|
||||
/// that service (<c>InstanceNotFound</c> or any non-cancellation fault), same
|
||||
/// ordinal-ignore-case ordering, so the page renders identically from either source.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private async Task<IReadOnlyList<AlarmStateChanged>> FanOutSnapshotsAsync(int siteId, CancellationToken ct)
|
||||
{
|
||||
@@ -343,7 +360,10 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
|
||||
var siteRepo = scope.ServiceProvider.GetRequiredService<ISiteRepository>();
|
||||
var site = await siteRepo.GetSiteByIdAsync(siteId, ct);
|
||||
if (site is null)
|
||||
{
|
||||
PublishNotReporting(siteId, EmptyNames);
|
||||
return Empty;
|
||||
}
|
||||
siteIdentifier = site.SiteIdentifier;
|
||||
|
||||
var instanceRepo = scope.ServiceProvider.GetRequiredService<ITemplateEngineRepository>();
|
||||
@@ -355,25 +375,48 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
|
||||
}
|
||||
|
||||
if (enabledInstanceNames.Count == 0)
|
||||
{
|
||||
PublishNotReporting(siteId, EmptyNames);
|
||||
return Empty;
|
||||
}
|
||||
|
||||
var rows = new ConcurrentBag<AlarmStateChanged>();
|
||||
var notReporting = new ConcurrentBag<string>();
|
||||
using var gate = new SemaphoreSlim(
|
||||
Math.Max(1, _options.LiveAlarmCacheSeedConcurrency),
|
||||
Math.Max(1, _options.LiveAlarmCacheSeedConcurrency));
|
||||
|
||||
var fetches = enabledInstanceNames.Select(name =>
|
||||
FetchInstanceSnapshotAsync(siteIdentifier, name, gate, rows, ct));
|
||||
FetchInstanceSnapshotAsync(siteIdentifier, name, gate, rows, notReporting, ct));
|
||||
await Task.WhenAll(fetches);
|
||||
|
||||
PublishNotReporting(
|
||||
siteId,
|
||||
notReporting.OrderBy(n => n, StringComparer.OrdinalIgnoreCase).ToList());
|
||||
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores the fan-out's not-reporting set on the site entry. Silently no-ops when the entry
|
||||
/// has been torn down mid-fan-out (last viewer left), matching every other late-write path
|
||||
/// in this service.
|
||||
/// </summary>
|
||||
private void PublishNotReporting(int siteId, IReadOnlyList<string> notReporting)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_sites.TryGetValue(siteId, out var entry))
|
||||
entry.NotReporting = notReporting;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task FetchInstanceSnapshotAsync(
|
||||
string siteIdentifier,
|
||||
string instanceUniqueName,
|
||||
SemaphoreSlim gate,
|
||||
ConcurrentBag<AlarmStateChanged> rows,
|
||||
ConcurrentBag<string> notReporting,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await gate.WaitAsync(ct);
|
||||
@@ -387,7 +430,10 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
|
||||
instanceUniqueName, Guid.NewGuid().ToString("N"), AlarmsOnly: true);
|
||||
var snapshot = await _communicationService.RequestDebugSnapshotAsync(siteIdentifier, request, ct);
|
||||
if (snapshot.InstanceNotFound)
|
||||
{
|
||||
notReporting.Add(instanceUniqueName);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var alarm in snapshot.AlarmStates)
|
||||
rows.Add(alarm);
|
||||
@@ -398,7 +444,9 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Degrade this one instance to "no rows" rather than failing the whole seed.
|
||||
// Degrade this one instance to "no rows, not reporting" rather than failing the
|
||||
// whole seed. Same rule as AlarmSummaryService.FetchInstanceAsync.
|
||||
notReporting.Add(instanceUniqueName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -474,6 +522,9 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
|
||||
entry.HasPublished = false;
|
||||
entry.StreamLive = false;
|
||||
entry.Current = Empty;
|
||||
// A dead aggregator's last fan-out result is stale too — clear it so the page's
|
||||
// poll fallback (now authoritative again) owns the not-reporting list outright.
|
||||
entry.NotReporting = EmptyNames;
|
||||
|
||||
if (entry.Subscribers.Count > 0 && !entry.Starting)
|
||||
{
|
||||
@@ -509,6 +560,13 @@ public sealed class SiteAlarmLiveCacheService : ISiteAlarmLiveCache
|
||||
/// <summary>True once the aggregator has seeded and published at least once.</summary>
|
||||
public bool HasPublished { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enabled instances that failed to answer the most recent snapshot fan-out, ordered by
|
||||
/// name. Refreshed on every seed/reconcile (see <c>FanOutSnapshotsAsync</c>) and cleared
|
||||
/// when liveness resets, so a dead aggregator never leaves a stale list behind.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> NotReporting { get; set; } = EmptyNames;
|
||||
|
||||
/// <summary>
|
||||
/// Liveness of the aggregator's site-wide gRPC stream as of the last publish. Both
|
||||
/// this and <see cref="HasPublished"/> must hold for <c>IsLive</c>: a published cache
|
||||
|
||||
Reference in New Issue
Block a user