Surface paused-shard failures to the assignment plane + critter stack bump (#3637, #3638, #3519) - #3658
Merged
Merged
Conversation
…, #3638, #3519) Bumps JasperFx/JasperFx.Events 2.34.0 -> 2.36.1, Marten 9.18.0 -> 9.20.0 and Polecat [5.5.0,6.0.0) -> [5.7.0,6.0.0), the release carrying #565/#567 (ShardFailure + ISubscriptionAgent.Failure) and #534/#540. WO-8 (#3637 / #3638): a shard the daemon pauses on an ApplyEventException was visible only as AgentStatus.Paused, so progress flatlined with nothing actionable to alert on. - IEventSubscriptionAgent.Failure (default-null DIM); EventSubscriptionAgent delegates it to the live inner agent the way Status has since GH-3519. - Health checks report the failure category, the failing event's sequence and type, and the root exception type instead of a fixed "paused due to errors". - Failures bound to a specific event (ApplyEvent, EventSerialization, UnknownEventType) or to two processes racing one shard (ProgressionOutOfOrder) are no longer swept back up by the GH-3519 wedge recovery -- they would die on the identical event on every restart. Only Other, the category auto-restart exists for, is still retried. - IWolverineObserver.AgentPaused(uri, failure) default-no-op hook plus a NodeRecordType.AgentPaused record, fired once per transition into failure by a per-node sweep in the health-check loop. Observe-only, per the analysis in #3637 s4: the anti-thrash guards already hold and detaching would risk the re-assignment churn WO-1..7 removed. #3519: the daemon-side causes of the wedged first-assignment start land with this bump. What was left here was WHEN the retry happens -- the node only retried on the next assignment reevaluation, so the loser of a sub-second startup race idled for a full CheckAssignmentPeriod. StartAgentAsync now retries locally, bounded by Durability.AgentStartRetryAttempts (default 2) and AgentStartRetryDelay (default 250ms, multiplied by attempt number), preserving the last cause when it gives up. 19 new tests in CoreTests; full CoreTests (2088) and MartenTests Distribution (57) green, wolverine.slnx builds clean in Release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DkTEuNQQdPp63WLY86KzC
portlogicsvn
pushed a commit
to portlogicsvn/wolverine
that referenced
this pull request
Aug 6, 2026
The JasperFxGH-3698 ledger closed the re-decision hole for first-time assignments and silently excluded every reassignment. An agent being moved is still listed in its SOURCE node's persisted ActiveAgents, so Agent.OriginalNode is set, and applyPendingAssignments skipped it outright: if (agent.OriginalNode != null) continue; The ledger armed on a ReassignAgent but could never apply one, and TryBuildAssignmentCommand's OriginalNode != null branch consulted no ledger at all. So the leader re-decided the same move from scratch on every evaluation until the source's assignment row finally disappeared. Against the reported cluster shape -- 512 databases x 17 agents across five nodes, group affinity, three incumbents and two newcomers ramping in -- that is 3,468 reassignment decisions per cycle against a frozen snapshot, repeating indefinitely. Roughly 13 cycles of that is the reported ~45,000 in six minutes. Each decision writes an AssignmentChanged row (the observer runs before batchCommands and before the dispatcher, so nothing downstream dedupes it), feeding the node-record volume problem of JasperFx#3658 at full rate. The outcome converged because ReassignAgents carries set-based value equality, so the dispatcher collapses an identical re-emitted batch while its lane is busy. That hold ends the moment the batch completes, though, so a re-decision landing before the snapshot catches up re-runs a real StopAgents round trip against a source that has already let go. Three changes: - applyPendingAssignments ends the wait only on a MATCHING node, so a move in flight sets PendingNode and holds its placement. - TryBuildAssignmentCommand returns false for a move already dispatched and still live. PendingRetryDue re-drives one that is never confirmed. - AgentCommandDispatcher tracks reassignments in a _moving map, keyed to the node the agents are moving TO (not the command's lane, which is the source). Kept separate from _inFlight because Enqueue must never suppress a reassignment -- it carries a stop. Without this the hold falls back to the ledger TTL (60s), which a batch's own reply window trivially outlives. Regression coverage in pending_reassignment_ledger; 4 of the 8 fail without the fix, the rest guard against over-suppression -- a pause still stops an agent mid-move, confirmation still clears the ledger, an unconfirmed move is still re-driven. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WHAuhdWS3XeAk16swV9G8m
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bumps the critter stack pins and lands the Wolverine half of the WO-8 pause-visibility work that was
deliberately deferred until the JasperFx.Events accessor shipped.
Dependency bump
[5.5.0,6.0.0)[5.7.0,6.0.0)This is the release carrying JasperFx/jasperfx#565 (
ShardFailure), #567 (ISubscriptionAgent.Failure),#534 and #540 — all three issues below need it.
Closes #3637 / #3638 — surface a paused shard's failure, with a reason
When a projection/subscription shard hits an
ApplyEventExceptionunder a non-skipping error policy thedaemon pauses it. The anti-thrash guards already kept it from being restart-looped, but nothing in the
assignment plane ever distinguished
PausedfromRunning: progress silently flatlined and the onlytrace was a log line on one node.
ShardFailurenow makes the reason portable, so this ships as thecoordinated pair the two issues describe.
IEventSubscriptionAgent.Failure— aShardFailure?default-null DIM;EventSubscriptionAgent.Failuredelegates to the live inner agent, the same wayStatushas sinceManaged event-subscription distribution: randomized agent fails first start on multi-(ancillary-)store Marten hosts and wedges in a permanent 30s retry loop #3519. Read-through rather than cached, so a shard that recovers stops reporting a stale reason.
it died on, and the root exception type, instead of the fixed
"Projection {uri} paused due to errors". An inner agent that reports no failure reads exactly as it did before.ApplyEvent,EventSerialization,UnknownEventTypeorProgressionOutOfOrderwill die on the identical event onevery restart, so the Managed event-subscription distribution: randomized agent fails first start on multi-(ancillary-)store Marten hosts and wedges in a permanent 30s retry loop #3519 wedge recovery in
NodeAgentController.StartAgentAsyncnow stands downfor those and surfaces the reason instead.
ProgressionOutOfOrdermeans two processes are on the sameshard — restarting adds a third contender. Only
Other(a database blip, a timeout — whatrestart-on-stall actually exists for) is still retried.
IWolverineObserver.AgentPaused(Uri, ShardFailure?)default-no-ophook plus a
NodeRecordType.AgentPausedrecord carrying the classified reason, so the failure isreadable from another process and CritterWatch can alert on it (Assert on Publish/SendAsync() if running in MediatorOnly mode #830). A sweep in the health-check loop
reports locally-owned paused agents on every node, not just the leader — the daemon pauses a shard on
whichever node owns it. Fires once per transition into failure, and re-arms if the shard recovers and
later fails anew.
Per the §4 finding on #3637, this takes the observe-only option rather than detaching from
distribution: the anti-thrash guards already hold, and detaching would risk exactly the re-assignment
churn WO-1..7 removed.
Closes #3519 — the wedged first-assignment start
The reported shape: on a multi-store Marten host, one event-subscription agent — a different one on
every boot — failed its very first assignment start and stayed wedged in a permanent 30-second retry loop.
The root causes were daemon-side and land with this bump: the start failure now arrives as a
ShardStartExceptionnaming its cause instead of a causelessException(#534), and a start thatfaults after spinning up the execution pipeline hard-stops the half-started shard rather than orphaning it
(#540), so the next attempt succeeds.
What was left on this side was when that next attempt happens. The node only retried on the next
assignment reevaluation, so the loser of a sub-second startup race sat idle for a full
CheckAssignmentPeriod(30s by default) while its high-water mark climbed.StartAgentAsyncnow retries afailed start locally, bounded and configurable:
A failure that outlives the retries is thrown as before with the last cause intact, logged by the existing
per-agent isolation, and picked up on the next reevaluation.
Tests
paused_shard_failure_surfacing— 15 tests across wrapper delegation, health-check reporting, restartsuppression by category, and the local sweep (once-per-transition, re-arm after recovery, silence for
healthy and for reason-less stops).
agent_start_retry_on_startup_race— 4 tests: recovery from a first-attempt race, giving up after theconfigured attempts with the daemon's reason preserved, opt-out, and no extra attempt for a healthy
agent.
CoreTestssuite green; fullwolverine.slnxbuilds clean against the new pins.Docs
New "When a Projection Fails" section on the Marten distribution page (categories, what each one means
for an operator, and the three surfaces the reason shows up on) plus "Agent Start Retries", with a
cross-reference from the Polecat page.
🤖 Generated with Claude Code
https://claude.ai/code/session_014DkTEuNQQdPp63WLY86KzC