feat(cluster): AllowSingleNodeCluster flag — single-seed installs drop the phantom seed
Validator lowers the seed-count floor to 1 when AllowSingleNodeCluster is set, otherwise still requires 2 and points operators at the flag. The plan's deploy/wonder-app-vd03/appsettings.Central.json edit is skipped: that production deploy artifact is not tracked in this repo (only its deployment record doc is).
This commit is contained in:
@@ -76,4 +76,12 @@ public class ClusterOptions
|
|||||||
/// other reachable members, rather than running as an isolated single-node cluster.
|
/// other reachable members, rather than running as an isolated single-node cluster.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool DownIfAlone { get; set; } = true;
|
public bool DownIfAlone { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Acknowledges a deliberate single-node deployment: permits exactly one seed
|
||||||
|
/// node (normally ≥2 are required so either node can start first). Without this
|
||||||
|
/// flag a single-node install must list a phantom second seed, which the node
|
||||||
|
/// dials forever — log noise and a defeated validation intent (review 01).
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowSingleNodeCluster { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,13 @@ public sealed class ClusterOptionsValidator : OptionsValidatorBase<ClusterOption
|
|||||||
// two. Accepting a single-seed configuration silently defeats the
|
// two. Accepting a single-seed configuration silently defeats the
|
||||||
// "no startup ordering dependency" guarantee called out by
|
// "no startup ordering dependency" guarantee called out by
|
||||||
// Component-ClusterInfrastructure.md (Node Configuration).
|
// Component-ClusterInfrastructure.md (Node Configuration).
|
||||||
builder.RequireThat(options.SeedNodes is not null && options.SeedNodes.Count >= 2,
|
var minSeeds = options.AllowSingleNodeCluster ? 1 : 2;
|
||||||
"ClusterOptions.SeedNodes must contain at least 2 seed nodes "
|
builder.RequireThat(options.SeedNodes is not null && options.SeedNodes.Count >= minSeeds,
|
||||||
+ "(Component-ClusterInfrastructure.md → Node Configuration: "
|
options.AllowSingleNodeCluster
|
||||||
+ "both nodes are seed nodes); a single-seed configuration defeats "
|
? "ClusterOptions.SeedNodes must contain at least 1 seed node."
|
||||||
+ "the no-startup-ordering-dependency guarantee.");
|
: "ClusterOptions.SeedNodes must contain at least 2 seed nodes "
|
||||||
|
+ "(Component-ClusterInfrastructure.md → Node Configuration: both nodes are seed nodes); "
|
||||||
|
+ "for a deliberate single-node install set ClusterOptions.AllowSingleNodeCluster = true instead of listing a phantom seed.");
|
||||||
|
|
||||||
builder.RequireThat(
|
builder.RequireThat(
|
||||||
!string.IsNullOrWhiteSpace(options.SplitBrainResolverStrategy)
|
!string.IsNullOrWhiteSpace(options.SplitBrainResolverStrategy)
|
||||||
|
|||||||
+41
@@ -69,6 +69,47 @@ public class ClusterOptionsValidatorTests
|
|||||||
Assert.Contains("SeedNodes", result.FailureMessage);
|
Assert.Contains("SeedNodes", result.FailureMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SingleSeed_WithoutAcknowledgement_Fails()
|
||||||
|
{
|
||||||
|
var options = ValidOptions();
|
||||||
|
options.SeedNodes = new List<string> { "akka.tcp://scadabridge@h:8081" };
|
||||||
|
|
||||||
|
var result = new ClusterOptionsValidator().Validate(null, options);
|
||||||
|
|
||||||
|
Assert.True(result.Failed);
|
||||||
|
Assert.Contains("AllowSingleNodeCluster", result.FailureMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SingleSeed_WithAllowSingleNodeCluster_Passes()
|
||||||
|
{
|
||||||
|
// Review 01 [Low]: the shipped single-node artifact satisfied the 2-seed
|
||||||
|
// rule with a phantom seed the node dials forever. An explicit
|
||||||
|
// acknowledgement flag replaces the fiction.
|
||||||
|
var options = ValidOptions();
|
||||||
|
options.SeedNodes = new List<string> { "akka.tcp://scadabridge@h:8081" };
|
||||||
|
options.AllowSingleNodeCluster = true;
|
||||||
|
|
||||||
|
var result = new ClusterOptionsValidator().Validate(null, options);
|
||||||
|
|
||||||
|
Assert.True(result.Succeeded, result.FailureMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EmptySeeds_FailsEvenWithFlag()
|
||||||
|
{
|
||||||
|
// Zero seeds is always invalid — the flag lowers the floor to 1, not 0.
|
||||||
|
var options = ValidOptions();
|
||||||
|
options.SeedNodes = new List<string>();
|
||||||
|
options.AllowSingleNodeCluster = true;
|
||||||
|
|
||||||
|
var result = new ClusterOptionsValidator().Validate(null, options);
|
||||||
|
|
||||||
|
Assert.True(result.Failed);
|
||||||
|
Assert.Contains("SeedNodes", result.FailureMessage);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SingleSeedNode_FailsValidation()
|
public void SingleSeedNode_FailsValidation()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user