Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. 📝 WalkthroughWalkthroughThe PR adds a WebSocket reader-draining utility and calls it from synchronous TTS cleanup paths. Tests cover close-frame handling, socket shutdown, reader-thread termination, failure handling, and legacy and current TTS sessions. ChangesWebSocket cleanup
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~25 minutes Change: Bug fix Merge Risk: ⚪ Minimal · up to The synchronous TTS cleanup now drains WebSocket readers before session closure, with coverage for normal, failing, and real-socket behavior. No actionable merge risk remains. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
…anism notes; real-socket test
Symptom
Intermittent
[SSL: WRONG_VERSION_NUMBER],record layer failure, or[X509] PEM liberrors on the WebSocket session opened right after a completed sync WebSocket TTS session (client.tts.stream_websocket(...)and legacyWebSocketSession.tts(...)). Linux only; worse on loaded hosts. The async client is unaffected.Mechanism
httpx-ws's sync
WebSocketSessionreads on a background thread and closes the socket from the calling thread (close()writes the close frame, thenstream.close()).A read that has not started yet is harmless: CPython's
SSLSocket._real_closesets_sslobj = Nonebefore closing the fd, so a later read falls through tosocket.recvon fd-1and getsEBADF.The culprit is the read that is already in flight. On Linux,
close()does not interrupt a thread blocked inrecv(). OpenSSL (read_ahead off) fetches a TLS record header and body with separateread(fd)calls on the fd number cached in its BIO. When the server's close-frame reply arrives, the secondread()lands on the next connection, which has reused the same fd number, and that new session fails during its TLS handshake / 101 read. macOS wakes the blocked reader withEBADFonclose(), which is why it is unaffected there.Fix
fishaudio.core._ws_utils.drain_reader(ws), called in afinallybefore leaving thewith connect_ws(...)block in both sync paths (fishaudio/resources/tts.pyandfish_audio_sdk/websocket.py). It:CloseConnection(1000)viaws.send(...), so the server still sees a clean 1000 close (not 1006) and wsproto moves toLOCAL_CLOSING, which makes httpx-ws's laterclose()skip its own frame;shutdown(SHUT_RDWR)on the socket fromws.stream(fallback:response.extensions["network_stream"]), which wakes the in-flight read with EOF;_background_receive_task(5 s timeout; only reached when the reader is stuck, where httpx-ws's own untimed join would hang afterwards anyway).Everything is best-effort; failures are logged at debug. The sender future is still awaited/raised as before. Async paths untouched.
Validation
GCP us-central1 Linux VM (Python 3.12, httpx-ws 0.9.0), 8 parallel processes x 10 sessions each, per batch:
new0/80,legacy0/80,new0/80,legacy0/80)The remaining non-SSL failures in those batches (19/320,
WebSocket disconnected unexpectedly/WebSocketErr) are a separate server-side issue that also appears without this patch.Unit tests: helper ordering (close frame, shutdown, join), stream fallback, send failure, shutdown failure, missing socket/thread, debug logging on unexpected errors; both sync paths call the helper before
__exit__(asserted onmock_callsorder); and a realhttpx_ws.WebSocketSessionover asocket.socketpair()that checks the reader thread exits, nothreading.excepthookfires, state isLOCAL_CLOSING, and the peer receives a 1000 close frame before FIN. Passes on Python 3.9 (httpx-ws 0.7.2), 3.12 (0.8.1) and 3.13 (0.9.0).Residual risk
If httpx-ws has already closed the socket on one of its own error paths (keepalive ping timeout, or a sender
WriteErrortriggering itsclose()), the fd is already freed,shutdown()fails withEBADF, and the in-flight read can still race. That can only be fixed inside httpx-ws'sclose()(shut down / join before closing the fd). Upstream fix: frankie567/httpx-ws#150. Once that lands and we bump the dependency, this helper becomes redundant but harmless.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit
Bug Fixes
Tests