Skip to content

fix: Correct pool ownership and close pooled connections synchronously#72

Open
jsonbailey wants to merge 1 commit into
mainfrom
jb/sdk-2600/fix-pool-ownership
Open

fix: Correct pool ownership and close pooled connections synchronously#72
jsonbailey wants to merge 1 commit into
mainfrom
jb/sdk-2600/fix-pool-ownership

Conversation

@jsonbailey

@jsonbailey jsonbailey commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

What

_HttpClientImpl owned the wrong pool. The ownership flag was inverted in the 2023 ConnectStrategy refactor (c516f1f8): it was renamed from http_pool is None (close a pool the library created) to params.pool is not None (close a caller-supplied pool). Combined with urllib3 2.x — where PoolManager.clear() no longer closes connections synchronously (no dispose_func; sockets close via weakref.finalize at GC time) — this meant:

  • A caller-supplied pool was clear()-ed out from under the caller on close(), and the underlying streaming socket's TCP FIN was deferred to garbage collection.
  • A library-created pool was never closed at all (leaked until GC).

The async client (async_http.py) already uses the correct rule (external_session is None), so this also brings the sync client back into line with it.

Changes

  • __should_close_pool = params.pool is None — own only the pool we created; leave a caller-supplied pool to the caller.
  • close() closes each HTTPConnectionPool synchronously (sends the FIN now) before clear().
  • Close errors logged at debug instead of silently swallowed.
  • Regression tests in test_http_connect_strategy.py for both halves of the ownership contract (there was previously zero coverage, which is why the inversion survived since 2023).

Why it matters

The LaunchDarkly server SDKs supply their own pool for FDv2 streaming. On the FDv1 fallback directive the SDK must close the streaming connection within 3s (spec 1.6.3(2)). Because the FIN was GC-deferred, the FDv2 contract test directive on streaming success applies payload then engages FDv1 failed on slow CI runners (Linux + Python 3.10) where GC slipped past the 3s window.

Verification

Instrumented FDv1-fallback contract run under Python 3.10:

Streaming FIN after halt Mechanism
Before ~0.56 s weakref.finalize (GC)
After ~0.0006 s synchronous, in the SDK's pool-close call stack

GC-deferred (~560 ms, variable) → synchronous (~0.6 ms, deterministic), well within the 3s window regardless of machine speed. Full unit suite: 126 passed.

Jira: SDK-2600


Note

Medium Risk
Touches connection lifecycle for all sync HTTP SSE clients; behavior change is intentional but affects SDK streaming shutdown timing and caller-supplied pool safety.

Overview
Fixes inverted PoolManager ownership in _HttpClientImpl: close() now tears down only pools the client created (params.pool is None), matching the async client’s external_session is None rule. Caller-supplied pools are no longer clear()’d on close.

When the client owns the pool, close() now closes each HTTPConnectionPool before PoolManager.clear(), so streaming TCP connections get a FIN immediately instead of lingering until urllib3 2.x GC/finalizers. Close failures are logged at debug.

Adds regression tests for both ownership paths (caller pool untouched vs. internally created pool closed + cleared).

Reviewed by Cursor Bugbot for commit ce86c6e. Bugbot is set up for automated code reviews on this repo. Configure here.

_HttpClientImpl owned the wrong pool: the ownership flag was inverted in the 2023 ConnectStrategy refactor (c516f1f), so close() cleared a caller-supplied pool instead of one it created. On urllib3 2.x, PoolManager.clear() no longer closes connections synchronously (sockets close at GC via weakref.finalize), so a caller's streaming connection lingered until garbage collection.

- Restore conventional ownership: close only a pool we created (params.pool is None), matching the async client's existing behavior.
- Close each HTTPConnectionPool synchronously before clear() so the TCP FIN is sent immediately instead of at GC.
- Log close errors at debug instead of swallowing them.

Adds regression tests covering both halves of the ownership contract.
@jsonbailey jsonbailey marked this pull request as ready for review June 25, 2026 03:12
@jsonbailey jsonbailey requested a review from a team as a code owner June 25, 2026 03:12
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.

1 participant