Repository navigation
Flush EF Core outbox before writing the HTTP response (GH-2917) - #2920
Merged
Merged
Conversation
… after the response Adds a failing reproduction for #2917. /issue2917 is an EF Core [Transactional] endpoint that returns (IResult, OutgoingMessages). EF Core's Eager transaction middleware (EnrollDbContextInTransaction) wraps the rest of the chain — including the WriteJsonAsync response postprocessor — in a try block ending with efCoreEnvelopeTransaction.CommitAsync(), and CommitAsync flushes the outbox AFTER the DB commit. So the cascading message is only sent after the HTTP response is written; an Alba TrackActivity().ExecuteAndWaitAsync(...) session (no explicit wait condition) completes when the client receives the response and finishes empty. - tracked_session_should_capture_the_transactional_outbox_message: FAILS today ("No activity detected"), demonstrating the bug. - workaround_with_explicit_wait_condition: PASSES, proving the message is in fact sent + received — just after the response — when an explicit wait condition keeps the session open. The fix (response written after the transaction commit + outbox flush) follows in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ponse (GH-2917) EF Core's Eager transaction middleware previously committed the transaction and flushed the outbox inside EnrollDbContextInTransaction.CommitAsync emitted at the END of its try block, i.e. AFTER the response-writing postprocessor it wraps. So a transactional HTTP endpoint wrote its response (and the Alba client received it) before the cascading outbox messages were sent, leaving an IHost.TrackActivity().ExecuteAndWaitAsync(...) session empty unless the caller added an explicit WaitForMessageToBeReceivedAt condition. Reorder so the commit + flush runs before the response is written: - EnrollDbContextInTransaction no longer emits CommitAsync at the end of its wrap; it just begins the transaction and provides the try/catch (rollback) wrapper. - A new CommitEfCoreEnvelopeTransaction postprocessor performs the commit + flush. EFCorePersistenceFrameProvider adds it right after the SaveChanges postprocessor, which is before the response writer that Wolverine.Http appends during codegen, so the generated try block now reads: handler; SaveChanges; commit+flush; WriteResponse. This keeps the existing ordering guarantees: the flush still runs AFTER the EF Core commit (so it does not reintroduce the flush-before-commit row-stranding bug guarded by Bug_efcore_outbox_flush_before_commit), and a failed commit now rolls back and never produces a success response (the response is no longer written before commit). Rollback-after-commit is a no-op because EF Core clears CurrentTransaction on commit. Scope: the non-multi-tenant Eager path (EnrollDbContextInTransaction). Lightweight mode (standalone FlushOutgoingMessages) and the multi-tenant StartDatabaseTransactionForDbContext path are unchanged. Message-handler chains using EnrollDbContextInTransaction are unaffected in practice (no response writer; the commit postprocessor remains the last frame). Validated: full Wolverine.Http.Tests green (771 passed), including Bug_efcore_outbox_flush_before_commit and the GH-2917 regression test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The multi-tenant path (StartDatabaseTransactionForDbContext + CreateTenantedDbContext) had the same response-before-flush ordering as the single-DbContext path, plus a flush-before-commit: StartDatabaseTransactionForDbContext committed the DbContext transaction at the END of its try block (after the response writer it wraps), and because it was not IFlushesMessages the chain added a standalone FlushOutgoingMessages postprocessor that ran AFTER the response writer and BEFORE that raw commit. Apply the same reordering rule as the single-DbContext fix: - StartDatabaseTransactionForDbContext no longer commits at the end of its wrap; it just begins the transaction and provides the try/catch rollback wrapper. - A new CommitTenantedDbContextTransaction postprocessor commits the DbContext transaction and then flushes the MessageContext, added right after SaveChanges so it runs before the response writer. It implements IFlushesMessages so the chain does not also add a standalone FlushOutgoingMessages (avoiding a double flush and the flush-before-commit ordering). Net generated order for multi-tenant chains is now: handler; SaveChanges; commit; flush; WriteResponse - matching the single-DbContext path. This also fixes the multi-tenant flush-before-commit ordering as a side effect. Validated: Wolverine.EntityFrameworkCore + Wolverine.Http.Tests build clean; the single-DbContext GH-2917 regression test and Bug_efcore_outbox_flush_before_commit still pass. The multi-tenant runtime paths (tenant databases) are validated by the EfCoreTests.MultiTenancy suite in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Member
Author
|
Follow-up commit
Net generated order for multi-tenant chains is now |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2917.
Problem
With Wolverine.Http + EF Core's transactional outbox, a transactional endpoint that returns cascading messages would leave an
IHost.TrackActivity().ExecuteAndWaitAsync(...)session empty (noSent/MessageSucceededrecords) unless the caller added an explicitWaitForMessageToBeReceivedAtcondition.Root cause (deterministic ordering, not a flaky race): EF Core's Eager transaction middleware
EnrollDbContextInTransactionwraps the rest of the chain intry { … }and emitsefCoreEnvelopeTransaction.CommitAsync()at the end of the try block.CommitAsynccommits the DB transaction and then flushes the outbox (sends the cascading messages). But the response-writing postprocessor (WriteJsonAsync) is inside that try block, beforeCommitAsync. So the generated endpoint wrote the HTTP response → the Alba client received it andExecuteAndWaitAsyncsaw no in-flight activity and returned → then the outbox was flushed. Tracking missed the send. (Marten is unaffected because it flushes via aSaveChangessession listener, before the response.)Fix
Reorder so the transaction commit + outbox flush run before the response is written:
EnrollDbContextInTransactionno longer emitsCommitAsync()at the end of its wrap — it just begins the transaction and provides thetry/catchrollback wrapper.A new
CommitEfCoreEnvelopeTransactionpostprocessor performs the commit + flush.EFCorePersistenceFrameProvideradds it right afterSaveChanges, which is before the response writer Wolverine.Http appends during codegen. The generated try block now reads:This preserves existing guarantees:
Bug_efcore_outbox_flush_before_commitstays green).200before the transaction committed). Rollback-after-commit is a no-op because EF Core clearsCurrentTransactionon commit.Scope: the non-multi-tenant Eager path (
EnrollDbContextInTransaction). Lightweight mode (standaloneFlushOutgoingMessages) and message-handler chains are unaffected. The multi-tenantStartDatabaseTransactionForDbContextpath still has the old ordering — a follow-up commit will extend the same reordering there.Validation
issue_2917_trackactivity_transactional_outboxpasses deterministically (no explicit wait condition).Bug_efcore_outbox_flush_before_commitstill passes (no flush-before-commit regression).Wolverine.Http.Tests: 771 passed, 10 skipped, 0 failed (EF Core + Marten HTTP).EfCoreTests,EfCoreTests.MultiTenancy,PersistenceTestscompile clean.🤖 Generated with Claude Code