Skip to content

feat(#367,#368,#369): JasperFx.Events 2.36.1 — classified shard failures, widened natural key contract, drain-timeout docs - #371

Merged
jeremydmiller merged 1 commit into
mainfrom
jasperfx-2361-batch
Jul 26, 2026
Merged

feat(#367,#368,#369): JasperFx.Events 2.36.1 — classified shard failures, widened natural key contract, drain-timeout docs#371
jeremydmiller merged 1 commit into
mainfrom
jasperfx-2361-batch

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Bumps the JasperFx family 2.34.0 → 2.36.1 and lands the three Polecat issues that ride on that line. They are batched because the natural key half is source breakingNaturalKeyEventMapping.Extractor widens from Func<object, object?> to Func<IEvent, object?>, so the bump does not compile without the call-site change.

Closes #367, closes #368, closes #369.

#369 — natural key extraction on the widened contract (jasperfx#569 / #571)

  • NaturalKeyProjection.QueueOperationForEvent passes the whole IEvent to mapping.Extractor instead of e.Data. One call site, shared by the inline append path and the Natural-key lookup table not repopulated on projection rebuild (parity with marten#4788) #259 rebuild re-emit path.
  • Fixed the error message in EventOperations.FindNaturalKeyDefinition, which told users to "Configure a natural key via NaturalKey() in a SingleStreamProjection registration" — an API that exists in neither Polecat nor JasperFx.Events. It now names [NaturalKey]/[NaturalKeySource] and the real escape hatch, NaturalKeyFor().
  • Regression tests (Bug_369_natural_key_source_discovery) for what Polecat inherits from upstream: an IEvent<T>-parameter [NaturalKeySource] used to yield no mapping at all — silently, no error, no log — so pc_natural_key_X was simply never written for that event type. Pinned live and after a rebuild, since Polecat's rebuild path is its own code. Plus the explicit NaturalKeyFor(x => x.SetBy/SetByEvent) registration, which was unreachable dead code until jasperfx#571.
  • Docs: a NaturalKeyFor() section in events/natural-keys.md, the loud-failure behaviour, and the constraint the attribute path now enforces — the key has to be a function of the event alone, because the lookup table is maintained inline at append time where no prior aggregate exists under an Async snapshot lifecycle.

#368IEventFailureContext + classified failure columns (jasperfx#565)

The daemon deliberately has no fallback type-name sniffing: a store's exception declares its own category, or the failure classifies as Other with no event details. Both halves land here.

Part 1 — the read-path exceptions. Polecat's event loader raised bare InvalidOperationExceptions, so every serialization or unknown-type failure reached ShardFailure as Other. Two new exceptions in Polecat.Exceptions implement IEventFailureContext:

  • EventDeserializationFailureExceptionEventSerialization, carrying the sequence and the store's event type alias (the type column, not the assembly-qualified dotnet_type — the alias is what a client can act on).
  • UnknownEventTypeExceptionUnknownEventType, kept distinct on purpose: a missing registration or a rollback is a deployment fix, not a data fix.

Part 2 — four extended-progression columns behind the existing EnableExtendedProgressionTracking gate: failure_category (the enum name, never the ordinal), failure_event_sequence, failure_event_type, failure_event_tenant_id. No column for the reason text — ShardFailure.Detail is exactly what pause_reason has always carried.

The failure columns follow a different write rule from the rest of extended progression: written when the state carries a Failure, cleared on a ShardAction.Started that has none (a recovered shard must stop reporting the reason it paused an hour ago), and otherwise left alone. That last case is load-bearing — SubscriptionAgent publishes a plain Stopped right behind a Paused, and an unconditional write would erase the reason microseconds after recording it. AllProjectionProgress rehydrates ShardState.Failure from the columns so a poller gets the same shape as a live observer.

The columns are nullable and additive, so no backfill is needed; stores that already have extended tracking on do need the migration, and until it runs the write degrades to no telemetry rather than failing anything (extended progression is best-effort by contract).

#367StopAndDrainTimeout docs (jasperfx#564)

New "Graceful Shutdown and the Drain Timeout" section in events/projections/async-daemon.md: what the timeout bounds (the in-flight page plus the progression flush, on all three stop paths), the ProgressionProgressOutOfOrderException it prevents when the drain is cut short, pairing a raised value with HostOptions.ShutdownTimeout and terminationGracePeriodSeconds, why a deployment might instead lower it, and what opting out with Timeout.InfiniteTimeSpan costs.

Tests

New: event_failure_context_tests (exception contract + ShardFailure.For classification through wrapping and AggregateException), shard_failure_progression_columns_tests (write / clear / preserve / rehydrate / never-insert), Bug_369_natural_key_source_discovery, plus two event-loader cases covering the classified throw sites. event_store_instrumentation_tests extended for the four new columns.

…res, widened natural key contract, drain-timeout docs

Bumps the JasperFx family 2.34.0 -> 2.36.1 and lands the three Polecat
issues that ride on that line. They are batched because the natural key
half is SOURCE BREAKING -- NaturalKeyEventMapping.Extractor widens from
Func<object, object?> to Func<IEvent, object?> -- so the bump does not
compile without the call-site change.

#369 (jasperfx#569/#571). NaturalKeyProjection.QueueOperationForEvent
passes the whole IEvent to mapping.Extractor instead of e.Data. One call
site, shared by the inline append path and the #259 rebuild re-emit path.
Widening to the event is what makes an IEvent<T> [NaturalKeySource]
bindable at all: discovery used to drop those silently -- no mapping, no
error, no log -- so pc_natural_key_X was never written for that event
type and a lookup after a key change came back null. Also fixes the error
message in FindNaturalKeyDefinition, which pointed users at a NaturalKey()
API that exists in neither Polecat nor JasperFx.Events; NaturalKeyFor() is
the real escape hatch, and it became reachable in jasperfx#571.

#368 (jasperfx#565). The daemon deliberately has no fallback type-name
sniffing: a store's exception declares its own category, or the failure
classifies as Other with no event details. Both halves land here.

  Part 1 -- Polecat's event loader raised bare InvalidOperationExceptions,
  so every read-side failure reached ShardFailure as Other. Two new
  exceptions implement IEventFailureContext:
  EventDeserializationFailureException (EventSerialization, carrying the
  sequence and the store's type alias rather than the assembly-qualified
  dotnet_type) and UnknownEventTypeException (UnknownEventType, kept
  distinct because a missing registration is a deployment fix, not a data
  fix).

  Part 2 -- four extended-progression columns behind the existing
  EnableExtendedProgressionTracking gate: failure_category (the enum NAME,
  never the ordinal), failure_event_sequence, failure_event_type,
  failure_event_tenant_id. No column for the reason text -- ShardFailure
  .Detail is exactly what pause_reason has always carried. They follow a
  different write rule from the rest: written when the state carries a
  Failure, CLEARED on a ShardAction.Started without one, otherwise LEFT
  ALONE. That last case is load-bearing -- SubscriptionAgent publishes a
  plain Stopped right behind a Paused, and an unconditional write would
  erase the reason microseconds after recording it. AllProjectionProgress
  rehydrates ShardState.Failure so a poller gets the same shape as a live
  observer.

#367 (jasperfx#564). Docs only: a "Graceful Shutdown and the Drain
Timeout" section covering what StopAndDrainTimeout bounds, the
ProgressionProgressOutOfOrderException it prevents when the drain is cut
short, pairing it with HostOptions.ShutdownTimeout and
terminationGracePeriodSeconds, and what opting out costs.

Full suite green: 1574 passed on net10, plus AspNetCore (53) and EF Core (37).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETF4fye5MJk3kGX5kunrRk
@jeremydmiller
jeremydmiller merged commit bd6a91f into main Jul 26, 2026
7 checks passed
@jeremydmiller
jeremydmiller deleted the jasperfx-2361-batch branch July 26, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant