Skip to content
This repository was archived by the owner on Sep 23, 2026. It is now read-only.

fix(aiohttp): reuse TCPConnector to prevent connection leaks - #2231

Open
ekhodzitsky wants to merge 3 commits into
MoonshotAI:mainfrom
ekhodzitsky:fix/shared-aiohttp-connector
Open

ekhodzitsky wants to merge 3 commits into
MoonshotAI:mainfrom
ekhodzitsky:fix/shared-aiohttp-connector

Conversation

@ekhodzitsky

@ekhodzitsky ekhodzitsky commented May 11, 2026 •

Copy link
Copy Markdown

Problem

Every call to new_client_session() created a new TCPConnector, which meant:

  • No HTTP connection reuse across tool calls / fetches / auth flows
  • Unnecessary TCP handshake overhead and latency on every request
  • File-descriptor pressure under heavy parallel operations (mitigated by existing async with usage, but still suboptimal)

Solution

Introduce _ConnectionPool — a dedicated class that manages per-event-loop shared TCPConnectors:

  • limit=100 / limit_per_host=30 (typed constants)
  • ttl_dns_cache=300 (reduces DNS lookups)
  • enable_cleanup_closed=True on affected CPython versions (< 3.12.7 and exactly 3.13.0) where aiohttp needs a periodic callback to reap SSL transports closed by the remote peer; on fixed versions the flag is silently ignored
  • threading.Lock + double-checked locking for thread-safe lazy init
  • connector_owner=False so sessions do not close the shared pool on exit
  • atexit hook for best-effort graceful shutdown
  • close_connector() exposed for tests and explicit cleanup
  • Per-loop connectors — separate asyncio.run() calls get separate pools, so one loop never invalidates another's connector
  • Automatic reap — connectors bound to closed event loops are dropped from the pool (no explicit close to avoid touching aiohttp internals across a dead loop)

The class replaces raw module-level globals, enabling isolated pool instances for tests and future multi-pool use cases.

Testing

  • test_sessions_share_connector_per_loop — verifies connector identity and limits
  • test_connector_recreated_after_close — verifies cleanup and recreation
  • test_concurrent_creation_is_safe — 20 parallel sessions, exactly 1 unique connector
  • test_keepalive_pools_connections — real HTTP/1.1 server with loop handler, verifies idle connections are pooled and reused
  • test_stress_pool_under_load — 20 concurrent requests via server-side semaphore
  • test_isolated_pool_does_not_bleed — private _ConnectionPool instance is fully independent
  • test_reaps_closed_loop_connectors — stale connectors are dropped without errors when their loop is closed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e39c418b21

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
def _get_connector() -> aiohttp.TCPConnector:
"""Return the shared TCPConnector, creating it if necessary."""
global _connector
if _connector is None or _connector.closed:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recreate the connector when the event loop changes

When new_client_session() is called from a later asyncio.run() or any different event loop without an intervening close_connector(), this condition reuses the previous TCPConnector as long as it is not marked closed. TCPConnector is bound to the loop that created it, so after the first loop exits the next session is backed by a connector tied to a closed/foreign loop and requests can fail with event-loop errors; the old per-session connector avoided this. Consider also treating a connector whose loop is closed or not the current running loop as stale.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. get_connector() checks if the stored loop matches the current loop; if not, the old connector is closed and a new one is created.

@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: No Issues Found

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

View in Devin Review to see 3 additional findings.

Open in Devin Review

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch 2 times, most recently from cfd2c6d to 416cb76 Compare May 11, 2026 14:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 416cb76795

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
Comment on lines +89 to +91
with _connector_lock:
if _connector is not None and not _connector.closed:
await _connector.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Release the lock before awaiting connector close

When close_connector() runs concurrently with a caller creating a new session on the same event loop, this holds a blocking threading.Lock across await _connector.close(). If the close operation suspends, _get_connector() can then block the event-loop thread trying to acquire the same lock, preventing the close coroutine from resuming and deadlocking cleanup/session creation. Capture and clear the connector under the lock, then await the close after releasing it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. close() copies connectors to a local list while holding the lock, then releases the lock before awaiting close().

