Repository navigation
fix flaky asgi tests and wait for the client's close frame in asgi websockets - #61
Merged
Merged
Conversation
…bsockets test_close_iterable_response raced against the server closing the iterable right after sending the last chunk. The iterable now holds the server until the test has checked that it's still open. test_close_handshake_server_initiated[asgi] failed when the server terminated the TCP connection before the client echoed the close frame: the echo then arrived on a closed socket and the kernel reset the connection. The ASGI adapter now waits for the client's disconnect after sending its close frame, up to close_timeout, before the app returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Motivation
Two tests failed intermittently, both in the ASGI (hypercorn) variants:
tests/test_asgi.py::test_close_iterable_response(failed CI run)tests/websocket/test_websockets.py::test_close_handshake_server_initiated[asgi], which failed occasionally when the whole suite ranChanges
test_close_iterable_response: race in the testOnce the server has written the last chunk, the iterator is exhausted and the adapter calls
close()on the iterable. The client can readbarbefore or after that happens, soassert not iterable.closedchecked a race. Atime.sleep(0.2)before that assertion makes it fail every time.Now the test's iterable waits on an event after its last packet, and the test sets the event once it has checked that the iterable is still open. The final
assert iterable.closedwas already safe: the adapter closes the iterable before it sends the end of the response body.test_close_handshake_server_initiated[asgi]: the close handshake didn't wait for the clientThis is the issue #50 fixed for Twisted serving. After the listener sends its close frame and returns, hypercorn terminates the TCP connection right away. When the client's close frame arrives on that closed socket, the kernel resets the connection. If the reset gets to the client before it reads,
recvraisesConnectionResetErrorinstead of returningb"". RFC 6455 section 5.5.1 says the server should close the TCP connection only after it has both sent and received a close frame.ASGIWebSocketAdapter.close()now records that the server sent its close frame.ASGIAdapter.handle_websocketawaitswait_closed(), which waits for the client'swebsocket.disconnectfor up toASGIWebSocketAdapter.close_timeout(5 seconds, the same as the Twisted channel'scloseTimeout). This happens on the event loop, so no listener thread is held. Frames the client sends in the meantime are dropped.close_timeoutfor clients that never echo the close frame. Four websocket tests left their client open, and each took 5 seconds to tear down, so they now callclient.close().Testing
test_close_handshake_server_initiatednow sleeps 0.1 s before echoing the close frame and 0.1 s before reading. Without the fix the asgi variant fails 5/5; with it, both variants pass 5/5.test_close_iterable_responsepasses with the injected sleep and 30/30 in a loop.🤖 Generated with Claude Code