fix(soniox): keep references to the prewarm and self-close tasks - #7051
fix(soniox): keep references to the prewarm and self-close tasks#7051Rehansanjay wants to merge 2 commits into
Conversation
Five `asyncio.create_task` calls in this module threw the task away. The event loop only holds a weak reference to a bare task, so it can be garbage collected before it finishes. `_Connection` self-closes from four places β `mark_non_current`, `unregister_stream`, and the `finally` of both the send and recv loops. Each one discarded the task that closes the WebSocket, so a collected task means the socket stays open. The class already stores its send, recv and keepalive tasks; these were the exception. They now go through `_schedule_close`, which also skips scheduling a second close while one is still in flight. `TTS.prewarm` had the ordering problem as well: its task calls `_current_connection`, which opens a new connection when there is none, and `aclose()` sets `__current_connection` to None. A prewarm in flight when the TTS closed would reconnect afterwards and leave a live WebSocket that nothing owns. It is now cancelled at the top of `aclose()`.
Two follow-ups from the review. `prewarm()` overwrote `_prewarm_task` unconditionally, so calling it twice dropped the only reference to the first task while it was still running. `aclose()` cancels just the latest one, which left the earlier attempt free to open a WebSocket after shutdown β the leak this PR is meant to close. It now starts a new task only when there is no live one, which is how `utils.ConnectionPool.prewarm` guards the same case. The prewarm failure log also interpolated the exception into the message body, where it cannot be redacted. It now goes through `exc_info`, as the send and recv loops in this file already do.
|
Both review points addressed in a1569b1. Repeated prewarm leaks a connection β correct, and it undercut the point of the PR. Prewarm errors bypass log redaction β moved to |
|
On the remaining open thread β the The earlier review on this series flagged the opposite problem: the prewarm failure interpolated the exception into the message body, where redaction cannot reach it. I moved it to Taken literally that leaves nowhere to put the exception, and it would put this call site out of step with the rest of the codebase: If the project does want a stricter rule for provider exceptions β a redacting formatter, or an |
The bug
Five
asyncio.create_taskcalls insoniox/tts.pydiscard the task they create. The event loop only holds a weak reference to a bare task, so it can be garbage collected before it finishes._Connectionself-close (four sites)The connection closes itself from
mark_non_current,unregister_stream, and thefinallyof both_send_loopand_recv_loop:If that task is collected,
aclose()never runs and the WebSocket stays open.Worth noting the class already gets this right for its own loops β
_send_task,_recv_taskand_keepalive_taskare all stored on the instance. The close tasks were the exception.TTS.prewarmβ an ordering problem too_current_connectionopens a new_Connectionwhen there is none, andaclose()sets__current_connection = Noneafter closing. So a prewarm still in flight when the TTS is closed reconnects afterwards, leaving a live WebSocket that nothing owns and nothing will close.The fix
The four self-close sites go through a small helper that keeps the task and skips scheduling a second close while one is still running:
prewarmstores its task, andaclose()cancels it before tearing the connection down β the same shape asopenai/tts.py, and whatutils.ConnectionPool.prewarm()does for the plugins that use it.No behaviour change on the success path.
Checks
ruff checkandruff format --checkpass on the changed file.Found with a small AST pass looking for
create_taskcalls whose result is discarded β the same sweep that turned up #7050.