Repository navigation
GH-4562: grandfather a group-affinity candidate per member, not per partition - #4563
Conversation
…ot per partition A node running one member of a capability partition became a candidate for the whole partition, so during blue/green a blue node running a tenant it never declared took the new version's agents with it and could never start them.
… it is running The per-member grandfathering removes the clause that used to carry a partially-running group: a node running ONE member no longer stands in as a candidate for the rest. That is the point of the fix for the blue/green case, but it also withdraws the only thing holding together a partition that NO node declares — a shard database provisioned after every surviving node captured its capability snapshot (JasperFxGH-3341). The JasperFxGH-3341 rescue below did not cover it, because it also required every member to be unassigned. A database with two tenants placed by an earlier evaluation and a third provisioned since fell through to the per-member fallback and scattered one agent per node, opening a connection pool per node to that database — the exact cost DistributeByGroupAffinity exists to avoid (JasperFx/marten#4806). Verified: that scenario is whole on main and split on the branch before this commit. Every member of a partition shares one capabilityKey, which is the sorted ids of its candidate nodes, so a partition is either wholly undeclared or wholly declared. The unassigned clause was therefore always redundant with the capability test, and only looked load-bearing while a single running member covered for the rest. Tests: - a_partially_running_undeclared_group_still_stays_whole reproduces it; red with the unassigned clause restored, green without. - a_node_running_part_of_a_partition_is_not_a_candidate_for_members_it_cannot_run now asserts all five placements rather than three. The unchanged agent that shares a capability set with the bumped ones moves off blue with them, which is the price of an indivisible partition and worth stating outright. - the_blue_green_partial_grandfather_split_settles_on_the_next_evaluation replays the result as the next evaluation's grid. It guards churn rather than the fix — it passes on the old predicate too, because the grid cannot model a node failing to BUILD what it was handed. 364 tests green in CoreTests.Runtime.Agents, 3035 in CoreTests, and wolverine.slnx builds clean in Release on net9.0 with no warnings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Thanks for the detailed writeup and the production replay — the diagnosis is right, and the fix is right. I pushed one commit to this branch ( The extra commitMoving the grandfathering inside The GH-3341 rescue below the candidate filter did not cover it, because it also required every member to be unassigned: if (members.All(m => m.AssignedNode == null && m.CandidateNodes.Count == 0))While a single running member grandfathered its node into the candidates, that extra clause never mattered — the candidate filter got there first. Per-member grandfathering removes that cover, so a database with two tenants placed by an earlier evaluation and a third provisioned since now falls through to the per-member fallback and scatters one agent per node. That opens a connection pool per node to that database, which is the cost Concretely, two nodes with divergent snapshots and neither declaring The fix drops the unassigned clause: if (members.All(m => m.CandidateNodes.Count == 0))That is safe because every member of a partition shares one Worth recording for whoever reads this next: that same Tests
Verification364 tests green in One note on those: Merging once CI is green. Thanks again for chasing this down to the grid snapshot. |
… breaks again (#4601) The reported symptom -- green-only projection agents assigned to blue nodes that cannot build them, re-sent on every evaluation for twelve hours -- reproduces at the reporter's fleet shape against the AssignmentGrid.Distribution.cs from before GH-4563, and does not reproduce against main. So GH-4562/#4563 fixed it; it shipped after the 6.39.1 they are running. The suite pins that at their shape rather than the two-node minimum the existing group-affinity tests use. The other half of the report is not fixed by that, and this adds it: the leader said nothing at all while re-issuing a placement the target had failed to honour, so the loop had to be reconstructed from application logs on the receiving side. The leader now warns when it dispatches an assignment to a node that does not advertise the agent, while some other node does. Scoped to the assignments actually being issued -- in a settled cluster that is none -- and latched on the count so a steady state is not re-logged every evaluation. Agents that NO node declares are ignored: durability and listener agents are advertised per family rather than per URI, and the GH-3341 rescue places an undeclared agent deliberately. AssignmentGrid.Node.Declares backs it with a lazily built set. A node in a fleet running tens of thousands of projection agents carries thousands of capabilities, and Capabilities is a List, so the existing TryAssign scan was linear per call; it takes the set too. Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #4562.
Problem
In
DistributeByGroupAffinity, a node became a candidate for a whole capability partition as soon as it ran one member (|| members.Any(m => m.OriginalNode == n)). The sibling preference then picks that node, because it already hosts the database's other partition.During a blue/green rollout, blue runs the agents of a tenant provisioned after blue started without declaring them. Only green declares those agents, so they share a partition with the new version's agents. Blue then took that whole partition, new version included. It cannot build those agents, so they never started, and the leader re-sent them to blue on every evaluation.
Fix
The grandfathering moves inside
All: a node qualifies when every member is either declared by it or already running on it. A node that runs the whole partition, the stale-snapshot case the grandfathering exists for, still qualifies. A node that runs only part of it no longer does.Tests
a_node_running_part_of_a_partition_is_not_a_candidate_for_members_it_cannot_runreproduces this. Onmainit fails because the bumped agent lands on the blue node.Everything else in
CoreTests.Runtime.Agentsis still green (387 tests), includinga_node_already_running_a_groups_agents_stays_a_candidate_despite_a_stale_capability_snapshot,an_incumbent_node_keeps_its_group_up_to_the_ceilingand the blue/green split tests. I did not run theMartenTestsblue/green integration tests locally.I also replayed a real 6-node production grid snapshot through
DistributeByGroupAffinity. The affected partitions were exactly the three databases holding such a tenant.