fix(runtime): Primary verdict published from host start, cache boot never adopts malformed bytes, strict drain-gate predicate, HostAbortedException passthrough, seed only Up members

Review R1:
- M1 (#609): DriverHostActor.StopChild releases the instance's resilience state before evicting the hub.
- m2 (#597): DriverHostActor publishes ShouldServiceAsPrimary() to IDriverPrimaryRoleSource at start and on
  cluster membership changes (deduped), so the native-ack gate denies in a pair's boot window.
- m4 (#601): ApplyCachedArtifact returns false on empty/malformed bytes; the cached revision is not adopted,
  Direct mode goes Stale (no RUNNING FROM CACHE), FetchAndCache lands Steady-with-no-revision and a
  re-dispatch of the same revision really applies.
- m3 (#603): LocalDbReplicationPolicy splits into CapturesChanges (PeerAddress||ApiKey, registration) and
  SharesRowsWithPeer (ApiKey && (PeerAddress||SyncListenPort>0), alarm drain gate).
- m5 (#618): Program.cs catch-all filters HostAbortedException (design-time tooling).
- m6 (Cluster-002): ClusterRoleInfo seeds only Up/WeaklyUp members.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 07:47:34 -04:00
parent 83e9ffd27a
commit e3dcb180c3
17 changed files with 453 additions and 65 deletions
@@ -76,7 +76,7 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
}
}
SeedFromCurrentState();
SeedFrom(_cluster.State);
_subscriber = system.ActorOf(Props.Create(() => new SubscriberActor(this)), "clusterroleinfo-subscriber");
}
@@ -121,12 +121,19 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
/// <summary>Occurs when the leader for a role changes.</summary>
public event EventHandler<RoleLeaderChangedEventArgs>? RoleLeaderChanged;
private void SeedFromCurrentState()
/// <summary>Seeds the member/leader maps from a cluster-state snapshot (the constructor passes the live
/// <c>Cluster.State</c>; exposed for tests).</summary>
/// <param name="snapshot">The cluster-state snapshot.</param>
internal void SeedFrom(ClusterEvent.CurrentClusterState snapshot)
{
var snapshot = _cluster.State;
lock (_lock)
{
foreach (var member in snapshot.Members)
// Cluster-002 (review R1 m6): seed only USABLE members. A member already Leaving / Exiting / Down
// (or still Joining) at seed time is not one, and a departing member never fires a later
// MemberLeft / MemberExited / MemberDowned that HandleMemberEvent would drop it on — it would
// linger until MemberRemoved, the very delay the member-event handling exists to avoid. A
// Joining member is added by its MemberUp.
foreach (var member in snapshot.Members.Where(m => m.Status is MemberStatus.Up or MemberStatus.WeaklyUp))
foreach (var role in member.Roles)
{
if (!_membersByRole.TryGetValue(role, out var set))