Skip to content

PYTHON-5867 - Close sockets on interruption or cancellation during async connection creation#2858

Draft
NoahStapp wants to merge 3 commits into
mongodb:masterfrom
NoahStapp:PYTHON-5867
Draft

PYTHON-5867 - Close sockets on interruption or cancellation during async connection creation#2858
NoahStapp wants to merge 3 commits into
mongodb:masterfrom
NoahStapp:PYTHON-5867

Conversation

@NoahStapp

Copy link
Copy Markdown
Contributor

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

  • Did you update the changelog (if necessary)?
  • Is there test coverage?
  • Is any followup work tracked in a JIRA ticket? If so, add link(s).

Checklist for Reviewer

  • Does the title of the PR reference a JIRA Ticket?
  • Do you fully understand the implementation? (Would you be comfortable explaining how this code works to someone else?)
  • Is all relevant documentation (README or docstring) updated?

Copilot AI 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.

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__) to AsyncNetworkingInterface to 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.

Comment thread pymongo/network_layer.py Outdated
Comment on lines +437 to +448
def sock(self) -> socket.socket:
return self.conn[0].get_extra_info("socket")

def __del__(self) -> None:
# Synchronously release the raw socket in case the event loop is already closed
# or this connection was orphaned.
# Safe even if asyncio has already closed the socket.
try:
if self.sock is not None:
self.sock.close()
except Exception: # noqa: S110
pass
Comment thread pymongo/pool_shared.py
Comment on lines +185 to 188
except BaseException:
# Protect against cancellation or interruption where the raw socket would otherwise leak
sock.close()
raise
@NoahStapp NoahStapp marked this pull request as draft June 8, 2026 18:41
@codecov-commenter

codecov-commenter commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 10 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pymongo/pool_shared.py 50.00% 8 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@NoahStapp NoahStapp requested a review from Copilot June 8, 2026 20:36

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment on lines +140 to +164
real_socket_cls = _socket.socket

def tracking_socket(*args, **kwargs):
s = real_socket_cls(*args, **kwargs)
created_sockets.append(s)
return s

loop = asyncio.get_running_loop()
started = asyncio.Event()
block_forever = asyncio.Event()

async def slow_sock_connect(sock, addr):
started.set()
await block_forever.wait()

with (
patch.object(_socket, "socket", tracking_socket),
patch.object(loop, "sock_connect", slow_sock_connect),
):
task = asyncio.create_task(pool_shared._async_create_connection(address, options))
await asyncio.wait_for(started.wait(), timeout=5)
task.cancel()
with self.assertRaises(asyncio.CancelledError):
await task

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.

3 participants