[EventHub] Fix aiohttp ClientSession leak in WebSocketTransportAsync close/connect#46829
[EventHub] Fix aiohttp ClientSession leak in WebSocketTransportAsync close/connect#46829MuhammadAliShahzad wants to merge 4 commits into
Conversation
|
Thank you for your contribution @MuhammadAliShahzad! We will review the pull request and get back to you soon. |
There was a problem hiding this comment.
Pull request overview
Fixes aiohttp.ClientSession resource leaks in the async WebSocket transport for azure-eventhub by making close()/connect() exception-safe and adding unit coverage to prevent regressions.
Changes:
- Made
WebSocketTransportAsync.connect()close any prior session before reconnect and ensure new sessions are closed on unexpectedws_connect()failures. - Made
WebSocketTransportAsync.close()resilient to exceptions from websocket/session close so cleanup still completes and state is reset. - Added focused unit tests for cleanup invariants and documented the fix in the changelog.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| sdk/eventhub/azure-eventhub/azure/eventhub/_pyamqp/aio/_transport_async.py | Adds safe session cleanup helper; hardens connect()/close() against exceptions to prevent ClientSession leaks. |
| sdk/eventhub/azure-eventhub/tests/pyamqp_tests/asynctests/test_websocket_transport_async_unit.py | New unit tests validating session/socket cleanup behavior across failure paths. |
| sdk/eventhub/azure-eventhub/CHANGELOG.md | Notes the leak fix under a new 5.15.2 (Unreleased) entry. |
| try: | ||
| if self.session is not None: | ||
| await self.session.close() | ||
| except Exception as e: # pylint: disable=broad-except | ||
| _LOGGER.debug( | ||
| "Error closing aiohttp session: %r", e, extra=self.network_trace_params | ||
| ) | ||
| self.sock = None | ||
| self.session = None |
|
Thanks for the review. Applied the suggestion in 5394f07 — |
|
@microsoft-github-policy-service agree |
|
Pushed 7e8fbc3 to address
|
|
Mind rebasing on current main? |
WebSocketTransportAsync.close() called self.sock.close() and self.session.close() sequentially without try/except. If sock.close() raised (e.g. the websocket was already disconnected or timed out), session.close() never ran and the aiohttp ClientSession leaked. WebSocketTransportAsync.connect() had two related leaks: - on reconnect, a previously assigned self.session was not closed before being overwritten; - if ws_connect() raised anything other than ClientConnectorError, the newly created session was not closed. This change makes both methods exception-safe, matching the cleanup pattern already used by AsyncTransport.close() in the same file (log via _LOGGER.debug with extra=network_trace_params, broad-except pragma, null state after close). Adds unit tests under tests/pyamqp_tests/asynctests/ that exercise the cleanup paths via mocked aiohttp ClientSession. Fixes Azure#46828 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Addresses Copilot review feedback: close() had inline session-cleanup logic that duplicated _close_session_safely(). Replacing it with the helper keeps the cleanup behavior and log message consistent across close paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Build Analyze (Verify ChangeLogEntries) requires that the entry matching VERSION in _version.py be the topmost entry in CHANGELOG.md, and that no subsections under it are empty. Bump VERSION to 5.15.2 so the verifier matches the new top-of-file entry, and remove the empty Features Added / Breaking Changes / Other Changes subsections. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7e8fbc3 to
031701e
Compare
Summary
Fixes #46828.
WebSocketTransportAsync.close()callsself.sock.close()andself.session.close()sequentially withouttry/except. Whensock.close()raises (e.g. the websocket is already disconnected or timed out),session.close()never runs and theaiohttp.ClientSessionleaks. The siblingAsyncTransport.close()in the same file already handles this correctly — this PR bringsWebSocketTransportAsyncin line with that pattern.WebSocketTransportAsync.connect()had two related leaks:self.sessionwas not closed before being overwritten.ws_connect()raised anything other thanClientConnectorError, the newly-created session was not cleaned up.Changes
azure/eventhub/_pyamqp/aio/_transport_async.pyclose(): wrapsock.close()andsession.close()independently intry/except, log via_LOGGER.debug(..., extra=self.network_trace_params)(same pattern asAsyncTransport.close()at line 370–385), and null state after close.self.connected = Falsemoved outside theasync withblock to match the sibling.connect(): close any previousself.sessionbefore assigning a new one; add a broadexcept Exception:clause that cleans up the new session before re-raising; refactor session cleanup into a small_close_session_safely()helper used by all three call sites.tests/pyamqp_tests/asynctests/test_websocket_transport_async_unit.py(new)aiohttp.ClientSessionand assert the cleanup invariants — no live Event Hubs needed.CHANGELOG.md: bullet under a new5.15.2 (Unreleased)### Bugs Fixedsection.Why the sibling sync transport is not affected
The sync
WebSocketTransportin_transport.pyuses thewebsocket-clientlibrary, notaiohttp, and does not maintain a separateClientSession. Itsclose()(line 743) andconnect()(line 655) already perform proper cleanup. So this fix is async-only.Test plan
sock.close()raises →session.close()never called)mypyreports zero new errors in the modified filepyrightreports zero new errors in the modified file (pre-existingreportOptionalMemberAccessin unchanged_read/_writeremain)pylintclean for the modified file (only unrelatedunknown-option-valuewarnings from thepylint-guidelines-checkerplugin in my local env)azpysdkchecks) — to be validated on PR runReproduction of the original bug
Run an EventHub consumer/producer over WebSocket transport (
TransportType.AmqpOverWebsocket) for an extended period with intermittent network disruptions. TheUnclosed client sessionerrors appear in logs on each reconnection cycle and on shutdown.🤖 Generated with Claude Code