fix(inworld): keep a reference to the prewarm task and cancel it on close - #7050
Conversation
…lose `prewarm()` discarded the task it created. Two things follow from that. The task is only weakly referenced by the event loop, so it can be garbage collected mid-execution and the prewarm silently never happens. The worse one is ordering. `_prewarm_impl` calls `_get_pool`, which builds a new `_ConnectionPool` whenever `self._pool` is None — and `aclose()` sets it to None after closing. A prewarm still in flight when the TTS is closed therefore recreates the pool afterwards, leaving a live connection that nothing owns and nothing will close. Store the task and cancel it at the top of `aclose()`, which is what `openai/tts.py` does with its own hand-rolled prewarm, and what `utils.ConnectionPool.prewarm()` does for the fifteen plugins that use it. Inworld was the only plugin calling `asyncio.create_task` in `prewarm()` without holding on to the result.
|
Bumping — this is the same leak you merged in #7023, one plugin over.
The fix follows what is already in the tree: One file, +10/-2, all checks green, no open review threads. cc @davidzhao — same shape as #7023. |
Follow-up to livekit#7050. `prewarm()` overwrites `_prewarm_task` unconditionally, so a second call drops the only reference to a task still in flight, and `aclose()` cancels just the latest one — leaving the earlier attempt free to build a connection pool after shutdown, which is the leak livekit#7050 set out to close. Starts a task only when there is no live one, matching the soniox plugin and `utils.ConnectionPool.prewarm`.
The bug
TTS.prewarm()throws away the task it creates:Two consequences.
The task is only weakly referenced. The event loop does not keep a strong reference to a bare task, so it can be garbage collected mid-execution and the prewarm silently never happens.
The ordering one is worse.
_prewarm_implcalls_get_pool, which builds a fresh_ConnectionPoolwheneverself._poolis None:and
aclose()setsself._pool = Noneafter closing it. So a prewarm still in flight when the TTS is closed recreates the pool after close, leaving a live websocket connection that nothing owns and nothing will ever close.The fix
Store the task and cancel it at the top of
aclose(), before the pool is torn down.This is the pattern already in the tree.
openai/tts.pyhand-rolls its prewarm the same way and holds the task:and
utils.ConnectionPool.prewarm()— used by the fifteen other plugins — keepsself._prewarm_taskand checks whether it is still running before starting another.Inworld was the only plugin calling
asyncio.create_taskinprewarm()without holding on to the result.Checks
ruff checkandruff format --checkpass on the changed file. No behaviour change on the success path: prewarm still runs in the background and is still fire-and-forget from the caller's point of view.Found with a small AST pass over the repo looking for
create_taskcalls whose result is discarded. Happy to send the other instances separately if useful.