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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy