Follow-up from the wolverine#3519 investigation (Wolverine-side recovery landed in wolverine#3550; the de-masking was #536). This tracks the daemon-side root cause that #536 only surfaced a message for.
Where
src/JasperFx.Events/Daemon/JasperFxAsyncDaemon.cs, tryStartAgentAsync (~lines 220-290):
try
{
...
await agent.StartAsync(request).ConfigureAwait(false); // ~273
agent.MarkHighWater(highWaterMark);
_agents = _agents.AddOrUpdate(agent.Name.Identity, agent); // ~276 — only reached on success
syncTenantPolling();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error trying to start agent {ShardName}", agent.Name.Identity);
return false; // ~282 — swallowed
}
Two problems
1. A failed start can leave an orphaned agent that is never stopped.
If agent.StartAsync(request) throws after spinning up any background execution / acquiring the shard's progression or advisory lock, control jumps to the catch and returns false without registering the agent in _agents and without stopping/disposing it. The registry never knows about it, so no later StopAgentAsync can reach it. On multi-store / Wolverine-managed hosts this is a candidate explanation for the permanent wedge in wolverine#3519: the first start loses a race with high-water detection, faults mid-start, and the orphan keeps holding shard state so every subsequent retry (fresh agent object) keeps failing to acquire — the randomized victim never recovers for the process lifetime. startContinuousShardAsync only disposes the agent when tryStartAgentAsync returns false for the idempotent reason, not when it faulted mid-start.
2. The failure cause is swallowed, forcing callers to reverse-engineer it.
tryStartAgentAsync returns a bare bool, collapsing "already running (idempotent skip)", "start faulted", and "positioning failed" into a single false. StartAgentAsync(ShardName) then has to guess the reason via describeStartFailure(name) (added in #536) by re-inspecting daemon state after the fact. The actual caught exception — the real cause — is only logged, never propagated, so the guess can be wrong.
Suggested direction
- On a mid-start fault, stop/dispose the partially-started agent (and reconcile
syncTenantPolling / any activated tenant) before returning, so no orphan can hold the shard.
- Consider distinguishing "idempotent skip" from "faulted" in the return (e.g. an enum/result, or rethrow the captured exception through a typed
ShardStartException at the tryStartAgentAsync boundary) so StartAgentAsync(ShardName) can attach the real cause instead of reconstructing a probable one.
Repro
JasperFx/CritterWatch MultiStoreHost sample consistently reproduces the wolverine#3519 wedge (main Marten store with UseWolverineManagedEventSubscriptionDistribution = true + two AddMartenStore<T> ancillary stores, three async shards, DurabilityMode.Solo); the randomized victim differs per boot.
Filed from the wolverine#3519 follow-up.
Follow-up from the wolverine#3519 investigation (Wolverine-side recovery landed in wolverine#3550; the de-masking was #536). This tracks the daemon-side root cause that #536 only surfaced a message for.
Where
src/JasperFx.Events/Daemon/JasperFxAsyncDaemon.cs,tryStartAgentAsync(~lines 220-290):Two problems
1. A failed start can leave an orphaned agent that is never stopped.
If
agent.StartAsync(request)throws after spinning up any background execution / acquiring the shard's progression or advisory lock, control jumps to the catch and returnsfalsewithout registering the agent in_agentsand without stopping/disposing it. The registry never knows about it, so no laterStopAgentAsynccan reach it. On multi-store / Wolverine-managed hosts this is a candidate explanation for the permanent wedge in wolverine#3519: the first start loses a race with high-water detection, faults mid-start, and the orphan keeps holding shard state so every subsequent retry (fresh agent object) keeps failing to acquire — the randomized victim never recovers for the process lifetime.startContinuousShardAsynconly disposes the agent whentryStartAgentAsyncreturnsfalsefor the idempotent reason, not when it faulted mid-start.2. The failure cause is swallowed, forcing callers to reverse-engineer it.
tryStartAgentAsyncreturns a barebool, collapsing "already running (idempotent skip)", "start faulted", and "positioning failed" into a singlefalse.StartAgentAsync(ShardName)then has to guess the reason viadescribeStartFailure(name)(added in #536) by re-inspecting daemon state after the fact. The actual caught exception — the real cause — is only logged, never propagated, so the guess can be wrong.Suggested direction
syncTenantPolling/ any activated tenant) before returning, so no orphan can hold the shard.ShardStartExceptionat thetryStartAgentAsyncboundary) soStartAgentAsync(ShardName)can attach the real cause instead of reconstructing a probable one.Repro
JasperFx/CritterWatch
MultiStoreHostsample consistently reproduces the wolverine#3519 wedge (main Marten store withUseWolverineManagedEventSubscriptionDistribution = true+ twoAddMartenStore<T>ancillary stores, three async shards,DurabilityMode.Solo); the randomized victim differs per boot.Filed from the wolverine#3519 follow-up.