fix(health-monitoring): resolve HealthMonitoring-013,014,016 — shorter-timeout cadence, options validation, injected TimeProvider; HealthMonitoring-015 left open (cross-module design decision)

This commit is contained in:
Joseph Doherty
2026-05-17 03:18:24 -04:00
parent da8c9f171b
commit eae4077414
8 changed files with 296 additions and 12 deletions

View File

@@ -0,0 +1,59 @@
using Microsoft.Extensions.Options;
namespace ScadaLink.HealthMonitoring;
/// <summary>
/// HealthMonitoring-014: validates <see cref="HealthMonitoringOptions"/> at
/// startup. The interval values are fed straight into <c>new PeriodicTimer(...)</c>
/// (and into a division for the offline-check cadence); a zero or negative value
/// makes <see cref="PeriodicTimer"/>'s constructor throw
/// <see cref="ArgumentOutOfRangeException"/>, crashing the
/// <see cref="HealthReportSender"/> / <see cref="CentralHealthReportLoop"/> /
/// <see cref="CentralHealthAggregator"/> hosted service with an opaque exception
/// that does not name the offending config key. Registered with
/// <c>ValidateOnStart()</c> so a bad <c>ScadaLink:HealthMonitoring</c> section
/// fails fast at boot with a clear, key-naming message.
/// </summary>
public sealed class HealthMonitoringOptionsValidator : IValidateOptions<HealthMonitoringOptions>
{
public ValidateOptionsResult Validate(string? name, HealthMonitoringOptions options)
{
var failures = new List<string>();
if (options.ReportInterval <= TimeSpan.Zero)
{
failures.Add(
$"ScadaLink:HealthMonitoring:ReportInterval must be a positive duration " +
$"(was {options.ReportInterval}); it is used directly as a PeriodicTimer period.");
}
if (options.OfflineTimeout <= TimeSpan.Zero)
{
failures.Add(
$"ScadaLink:HealthMonitoring:OfflineTimeout must be a positive duration " +
$"(was {options.OfflineTimeout}); it drives the offline-check PeriodicTimer cadence.");
}
if (options.CentralOfflineTimeout <= TimeSpan.Zero)
{
failures.Add(
$"ScadaLink:HealthMonitoring:CentralOfflineTimeout must be a positive duration " +
$"(was {options.CentralOfflineTimeout}).");
}
if (options.OfflineTimeout > TimeSpan.Zero
&& options.CentralOfflineTimeout > TimeSpan.Zero
&& options.CentralOfflineTimeout < options.OfflineTimeout)
{
failures.Add(
$"ScadaLink:HealthMonitoring:CentralOfflineTimeout ({options.CentralOfflineTimeout}) " +
$"must be >= OfflineTimeout ({options.OfflineTimeout}): the synthetic 'central' site has " +
"no heartbeat source and is fed only by the slower self-report loop, so it needs at " +
"least as much offline grace as a real site.");
}
return failures.Count > 0
? ValidateOptionsResult.Fail(failures)
: ValidateOptionsResult.Success;
}
}