using ZB.MOM.WW.Configuration; namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring; /// /// Validates at /// startup. The interval values are fed straight into new PeriodicTimer(...) /// (and into a division for the offline-check cadence); a zero or negative value /// makes 's constructor throw /// , crashing the /// / / /// hosted service with an opaque exception /// that does not name the offending config key. Registered with /// ValidateOnStart() so a bad ScadaBridge:HealthMonitoring section /// fails fast at boot with a clear, key-naming message. /// public sealed class HealthMonitoringOptionsValidator : OptionsValidatorBase { /// protected override void Validate(ValidationBuilder builder, HealthMonitoringOptions options) { builder.RequireThat(options.ReportInterval > TimeSpan.Zero, $"ScadaBridge:HealthMonitoring:ReportInterval must be a positive duration " + $"(was {options.ReportInterval}); it is used directly as a PeriodicTimer period."); builder.RequireThat(options.OfflineTimeout > TimeSpan.Zero, $"ScadaBridge:HealthMonitoring:OfflineTimeout must be a positive duration " + $"(was {options.OfflineTimeout}); it drives the offline-check PeriodicTimer cadence."); builder.RequireThat(options.CentralOfflineTimeout > TimeSpan.Zero, $"ScadaBridge:HealthMonitoring:CentralOfflineTimeout must be a positive duration " + $"(was {options.CentralOfflineTimeout})."); // Valid when CentralOfflineTimeout >= OfflineTimeout (both already // required to be positive above). The De Morgan'd guard !(both positive // AND Central < Offline) is true unless BOTH timeouts are positive and // Central is strictly smaller — so it stays silent when either field is // non-positive, leaving that failure to the dedicated positive-duration // checks above rather than double-firing here. builder.RequireThat( !(options.OfflineTimeout > TimeSpan.Zero && options.CentralOfflineTimeout > TimeSpan.Zero && options.CentralOfflineTimeout < options.OfflineTimeout), $"ScadaBridge: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."); builder.RequireThat(options.MetricsStaleTimeout > TimeSpan.Zero, $"ScadaBridge:HealthMonitoring:MetricsStaleTimeout must be a positive duration " + $"(was {options.MetricsStaleTimeout}); it is the window an online site may go without " + "a full report before being flagged metrics-stale."); // A stale window shorter than one report interval flags every healthy // site (reports simply can't arrive faster than they are emitted). Same // De Morgan guard shape as the Central/Offline pair above: stay silent // when either field is non-positive so the dedicated positive-duration // checks own that failure rather than double-firing here. builder.RequireThat( !(options.ReportInterval > TimeSpan.Zero && options.MetricsStaleTimeout > TimeSpan.Zero && options.MetricsStaleTimeout < options.ReportInterval), $"ScadaBridge:HealthMonitoring:MetricsStaleTimeout ({options.MetricsStaleTimeout}) " + $"must be >= ReportInterval ({options.ReportInterval}): a stale window shorter than one " + "report interval would flag every healthy site, since reports cannot arrive faster than " + "they are emitted."); builder.RequireThat(options.LocalDbBacklogThreshold > 0, $"ScadaBridge:HealthMonitoring:LocalDbBacklogThreshold must be positive " + $"(was {options.LocalDbBacklogThreshold}); a zero/negative threshold would badge every " + "replicated site permanently, since an idle backlog of 0 already sits at or above it."); builder.RequireThat(options.LocalDbBacklogSustainedFor > TimeSpan.Zero, $"ScadaBridge:HealthMonitoring:LocalDbBacklogSustainedFor must be a positive duration " + $"(was {options.LocalDbBacklogSustainedFor}); it is the dwell time a backlog must stay " + "above the threshold before the site is badged."); // A dwell shorter than one report interval can be satisfied by a single sample, // degrading the sustained check back into the bare threshold it exists to replace — // which would fire on every daily purge burst. Same De Morgan guard shape as the // pairs above: silent unless BOTH fields are positive and the dwell is too short, // so the dedicated positive-value checks own that failure. builder.RequireThat( !(options.ReportInterval > TimeSpan.Zero && options.LocalDbBacklogSustainedFor > TimeSpan.Zero && options.LocalDbBacklogSustainedFor < options.ReportInterval), $"ScadaBridge:HealthMonitoring:LocalDbBacklogSustainedFor ({options.LocalDbBacklogSustainedFor}) " + $"must be >= ReportInterval ({options.ReportInterval}): a dwell shorter than one report " + "interval can be satisfied by a single sample, which turns the sustained check back into " + "a bare threshold and re-introduces the daily purge-burst false positive it exists to avoid."); } }