Merge branch 'fix/r3-04-one-role-authority' — R3-04: EffectiveRoles is the one role authority (Cluster:Roles wins over OTOPCUA_ROLES, normalised, validated, fail-start on unknown/overlay-changed roles); Program.cs + all four validators consume it; case-insensitive gossip predicates; source-scan guard (archreview 01/S-12 = 03/C5, 01/C-9)

This commit is contained in:
Joseph Doherty
2026-08-20 11:05:09 -04:00
41 changed files with 2176 additions and 159 deletions
@@ -5,6 +5,7 @@ using Akka.Event;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.OtOpcUa.Cluster;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
@@ -1648,8 +1649,12 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers, IWithUnboundedS
if (_driverMemberCountProvider is not null) return _driverMemberCountProvider();
try
{
// R3-04 (01/C-9): GossipedRolesHave, not ordinal Contains — a peer gossiping a miscased
// 'Driver' (rolling-upgrade window) must still count toward the R2-04 primary-gate member
// count, or a genuine 2-node pair silently takes the single-node allow branch.
return Akka.Cluster.Cluster.Get(Context.System).State.Members
.Count(m => m.Status == Akka.Cluster.MemberStatus.Up && m.Roles.Contains(ServiceCollectionExtensions.DriverRole));
.Count(m => m.Status == Akka.Cluster.MemberStatus.Up
&& RoleParser.GossipedRolesHave(m.Roles, ServiceCollectionExtensions.DriverRole));
}
catch (Exception)
{