Repository navigation
fix: refuse OutboxResponse on a closed session, write the recorded outcome on config errors - #225
Merged
Merged
Conversation
…tcome on config errors, take correlation_id from headers
Benchmark gate✅ gate passed
Gated (fails the build): |
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 #214.
A follow-on row on a closed session
FastStream and FastAPI both tear
Dependsdown inside the handler call, and the response is published after that. With the session dependency the docs showed, the follow-on INSERT ran on a session whose transaction had already committed. SQLAlchemy opened a new transaction for it that nobody committed, so the row and its NOTIFY were rolled back. Meanwhile the inbound row was acked andpublishedrecorded success.OutboxFakePublisher._publishnow checkssession.in_transaction()and raises_OutboxConfigErrorwhen there's no open transaction. That follows the rule from the grilling for config errors: retry, and log at ERROR. The message names both fixes._OutboxConfigErrormoves fromsubscriber/usecase.pytoresponse.py, so the response publisher can raise it without a circular import.usecase.pystill imports the name.One limitation: a session that is still open but hasn't started a transaction, because the handler made no writes and nothing called
begin(), is refused too. The documented pattern opens its transaction up front, so it isn't affected.Where the session should live
I checked this against real Postgres before writing the docs. Middleware
consume_scoperuns insideh.call, so it closes before the publish just likeDepends. A broker middleware'son_receive/after_processedwrap the whole dispatch, response publish included. FastStream puts the ack middleware outermost, so a session committed inafter_processedcommits before the ack decision, and a failed commit nacks the inbound row.docs/usage/publisher.mdnow shows that middleware, with the handler taking the session throughContext().docs/usage/fastapi.mdno longer saysOutboxResponsecommits with aDependssession; it points FastAPI handlers atrouter.broker.publish(..., session=session)instead. I haven't checkedContext()inside FastAPI-wrapped handlers, so the FastAPI page doesn't suggest the middleware. The troubleshooting page has an entry for the new error.Config errors write the recorded outcome
A config error comes after the handler has run and unwinds through the ack middleware, which records a nack. The worker used to drop that nack and leave the row leased until expiry. With
max_deliveries=None, the handler then re-ran everylease_ttl_secondsforever.dispatch_onenow writes the recorded outcome inline and re-raises. The test broker still surfaces the error frompublish, and_worker_innerstill logs it at ERROR. The write is inline because a buffered row would get a secondtask_doneas the error unwinds. Both theconsume()docstring and the comment in_worker_innernow describe this behaviour (B15).Under
AckPolicy.MANUALthe capture middleware records the exception, soassert_state_setnacks and the row is retried rather than rejected. A test pins that.correlation_idfrom headersUpstream fills an empty
correlation_idwith the inbound id, soOutboxResponse(headers={"correlation_id": X})always hit the envelope's mismatch check after the handler succeeded, and the inbound row was retried.OutboxResponsenow takes the header value when the argument isn't passed.Tests
Each of these failed before the change:
Dependssession. Before, there was no error and the inbound row was acked. Now the row is retried once and an ERROR log names the transaction.correlation_idheader reaches the downstream handler.These pass on both the old and the new code:
AckPolicy.MANUALguard.Performance
The only addition on the hot path is one
in_transaction()call per response publish, and that call doesn't touch the database.just bench-checkpasses.Verified locally: 662 tests pass in docker at 100% coverage, and the full suite also passes on SQLAlchemy 2.0.0.
ruffis clean,tyshows the same 6 diagnostics asmain, and the docs build.