Skip to content

JasperFxAsyncDaemon.StartAgentAsync throws a causeless 'Unable to start a subscription agent' — failed-start reasons are swallowed (startContinuousShardAsync bool ignored, silent false paths) #534

Description

@jeremydmiller

JasperFx.Events 2.30.1, hit while debugging JasperFx/wolverine#3519 (agent fails to start on a multi-store Marten host with Wolverine-managed distribution).

The public StartAgentAsync masks the real failure reason

JasperFxAsyncDaemon<TOperations,TQuerySession>.StartAgentAsync(ShardName, CancellationToken):

public async Task<ISubscriptionAgent> StartAgentAsync(ShardName name, CancellationToken token)
{
    await StartAgentAsync(name.Identity, token);
    if (_agents.TryFind(name.Identity, out var agent)) return agent;

    // Should not ever happen, but real life man
    throw new Exception("Unable to start a subscription agent for " + name);
}

When the start fails, the caller (e.g. Wolverine's EventSubscriptionAgent.StartAsync) gets only System.Exception: Unable to start a subscription agent for Identity: X:All — with no cause attached.

The information exists but is dropped along the way:

  • startContinuousShardAsync returns bool and the exact-identity branch of the string overload ignores the return value (await startContinuousShardAsync(shard); return;), so a failed start falls through to the generic throw with nothing to say.
  • The false paths themselves are uneven: tryStartAgentAsync's catch logs the exception at Error level (good), but the side-effect-version-gate failure and the idempotent path return false silently — and in our production repro no Error log appeared at all, so whichever path failed was one of the silent ones. We burned the whole diagnosis on "which invisible branch was it."

Ask

  1. Make startContinuousShardAsync (or the paths under it) carry the failure reason out — return a result object or stash the last-start-failure per shard — and include it in the wrapper's exception (throw new …("Unable to start …", lastFailure)).
  2. Log every false path at Warning+ with the shard identity, including the gate skip and the idempotent-already-running case (Debug is fine for that one).
  3. Since callers like Wolverine retry every 30s forever, an unchanged generic exception means the operator's log fills with identical stacks that never explain themselves.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions