GH-3519: Isolate agent start failures so one wedged agent doesn't skip its siblings - #3535
Merged
Merged
Conversation
…p its siblings In DurabilityMode.Solo, startAllAgentsAsync iterated each family's agents and awaited StartAgentAsync per agent inside a single per-family try/catch. When one agent threw (e.g. an event-subscription shard that loses a first-assignment startup race with high-water detection), the throw broke out of the family loop, so every sibling agent listed after the wedged one was never started that tick -- and it repeated every 30s reevaluation. In the wild this showed up as "two of three projection shards dead on that boot; only one ran." Wrap each agent's StartAgentAsync in its own try/catch (log + continue) so a single failing agent only blocks itself. The wedged agent is still retried on the next tick; it just no longer takes healthy siblings down with it. Regression test in CoreTests asserts a sibling listed after a failing agent still starts. This contains the blast radius of the startup race; the underlying JasperFx high-water start race (the reason the wedged shard fails at all) is tracked separately and still requires the CritterWatch repro to validate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKAxzuZ36VP6UPcTQf3MUs
This was referenced Jul 20, 2026
jeremydmiller
added a commit
that referenced
this pull request
Jul 20, 2026
… it (#3550) Follow-up to GH-3535 (start-failure isolation) and GH-3520 (rebuild/rewind resume). On multi-store, Wolverine-managed Marten hosts a projection shard can start successfully and then die underneath its EventSubscriptionAgent wrapper -- a lost first-assignment start race that wedged daemon-side, a daemon-side stop, or an execution-loop fault. Two gaps kept it dead for the process lifetime: 1. EventSubscriptionAgent.Status latched Running once started, so a shard that stopped underneath the wrapper kept reporting Running. NodeAgentController only restarts agents it can see are non-Running, so it saw nothing to fix. 2. NodeAgentController.StartAgentAsync short-circuited on a bare Agents.ContainsKey, treating any registered agent as healthy forever. The recurring Solo reevaluation therefore never resurrected a registered-but-dead agent -- it just sat in the observable 30s retry loop reporting a stale Running while its high-water climbed. Fix both: Status now delegates to the live inner daemon agent once started (and still reads Stopped before start / after an explicit stop), and StartAgentAsync re-drives a registered agent whose shard has Stopped -- stopping it first to release any lingering daemon-side shard state -- while leaving genuinely Running and deliberately Paused (error backoff / blue-green gate) agents untouched. Red-green regression test agent_restart_when_wedged in CoreTests. Also bumps to 6.22.0-alpha.3. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Contributes to #3519
Scope
This PR contains the blast radius of the event-subscription startup race described in #3519. It does not claim to fix the underlying race (see "What this does not fix" below).
The bug
In
DurabilityMode.Solo,NodeAgentController.startAllAgentsAsynciterated each agent family's agents and awaitedStartAgentAsyncper agent inside a single per-familytry/catch:When one agent's start throws — e.g. an event-subscription shard that loses a first-assignment startup race with high-water detection, surfacing as
AgentStartingException→System.Exception: Unable to start a subscription agent for Identity: …— the throw breaks out of the family loop. Every sibling agent listed after the wedged one is never started that tick, and because the 30s reevaluation re-runs the same ordering, those siblings stay dead for the life of the process.This matches the field report in #3519: "two of three shards dead on that boot; only one ran … its progression frozen while its store's high-water climbed thousands of events."
The fix
Isolate each agent's start in its own
try/catch(log + continue) so a single failing agent only blocks itself:AllKnownAgentsAsync()failure still logs and skips to the next family (unchanged semantics).StartAgentAsync(uri)now catches independently — a wedged agent is logged and skipped, and its healthy siblings in the same family still start.Test
CoreTests/Runtime/Agents/agent_start_isolation.csbuilds a Solo-modeNodeAgentControllerwith a fake family of three agents where the middle one throws on start, and asserts the sibling listed after the failing one still lands inAgents. Verified red before the change (fails on exactly that assertion) and green after.What this does not fix
The reason the wedged shard fails to start in the first place is a startup race inside the JasperFx projection daemon (high-water detection not yet primed for a
SubscribeFromPresentshard during the first assignment evaluation). That is tracked separately and still requires the CritterWatch multi-store repro to validate a root-cause fix. This PR is the defensive, independently-valuable half: the race can no longer wedge unrelated sibling agents on the node.🤖 Generated with Claude Code
https://claude.ai/code/session_01FKAxzuZ36VP6UPcTQf3MUs