refactor: ScadaBridge validators onto OptionsValidatorBase (messages unchanged)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.Configuration;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.ClusterInfrastructure;
|
||||
/// Registered with <c>ValidateOnStart()</c> so a bad <c>appsettings.json</c>
|
||||
/// fails fast at boot rather than failing far from the cause.
|
||||
/// </summary>
|
||||
public sealed class ClusterOptionsValidator : IValidateOptions<ClusterOptions>
|
||||
public sealed class ClusterOptionsValidator : OptionsValidatorBase<ClusterOptions>
|
||||
{
|
||||
/// <summary>Split-brain resolver strategies safe for ScadaBridge's two-node clusters.</summary>
|
||||
private static readonly HashSet<string> AllowedStrategies = new(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -19,77 +19,51 @@ public sealed class ClusterOptionsValidator : IValidateOptions<ClusterOptions>
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Validates the cluster options, returning a failure result if any critical settings are misconfigured.
|
||||
/// Validates the cluster options, recording a failure if any critical settings are misconfigured.
|
||||
/// </summary>
|
||||
/// <param name="name">Named options instance name (unused; all instances are validated identically).</param>
|
||||
/// <param name="builder">The accumulator to record failures on.</param>
|
||||
/// <param name="options">The cluster options to validate.</param>
|
||||
public ValidateOptionsResult Validate(string? name, ClusterOptions options)
|
||||
protected override void Validate(ValidationBuilder builder, ClusterOptions options)
|
||||
{
|
||||
var failures = new List<string>();
|
||||
// CI-012: design doc states "both nodes are seed nodes — each node lists
|
||||
// both itself and its partner" so a properly-configured deployment lists
|
||||
// two. Accepting a single-seed configuration silently defeats the
|
||||
// "no startup ordering dependency" guarantee called out by
|
||||
// Component-ClusterInfrastructure.md (Node Configuration).
|
||||
builder.RequireThat(options.SeedNodes is not null && options.SeedNodes.Count >= 2,
|
||||
"ClusterOptions.SeedNodes must contain at least 2 seed nodes "
|
||||
+ "(Component-ClusterInfrastructure.md → Node Configuration: "
|
||||
+ "both nodes are seed nodes); a single-seed configuration defeats "
|
||||
+ "the no-startup-ordering-dependency guarantee.");
|
||||
|
||||
if (options.SeedNodes is null || options.SeedNodes.Count < 2)
|
||||
{
|
||||
// CI-012: design doc states "both nodes are seed nodes — each node lists
|
||||
// both itself and its partner" so a properly-configured deployment lists
|
||||
// two. Accepting a single-seed configuration silently defeats the
|
||||
// "no startup ordering dependency" guarantee called out by
|
||||
// Component-ClusterInfrastructure.md (Node Configuration).
|
||||
failures.Add(
|
||||
"ClusterOptions.SeedNodes must contain at least 2 seed nodes "
|
||||
+ "(Component-ClusterInfrastructure.md → Node Configuration: "
|
||||
+ "both nodes are seed nodes); a single-seed configuration defeats "
|
||||
+ "the no-startup-ordering-dependency guarantee.");
|
||||
}
|
||||
builder.RequireThat(
|
||||
!string.IsNullOrWhiteSpace(options.SplitBrainResolverStrategy)
|
||||
&& AllowedStrategies.Contains(options.SplitBrainResolverStrategy),
|
||||
$"ClusterOptions.SplitBrainResolverStrategy must be 'keep-oldest' for a two-node cluster; " +
|
||||
$"'{options.SplitBrainResolverStrategy}' would risk a total cluster shutdown on a partition.");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(options.SplitBrainResolverStrategy)
|
||||
|| !AllowedStrategies.Contains(options.SplitBrainResolverStrategy))
|
||||
{
|
||||
failures.Add(
|
||||
$"ClusterOptions.SplitBrainResolverStrategy must be 'keep-oldest' for a two-node cluster; " +
|
||||
$"'{options.SplitBrainResolverStrategy}' would risk a total cluster shutdown on a partition.");
|
||||
}
|
||||
builder.RequireThat(options.MinNrOfMembers == 1,
|
||||
$"ClusterOptions.MinNrOfMembers must be 1 (was {options.MinNrOfMembers}); " +
|
||||
"any other value blocks the cluster singleton after failover and halts all data collection.");
|
||||
|
||||
if (options.MinNrOfMembers != 1)
|
||||
{
|
||||
failures.Add(
|
||||
$"ClusterOptions.MinNrOfMembers must be 1 (was {options.MinNrOfMembers}); " +
|
||||
"any other value blocks the cluster singleton after failover and halts all data collection.");
|
||||
}
|
||||
builder.RequireThat(options.StableAfter > TimeSpan.Zero,
|
||||
"ClusterOptions.StableAfter must be a positive duration.");
|
||||
|
||||
if (options.StableAfter <= TimeSpan.Zero)
|
||||
{
|
||||
failures.Add("ClusterOptions.StableAfter must be a positive duration.");
|
||||
}
|
||||
builder.RequireThat(options.HeartbeatInterval > TimeSpan.Zero,
|
||||
"ClusterOptions.HeartbeatInterval must be a positive duration.");
|
||||
|
||||
if (options.HeartbeatInterval <= TimeSpan.Zero)
|
||||
{
|
||||
failures.Add("ClusterOptions.HeartbeatInterval must be a positive duration.");
|
||||
}
|
||||
builder.RequireThat(options.FailureDetectionThreshold > TimeSpan.Zero,
|
||||
"ClusterOptions.FailureDetectionThreshold must be a positive duration.");
|
||||
|
||||
if (options.FailureDetectionThreshold <= TimeSpan.Zero)
|
||||
{
|
||||
failures.Add("ClusterOptions.FailureDetectionThreshold must be a positive duration.");
|
||||
}
|
||||
builder.RequireThat(options.HeartbeatInterval < options.FailureDetectionThreshold,
|
||||
$"ClusterOptions.HeartbeatInterval ({options.HeartbeatInterval}) must be well below " +
|
||||
$"FailureDetectionThreshold ({options.FailureDetectionThreshold}); otherwise nodes are " +
|
||||
"declared unreachable before a heartbeat can arrive.");
|
||||
|
||||
if (options.HeartbeatInterval >= options.FailureDetectionThreshold)
|
||||
{
|
||||
failures.Add(
|
||||
$"ClusterOptions.HeartbeatInterval ({options.HeartbeatInterval}) must be well below " +
|
||||
$"FailureDetectionThreshold ({options.FailureDetectionThreshold}); otherwise nodes are " +
|
||||
"declared unreachable before a heartbeat can arrive.");
|
||||
}
|
||||
|
||||
if (!options.DownIfAlone)
|
||||
{
|
||||
failures.Add(
|
||||
"ClusterOptions.DownIfAlone must be true for the keep-oldest resolver "
|
||||
+ "(Component-ClusterInfrastructure.md → Split-Brain Resolution); with it false the "
|
||||
+ "oldest node can run as an isolated single-node cluster during a partition while the "
|
||||
+ "younger node forms its own, producing two live clusters.");
|
||||
}
|
||||
|
||||
return failures.Count > 0
|
||||
? ValidateOptionsResult.Fail(failures)
|
||||
: ValidateOptionsResult.Success;
|
||||
builder.RequireThat(options.DownIfAlone,
|
||||
"ClusterOptions.DownIfAlone must be true for the keep-oldest resolver "
|
||||
+ "(Component-ClusterInfrastructure.md → Split-Brain Resolution); with it false the "
|
||||
+ "oldest node can run as an isolated single-node cluster during a partition while the "
|
||||
+ "younger node forms its own, producing two live clusters.");
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -10,6 +10,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
<PackageReference Include="ZB.MOM.WW.Configuration" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user