Skip to content

Commit b127460

Browse files
authored
[v4.13] PYTHON 5212 - Use asyncio.loop.sock_connect in _async_create_connection (#2387)
1 parent a2077f6 commit b127460

File tree

4 files changed

+86
-4
lines changed

4 files changed

+86
-4
lines changed

doc/changelog.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
Changelog
22
=========
33

4+
Changes in Version 4.13.2 (2025/06/17)
5+
--------------------------------------
6+
7+
Version 4.13.2 is a bug fix release.
8+
9+
- Fixed a bug where ``AsyncMongoClient`` would block the event loop while creating new connections,
10+
potentially significantly increasing latency for ongoing operations.
11+
12+
Issues Resolved
13+
...............
14+
15+
See the `PyMongo 4.13.2 release notes in JIRA`_ for the list of resolved issues
16+
in this release.
17+
18+
.. _PyMongo 4.13.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43937
19+
420
Changes in Version 4.13.1 (2025/06/10)
521
--------------------------------------
622

pymongo/pool_shared.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ async def _async_create_connection(address: _Address, options: PoolOptions) -> s
206206
# SOCK_CLOEXEC not supported for Unix sockets.
207207
_set_non_inheritable_non_atomic(sock.fileno())
208208
try:
209-
sock.connect(host)
209+
sock.setblocking(False)
210+
await asyncio.get_running_loop().sock_connect(sock, host)
210211
return sock
211212
except OSError:
212213
sock.close()
@@ -241,14 +242,22 @@ async def _async_create_connection(address: _Address, options: PoolOptions) -> s
241242
timeout = options.connect_timeout
242243
elif timeout <= 0:
243244
raise socket.timeout("timed out")
244-
sock.settimeout(timeout)
245245
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True)
246246
_set_keepalive_times(sock)
247-
sock.connect(sa)
247+
# Socket needs to be non-blocking during connection to not block the event loop
248+
sock.setblocking(False)
249+
await asyncio.wait_for(
250+
asyncio.get_running_loop().sock_connect(sock, sa), timeout=timeout
251+
)
252+
sock.settimeout(timeout)
248253
return sock
254+
except asyncio.TimeoutError as e:
255+
sock.close()
256+
err = socket.timeout("timed out")
257+
err.__cause__ = e
249258
except OSError as e:
250-
err = e
251259
sock.close()
260+
err = e # type: ignore[assignment]
252261

253262
if err is not None:
254263
raise err
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2025-present MongoDB, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Test that the asynchronous API does not block the event loop."""
16+
from __future__ import annotations
17+
18+
import asyncio
19+
import time
20+
from test.asynchronous import AsyncIntegrationTest
21+
22+
from pymongo.errors import ServerSelectionTimeoutError
23+
24+
25+
class TestClientLoopUnblocked(AsyncIntegrationTest):
26+
async def test_client_does_not_block_loop(self):
27+
# Use an unreachable TEST-NET host to ensure that the client times out attempting to create a connection.
28+
client = self.simple_client("192.0.2.1", serverSelectionTimeoutMS=500)
29+
latencies = []
30+
31+
# If the loop is being blocked, at least one iteration will have a latency much more than 0.1 seconds
32+
async def background_task():
33+
start = time.monotonic()
34+
try:
35+
while True:
36+
start = time.monotonic()
37+
await asyncio.sleep(0.1)
38+
latencies.append(time.monotonic() - start)
39+
except asyncio.CancelledError:
40+
latencies.append(time.monotonic() - start)
41+
raise
42+
43+
t = asyncio.create_task(background_task())
44+
45+
with self.assertRaisesRegex(ServerSelectionTimeoutError, "No servers found yet"):
46+
await client.admin.command("ping")
47+
48+
t.cancel()
49+
with self.assertRaises(asyncio.CancelledError):
50+
await t
51+
52+
self.assertLessEqual(
53+
sorted(latencies, reverse=True)[0],
54+
1.0,
55+
"Background task was blocked from running",
56+
)

tools/synchro.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ def async_only_test(f: str) -> bool:
186186
"test_async_cancellation.py",
187187
"test_async_loop_safety.py",
188188
"test_async_contextvars_reset.py",
189+
"test_async_loop_unblocked.py",
189190
]
190191

191192

0 commit comments

Comments
 (0)