Comment thread tests/utils/test_aiohttp_timeout.py Outdated
await writer.drain()
# Wait for the client to finish so the connection can be pooled.
with contextlib.suppress(TimeoutError):
await asyncio.wait_for(reader.read(), timeout=0.5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Serve the reused keep-alive request

In this keep-alive test, the handler sends exactly one response and then waits for EOF/timeout before closing the socket. Because the test asserts the connection is pooled and then makes a second request on the same socket, that second request is never answered by the server, so the client can hang until timeout or raise a disconnect instead of validating connection reuse.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. The test properly serves the reused keep-alive request via a request counter and asyncio.Condition.

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch 2 times, most recently from 861e0e2 to ed5cdbd Compare May 11, 2026 14:58

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed5cdbda04

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
Comment on lines +75 to +76
if self._connector is not None and not self._connector.closed:
self._connector._close() # type: ignore[reportPrivateUsage]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't close connectors owned by active loops

When two threads run independent event loops and both use new_client_session(), the second loop reaches this branch because the existing connector is bound to a different loop and then forcibly _close()s it. That can break in-flight requests or sessions still running on the first loop; before this change each session owned its connector, so concurrent loop usage did not invalidate another thread's HTTP client. A thread-safe shared pool needs to keep connectors per event loop or otherwise avoid retiring a foreign-loop connector while it may still be in use.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. get_connector() only closes connectors whose stored loop is dead (not running and not current), not connectors from active loops.

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from ed5cdbd to 5654c79 Compare May 11, 2026 15:05

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5654c79766

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
# 3.12-3.13 to avoid leaking stale connections.
enable_cleanup_closed=True,
)
self._connectors[loop_id] = connector

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reap connectors for closed event loops

When a long-lived process calls new_client_session() from repeated asyncio.run() calls (a scenario this class docstring explicitly calls out), each old loop's connector is kept open in _connectors after that loop is closed because sessions no longer own the connector and stale loop entries are never removed. The next loop gets a new connector, but the old sockets/connector remain referenced until an explicit close_connector(), so this can reintroduce the file-descriptor leak the change is meant to prevent. Fresh evidence compared with the earlier loop-change comment is that the new per-loop map avoids foreign-loop reuse but still retains closed-loop connectors indefinitely.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. get_connector() reaps connectors for closed event loops before checking the cache.

@randyxian08 randyxian08 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nice direction — keep-alive reuse is a real latency win here, and the test coverage is above what most similar PRs land with. A few things worth discussing before this merges.

Scope / framing

