Skip to content

fix(python-sdk): close the per-call httpx client instead of dropping it - #652

Merged
HoangDucBach merged 1 commit into
devfrom
fix/sync-http-client-lifecycle
Aug 18, 2026
Merged

fix(python-sdk): close the per-call httpx client instead of dropping it#652
HoangDucBach merged 1 commit into
devfrom
fix/sync-http-client-lifecycle

Conversation

@HoangDucBach

@HoangDucBach HoangDucBach commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Ticket

Fixes #606

What changed?

  • New _with_fresh_http_client() in client.py: closes whatever is cached on entry, and closes the client this call created in finally, while the loop owning it is still alive.
  • _discard_http_client() clears _client even when a cross-loop aclose() raises, so the next request can always build one in a live loop.
  • Both drop sites route through it: MemWalSync._run() and _wrap_sync_openai._run_memwal().
  • _SyncRunInner test stand-in now mirrors MemWal's real close() / aclose() contract.

Why is this needed?

Both sync paths cleared _client before every asyncio.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 open httpx.AsyncClient and 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?

  • Unit tests
  • Integration tests
  • End-to-end tests
  • Manual testing
  • Not applicable

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?

cd packages/python-sdk-memwal
python3 -m pytest tests/ -q -m "not integration"    # 119 passed
python3 -m ruff check memwal/ tests/test_client.py  # clean

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

Author checklist

  • This pull request maps to one ticket and one logical outcome.
  • I reviewed the complete diff myself.
  • I removed unrelated, debug, and temporary changes.
  • I ran the relevant tests.
  • CI is green.
  • The branch is up to date with its target branch.
  • I added or updated tests where appropriate.
  • I documented any important risk, dependency, rollout, or follow-up.
  • I provided clear verification steps.
  • The pull request is ready for review and is no longer a Draft.

@ducnmm ducnmm left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
  • _FakeHttpClient and _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
HoangDucBach force-pushed the fix/sync-http-client-lifecycle branch from 49a252c to b9b6e23 Compare August 18, 2026 08:35
@HoangDucBach
HoangDucBach merged commit 681a5e8 into dev Aug 18, 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.

[Python SDK/Bug] MemWalSync._run drops cached httpx client without closing it

2 participants