Skip to content

Commit c7b822d

Browse files
authored
Merge branch 'v4.13' into PYTHON-5414-v4.13
2 parents e7114f4 + b127460 commit c7b822d

File tree

4 files changed

+72
-4
lines changed

4 files changed

+72
-4
lines changed

doc/changelog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ Changes in Version 4.13.2 (2025/06/17)
66

77
Version 4.13.2 is a bug fix release.
88

9+
- Fixed a bug where ``AsyncMongoClient`` would block the event loop while creating new connections,
10+
potentially significantly increasing latency for ongoing operations.
911
- Fixed a bug that resulted in confusing error messages after hostname verification errors when using PyOpenSSL.
1012

1113
Issues Resolved

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)