fix(cluster): AkkaClusterOptions.Roles is the EffectiveRoles instance, not a second derivation
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
{
|
||||
"id": "T2",
|
||||
"subject": "AddOtOpcUaCluster post-configures AkkaClusterOptions.Roles from the registered EffectiveRoles instance (structural identity, no second derivation)",
|
||||
"status": "pending",
|
||||
"status": "completed",
|
||||
"blockedBy": [
|
||||
"T1"
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ using Akka.Logger.Serilog;
|
||||
using Akka.Remote.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.Configuration;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
|
||||
@@ -34,21 +35,31 @@ public static class ServiceCollectionExtensions
|
||||
services.AddValidatedOptions<AkkaClusterOptions, AkkaClusterOptionsValidator>(
|
||||
configuration, AkkaClusterOptions.SectionName);
|
||||
|
||||
// Gitea #532: apply the OTOPCUA_ROLES fallback the Roles property has always DOCUMENTED, in the
|
||||
// one place every consumer reads through. Before this, a node configured only through the env
|
||||
// var — the knob that drives all of Program.cs's role wiring — bound an EMPTY Roles array and so
|
||||
// joined the cluster role-less: invisible to RoleLeader/MembersWithRole, ineligible for every
|
||||
// role-scoped singleton, and un-deployable ("no driver-role nodes are Up") with nothing in its
|
||||
// own logs to say so. Doing it here rather than at each call site means BuildClusterOptions
|
||||
// (Akka's own member roles), ClusterRoleInfo, the ControlPlane singleton scope and
|
||||
// ConfigDbApiKeyStore's ClusterId all see the same answer.
|
||||
// R3-04 (structural identity, choice 3): EffectiveRoles is resolved ONCE and registered as a
|
||||
// singleton, off the SAME IConfiguration this call received — not off a DI-resolved
|
||||
// IConfiguration, which a plain ServiceCollection (as opposed to a WebApplicationBuilder) never
|
||||
// registers. Every consumer (Program.cs's hasAdmin/hasDriver, the four role-scoped validators,
|
||||
// AkkaClusterOptions.Roles below) therefore reads the exact same instance, so there is no second
|
||||
// derivation left to disagree with — the boot-guard OVERALL asked for is this identity, not a
|
||||
// runtime assert (see EffectiveRoles + AkkaClusterOptionsValidator's post-overlay guard).
|
||||
// TryAddSingleton keeps this usable from a harness that registers its own EffectiveRoles first.
|
||||
services.TryAddSingleton(_ => EffectiveRoles.Resolve(configuration));
|
||||
|
||||
// Gitea #532 (superseded by R3-04): apply the OTOPCUA_ROLES fallback the Roles property has
|
||||
// always DOCUMENTED, in the one place every consumer reads through. Before this, a node
|
||||
// configured only through the env var — the knob that drives all of Program.cs's role wiring —
|
||||
// bound an EMPTY Roles array and so joined the cluster role-less: invisible to
|
||||
// RoleLeader/MembersWithRole, ineligible for every role-scoped singleton, and un-deployable
|
||||
// ("no driver-role nodes are Up") with nothing in its own logs to say so. Doing it here rather
|
||||
// than at each call site means BuildClusterOptions (Akka's own member roles), ClusterRoleInfo,
|
||||
// the ControlPlane singleton scope and ConfigDbApiKeyStore's ClusterId all see the same answer.
|
||||
//
|
||||
// PostConfigure, not a bind trick: it runs after the section binds and before ValidateOnStart,
|
||||
// so the validators below see the effective list. An unknown role in the env var throws out of
|
||||
// RoleParser.Parse — the same throw Program.cs already takes on the same value, so an invalid
|
||||
// env var cannot reach here on the production path in the first place.
|
||||
services.PostConfigure<AkkaClusterOptions>(
|
||||
options => options.Roles = RoleParser.ResolveEffectiveRoles(options.Roles));
|
||||
// so the validators below see the effective list. Now consumes the registered EffectiveRoles
|
||||
// instance rather than re-deriving from options.Roles + the environment — options.Roles becomes
|
||||
// BY CONSTRUCTION the same list Program.cs's hasAdmin/hasDriver are computed from.
|
||||
services.AddOptions<AkkaClusterOptions>().PostConfigure<EffectiveRoles>(
|
||||
(options, roles) => options.Roles = [.. roles.Gossiped]);
|
||||
|
||||
// Per-cluster mesh Phase 2: which transport carries central→node commands. Validated at
|
||||
// startup for the same reason the cluster options are — a contact point that cannot resolve
|
||||
|
||||
@@ -94,4 +94,34 @@ public class AkkaClusterOptionsRoleFallbackTests
|
||||
RoleParser.ClusterIdFromRoles(Bind().Roles).ShouldBe("site-a");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R3-04 (structural identity, choice 3): <c>AkkaClusterOptions.Roles</c> is not a SECOND
|
||||
/// derivation of the role rule — it is the registered <see cref="EffectiveRoles"/> instance's
|
||||
/// <see cref="EffectiveRoles.Gossiped"/> list, verbatim. A mixed-case configured role proves
|
||||
/// both the normalization (plain lowered, cluster suffix preserved) and the identity: the two
|
||||
/// views can never disagree because there is only one derivation.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Bound_options_Roles_IS_the_registered_EffectiveRoles_Gossiped_list()
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Cluster:Roles:0"] = "Driver",
|
||||
["Cluster:Roles:1"] = "Cluster-SITE-A",
|
||||
})
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
services.AddOtOpcUaCluster(configuration);
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
var bound = provider.GetRequiredService<IOptions<AkkaClusterOptions>>().Value;
|
||||
var effectiveRoles = provider.GetRequiredService<EffectiveRoles>();
|
||||
|
||||
bound.Roles.ShouldBe(new[] { "driver", "cluster-SITE-A" });
|
||||
bound.Roles.ShouldBe(effectiveRoles.Gossiped);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user