fix(python-sdk): close the per-call httpx client instead of dropping it - #652
Merged
Conversation
HoangDucBach
force-pushed
the
fix/sync-http-client-lifecycle
branch
from
August 15, 2026 02:39
6a2671d to
49a252c
Compare
15 tasks
ducnmm
approved these changes
Aug 17, 2026
ducnmm
left a comment
Collaborator
There was a problem hiding this comment.
Review
Verdict: ✅ Approve
Core fix is correct: _with_fresh_http_client() closes on both entry (orphan from prior loop) and exit (client created this call) — fully addresses the leak GH #606.
_discard_http_client()with try/finally is defensive and correct- 3 tests covering replaced client, per-call client, and exception path are thorough
_FakeHttpClientand_SyncRunInner.close()mirroring the real contract is clean test design- No blocking issues. Ready to merge.
MemWalSync._run() and the sync OpenAI middleware wrapper cleared `_client` before every asyncio.run() path so a client would be rebuilt inside the loop that uses it, but never closed the one they replaced. Every sync call therefore orphaned an open httpx.AsyncClient and its connection pool until GC, which shows up in notebooks and long-running scripts as resource warnings, stale sockets, and needless connection churn. Route both sites through _with_fresh_http_client(), which closes at both ends: on entry for whatever an earlier event loop left behind (a caller mixing `await memwal.recall()` with the sync wrapper), and in `finally` for the client this call created, while the loop that owns it is still alive. Closing on the way out is what actually removes the leak — closing only on the way in would still leave one open client between calls. Fixes #606
HoangDucBach
force-pushed
the
fix/sync-http-client-lifecycle
branch
from
August 18, 2026 08:35
49a252c to
b9b6e23
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ticket
Fixes #606
What changed?
_with_fresh_http_client()inclient.py: closes whatever is cached on entry, and closes the client this call created infinally, while the loop owning it is still alive._discard_http_client()clears_clienteven when a cross-loopaclose()raises, so the next request can always build one in a live loop.MemWalSync._run()and_wrap_sync_openai._run_memwal()._SyncRunInnertest stand-in now mirrorsMemWal's realclose()/aclose()contract.Why is this needed?
Both sync paths cleared
_clientbefore everyasyncio.run()so a client is rebuilt inside the loop that uses it, but never closed the one they replaced. Every sync call therefore orphaned an openhttpx.AsyncClientand its pool until GC — resource warnings, stale sockets and connection churn in notebooks and long-running scripts.Worth flagging for review: closing on the way out is what removes the leak. Closing only on entry, as the issue suggests, still leaves an open client idle between every pair of calls.
Scope
httpx client lifecycle on the sync entry points. Async paths are untouched — they already own their client for the caller's loop.
Out of scope
Sync LangChain wrapper skipping recall (#607), branched off this PR.
How was this tested?
3 new tests in
TestMemWalSyncRun: replaced client closed, per-call client closed, and both when the coroutine raises. Each verified to fail against the pre-fix_run(). Suite: 119 passed, ruff clean.How can the reviewer verify it?
To watch the tests catch the bug, restore the pre-fix body of
MemWalSync._run()(self._inner._client = None; wrapped = coro) and rerun with-k SyncRun→ 3 failures.Risks and dependencies
_clientis now alwaysNonebetween sync calls. Nothing reads it across calls;_httprebuilds lazily.aclose()failures are swallowed by design — a client from a finished loop cannot always be closed, and cleanup must not mask the coroutine's result. Logged at debug.Author checklist