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:
+25
@@ -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(services);
|
||||||
ArgumentNullException.ThrowIfNull(config);
|
ArgumentNullException.ThrowIfNull(config);
|
||||||
|
|
||||||
services.AddOptions<AuditLogPartitionMaintenanceOptions>()
|
services.AddValidatedOptions<AuditLogPartitionMaintenanceOptions,
|
||||||
.Bind(config.GetSection(PartitionMaintenanceSectionName));
|
AuditLogPartitionMaintenanceOptionsValidator>(config, PartitionMaintenanceSectionName);
|
||||||
services.AddHostedService<AuditLogPartitionMaintenanceService>();
|
services.AddHostedService<AuditLogPartitionMaintenanceService>();
|
||||||
|
|
||||||
// I1 (review): bind the two central-singleton options HERE rather than in
|
// 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 /
|
// 5 min reconciliation tick); production exposes IntervalHours /
|
||||||
// ReconciliationIntervalSeconds only — the test-only *Override knobs are
|
// ReconciliationIntervalSeconds only — the test-only *Override knobs are
|
||||||
// not intended to be set from config (see the options classes' remarks).
|
// not intended to be set from config (see the options classes' remarks).
|
||||||
services.AddOptions<AuditLogPurgeOptions>()
|
services.AddValidatedOptions<AuditLogPurgeOptions, AuditLogPurgeOptionsValidator>(
|
||||||
.Bind(config.GetSection(PurgeSectionName));
|
config, PurgeSectionName);
|
||||||
services.AddOptions<SiteAuditReconciliationOptions>()
|
services.AddValidatedOptions<SiteAuditReconciliationOptions, SiteAuditReconciliationOptionsValidator>(
|
||||||
.Bind(config.GetSection(ReconciliationSectionName));
|
config, ReconciliationSectionName);
|
||||||
|
|
||||||
// Central health snapshot — a single object
|
// Central health snapshot — a single object
|
||||||
// that owns the CentralAuditWriteFailures + AuditRedactionFailure
|
// that owns the CentralAuditWriteFailures + AuditRedactionFailure
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eager startup validation for <see cref="AuditLogPartitionMaintenanceOptions"/>
|
||||||
|
/// (arch-review 08 round 2 NF4). A zero tick interval spins the maintenance
|
||||||
|
/// hosted service; a zero look-ahead leaves the partition function without a
|
||||||
|
/// future monthly boundary, risking inserts into the unbounded tail partition.
|
||||||
|
/// </summary>
|
||||||
|
public class AuditLogPartitionMaintenanceOptionsValidatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DefaultOptions_AreValid()
|
||||||
|
{
|
||||||
|
var validator = new AuditLogPartitionMaintenanceOptionsValidator();
|
||||||
|
Assert.True(validator.Validate(null, new AuditLogPartitionMaintenanceOptions()).Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroIntervalSeconds_Fails()
|
||||||
|
{
|
||||||
|
var validator = new AuditLogPartitionMaintenanceOptionsValidator();
|
||||||
|
var result = validator.Validate(null,
|
||||||
|
new AuditLogPartitionMaintenanceOptions { IntervalSeconds = 0 });
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Contains(result.Failures!,
|
||||||
|
f => f.Contains(nameof(AuditLogPartitionMaintenanceOptions.IntervalSeconds), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroLookaheadMonths_Fails()
|
||||||
|
{
|
||||||
|
var validator = new AuditLogPartitionMaintenanceOptionsValidator();
|
||||||
|
var result = validator.Validate(null,
|
||||||
|
new AuditLogPartitionMaintenanceOptions { LookaheadMonths = 0 });
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Contains(result.Failures!,
|
||||||
|
f => f.Contains(nameof(AuditLogPartitionMaintenanceOptions.LookaheadMonths), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eager startup validation for <see cref="AuditLogPurgeOptions"/>
|
||||||
|
/// (arch-review 08 round 2 NF4). The class self-clamps at resolve time, but a
|
||||||
|
/// plainly-wrong config (zero cadence, zero batch, zero command timeout) should
|
||||||
|
/// still fail the boot so the operator learns immediately.
|
||||||
|
/// <see cref="AuditLogPurgeOptions.IntervalOverride"/> is test-only and left
|
||||||
|
/// unvalidated.
|
||||||
|
/// </summary>
|
||||||
|
public class AuditLogPurgeOptionsValidatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DefaultOptions_AreValid()
|
||||||
|
{
|
||||||
|
var validator = new AuditLogPurgeOptionsValidator();
|
||||||
|
Assert.True(validator.Validate(null, new AuditLogPurgeOptions()).Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroIntervalHours_Fails()
|
||||||
|
{
|
||||||
|
var validator = new AuditLogPurgeOptionsValidator();
|
||||||
|
var result = validator.Validate(null, new AuditLogPurgeOptions { IntervalHours = 0 });
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Contains(result.Failures!,
|
||||||
|
f => f.Contains(nameof(AuditLogPurgeOptions.IntervalHours), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroChannelPurgeBatchSize_Fails()
|
||||||
|
{
|
||||||
|
var validator = new AuditLogPurgeOptionsValidator();
|
||||||
|
var result = validator.Validate(null,
|
||||||
|
new AuditLogPurgeOptions { ChannelPurgeBatchSizeConfigured = 0 });
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Contains(result.Failures!,
|
||||||
|
f => f.Contains(nameof(AuditLogPurgeOptions.ChannelPurgeBatchSizeConfigured), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroMaintenanceCommandTimeout_Fails()
|
||||||
|
{
|
||||||
|
var validator = new AuditLogPurgeOptionsValidator();
|
||||||
|
var result = validator.Validate(null,
|
||||||
|
new AuditLogPurgeOptions { MaintenanceCommandTimeoutMinutes = 0 });
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Contains(result.Failures!,
|
||||||
|
f => f.Contains(nameof(AuditLogPurgeOptions.MaintenanceCommandTimeoutMinutes), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Eager startup validation for <see cref="SiteAuditReconciliationOptions"/>
|
||||||
|
/// (arch-review 08 round 2 NF4). A zero tick interval spins the reconciliation
|
||||||
|
/// singleton, a zero batch pulls nothing, and a zero stalled-cycle threshold
|
||||||
|
/// would fire the stalled signal on the first non-draining cycle.
|
||||||
|
/// <see cref="SiteAuditReconciliationOptions.ReconciliationIntervalOverride"/>
|
||||||
|
/// is test-only and left unvalidated.
|
||||||
|
/// </summary>
|
||||||
|
public class SiteAuditReconciliationOptionsValidatorTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void DefaultOptions_AreValid()
|
||||||
|
{
|
||||||
|
var validator = new SiteAuditReconciliationOptionsValidator();
|
||||||
|
Assert.True(validator.Validate(null, new SiteAuditReconciliationOptions()).Succeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroReconciliationInterval_Fails()
|
||||||
|
{
|
||||||
|
var validator = new SiteAuditReconciliationOptionsValidator();
|
||||||
|
var result = validator.Validate(null,
|
||||||
|
new SiteAuditReconciliationOptions { ReconciliationIntervalSeconds = 0 });
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Contains(result.Failures!,
|
||||||
|
f => f.Contains(nameof(SiteAuditReconciliationOptions.ReconciliationIntervalSeconds), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroBatchSize_Fails()
|
||||||
|
{
|
||||||
|
var validator = new SiteAuditReconciliationOptionsValidator();
|
||||||
|
var result = validator.Validate(null,
|
||||||
|
new SiteAuditReconciliationOptions { BatchSize = 0 });
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Contains(result.Failures!,
|
||||||
|
f => f.Contains(nameof(SiteAuditReconciliationOptions.BatchSize), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ZeroStalledCycles_Fails()
|
||||||
|
{
|
||||||
|
var validator = new SiteAuditReconciliationOptionsValidator();
|
||||||
|
var result = validator.Validate(null,
|
||||||
|
new SiteAuditReconciliationOptions { StalledAfterNonDrainingCycles = 0 });
|
||||||
|
Assert.False(result.Succeeded);
|
||||||
|
Assert.Contains(result.Failures!,
|
||||||
|
f => f.Contains(nameof(SiteAuditReconciliationOptions.StalledAfterNonDrainingCycles), StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user