fix(cluster): ClusterRoleInfo._roleLeaders stays Ordinal — Akka tracks role-leadership per exact gossiped role string (review finding, R3-04)

This commit is contained in:
Joseph Doherty
2026-08-20 10:52:51 -04:00
parent 027c423502
commit 3648d3c129
2 changed files with 144 additions and 6 deletions
@@ -23,12 +23,21 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
private readonly string? _clusterRole;
private readonly string? _clusterId;
private readonly object _lock = new();
// R3-04 (01/C-9, census row 17): OrdinalIgnoreCase — Akka compares gossiped role names ordinally,
// so a PEER running an older build can still gossip a miscased plain role during a rolling
// upgrade. Keying these by the exact-cased role string would leave MembersWithRole("driver") and
// RoleLeader("driver") blind to a peer that gossips "Driver", the same class of gap #532 closed
// for _localRoles above.
private readonly Dictionary<string, Member?> _roleLeaders = new(StringComparer.OrdinalIgnoreCase);
// R3-04 (01/C-9, census row 17) — the two dictionaries below have DIFFERENT correct comparers,
// and that split is deliberate, not an oversight (review finding, 2026-08-20).
//
// _membersByRole answers "every member carrying (a spelling of) this role" — OrdinalIgnoreCase,
// so a PEER running an older build that still gossips a miscased plain role during a rolling
// upgrade is not invisible to MembersWithRole("driver"). Merging casings IS the point here.
//
// _roleLeaders must stay Ordinal. Akka computes — and raises RoleLeaderChanged for — role
// leadership PER EXACT GOSSIPED ROLE STRING: "driver" and "Driver" are two entirely distinct
// roles to Akka's own role-leader machinery, each independently electing its own leader. An
// OrdinalIgnoreCase dictionary here would collapse both onto ONE key, so whichever
// RoleLeaderChanged event landed last would silently overwrite the other's leader — turning two
// real, independent facts Akka tracks separately into one, wrong, merged one. Pinned by
// ClusterRoleInfoRoleLeaderCaseTests.RoleLeaderChanged_for_driver_then_Driver_keeps_distinct_entries.
private readonly Dictionary<string, Member?> _roleLeaders = new(StringComparer.Ordinal);
private readonly Dictionary<string, HashSet<Member>> _membersByRole = new(StringComparer.OrdinalIgnoreCase);
private IActorRef? _subscriber;