Skip to content

fix(simplismart): close the streams the STT collects - #7012

Merged
tinalenguyen merged 1 commit into
livekit:mainfrom
Rehansanjay:fix/stt-aclose-tracked-streams
Aug 31, 2026
Merged

fix(simplismart): close the streams the STT collects#7012
tinalenguyen merged 1 commit into
livekit:mainfrom
Rehansanjay:fix/stt-aclose-tracked-streams

Conversation

@Rehansanjay

@Rehansanjay Rehansanjay commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Rescoped to simplismart. This PR originally covered sarvam and simplismart. @dhruvladia-sarvam's #7058 fixes the sarvam half properly, with a correction I had missed and a full test suite, so that half is dropped here. simplismart has the identical bug and is not covered by #7058, so this keeps it — and follows #7058's approach so the two plugins stay consistent.

The leak

STT.stream() gives every SpeechStream its own aiohttp.ClientSession ("a fresh session for this stream to avoid conflicts"), and only SpeechStream.aclose() closes it. The STT collects its streams into self._streams, but nothing ever reads that set and the class defines no aclose() — so STT.aclose() resolves to the no-op on the base class. The streams are never closed, their sessions stay open, and aiohttp reports unclosed client sessions once the streams are dropped.

The base contract asks for this: STT.aclose is documented as "Close the STT, and every stream/requests associated with it", and STT.__aexit__ delegates to it, so async with stt: currently leaks the streams.

The change

An aclose() on STT that closes what it collected, plus the ownership fix from #7058:

  • self._streams becomes a strong set instead of a weakref.WeakSet. A stream that finishes and is garbage collected would otherwise take its still-open session with it, before aclose() could ever reach it — which is the case most likely to leak in practice.
  • SpeechStream.aclose() discards itself from the STT once its session is actually closed, so the set does not grow without bound.
  • aclose() removes only the streams it closed, so one created concurrently (or one whose cleanup was cancelled) stays tracked for a later close.

Validation

New hermetic tests in tests/test_plugin_simplismart_stt.py_run is stubbed, no network:

  • aclose() closes every per-stream session and empties the set
  • the async with path closes them via __aexit__
  • aclose() tolerates an already-closed stream
  • a stream the caller dropped and that was garbage collected still has its session closed — the WeakSet case

With the fix reverted, three of the four fail and aiohttp's Unclosed client session appears exactly as the leak predicts. With the fix, all four pass.

ruff check and ruff format --check pass on both changed files.

@Rehansanjay
Rehansanjay requested a review from a team as a code owner August 27, 2026 10:54
@CLAassistant

CLAassistant commented Aug 27, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Devin Review

@Rehansanjay

Copy link
Copy Markdown
Contributor Author

recheck

@Rehansanjay
Rehansanjay force-pushed the fix/stt-aclose-tracked-streams branch from 9572437 to 6838386 Compare August 27, 2026 17:35
@Rehansanjay

Copy link
Copy Markdown
Contributor Author

Same class of leak as #7023, which was merged over the weekend: the sarvam and simplismart STTs append every stream to self._streams, but nothing ever drains it, so the streams outlive the STT that made them.

cc @davidzhao — same shape as #7023. @dhruvladia may want to sanity-check the sarvam side.

@dhruvladia-sarvam

Copy link
Copy Markdown
Contributor

Thanks for the detailed issue @Rehansanjay
@davidzhao raising solution here #7058

`STT.stream()` gives every `SpeechStream` its own `aiohttp.ClientSession`
("a fresh session for this stream to avoid conflicts"), and only
`SpeechStream.aclose()` closes it. The STT collects its streams but
defines no `aclose()`, so `STT.aclose()` resolves to the base class
no-op: the streams are never closed, their sessions stay open, and
aiohttp reports unclosed client sessions once the streams are dropped.

The base contract asks for this — `STT.aclose` is documented as "Close
the STT, and every stream/requests associated with it", and
`__aexit__` delegates to it, so `async with stt:` leaks today.

Streams are now owned strongly rather than through a `WeakSet`: a stream
that finishes and is garbage collected would otherwise take its
still-open session with it, before `aclose()` ever sees it. Closed
streams discard themselves from the STT, so the set does not grow
without bound, and `aclose()` removes only what it actually closed so a
concurrently created stream stays tracked.

Regression tests are hermetic — `_run` is stubbed, no network. Three of
the four fail with the fix reverted, with aiohttp's "Unclosed client
session" appearing exactly as the leak predicts.

The sarvam half of this change is superseded by livekit#7058, which fixes the
identical bug in that plugin; this keeps the simplismart half, which
livekit#7058 does not cover. The strong-ownership and self-discard approach here
follows livekit#7058 so the two plugins stay consistent.
@Rehansanjay
Rehansanjay force-pushed the fix/stt-aclose-tracked-streams branch from 6838386 to 7e79bc1 Compare August 31, 2026 13:12
@Rehansanjay Rehansanjay changed the title fix(sarvam, simplismart): close the streams the STT collects fix(simplismart): close the streams the STT collects Aug 31, 2026

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment on lines +309 to +320
async def aclose(self) -> None:
"""Close every stream this instance created.

``stream()`` gives each ``SpeechStream`` its own ``aiohttp.ClientSession``,
and only ``SpeechStream.aclose()`` closes it. Streams are owned strongly
(see ``self._streams``) so a finished, garbage-collected stream cannot take
its open session with it; closed streams discard themselves, so the set
never grows without bound.
"""
closed = list(self._streams)
for stream in closed:
await stream.aclose()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Sarvam stream sessions still leak

The cleanup omits STT.aclose for Sarvam's stream, which creates a client session per stream. Closing that recognizer leaves collected sessions open.

Prompt for agents
Implement the same ownership and lifecycle cleanup for livekit-plugins/livekit-plugins-sarvam/livekit/plugins/sarvam/stt.py. Its STT.stream creates a fresh aiohttp.ClientSession and records the SpeechStream in a WeakSet, but STT has no aclose override. Ensure STT.aclose closes every tracked stream, ensure individually closed streams stop being retained if strong ownership is used, and add Sarvam regression coverage equivalent to the new SimpliSmart tests.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sarvam is covered by #7058, which fixes the identical bug in that plugin with the same ownership model. This PR originally covered both and was rescoped to simplismart so the two do not overlap — see the discussion on #7058.

If #7058 does not land, I am happy to bring the sarvam half back here.

@tinalenguyen tinalenguyen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the pr!

@tinalenguyen
tinalenguyen merged commit 2a50f4f into livekit:main Aug 31, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants