PYTHON-5867 - Close sockets on interruption or cancellation during async connection creation#2858
PYTHON-5867 - Close sockets on interruption or cancellation during async connection creation#2858NoahStapp wants to merge 14 commits into
Conversation
…ync connection creation
There was a problem hiding this comment.
Pull request overview
This PR addresses a resource-leak scenario in PyMongo’s async connection setup by ensuring sockets/transports are closed when connection creation is interrupted (e.g., task cancellation), which is especially important for event-loop reliability and preventing leaked file descriptors.
Changes:
- Add cancellation/interruption-safe cleanup (
except BaseException: close(); raise) in async socket creation and TLS configuration paths. - Ensure transports are aborted / sockets are closed when failures occur during async protocol interface setup.
- Add a destructor (
__del__) toAsyncNetworkingInterfaceto synchronously close the underlying raw socket if the connection is orphaned (e.g., loop already closed).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
pymongo/pool_shared.py |
Adds broad-exception cleanup to prevent socket leaks during async connection creation/configuration and protocol setup. |
pymongo/network_layer.py |
Adds AsyncNetworkingInterface.__del__ to defensively close raw sockets when async connections are orphaned. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| import asyncio | ||
| import functools | ||
| import socket as _socket | ||
| import ssl as _ssl |
There was a problem hiding this comment.
Do _socket and _ssl need to be private here ?
There was a problem hiding this comment.
Good catch, vestigial import naming from an earlier version that overrode them.
|
How about a context manager to avoid repeated E.g. add something like: So then: would become something like: |
I like it, good cleanup! |
| else: | ||
| loop = asyncio.get_running_loop() | ||
| ssl_sock = await loop.run_in_executor(None, ssl_context.wrap_socket, sock) # type: ignore[assignment, misc, unused-ignore] | ||
| with _cleanup_on_error(sock.close): |
There was a problem hiding this comment.
Hmmm, the exhaustive code review 🤖 found a "cross-thread fd-reuse hazard":
_async_configured_socket hands sock to a worker thread via
run_in_executor(wrap_socket, sock)and asyncio can't stop that thread on cancel andwrap_socketcalls sock.detach() before the slow handshake
It suggests replacing the above with _clean_on_error block with
ssl_sock = await asyncio.shield(future)
and then after the except (OSError, *SSLErrors) as exc add another except BaseException:
except BaseException:
future.add_done_callback(_close_executor_socket)
raise
and add the proposed corresponding _close_executor_socket helper:
def _close_executor_socket(future):
try:
ssl_sock = future.result()
except BaseException:
pass
else:
ssl_sock.close()
PYTHON-5867
Changes in this PR
Close sockets opened as part of async connection creation when an interruption or cancellation occurs.
Test Plan
Fix existing PyPy test failure.
Checklist
Checklist for Author
Checklist for Reviewer