feat(auditlog): make SiteAuditBacklogReporter cadence configurable (deferred #21)
Consolidates the reporter's poll cadence onto SqliteAuditWriterOptions instead of the hard-coded 30 s constant, closing deferred-work register item #21 (the config- shape cleanup its own XML doc flagged as a follow-up). - SqliteAuditWriterOptions.BacklogPollIntervalSeconds (default 30). - Reporter reads it via IOptions; precedence explicit-override (tests) > configured > 30 s default; a non-positive value falls back to the default. Corrected the stale "hard-code / tunable in a follow-up" class-doc. - Cadence tests + register updated (row 21 -> Resolved). Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -27,7 +27,6 @@ them and are removed from this table when that plan's task lands.
|
|||||||
| 17 | Unified notifications+site-calls outbox page | stillpending :118 | Explicit M9 decision to keep two pages | Operator confusion reports |
|
| 17 | Unified notifications+site-calls outbox page | stillpending :118 | Explicit M9 decision to keep two pages | Operator confusion reports |
|
||||||
| 18 | Folder drag-drop | same, [PERM] | Permanently closed; menu reorder shipped | — (closed) |
|
| 18 | Folder drag-drop | same, [PERM] | Permanently closed; menu reorder shipped | — (closed) |
|
||||||
| 19 | Bundle signing / cluster-to-cluster pull / differential bundles | transport-design :402 | v1 manifest hash + AES-GCM held sufficient | Non-repudiation requirement across orgs |
|
| 19 | Bundle signing / cluster-to-cluster pull / differential bundles | transport-design :402 | v1 manifest hash + AES-GCM held sufficient | Non-repudiation requirement across orgs |
|
||||||
| 21 | SiteAuditBacklogReporter threshold consolidation | SiteAuditBacklogReporter.cs:28 | Config-shape cleanup only | Next SqliteAuditWriterOptions change |
|
|
||||||
| 22 | KPI history hourly rollups | Component-KpiHistory.md | YAGNI; 90-day retention bounds table | KpiSample query latency on dashboards |
|
| 22 | KPI history hourly rollups | Component-KpiHistory.md | YAGNI; 90-day retention bounds table | KpiSample query latency on dashboards |
|
||||||
|
|
||||||
## Resolved (verified against the code 2026-07-10)
|
## Resolved (verified against the code 2026-07-10)
|
||||||
@@ -41,6 +40,7 @@ Rows removed from the Deferred table above once confirmed shipped. Kept here for
|
|||||||
| 15 | BrowseNext final-page signal not surfaced | Already surfaced (M7 browse work): `RealOpcUaClient` sets `Truncated=false`/`ContinuationToken=null` on the last page; `BrowseNodeResult` carries both; `TreeRow.razor` renders "Load more" only when a continuation token remains — no wasted BrowseNext. |
|
| 15 | BrowseNext final-page signal not surfaced | Already surfaced (M7 browse work): `RealOpcUaClient` sets `Truncated=false`/`ContinuationToken=null` on the last page; `BrowseNodeResult` carries both; `TreeRow.razor` renders "Load more" only when a continuation token remains — no wasted BrowseNext. |
|
||||||
| 16 | StubOpcUaClient throws on browse | Already resolved: `StubOpcUaClient` supports browse + address-space search, covered by `StubOpcUaClientBrowseTests`/`StubOpcUaClientSearchTests`. |
|
| 16 | StubOpcUaClient throws on browse | Already resolved: `StubOpcUaClient` supports browse + address-space search, covered by `StubOpcUaClientBrowseTests`/`StubOpcUaClientSearchTests`. |
|
||||||
| 20 | Deployment EXPIRED-row purge | Already resolved (PLAN-04): `PendingDeploymentPurgeActor` central singleton (spawned in `AkkaHostedService`) ticks `IDeploymentManagerRepository.PurgeExpiredPendingDeploymentsAsync` every `CommunicationOptions.PendingDeploymentPurgeInterval` (default 1h), options-validated, tested. |
|
| 20 | Deployment EXPIRED-row purge | Already resolved (PLAN-04): `PendingDeploymentPurgeActor` central singleton (spawned in `AkkaHostedService`) ticks `IDeploymentManagerRepository.PurgeExpiredPendingDeploymentsAsync` every `CommunicationOptions.PendingDeploymentPurgeInterval` (default 1h), options-validated, tested. |
|
||||||
|
| 21 | SiteAuditBacklogReporter threshold consolidation | Shipped 2026-07-10: `SqliteAuditWriterOptions.BacklogPollIntervalSeconds` (default 30) now drives the reporter's poll cadence; explicit ctor override still wins (tests), non-positive falls back to the 30 s default. Cadence tests added; stale "hard-code / follow-up" class-doc corrected. |
|
||||||
|
|
||||||
## New deferrals from review 08 (this plan)
|
## New deferrals from review 08 (this plan)
|
||||||
| Item | Rationale | Revisit trigger |
|
| Item | Rationale | Revisit trigger |
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||||
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||||
|
|
||||||
@@ -25,9 +26,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Site;
|
|||||||
/// <b>Cadence.</b> 30 s by default — coarse enough to amortise the SQL probe
|
/// <b>Cadence.</b> 30 s by default — coarse enough to amortise the SQL probe
|
||||||
/// across many reports, fine enough that the central dashboard never lags by
|
/// across many reports, fine enough that the central dashboard never lags by
|
||||||
/// more than one health-report interval. Tunable via
|
/// more than one health-report interval. Tunable via
|
||||||
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Site.SqliteAuditWriterOptions"/> in a follow-up
|
/// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Site.SqliteAuditWriterOptions.BacklogPollIntervalSeconds"/>
|
||||||
/// if ops needs a different cadence; for now we hard-code the value because the
|
/// (a non-positive value falls back to the 30 s default).
|
||||||
/// brief calls it out explicitly.
|
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <b>Failure containment.</b> The probe call is wrapped in a try/catch so a
|
/// <b>Failure containment.</b> The probe call is wrapped in a try/catch so a
|
||||||
@@ -56,17 +56,42 @@ public sealed class SiteAuditBacklogReporter : IHostedService, IDisposable
|
|||||||
/// <param name="queue">The site audit queue used to probe the backlog count.</param>
|
/// <param name="queue">The site audit queue used to probe the backlog count.</param>
|
||||||
/// <param name="collector">The site health collector that receives the backlog snapshot.</param>
|
/// <param name="collector">The site health collector that receives the backlog snapshot.</param>
|
||||||
/// <param name="logger">Logger instance.</param>
|
/// <param name="logger">Logger instance.</param>
|
||||||
/// <param name="refreshInterval">Poll interval override; defaults to <see cref="DefaultRefreshInterval"/> (30 s).</param>
|
/// <param name="refreshInterval">
|
||||||
|
/// Explicit poll-interval override (used by tests). When null, the cadence comes from
|
||||||
|
/// <see cref="SqliteAuditWriterOptions.BacklogPollIntervalSeconds"/>, falling back to
|
||||||
|
/// <see cref="DefaultRefreshInterval"/> (30 s) when that is unset or non-positive.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="options">
|
||||||
|
/// SQLite audit-writer options supplying the configurable poll cadence
|
||||||
|
/// (<see cref="SqliteAuditWriterOptions.BacklogPollIntervalSeconds"/>). Optional so
|
||||||
|
/// direct construction in tests need not build the options graph.
|
||||||
|
/// </param>
|
||||||
public SiteAuditBacklogReporter(
|
public SiteAuditBacklogReporter(
|
||||||
ISiteAuditQueue queue,
|
ISiteAuditQueue queue,
|
||||||
ISiteHealthCollector collector,
|
ISiteHealthCollector collector,
|
||||||
ILogger<SiteAuditBacklogReporter> logger,
|
ILogger<SiteAuditBacklogReporter> logger,
|
||||||
TimeSpan? refreshInterval = null)
|
TimeSpan? refreshInterval = null,
|
||||||
|
IOptions<SqliteAuditWriterOptions>? options = null)
|
||||||
{
|
{
|
||||||
_queue = queue ?? throw new ArgumentNullException(nameof(queue));
|
_queue = queue ?? throw new ArgumentNullException(nameof(queue));
|
||||||
_collector = collector ?? throw new ArgumentNullException(nameof(collector));
|
_collector = collector ?? throw new ArgumentNullException(nameof(collector));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
_refreshInterval = refreshInterval ?? DefaultRefreshInterval;
|
// Precedence: explicit override (tests) → configured cadence → 30 s default.
|
||||||
|
_refreshInterval = refreshInterval ?? ResolveInterval(options) ?? DefaultRefreshInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The resolved poll cadence (exposed for tests).</summary>
|
||||||
|
internal TimeSpan RefreshInterval => _refreshInterval;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps <see cref="SqliteAuditWriterOptions.BacklogPollIntervalSeconds"/> to a
|
||||||
|
/// <see cref="TimeSpan"/>, or null when unconfigured / non-positive (caller then
|
||||||
|
/// falls back to <see cref="DefaultRefreshInterval"/>).
|
||||||
|
/// </summary>
|
||||||
|
private static TimeSpan? ResolveInterval(IOptions<SqliteAuditWriterOptions>? options)
|
||||||
|
{
|
||||||
|
var seconds = options?.Value.BacklogPollIntervalSeconds ?? 0;
|
||||||
|
return seconds > 0 ? TimeSpan.FromSeconds(seconds) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Starts the background polling loop, running an immediate first probe before entering the timed cycle.</summary>
|
/// <summary>Starts the background polling loop, running an immediate first probe before entering the timed cycle.</summary>
|
||||||
|
|||||||
@@ -24,4 +24,13 @@ public sealed class SqliteAuditWriterOptions
|
|||||||
|
|
||||||
/// <summary>Soft flush interval the writer enforces when fewer than BatchSize events are queued.</summary>
|
/// <summary>Soft flush interval the writer enforces when fewer than BatchSize events are queued.</summary>
|
||||||
public int FlushIntervalMs { get; set; } = 50;
|
public int FlushIntervalMs { get; set; } = 50;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Poll cadence, in seconds, for the <c>SiteAuditBacklogReporter</c> hosted service
|
||||||
|
/// that probes the SQLite audit backlog and pushes the snapshot onto the site health
|
||||||
|
/// report. Half a typical 60 s health-report interval keeps the snapshot fresh without
|
||||||
|
/// spinning the SQL probe more often than necessary. A non-positive value falls back to
|
||||||
|
/// the reporter's 30 s default.
|
||||||
|
/// </summary>
|
||||||
|
public int BacklogPollIntervalSeconds { get; set; } = 30;
|
||||||
}
|
}
|
||||||
|
|||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NSubstitute;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||||
|
using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deferred-work #21: the backlog reporter's poll cadence is configurable via
|
||||||
|
/// <see cref="SqliteAuditWriterOptions.BacklogPollIntervalSeconds"/> instead of the
|
||||||
|
/// old hard-coded 30 s constant.
|
||||||
|
/// </summary>
|
||||||
|
public class SiteAuditBacklogReporterCadenceTests
|
||||||
|
{
|
||||||
|
private static SiteAuditBacklogReporter Create(
|
||||||
|
IOptions<SqliteAuditWriterOptions>? options, TimeSpan? explicitInterval = null) =>
|
||||||
|
new(
|
||||||
|
Substitute.For<ISiteAuditQueue>(),
|
||||||
|
Substitute.For<ISiteHealthCollector>(),
|
||||||
|
NullLogger<SiteAuditBacklogReporter>.Instance,
|
||||||
|
explicitInterval,
|
||||||
|
options);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Cadence_ComesFromOptions_WhenConfigured()
|
||||||
|
{
|
||||||
|
var options = Options.Create(new SqliteAuditWriterOptions { BacklogPollIntervalSeconds = 12 });
|
||||||
|
|
||||||
|
var reporter = Create(options);
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(12), reporter.RefreshInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Cadence_FallsBackToDefault_WhenOptionsNonPositive()
|
||||||
|
{
|
||||||
|
var options = Options.Create(new SqliteAuditWriterOptions { BacklogPollIntervalSeconds = 0 });
|
||||||
|
|
||||||
|
var reporter = Create(options);
|
||||||
|
|
||||||
|
Assert.Equal(SiteAuditBacklogReporter.DefaultRefreshInterval, reporter.RefreshInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Cadence_FallsBackToDefault_WhenNoOptions()
|
||||||
|
{
|
||||||
|
var reporter = Create(options: null);
|
||||||
|
|
||||||
|
Assert.Equal(SiteAuditBacklogReporter.DefaultRefreshInterval, reporter.RefreshInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExplicitInterval_WinsOverOptions()
|
||||||
|
{
|
||||||
|
var options = Options.Create(new SqliteAuditWriterOptions { BacklogPollIntervalSeconds = 12 });
|
||||||
|
|
||||||
|
var reporter = Create(options, explicitInterval: TimeSpan.FromSeconds(3));
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(3), reporter.RefreshInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user