Skip to content

WO-8: surface a paused shard's ShardFailure to the assignment plane and health checks #3638

Description

@jeremydmiller

Summary

WO-8, the Wolverine half of JasperFx/jasperfx#565 (implemented in JasperFx/jasperfx#567). Surface an ApplyEventException-stopped shard to the assignment/observer plane with a reason, so CritterWatch can alert on it and progress doesn't silently flatline. This was deliberately deferred to ship as a coordinated pair with the JasperFx.Events accessor, which has now landed.

What JasperFx.Events gives us

  • ISubscriptionAgent.FailureShardFailure? — a default interface member (non-breaking), set alongside Status when a shard pauses or stops and cleared on start/replay.
  • ShardFailure carries Category (ApplyEvent / EventSerialization / UnknownEventType / ProgressionOutOfOrder / Other), ExceptionType, RootExceptionType, Message, Detail (the full text ShardState.PauseReason has always carried), Event (sequence, event type, stream, tenant, version when known) and OccurredAt. It is a plain serializable record, not an Exception, so it survives the hop to the assignment plane and into a UI.
  • ShardStateTracker.CurrentState(...) / TryGetCurrentState(...) / CurrentStates() — a synchronous snapshot for pollers that aren't subscribed.
  • ShardState.Failure rides along on the published paused/stopped state, and Marten/Polecat persist the classified fields onto the extended progression row (Implement IEventFailureContext + extended progression failure columns (jasperfx#565) marten#5048, Implement IEventFailureContext + extended progression failure columns (jasperfx#565) polecat#368) so it is readable even when the publishing node is down.

Proposed work in Wolverine

1. Delegate the reason on the wrapper. EventSubscriptionAgent.Status already delegates to the live inner agent (GH-3519); do the same for the reason:

public ShardFailure? Failure => _innerAgent?.Failure;

Expose it on IEventSubscriptionAgent so CritterWatch can read it through the agent surface it already resolves via IEventSubscriptionAgentFamily.FindAgentUriAsync.

2. Make the health check say something actionable. EventSubscriptionAgent.CheckHealthAsync currently returns a fixed string:

if (Status == AgentStatus.Paused)
    return HealthCheckResult.Unhealthy($"Projection {Uri} paused due to errors");

With the failure in hand that becomes the category, the failing event sequence and the root exception type — the difference between an alert an operator can act on and one they have to go dig for.

3. Stop auto-restarting failures that cannot self-heal. This is the part worth the most. The stall detector auto-restarts after MaxConsecutiveStallsBeforeRestart stalled health checks. A shard paused on ApplyEvent, EventSerialization or UnknownEventType will fail on the same event every time it restarts, so the restart loop is pure churn that also keeps resetting the operator's view. Gate the auto-restart on the category: retry Other (a transient database blip is exactly what auto-restart is for), and for the per-event categories surface the failure instead and leave it stopped. ProgressionOutOfOrder means two processes are on the same shard — restarting makes that worse, not better.

4. Carry it into the observer/assignment plane so CritterWatch renders the reason next to the paused shard rather than just the status.

Notes

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