Skip to content

Fix NoHandlerForEndpointException during concurrent saga chain compile#2556

Merged
jeremydmiller merged 1 commit intoJasperFx:mainfrom
Bishbulb:fix/handlergraph-saga-compile-race
Apr 27, 2026
Merged

Fix NoHandlerForEndpointException during concurrent saga chain compile#2556
jeremydmiller merged 1 commit intoJasperFx:mainfrom
Bishbulb:fix/handlergraph-saga-compile-race

Conversation

@Bishbulb
Copy link
Copy Markdown
Contributor

Summary

I ran into this running saga workloads under dynamic codegen. At cold-start, when two messages for the same saga type arrived concurrently, one would sometimes throw NoHandlerForEndpointException even though a valid handler was about to be assigned on the other thread. I traced it to HandlerGraph.resolveHandlerFromChain.

Root cause

SagaChain.DetermineFrames calls Handlers.Clear() as a side effect of codegen before the generated handler gets registered.
resolveHandlerFromChain checks HasDefaultNonStickyHandlers() outside the compile lock:

  else if (!chain.HasDefaultNonStickyHandlers())   // read outside the lock
  {
      throw new NoHandlerForEndpointException(messageType);
  }
  else
  {
      lock (_compilingLock) { /* compile */ }
  }

A concurrent second caller arriving while the first is mid-compile sees the temporarily-empty Handlers, HasDefaultNonStickyHandlers() returns false, and the throw happens (even though the first thread is about to assign chain.Handler)

Note that this is only reproducible under dynamic codegen because pre-generated code doesn't hit this path.

Fix

Move the full "handler present / compile / throw" decision inside _compilingLock, with a fast-path re-check on chain.Handler for threads that arrived after compilation finished.

@Bishbulb Bishbulb force-pushed the fix/handlergraph-saga-compile-race branch from 14d0403 to 14bb7b4 Compare April 21, 2026 15:52
@jeremydmiller jeremydmiller merged commit 2c9180a into JasperFx:main Apr 27, 2026
17 of 19 checks passed
jeremydmiller added a commit that referenced this pull request Apr 27, 2026
Release v5.33.0 includes:
- Fix #2602: leader split-brain via stale Postgres advisory lock (#2607)
- Port Polecat 2.x event store integration from Marten (#2598)
- Fix #2571: preserve context fields on scheduled-send wrap/unwrap (#2605)
- Add launchSettings.json to sample projects (#2600)
- gRPC: middleware weaving, validate convention, user exception mapping,
  bidirectional streaming, code-first codegen, new samples (#2565)
- Move non-sticky-handlers guard inside the compile lock (#2556)
- Allow RabbitMQ exchanges to be declared passive (#2574)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants