Skip to content

Commit 4cd2a64

Browse files
committed
Merge branch 'master' of github.com:mongodb/mongo-python-driver
2 parents 1858879 + 87c015f commit 4cd2a64

File tree

11 files changed

+117
-16
lines changed

11 files changed

+117
-16
lines changed

.github/workflows/codeql.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646

4747
# Initializes the CodeQL tools for scanning.
4848
- name: Initialize CodeQL
49-
uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3
49+
uses: github/codeql-action/init@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3
5050
with:
5151
languages: ${{ matrix.language }}
5252
build-mode: ${{ matrix.build-mode }}
@@ -63,6 +63,6 @@ jobs:
6363
pip install -e .
6464
6565
- name: Perform CodeQL Analysis
66-
uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3
66+
uses: github/codeql-action/analyze@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3
6767
with:
6868
category: "/language:${{matrix.language}}"

.github/workflows/zizmor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
env:
2727
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2828
- name: Upload SARIF file
29-
uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3
29+
uses: github/codeql-action/upload-sarif@ce28f5bb42b7a9f2c824e633a3f6ee835bab6858 # v3
3030
with:
3131
sarif_file: results.sarif
3232
category: zizmor

doc/changelog.rst

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@ PyMongo 4.14 brings a number of changes including:
88
- Added :attr:`bson.codec_options.TypeRegistry.codecs` and :attr:`bson.codec_options.TypeRegistry.fallback_encoder` properties
99
to allow users to directly access the type codecs and fallback encoder for a given :class:`bson.codec_options.TypeRegistry`.
1010

11+
Changes in Version 4.13.2 (2025/06/17)
12+
--------------------------------------
13+
14+
Version 4.13.2 is a bug fix release.
15+
16+
- Fixed a bug where ``AsyncMongoClient`` would block the event loop while creating new connections,
17+
potentially significantly increasing latency for ongoing operations.
18+
19+
Issues Resolved
20+
...............
21+
22+
See the `PyMongo 4.13.2 release notes in JIRA`_ for the list of resolved issues
23+
in this release.
24+
25+
.. _PyMongo 4.13.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43937
26+
27+
28+
Changes in Version 4.13.1 (2025/06/10)
29+
--------------------------------------
30+
31+
Version 4.13.1 is a bug fix release.
32+
33+
- Fixed a bug that could raise ``ServerSelectionTimeoutError`` when using timeouts with ``AsyncMongoClient``.
34+
- Fixed a bug that could raise ``NetworkTimeout`` errors on Windows.
35+
36+
Issues Resolved
37+
...............
38+
39+
See the `PyMongo 4.13.1 release notes in JIRA`_ for the list of resolved issues
40+
in this release.
41+
42+
.. _PyMongo 4.13.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43924
43+
1144
Changes in Version 4.13.0 (2025/05/14)
1245
--------------------------------------
1346

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

pymongo/pyopenssl_context.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -420,9 +420,9 @@ def wrap_socket(
420420
pyopenssl.verify_ip_address(ssl_conn, server_hostname)
421421
else:
422422
pyopenssl.verify_hostname(ssl_conn, server_hostname)
423-
except ( # type:ignore[misc]
424-
service_identity.SICertificateError,
425-
service_identity.SIVerificationError,
423+
except (
424+
service_identity.CertificateError,
425+
service_identity.VerificationError,
426426
) as exc:
427427
raise _CertificateError(str(exc)) from None
428428
return ssl_conn
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+
)

test/asynchronous/test_session.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ async def test_implicit_sessions_checkout(self):
196196
lsid_set = set()
197197
listener = OvertCommandListener()
198198
client = await self.async_rs_or_single_client(event_listeners=[listener], maxPoolSize=1)
199-
# Retry up to 10 times because there is a known race that can cause multiple
199+
# Retry up to 10 times because there is a known race condition that can cause multiple
200200
# sessions to be used: connection check in happens before session check in
201201
for _ in range(10):
202202
cursor = client.db.test.find({})
@@ -235,7 +235,6 @@ async def target(op, *args):
235235
for t in tasks:
236236
await t.join()
237237
self.assertIsNone(t.exc)
238-
await client.close()
239238
lsid_set.clear()
240239
for i in listener.started_events:
241240
if i.command.get("lsid"):

test/asynchronous/test_ssl.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ async def test_cert_ssl_validation_hostname_matching(self):
323323

324324
response = await self.client.admin.command(HelloCompat.LEGACY_CMD)
325325

326-
with self.assertRaises(ConnectionFailure):
326+
with self.assertRaises(ConnectionFailure) as cm:
327327
await connected(
328328
self.simple_client(
329329
"server",
@@ -335,6 +335,8 @@ async def test_cert_ssl_validation_hostname_matching(self):
335335
**self.credentials, # type: ignore[arg-type]
336336
)
337337
)
338+
# PYTHON-5414 Check for "module service_identity has no attribute SICertificateError"
339+
self.assertNotIn("has no attribute", str(cm.exception))
338340

339341
await connected(
340342
self.simple_client(

test/test_session.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def test_implicit_sessions_checkout(self):
196196
lsid_set = set()
197197
listener = OvertCommandListener()
198198
client = self.rs_or_single_client(event_listeners=[listener], maxPoolSize=1)
199-
# Retry up to 10 times because there is a known race that can cause multiple
199+
# Retry up to 10 times because there is a known race condition that can cause multiple
200200
# sessions to be used: connection check in happens before session check in
201201
for _ in range(10):
202202
cursor = client.db.test.find({})
@@ -235,7 +235,6 @@ def target(op, *args):
235235
for t in tasks:
236236
t.join()
237237
self.assertIsNone(t.exc)
238-
client.close()
239238
lsid_set.clear()
240239
for i in listener.started_events:
241240
if i.command.get("lsid"):

test/test_ssl.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ def test_cert_ssl_validation_hostname_matching(self):
323323

324324
response = self.client.admin.command(HelloCompat.LEGACY_CMD)
325325

326-
with self.assertRaises(ConnectionFailure):
326+
with self.assertRaises(ConnectionFailure) as cm:
327327
connected(
328328
self.simple_client(
329329
"server",
@@ -335,6 +335,8 @@ def test_cert_ssl_validation_hostname_matching(self):
335335
**self.credentials, # type: ignore[arg-type]
336336
)
337337
)
338+
# PYTHON-5414 Check for "module service_identity has no attribute SICertificateError"
339+
self.assertNotIn("has no attribute", str(cm.exception))
338340

339341
connected(
340342
self.simple_client(

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)