Summary
The extended-progression running_on_node column stays NULL forever, even under genuine
Balanced multi-node managed distribution where the projection agents are assigned to a real node.
The sibling telemetry columns (heartbeat, agent_status, last_updated) write correctly on the
same code path and in the same row — only running_on_node never populates.
This is the last open gap from the extended-progression write path wired in marten#4990
(MartenDatabase.WriteExtendedProgressionAsync) / #537–#538 (ExtendedProgressionWriter).
Core #750 is otherwise confirmed live on 9.17.1.
Root cause (traced end to end)
The write op consumes state.RunningOnNode:
// Marten: MartenDatabase.EventStorage.cs — WriteExtendedProgressionAsync
.With("node", (object?)state.RunningOnNode ?? DBNull.Value, NpgsqlDbType.Integer)
The JasperFx.Events observer that drives it tries to source that value from the agent's assigned
node number:
// JasperFx.Events: ExtendedProgressionWriter.OnNext (release/2.33.0, commit 33a2f46)
if (value.RunningOnNode == null && value.AssignedNodeNumber != 0)
{
value.RunningOnNode = value.AssignedNodeNumber;
}
…but nothing ever stamps AssignedNodeNumber onto a runtime-published ShardState. A grep of
the entire JasperFx.Events tree finds AssignedNodeNumber = … in exactly one place — Marten's
ShardStateSelector, which populates it only when reading a row back from the DB (the
assigned_node column). The telemetry ShardState the daemon publishes on the heartbeat timer /
status transitions never has it set, so AssignedNodeNumber == 0, the carry block is dead, and the
write persists NULL.
So the carry logic is correct but its input is never supplied on the runtime publish path. The fix
is to stamp the agent's actual assigned node number onto the ShardState a subscription/projection
agent publishes (the ones ShardStateTracker broadcasts and ExtendedProgressionWriter observes).
Under Wolverine-managed subscription distribution the assignment lives in
wolverine_node_assignments / is known to the coordinator, so this likely spans JasperFx.Events (the
daemon publish surface) and the distribution layer that owns the assignment — happy to split this to
a jasperfx issue if that's the better home.
Reproduction (already written, green, pinning the gap)
CritterWatch src/EventStoreCoordinationTests/RunningOnNodeMultiNodeTests.cs boots two
MartenProjectionService nodes, Balanced + UseWolverineManagedEventSubscriptionDistribution,
extended progression forced on via the CritterWatch #321 instrumentation path. The leader distributes
the Trip/Day/Distance agents across both nodes (logs show node-2 join and the distance agent
redistributing node 1 → node 2). After the daemon advances the Trip shard and a heartbeat tick lands,
projection_tests.mt_event_progression for the Trip row reads:
running_on_node = NULL, agent_status = Running, heartbeat = <set>, last_seq_id = 30
heartbeat + agent_status write under genuine managed distribution; running_on_node stays NULL in
the same telemetry row → this is a write-path/publish gap, not a "single-node, nothing to stamp"
expected-NULL (that interpretation was ruled out — the agents are assigned to node_number 1, confirmed
via wolverine_node_assignments + wolverine_nodes).
The test is a characterization guard: it asserts the working columns and pins the gap with a
// FLIP-WHEN-FIXED marker on runningOnNode.ShouldBeNull(...). When this is fixed, that assertion
flips to ShouldBe(assignedNodeNumber).
Acceptance
running_on_node reflects the projection agent's assigned node number under Balanced managed
distribution, populated on the same heartbeat/transition publications that already write
heartbeat/agent_status.
References
Summary
The extended-progression
running_on_nodecolumn stays NULL forever, even under genuineBalanced multi-node managed distribution where the projection agents are assigned to a real node.
The sibling telemetry columns (
heartbeat,agent_status,last_updated) write correctly on thesame code path and in the same row — only
running_on_nodenever populates.This is the last open gap from the extended-progression write path wired in marten#4990
(
MartenDatabase.WriteExtendedProgressionAsync) / #537–#538 (ExtendedProgressionWriter).Core #750 is otherwise confirmed live on 9.17.1.
Root cause (traced end to end)
The write op consumes
state.RunningOnNode:The JasperFx.Events observer that drives it tries to source that value from the agent's assigned
node number:
…but nothing ever stamps
AssignedNodeNumberonto a runtime-publishedShardState. A grep ofthe entire JasperFx.Events tree finds
AssignedNodeNumber = …in exactly one place — Marten'sShardStateSelector, which populates it only when reading a row back from the DB (theassigned_nodecolumn). The telemetryShardStatethe daemon publishes on the heartbeat timer /status transitions never has it set, so
AssignedNodeNumber == 0, the carry block is dead, and thewrite persists
NULL.So the carry logic is correct but its input is never supplied on the runtime publish path. The fix
is to stamp the agent's actual assigned node number onto the
ShardStatea subscription/projectionagent publishes (the ones
ShardStateTrackerbroadcasts andExtendedProgressionWriterobserves).Under Wolverine-managed subscription distribution the assignment lives in
wolverine_node_assignments/ is known to the coordinator, so this likely spans JasperFx.Events (thedaemon publish surface) and the distribution layer that owns the assignment — happy to split this to
a jasperfx issue if that's the better home.
Reproduction (already written, green, pinning the gap)
CritterWatch
src/EventStoreCoordinationTests/RunningOnNodeMultiNodeTests.csboots twoMartenProjectionServicenodes, Balanced +UseWolverineManagedEventSubscriptionDistribution,extended progression forced on via the CritterWatch #321 instrumentation path. The leader distributes
the Trip/Day/Distance agents across both nodes (logs show node-2 join and the
distanceagentredistributing node 1 → node 2). After the daemon advances the Trip shard and a heartbeat tick lands,
projection_tests.mt_event_progressionfor the Trip row reads:heartbeat+agent_statuswrite under genuine managed distribution;running_on_nodestays NULL inthe same telemetry row → this is a write-path/publish gap, not a "single-node, nothing to stamp"
expected-NULL (that interpretation was ruled out — the agents are assigned to node_number 1, confirmed
via
wolverine_node_assignments+wolverine_nodes).The test is a characterization guard: it asserts the working columns and pins the gap with a
// FLIP-WHEN-FIXEDmarker onrunningOnNode.ShouldBeNull(...). When this is fixed, that assertionflips to
ShouldBe(assignedNodeNumber).Acceptance
running_on_nodereflects the projection agent's assigned node number under Balanced manageddistribution, populated on the same heartbeat/transition publications that already write
heartbeat/agent_status.References
WriteExtendedProgressionAsync)ExtendedProgressionWriterobserver