Skip to content

fix: close child httpx clients in AsyncStream.aclose()#230

Merged
aliev merged 4 commits intomainfrom
fix/close-child-clients-on-aclose
Mar 28, 2026
Merged

fix: close child httpx clients in AsyncStream.aclose()#230
aliev merged 4 commits intomainfrom
fix/close-child-clients-on-aclose

Conversation

@aliev
Copy link
Copy Markdown
Member

@aliev aliev commented Mar 27, 2026

Why

AsyncStream.aclose() only closes the main httpx client, but the cached video, chat, and moderation sub-clients each create their own httpx connection pool via @cached_property. When the parent is closed, these child pools remain open, leaking TCP connections.

Discovered during memory leak investigation in vision-agents: after 50 session create+close cycles, 87 orphaned TCP connections remained. After this fix + companion Vision-Agents PR, leaked connections dropped to 2 (-98%) and memory returns to baseline.

Changes

  • Override aclose() in AsyncStream to close all cached child httpx clients before closing the main client
  • Tests verifying that main, video, and chat httpx clients are is_closed == True after aclose()

Companion PR

Summary by CodeRabbit

  • Bug Fixes

    • Async streams now reliably close all associated child service clients (video, chat, moderation) and the underlying connection, preventing resource leaks on shutdown.
  • Tests

    • Added asynchronous tests validating proper shutdown behavior for the main client and optional child services.

AsyncStream.aclose() only closed the main httpx client but not
the cached video/chat/moderation sub-clients, each of which has
its own httpx connection pool. After profiling with 50 session
create+close cycles, 87 orphaned TCP connections remained.

Override aclose() to close all cached child clients before
closing the main client.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

Warning

Rate limit exceeded

@aliev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 31 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 10 minutes and 31 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dd0e4871-16e4-45b7-82e4-2e49581a4955

📥 Commits

Reviewing files that changed from the base of the PR and between d063763 and 999715e.

📒 Files selected for processing (1)
  • getstream/stream.py
📝 Walkthrough

Walkthrough

Adds an async aclose() to AsyncStream that awaits aclose() on lazily-created child clients (video, chat, moderation) when present, then always calls super().aclose() to shut down the main client. Includes async tests validating these close paths.

Changes

Cohort / File(s) Summary
Core Async Cleanup
getstream/stream.py
Added AsyncStream.aclose(self) coroutine: checks for cached child clients (video, chat, moderation) in self.__dict__, awaits each child's aclose() if present, and ensures super().aclose() runs in finally.
Async Close Tests
tests/test_async_stream_close.py
New async pytest file with tests (5 cases) that verify aclose() closes the main client and each child client when accessed, and that absent children are handled gracefully.

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant AsyncStream
    participant Child (video/chat/moderation)
    participant BaseClient

    Caller->>AsyncStream: await aclose()
    AsyncStream->>AsyncStream: for each child in ['video','chat','moderation']
    alt child exists and has aclose
        AsyncStream->>Child: await child.aclose()
        Child-->>AsyncStream: closed
    else child missing or no aclose
        AsyncStream-->>AsyncStream: skip
    end
    AsyncStream->>BaseClient: await super().aclose()
    BaseClient-->>AsyncStream: base closed
    AsyncStream-->>Caller: return
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble bytes in moonlit streams so deep,

I close the doors where sleepy clients sleep,
Video, chat, moderation tucked away,
A final super call ends the day,
Hooray — no leaks, hop on, I leap! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing resource leaks by ensuring child httpx clients are properly closed in AsyncStream.aclose().
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/close-child-clients-on-aclose

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tests that main, video, and chat httpx clients are closed after
aclose(), and that aclose() works when child clients were never
accessed (cached_property not triggered).
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_async_stream_close.py (1)

6-35: Use a fixture to inject AsyncStream across these tests.

All tests construct clients inline; please move client creation to a fixture and inject it into test methods to match the repository’s test structure requirements and centralize cleanup behavior.

As per coding guidelines, "Use fixtures to inject objects in tests; test client fixtures can use the Stream API client."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_async_stream_close.py` around lines 6 - 35, Replace inline
AsyncStream construction with a pytest fixture that returns an AsyncStream and
ensures proper async cleanup; create a fixture (e.g., async_stream) that
instantiates AsyncStream(api_key="fake", api_secret="fake") and yields it, then
awaits fixture.aclose() in the teardown, and update the tests
(test_aclose_closes_main_client, test_aclose_closes_video_client,
test_aclose_closes_chat_client, test_aclose_without_child_clients) to accept the
fixture as an argument and remove their local client creation so they use the
injected async_stream; keep uses that trigger cached properties (accessing
.video and .chat) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@getstream/stream.py`:
- Around line 210-216: The aclose method must always close the main client even
if a child close fails and should call the child's aclose() to preserve
encapsulation: iterate the attributes ("video","chat","moderation") and for each
existing child call await child.aclose() inside a try/except that logs or
suppresses the error so one failure doesn't stop the loop, then after the loop
ensure await super().aclose() is always executed (i.e., place it after the loop,
not inside the try that could be aborted) so the main HTTPX client is closed
regardless of child errors.

In `@tests/test_async_stream_close.py`:
- Around line 15-35: Add a test mirroring the existing video/chat checks to
assert AsyncStream.aclose() closes the moderation child: instantiate
AsyncStream, access the moderation cached property (e.g., _ =
client.moderation), assert client.moderation.client.is_closed is False, await
client.aclose(), then assert client.moderation.client.is_closed is True; this
ensures AsyncStream.aclose() closes the moderation client like video and chat.

---

Nitpick comments:
In `@tests/test_async_stream_close.py`:
- Around line 6-35: Replace inline AsyncStream construction with a pytest
fixture that returns an AsyncStream and ensures proper async cleanup; create a
fixture (e.g., async_stream) that instantiates AsyncStream(api_key="fake",
api_secret="fake") and yields it, then awaits fixture.aclose() in the teardown,
and update the tests (test_aclose_closes_main_client,
test_aclose_closes_video_client, test_aclose_closes_chat_client,
test_aclose_without_child_clients) to accept the fixture as an argument and
remove their local client creation so they use the injected async_stream; keep
uses that trigger cached properties (accessing .video and .chat) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f69dcb25-6052-4a2b-8f28-16c4bdd0d2d2

📥 Commits

Reviewing files that changed from the base of the PR and between ebcd026 and 74fde39.

📒 Files selected for processing (2)
  • getstream/stream.py
  • tests/test_async_stream_close.py

Replaces try/finally with AsyncExitStack to ensure all child clients
are closed even if one fails. Also checks __dict__ instead of hasattr
to only close cached_property clients that were actually accessed.
@aliev aliev merged commit 4e3c87d into main Mar 28, 2026
30 of 31 checks passed
@aliev aliev deleted the fix/close-child-clients-on-aclose branch March 28, 2026 18:27
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.

2 participants