From ce66e194a8a8ef757f36de73a06af91e280480a0 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 8 Jul 2026 16:39:12 -0400 Subject: [PATCH] =?UTF-8?q?feat(cluster):=20AllowSingleNodeCluster=20flag?= =?UTF-8?q?=20=E2=80=94=20single-seed=20installs=20drop=20the=20phantom=20?= =?UTF-8?q?seed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../ClusterOptions.cs | 8 ++++ .../ClusterOptionsValidator.cs | 12 +++--- .../ClusterOptionsValidatorTests.cs | 41 +++++++++++++++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs index 8e4c297b..c52f7edc 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptions.cs @@ -76,4 +76,12 @@ public class ClusterOptions /// other reachable members, rather than running as an isolated single-node cluster. /// public bool DownIfAlone { get; set; } = true; + + /// + /// 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). + /// + public bool AllowSingleNodeCluster { get; set; } } diff --git a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs index 2b30c65f..21abae02 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure/ClusterOptionsValidator.cs @@ -26,11 +26,13 @@ public sealed class ClusterOptionsValidator : OptionsValidatorBase= 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."); + var minSeeds = options.AllowSingleNodeCluster ? 1 : 2; + builder.RequireThat(options.SeedNodes is not null && options.SeedNodes.Count >= minSeeds, + options.AllowSingleNodeCluster + ? "ClusterOptions.SeedNodes must contain at least 1 seed node." + : "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( !string.IsNullOrWhiteSpace(options.SplitBrainResolverStrategy) diff --git a/tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsValidatorTests.cs index 58cdbabd..deb05d3e 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsValidatorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests/ClusterOptionsValidatorTests.cs @@ -69,6 +69,47 @@ public class ClusterOptionsValidatorTests Assert.Contains("SeedNodes", result.FailureMessage); } + [Fact] + public void SingleSeed_WithoutAcknowledgement_Fails() + { + var options = ValidOptions(); + options.SeedNodes = new List { "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 { "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(); + options.AllowSingleNodeCluster = true; + + var result = new ClusterOptionsValidator().Validate(null, options); + + Assert.True(result.Failed); + Assert.Contains("SeedNodes", result.FailureMessage); + } + [Fact] public void SingleSeedNode_FailsValidation() {