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:
@@ -331,8 +331,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// _notReporting is the poll's unique authority — the alarm-only live cache
|
||||
// cannot compute it — so it is always refreshed.
|
||||
// _notReporting always comes from this call, live or not. While the cache is
|
||||
// serving the site the façade sources it from the aggregator's own fan-out (no
|
||||
// second fan-out is run); while it is cold the poll computes it. Either way the
|
||||
// shape and ordering are identical.
|
||||
_notReporting = result.NotReportingInstances;
|
||||
|
||||
// While the cache is live, the live deltas own the row set: a poll whose fan-out
|
||||
|
||||
@@ -154,9 +154,11 @@ public sealed class AlarmSummaryService : IAlarmSummaryService
|
||||
.ThenBy(r => r.Alarm.AlarmName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
// The live cache is alarm-only, so it cannot enumerate "not reporting"
|
||||
// instances — left empty here; the periodic poll (GetSiteAlarmsAsync)
|
||||
// remains the authority for that list.
|
||||
// Alarm rows alone cannot name the instances that failed to answer, so this pure
|
||||
// flattening leaves the list empty. The names come from the fan-out that produced the
|
||||
// snapshot: while the live cache is serving a site, SharedAlarmSummaryService pairs
|
||||
// these rows with ISiteAlarmLiveCache.GetNotReportingInstances (the aggregator's own
|
||||
// seed/reconcile result); while it is cold, GetSiteAlarmsAsync's poll supplies both.
|
||||
return new AlarmSummaryResult(orderedRows, Array.Empty<string>());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming;
|
||||
using ZB.MOM.WW.ScadaBridge.Communication;
|
||||
|
||||
@@ -18,12 +17,25 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services;
|
||||
/// viewers are watching.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>The freshness window follows the live cache.</b> While
|
||||
/// <see cref="ISiteAlarmLiveCache.IsLive"/> is true the page deliberately ignores the poll's
|
||||
/// alarm rows (the live deltas own them — arch-review R2 N5), so the only thing the poll
|
||||
/// still supplies is the not-reporting list, and the window widens to the aggregator's own
|
||||
/// reconcile interval. While the cache is cold the poll is the page's full-rebuild safety
|
||||
/// net, so the window stays just under the page's 15s tick and every tick gets fresh data.
|
||||
/// <b>While the live cache is serving the site there is NO fan-out at all.</b> The page already
|
||||
/// ignores the poll's alarm rows in that state (the live deltas own them — arch-review R2 N5),
|
||||
/// and the aggregator's own seed/reconcile fan-out now publishes the not-reporting set through
|
||||
/// <see cref="ISiteAlarmLiveCache.GetNotReportingInstances"/> — the one thing the poll used to
|
||||
/// still supply. So the live path is answered entirely from the cache: rows flattened from
|
||||
/// <see cref="ISiteAlarmLiveCache.GetCurrentAlarms"/> through the same
|
||||
/// <c>AlarmSummaryService.BuildFromLiveAlarmsCore</c> the page's live subscription uses, plus the
|
||||
/// cached not-reporting names. One fan-out per site per reconcile window, run by the aggregator,
|
||||
/// instead of two.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The rows are returned rather than left empty on purpose: liveness can flip to false in the
|
||||
/// window between this call and the page's own <c>IsLive</c> re-check, and the page would then
|
||||
/// adopt whatever came back. Handing it the live snapshot makes that race land on last-known
|
||||
/// state instead of a blank grid.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// While the cache is cold the poll is the page's full-rebuild safety net, so it still fans out,
|
||||
/// memoized with a window just under the page's 15s tick so every tick gets fresh data.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class SharedAlarmSummaryService : IAlarmSummaryService
|
||||
@@ -36,7 +48,6 @@ public sealed class SharedAlarmSummaryService : IAlarmSummaryService
|
||||
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ISiteAlarmLiveCache _liveCache;
|
||||
private readonly TimeSpan _liveCacheTtl;
|
||||
private readonly Func<DateTimeOffset>? _clock;
|
||||
|
||||
private readonly ConcurrentDictionary<int, SingleFlightMemo<AlarmSummaryResult>> _bySite = new();
|
||||
@@ -44,36 +55,34 @@ public sealed class SharedAlarmSummaryService : IAlarmSummaryService
|
||||
/// <summary>
|
||||
/// Initializes the shared alarm summary façade.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// There is no live-cache freshness window any more: the live path reads the aggregator's
|
||||
/// cache directly and never fans out, so the only window left is <see cref="ColdCacheTtl"/>.
|
||||
/// The reconcile interval this ctor used to read from <c>CommunicationOptions</c> is now
|
||||
/// implicit — it IS the cadence at which the aggregator refreshes what the live path returns.
|
||||
/// </remarks>
|
||||
/// <param name="scopeFactory">Opens a fresh DI scope per fan-out (fresh repositories, off any circuit scope).</param>
|
||||
/// <param name="liveCache">The shared live alarm cache, consulted only for its per-site liveness.</param>
|
||||
/// <param name="options">Communication options; supplies the aggregator reconcile interval.</param>
|
||||
/// <param name="liveCache">The shared live alarm cache: liveness, alarm rows, and not-reporting names.</param>
|
||||
public SharedAlarmSummaryService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ISiteAlarmLiveCache liveCache,
|
||||
IOptions<CommunicationOptions> options)
|
||||
: this(scopeFactory, liveCache, (options ?? throw new ArgumentNullException(nameof(options)))
|
||||
.Value.LiveAlarmCacheReconcileInterval, clock: null)
|
||||
ISiteAlarmLiveCache liveCache)
|
||||
: this(scopeFactory, liveCache, clock: null)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test seam: same façade with an explicit live-cache window and clock.
|
||||
/// Test seam: same façade with an explicit clock.
|
||||
/// </summary>
|
||||
/// <param name="scopeFactory">Opens a fresh DI scope per fan-out.</param>
|
||||
/// <param name="liveCache">The shared live alarm cache.</param>
|
||||
/// <param name="liveCacheTtl">Freshness window used while the live cache is serving the site.</param>
|
||||
/// <param name="clock">Clock used for freshness.</param>
|
||||
internal SharedAlarmSummaryService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ISiteAlarmLiveCache liveCache,
|
||||
TimeSpan liveCacheTtl,
|
||||
Func<DateTimeOffset>? clock)
|
||||
{
|
||||
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
|
||||
_liveCache = liveCache ?? throw new ArgumentNullException(nameof(liveCache));
|
||||
// Never shorter than the cold window — a misconfigured reconcile interval must not
|
||||
// silently make the memo useless.
|
||||
_liveCacheTtl = liveCacheTtl > ColdCacheTtl ? liveCacheTtl : ColdCacheTtl;
|
||||
_clock = clock;
|
||||
}
|
||||
|
||||
@@ -81,17 +90,25 @@ public sealed class SharedAlarmSummaryService : IAlarmSummaryService
|
||||
public Task<AlarmSummaryResult> GetSiteAlarmsAsync(
|
||||
int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Live: answer from the shared aggregator's cache and skip the fan-out entirely. Not
|
||||
// memoized — there is nothing to amortize, and reading the cache directly keeps the
|
||||
// answer as fresh as the aggregator itself.
|
||||
if (_liveCache.IsLive(siteId))
|
||||
{
|
||||
var live = AlarmSummaryService.BuildFromLiveAlarmsCore(_liveCache.GetCurrentAlarms(siteId));
|
||||
return Task.FromResult(new AlarmSummaryResult(
|
||||
live.Alarms, _liveCache.GetNotReportingInstances(siteId)));
|
||||
}
|
||||
|
||||
var memo = _bySite.GetOrAdd(
|
||||
siteId,
|
||||
_ => new SingleFlightMemo<AlarmSummaryResult>(ColdCacheTtl, _clock));
|
||||
|
||||
var ttl = _liveCache.IsLive(siteId) ? _liveCacheTtl : ColdCacheTtl;
|
||||
|
||||
return memo.GetAsync(
|
||||
() => FanOutAsync(siteId),
|
||||
forceRefresh: false,
|
||||
cancellationToken,
|
||||
ttlOverride: ttl);
|
||||
ttlOverride: ColdCacheTtl);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
Reference in New Issue
Block a user