fix(options): eager startup validation for AuditLog central sub-options — PartitionMaintenance, Purge, Reconciliation (plan R2-08 T6, arch-review 08r2 NF4)

This commit is contained in:
Joseph Doherty
2026-07-13 10:39:03 -04:00
parent e0dddac0be
commit f85f036b09
7 changed files with 241 additions and 6 deletions
@@ -0,0 +1,25 @@
using ZB.MOM.WW.Configuration;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Validates <see cref="AuditLogPartitionMaintenanceOptions"/> at host startup
/// (arch-review 08 round 2 NF4). A zero tick interval spins the maintenance
/// hosted service; a zero look-ahead leaves <c>pf_AuditLog_Month</c> without a
/// future monthly boundary, risking inserts into the unbounded tail partition.
/// </summary>
public sealed class AuditLogPartitionMaintenanceOptionsValidator
: OptionsValidatorBase<AuditLogPartitionMaintenanceOptions>
{
/// <inheritdoc />
protected override void Validate(ValidationBuilder builder, AuditLogPartitionMaintenanceOptions options)
{
builder.RequireThat(options.IntervalSeconds > 0,
$"AuditLog:PartitionMaintenance:{nameof(AuditLogPartitionMaintenanceOptions.IntervalSeconds)} " +
$"({options.IntervalSeconds}) must be > 0.");
builder.RequireThat(options.LookaheadMonths > 0,
$"AuditLog:PartitionMaintenance:{nameof(AuditLogPartitionMaintenanceOptions.LookaheadMonths)} " +
$"({options.LookaheadMonths}) must be > 0.");
}
}
@@ -0,0 +1,31 @@
using ZB.MOM.WW.Configuration;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Validates <see cref="AuditLogPurgeOptions"/> at host startup (arch-review 08
/// round 2 NF4). The class self-clamps at resolve time, but a plainly-wrong
/// config (zero cadence, zero batch, zero maintenance-command timeout) should
/// still fail the boot so the operator learns immediately.
/// <see cref="AuditLogPurgeOptions.IntervalOverride"/> is test-only (per the
/// class remarks) and is deliberately not validated.
/// </summary>
public sealed class AuditLogPurgeOptionsValidator : OptionsValidatorBase<AuditLogPurgeOptions>
{
/// <inheritdoc />
protected override void Validate(ValidationBuilder builder, AuditLogPurgeOptions options)
{
builder.RequireThat(options.IntervalHours > 0,
$"AuditLog:Purge:{nameof(AuditLogPurgeOptions.IntervalHours)} " +
$"({options.IntervalHours}) must be > 0.");
// Backing field for the documented "ChannelPurgeBatchSize" config key.
builder.RequireThat(options.ChannelPurgeBatchSizeConfigured > 0,
$"AuditLog:Purge:{nameof(AuditLogPurgeOptions.ChannelPurgeBatchSizeConfigured)} " +
$"(config key ChannelPurgeBatchSize = {options.ChannelPurgeBatchSizeConfigured}) must be > 0.");
builder.RequireThat(options.MaintenanceCommandTimeoutMinutes > 0,
$"AuditLog:Purge:{nameof(AuditLogPurgeOptions.MaintenanceCommandTimeoutMinutes)} " +
$"({options.MaintenanceCommandTimeoutMinutes}) must be > 0.");
}
}
@@ -0,0 +1,31 @@
using ZB.MOM.WW.Configuration;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Central;
/// <summary>
/// Validates <see cref="SiteAuditReconciliationOptions"/> at host startup
/// (arch-review 08 round 2 NF4). A zero tick interval spins the reconciliation
/// singleton, a zero batch pulls nothing per RPC, and a zero stalled-cycle
/// threshold would fire the stalled signal on the first non-draining cycle.
/// <see cref="SiteAuditReconciliationOptions.ReconciliationIntervalOverride"/>
/// is test-only (per the class remarks) and is deliberately not validated.
/// </summary>
public sealed class SiteAuditReconciliationOptionsValidator
: OptionsValidatorBase<SiteAuditReconciliationOptions>
{
/// <inheritdoc />
protected override void Validate(ValidationBuilder builder, SiteAuditReconciliationOptions options)
{
builder.RequireThat(options.ReconciliationIntervalSeconds > 0,
$"AuditLog:Reconciliation:{nameof(SiteAuditReconciliationOptions.ReconciliationIntervalSeconds)} " +
$"({options.ReconciliationIntervalSeconds}) must be > 0.");
builder.RequireThat(options.BatchSize > 0,
$"AuditLog:Reconciliation:{nameof(SiteAuditReconciliationOptions.BatchSize)} " +
$"({options.BatchSize}) must be > 0.");
builder.RequireThat(options.StalledAfterNonDrainingCycles > 0,
$"AuditLog:Reconciliation:{nameof(SiteAuditReconciliationOptions.StalledAfterNonDrainingCycles)} " +
$"({options.StalledAfterNonDrainingCycles}) must be > 0.");
}
}
@@ -368,8 +368,8 @@ public static class ServiceCollectionExtensions
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(config);
services.AddOptions<AuditLogPartitionMaintenanceOptions>()
.Bind(config.GetSection(PartitionMaintenanceSectionName));
services.AddValidatedOptions<AuditLogPartitionMaintenanceOptions,
AuditLogPartitionMaintenanceOptionsValidator>(config, PartitionMaintenanceSectionName);
services.AddHostedService<AuditLogPartitionMaintenanceService>();
// I1 (review): bind the two central-singleton options HERE rather than in
@@ -385,10 +385,10 @@ public static class ServiceCollectionExtensions
// 5 min reconciliation tick); production exposes IntervalHours /
// ReconciliationIntervalSeconds only — the test-only *Override knobs are
// not intended to be set from config (see the options classes' remarks).
services.AddOptions<AuditLogPurgeOptions>()
.Bind(config.GetSection(PurgeSectionName));
services.AddOptions<SiteAuditReconciliationOptions>()
.Bind(config.GetSection(ReconciliationSectionName));
services.AddValidatedOptions<AuditLogPurgeOptions, AuditLogPurgeOptionsValidator>(
config, PurgeSectionName);
services.AddValidatedOptions<SiteAuditReconciliationOptions, SiteAuditReconciliationOptionsValidator>(
config, ReconciliationSectionName);
// Central health snapshot — a single object
// that owns the CentralAuditWriteFailures + AuditRedactionFailure