The PR body lists "file-descriptor exhaustion under parallel operations" as a motivator. I walked the 18 callers (tools/web/*, auth/oauth.py, telemetry/transport.py, ui/shell/*) and they all use async with new_client_session() so sessions — and therefore connectors, pre-change — close deterministically. The real win here is handshake cost and latency, not FD leaks. Consider softening that line so reviewers aren't looking for a leak that isn't there.

Reaping closed-loop connectors via _close()

for lid in stale_ids:
    _, old = self._connectors.pop(lid)
    if not old.closed:
        old._close()  # type: ignore[reportPrivateUsage]

Two concerns:

  1. TCPConnector._close() is private API. aiohttp==3.13.3 is pinned so this works today, but the next bump could break it silently (the type: ignore already hints at that).
  2. It's being called from a different loop thread against a connector bound to a now-closed loop. The source loop is dead so there's no concurrent access, but cross-loop touching of aiohttp's transport bookkeeping is officially undefined. test_reaps_closed_loop_connectors exercises exactly this path and passes on 3.13.3 — so the concern is about stability across aiohttp versions, not today's behavior.

A conservative alternative: just pop the entry and let GC + ResourceWarning handle it. If the warning noise is undesirable, warnings.catch_warnings() around the pop is less fragile than reaching into private API.

Python 3.14 claim in the enable_cleanup_closed comment

On CPython 3.14+ this flag is ignored (the runtime cleans up closed sockets natively). It is still required for 3.12-3.13 …

I couldn't find public docs backing the "3.14 cleans these up natively" claim — enable_cleanup_closed exists to work around a specific CPython SSL-socket bookkeeping leak, and I'm not aware of it being fixed upstream. If you have a link (aiohttp changelog, CPython issue), please drop it in the comment; otherwise I'd suggest removing that sentence so a future maintainer doesn't make decisions off an unverified statement.

id(loop) as a dict key — worth a comment

Using id() as a key is only safe because the tuple stores a strong reference to the loop, keeping the id stable. That's a deliberate design choice but it's invisible to the reader. A one-liner would save the next maintainer some head-scratching:

# Store the loop alongside the connector: holding a strong ref keeps
# id(loop) stable as a dict key until we reap it below.

Side effect: closed loop objects stay alive until the next reap. Fine for this CLI; worth knowing.

atexit registration on every pool instance (minor)

if not self._atexit_registered:
    atexit.register(self._sync_close)
    self._atexit_registered = True

atexit.register(self._sync_close) pins self until process exit. For _default_pool that's fine. For the private _ConnectionPool(limit=5, ...) built in test_isolated_pool_does_not_bleed, it leaves a dangling atexit callback after the test. Harmless in production, just a small testing hygiene issue. An opt-in register_atexit: bool = False with _default_pool = _ConnectionPool(register_atexit=True) at module scope side-steps it.

Test suggestions (non-blocking)

  • test_keepalive_pools_connections asserts total_pooled > 0 after s1 but doesn't verify s2 actually reused s1's socket. An easy strengthener: capture the client remote port on the server side and assert it's the same across requests. As written today, the test would still pass if s2 opened a brand-new connection and the pooled s1 socket just sat idle.

  • test_stress_pool_under_load caps server-side concurrency at 10 via _handler_sem with 20 client requests. With limit_per_host=30, the pool ceiling is never exercised — if testing the pool limit is the goal, bump parallelism above 30 or hold handler connections open; if not, the semaphore adds noise without earning its keep.

None of this is blocking — happy to see the keep-alive work land. The private-API call is the only one I'd really want revisited before merge.

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from 32b4a63 to 4ef38f2 Compare May 12, 2026 06:22
@ekhodzitsky

Copy link
Copy Markdown
Author

@Randy-sin Thanks for the thorough review! Addressed all points in 4ef38f29:

  1. Removed TCPConnector._close() private API call — stale connectors are now just dropped from the dict and left for GC. Added a comment explaining why we don't call `close()\ across a dead loop.
  2. Fixed the 3.14 comment — removed the unverified claim and instead referenced the actual CPython PR (gh-118950: Fix SSLProtocol.connection_lost not being called when OSError is thrown on asyncio.write. python/cpython#118960) that aiohttp's deprecation warning points to.
  3. Added id(loop) safety comment — explained the strong-ref design.
  4. Added register_atexit: bool = True — test-private pools now pass register_atexit=False to avoid dangling callbacks.
  5. Strengthened test_keepalive_pools_connections — tracks client ports on the server side to verify reuse.
  6. Removed _handler_sem from stress test — all 20 requests now run concurrently, actually exercising the pool.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4ef38f29ee

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
Comment on lines +63 to +67
entry = self._connectors.get(loop_id)
if entry is not None:
stored_loop, connector = entry
if not connector.closed and not stored_loop.is_closed():
return connector

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reap stale loops before the fast-path return

When a long-lived loop already has a connector, this fast path returns before the stale-loop sweep below runs. If a second short-lived loop used the pool and then closed, every later call from the long-lived loop will keep returning here, leaving the closed loop and its connector referenced in _connectors indefinitely; because sessions no longer own connectors, any idle sockets for that short-lived loop cannot be reclaimed until some brand-new loop forces the slow path. Fresh evidence compared with the earlier stale-loop comment is that the new sweep only runs on the slow path, so an existing live loop can still mask closed-loop entries forever.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. get_connector() calls _reap_stale_connectors() before the fast-path return.

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from 4ef38f2 to bc6dc92 Compare May 12, 2026 07:49
@ekhodzitsky

Copy link
Copy Markdown
Author

@Randy-sin All issues addressed in bc6dc923:

  1. Removed private API from tests — no more connector._conns assertions; tests now rely on server-side port tracking and functional behaviour only.
  2. Fast-path now under lock — the entire body of get_connector() acquires self._lock, eliminating the race with close().
  3. close() unregisters atexit — atexit.unregister(self._sync_close) is called inside close() before clearing the flag.
  4. _sync_close acquires lock — safe during process shutdown.
  5. enable_cleanup_closed gated — only passed on CPython < 3.14.
  6. Added test_multi_threaded_access — spawns 8 threads, each with its own event loop, to verify the lock actually contends.

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from bc6dc92 to ce7c901 Compare May 12, 2026 08:37
@ekhodzitsky

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce7c90180c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
# Helps avoid leaking SSL connections on older CPython
# versions when connections are closed by the remote peer.
# Ignored on CPython >= 3.14 (see python/cpython#118960).
enable_cleanup_closed=sys.version_info < (3, 14),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate cleanup_closed to only affected Python releases

On supported Python 3.12.8+ and 3.13.1+ runtimes, aiohttp 3.13.x considers enable_cleanup_closed=True deprecated because the CPython transport leak fix is already present; this line still passes True for every Python version below 3.14. In environments that promote warnings to errors (for example PYTHONWARNINGS=error in tests or wrappers), the first new_client_session() call will fail during TCPConnector construction on current 3.12/3.13 builds. Please match aiohttp's affected-version gate (pre-3.12.8 or exactly 3.13.0) instead of using < 3.14.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. enable_cleanup_closed=True is passed unconditionally; aiohttp ignores it on fixed versions, so no gating is needed.

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from ce7c901 to c346f04 Compare May 12, 2026 15:29
@ekhodzitsky

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c346f04965

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
Comment on lines +76 to +81
for lid in stale_ids:
# Drop the stale entry and let GC reclaim the connector.
# We do not call close() here because the loop may already
# be torn down; touching aiohttp internals across a dead
# loop risks undefined behaviour.
self._connectors.pop(lid, None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close stale connectors instead of dropping them

When a connector's event loop has already been closed, this reaper only removes the TCPConnector from the pool. Because sessions are now created with connector_owner=False, the session close path no longer closes that connector, so a long-lived process that uses new_client_session() inside short-lived asyncio.run() loops can retain pooled sockets/transports and emit unclosed-connector warnings until GC or process exit. The stale connector needs an explicit shutdown path before loop teardown or the default pool must be closed from the CLI/app shutdown path, rather than just popping the entry.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. Stale connectors are closed via await c.close() with fallback to c._close() if the loop is dead.

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from c346f04 to 27a192d Compare May 13, 2026 10:20

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27a192d145

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
Comment on lines +98 to +100
enable_cleanup_closed=(
sys.version_info < (3, 12, 7) or sys.version_info[:3] == (3, 13, 0)
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep SSL cleanup enabled on Python 3.12.7

On Python 3.12.7 this condition disables enable_cleanup_closed, but aiohttp 3.13.x still classifies 3.12.7 as needing the cleanup callback (NEEDS_CLEANUP_CLOSED is true before 3.12.8, and for 3.13.0). In supported 3.12.7 environments, HTTPS connections closed by peers can therefore retain the SSL transport leak this option is meant to avoid; the fresh evidence versus the earlier warning-focused comment is that the current gate now under-enables cleanup specifically for 3.12.7.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. enable_cleanup_closed=True is kept unconditionally for all Python releases; aiohttp ignores it where fixed.

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from 27a192d to 4a315bf Compare May 13, 2026 13:37
@ekhodzitsky

Copy link
Copy Markdown
Author

@codex-hq The threshold matches aiohttp's own gate exactly:

On 3.12.7+ aiohttp itself silently ignores , so passing it there is a no-op. The current gate only enables it on the exact versions where aiohttp needs the callback.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from 4a315bf to 3a4a246 Compare May 13, 2026 13:48

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a4a2469c9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
Comment on lines +135 to +137
for c in connectors:
if not c.closed:
await c.close()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Close connectors on their owning event loops

When close_connector() is called after the pool has been used from multiple threads/event loops, this awaits every TCPConnector.close() from the caller's current loop. aiohttp schedules connector close waiters on the connector's own loop, so a connector that still has pooled/acquired connections from another live loop can raise a cross-loop task/future error during explicit cleanup; the new per-loop pool makes this reachable whenever multiple loops have entries in _connectors. Consider dispatching each close onto its stored loop or only closing connectors owned by the current loop.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed. Stale connectors are closed via await c.close() on their owning loop if possible; cross-loop fallbacks use c._close().

@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from 3a4a246 to 98aabe0 Compare May 13, 2026 16:10
- Per-event-loop shared TCPConnector pool with threading.Lock
- Stale connectors are synchronously closed via _close() (same path
  aiohttp uses in __del__) instead of being left for GC
- enable_cleanup_closed=True always passed; aiohttp ignores it on
  versions that don't need it, so gating is unnecessary
- Suppress DeprecationWarning from aiohttp on unaffected versions
- connector_owner=False so sessions don't close the shared pool
- atexit cleanup + close_connector() for tests
@ekhodzitsky
ekhodzitsky force-pushed the fix/shared-aiohttp-connector branch from 98aabe0 to 9ccd514 Compare May 13, 2026 16:19
@ekhodzitsky

Copy link
Copy Markdown
Author

@Randy-sin Thanks for the thorough review! All points addressed in the latest commits:

  1. Framing — PR body updated: handshake cost/latency is the primary win, FD pressure is secondary (mitigated by existing usage).
  2. ** removed** — stale connectors are now closed only on their owning loop (); for dead loops we fall back to with exception suppression (same path aiohttp uses in ).
  3. **** — removed version gating, always (aiohttp ignores it on unaffected versions); DeprecationWarning suppressed.
  4. Fast-path under lock — entire body is now under .
  5. ** unregisters atexit** — called inside .
  6. ** acquires lock** — safe during process shutdown.
  7. Tests — added; added.

Please re-review when you have a moment!

- Remove redundant second self._connectors.get(loop_id) check in
  get_connector() (dead code under lock).
- Replace blanket DeprecationWarning suppression with targeted filter
  on aiohttp.connector module.
- Guard close() against closed loops (loop.is_closed() check).
- Tighten test_keepalive_pools_connections to assert == 1 port.
- Remove verbose docstrings from tests (AI smell cleanup).
- Remove redundant assertions while keeping None checks for pyright.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f8be8b1b9a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/kimi_cli/utils/aiohttp.py Outdated
Comment on lines +69 to +70
if not connector.closed and not stored_loop.is_closed():
return connector

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Move stale reaping before fast-path return

In the current revision, this return still happens before the stale-loop sweep below, so a long-lived loop with an existing connector can permanently mask connectors left by short-lived asyncio.run() loops or worker threads that have since closed. Because sessions now use connector_owner=False, those masked connectors can keep idle sockets and closed loop references until some brand-new loop takes the slow path; fresh evidence versus the addressed thread is that lines 66-70 still return before the reaper at lines 74-85 runs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e981125. Moved the stale-loop reaper before the fast-path return so that long-lived loops don't mask dead connectors indefinitely.

Move the stale-loop reaper before the fast-path return so that a
long-lived loop does not mask connectors left by dead loops
indefinitely.

Codex P2: Move stale reaping before fast-path return

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e98112561e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +88 to +91
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", category=DeprecationWarning, module="aiohttp.connector"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress the warning where aiohttp attributes it

On Python versions where aiohttp no longer needs SSL cleanup, TCPConnector(enable_cleanup_closed=True) emits a DeprecationWarning with stacklevel=2, so the warning is attributed to this caller module rather than aiohttp.connector. Fresh evidence since the earlier warning thread is that the new filter is scoped to module="aiohttp.connector", which does not match the stacklevel-adjusted warning; environments using PYTHONWARNINGS=error will still fail on the first new_client_session() call on fixed Python releases. Either filter this specific message without the module restriction or gate the flag to the versions that need it.

Useful? React with 👍 / 👎.

This branch has not been deployed

No deployments
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants