Skip to content

fix(publish): a send on a closed connection raises ConnectionClosed - #178

Open
nhobin219 wants to merge 1 commit into
mainfrom
fix/publish-closed-raises-connection-closed
Open

nhobin219 wants to merge 1 commit into
mainfrom
fix/publish-closed-raises-connection-closed

Conversation

@nhobin219

Copy link
Copy Markdown
Owner

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 only ConnectionClosed died on it.

#106: AttributeError from websockets' abort

On a connection that is no longer open, websockets' send_context calls transport.abort() before raising ConnectionClosed. After a close with data still buffered, asyncio's selector transport has already run connection_lost without counting it, so its _loop is None. abort() then raises AttributeError: '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 an Exception from connection.send, other than ConnectionClosed, when the protocol state is no longer OPEN.

  • It raises the connection's own close_exc, chained from the original, or the refusal its close code carries (_refusal, as the ConnectionClosed branch already did).
  • On an open connection the exception propagates unchanged.
  • 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. _unowed removes by identity; it shipped in 0.23.0 with #167, untested. Two things were still wrong:

Tests: tests/test_publish_closed.py

These run on a real loopback server and publisher, with only the faulty piece injected.

On unfixed main (26e903d) all 5 fail:

E       AttributeError: 'NoneType' object has no attribute 'call_soon'      (x4)
E       AssertionError: the dropped reply's failure is retrieved
E         Left contains one more item: {'message': 'Future exception was never retrieved', 'exception': ConnectionClosedOK(...)}

Falsified. Each break below made tests fail; each was restored and passes:

what I broke result
no retrieval in _unowed the race test fails
_unowed back to self._pending.pop(), the 0.22 code the race test fails with IndexError: pop from an empty deque, #154's exact error
the closed-state branch removed all 4 abort tests fail with AttributeError

The new tests take about 2 s in all.

Full suite, locally: STREAMCAST_REQUIRE_S3=1 STREAMCAST_REQUIRE_INTEROP=1 STREAMCAST_REQUIRE_POSTGRES=1 pytest gave 959 passed, 2 failed, 2 skipped. Neither failure is in the publish path.

  • test_maintain.py::TestFlushing::test_a_served_row_reaches_the_published_table fails the same way on unmodified main on this box.
  • test_snapshot_readers.py::TestAReader::test_the_event_loop_runs_while_it_reads is a tick-rate timing check (212 ticks in 4.78s). Run alone here it passed 2 of 3 times. The box has 2 vCPUs.
  • Node is not installed here, so test_node.py is 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 AttributeError or IndexError as a workaround can drop that.

🤖 Generated with Claude Code

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

No deployments
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