Repository navigation
Conversation
A publisher reconnects on ConnectionClosed and dies on anything else. websockets aborts the transport before raising it, and on an asyncio transport whose connection_lost already ran the abort raised AttributeError (#106). Any exception from a connection that is no longer open is now raised as the connection's end, chained from the original. A send that loses the race with the reader retrieves its failed reply, so asyncio no longer logs it as never retrieved (#154). The empty-deque IndexError of the same race, fixed in 0.23.0, gets its test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been deployed
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.
Fixes #106 and #154.
A publisher's contract is to reconnect on
ConnectionClosed. A send on a connection that is closing or closed raised something else in two ways, and a publisher that catches onlyConnectionCloseddied on it.#106:
AttributeErrorfrom websockets' abortOn a connection that is no longer open, websockets'
send_contextcallstransport.abort()before raisingConnectionClosed. After a close with data still buffered, asyncio's selector transport has already runconnection_lostwithout counting it, so its_loopis None.abort()then raisesAttributeError: 'NoneType' object has no attribute 'call_soon', and the send raised that. websockets 17.1 still has the unguarded abort.Fix (
_publish.py,submit_many, which all four send methods go through): handles anExceptionfromconnection.send, other thanConnectionClosed, when the protocol state is no longerOPEN.close_exc, chained from the original, or the refusal its close code carries (_refusal, as theConnectionClosedbranch already did).BaseException(cancellation) is untouched.#154: the reader drains the pending replies first
The
IndexError('pop from an empty deque')itself is already fixed on main._unowedremoves by identity; it shipped in 0.23.0 with #167, untested. Two things were still wrong:_unowednow retrieves it.StreamRetired) is covered on main. The send's ownConnectionClosedcarries the same received close frame, and_refusalmaps it.Tests:
tests/test_publish_closed.pyThese run on a real loopback server and publisher, with only the faulty piece injected.
TestTheAbortItself::test_a_failing_abort_raises_connection_closed[send|submit|send_many|submit_many]closes the connection and makestransport.abortraise, as asyncio's does. It assertsConnectionClosed, caused by theAttributeError.TestTheReaderGotThereFirst::test_the_send_raises_connection_closedforces publish: a send racing the connection's close raises IndexError('pop from an empty deque') instead of ConnectionClosed #154's interleaving from a hook inconnection.send: close, wait for the reader to drain, then send. It assertsConnectionClosed, and that asyncio reports nothing never retrieved.On unfixed main (
26e903d) all 5 fail:Falsified. Each break below made tests fail; each was restored and passes:
_unowed_unowedback toself._pending.pop(), the 0.22 codeIndexError: pop from an empty deque, #154's exact errorAttributeErrorThe new tests take about 2 s in all.
Full suite, locally:
STREAMCAST_REQUIRE_S3=1 STREAMCAST_REQUIRE_INTEROP=1 STREAMCAST_REQUIRE_POSTGRES=1 pytestgave 959 passed, 2 failed, 2 skipped. Neither failure is in the publish path.test_maintain.py::TestFlushing::test_a_served_row_reaches_the_published_tablefails the same way on unmodifiedmainon this box.test_snapshot_readers.py::TestAReader::test_the_event_loop_runs_while_it_readsis a tick-rate timing check (212 ticks in 4.78s). Run alone here it passed 2 of 3 times. The box has 2 vCPUs.test_node.pyis left to CI.No critical review was run. This is routine error mapping on the client side. It touches no durable state, no frames and no ordering, and the set of futures owed a reply is unchanged: a send that raised was already removed.
Downstream
Not breaking, so it should ship as a patch. No consumer pins move. A publisher that also caught
AttributeErrororIndexErroras a workaround can drop that.🤖 Generated with Claude Code