Compare commits

...

12 Commits

Author SHA1 Message Date
Steven Silvester
9106250ad4
[v4.13] PYTHON-5430 Use the zizmor action (#2419) 2025-07-22 07:48:48 -05:00
mongodb-dbx-release-bot[bot]
51924e0eea
BUMP 4.13.3.dev0
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-06-16 18:19:48 +00:00
Steven Silvester
0a150feeef
PYTHON-5416 Prep for 4.13.2 release (#2389) 2025-06-16 07:59:52 -05:00
Shane Harvey
ad3417411a
[v4.13] PYTHON-5414 Fix "module service_identity has no attribute SICertificateError" when using pyopenssl (#2386)
Co-authored-by: Maarten Sijm <9739541+mpsijm@users.noreply.github.com>
2025-06-16 06:27:20 -05:00
Noah Stapp
b127460c90
[v4.13] PYTHON 5212 - Use asyncio.loop.sock_connect in _async_create_connection (#2387) 2025-06-13 16:07:23 -04:00
mongodb-dbx-release-bot[bot]
a2077f60ea
BUMP 4.13.2.dev0
Signed-off-by: mongodb-dbx-release-bot[bot] <167856002+mongodb-dbx-release-bot[bot]@users.noreply.github.com>
2025-06-11 19:27:37 +00:00
Steven Silvester
5f800da807
[v4.13] PYTHON-5406 - Use correct client for test (#2378)
Co-authored-by: Noah Stapp <noah.stapp@mongodb.com>
2025-06-11 13:24:12 -05:00
Steven Silvester
386d1afcb5
[v4.13] PYTHON-5400 Migrate away from Windows Server 2019 runner image on Github Actions (#2375) 2025-06-10 12:53:34 -05:00
Steven Silvester
1eee90f0e5
[v4.13] PYTHON-5405 Use legacy wait_for_read cancellation approach on Windows (#2368) 2025-06-10 12:43:47 -05:00
Steven Silvester
09a32f6d40
[v4.13] PYTHON-5406 - AsyncPeriodicExecutor must reset CSOT contextvars before executing (#2373)
Co-authored-by: Noah Stapp <noah.stapp@mongodb.com>
2025-06-10 12:26:43 -05:00
Steven Silvester
14417adc3f
PYTHON-5411 Prep for 4.13.1 release (#2370) 2025-06-10 10:44:31 -05:00
Steven Silvester
56e6f1c358
[v4.13] Update release metadata (#2365) 2025-06-09 12:30:14 -05:00
15 changed files with 173 additions and 27 deletions

View File

@ -39,8 +39,8 @@ jobs:
- [ubuntu-latest, "manylinux_ppc64le", "cp3*-manylinux_ppc64le"] - [ubuntu-latest, "manylinux_ppc64le", "cp3*-manylinux_ppc64le"]
- [ubuntu-latest, "manylinux_s390x", "cp3*-manylinux_s390x"] - [ubuntu-latest, "manylinux_s390x", "cp3*-manylinux_s390x"]
- [ubuntu-latest, "manylinux_i686", "cp3*-manylinux_i686"] - [ubuntu-latest, "manylinux_i686", "cp3*-manylinux_i686"]
- [windows-2019, "win_amd6", "cp3*-win_amd64"] - [windows-2022, "win_amd6", "cp3*-win_amd64"]
- [windows-2019, "win32", "cp3*-win32"] - [windows-2022, "win32", "cp3*-win32"]
- [macos-14, "macos", "cp*-macosx_*"] - [macos-14, "macos", "cp*-macosx_*"]
steps: steps:

View File

@ -16,7 +16,7 @@ env:
# Changes per repo # Changes per repo
PRODUCT_NAME: PyMongo PRODUCT_NAME: PyMongo
# Changes per branch # Changes per branch
EVERGREEN_PROJECT: mongo-python-driver EVERGREEN_PROJECT: mongo-python-driver-release
# Constant # Constant
# inputs will be empty on a scheduled run. so, we only set dry_run # inputs will be empty on a scheduled run. so, we only set dry_run
# to 'false' when the input is set to 'false'. # to 'false' when the input is set to 'false'.

View File

@ -17,16 +17,5 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Setup Rust
uses: actions-rust-lang/setup-rust-toolchain@9d7e65c320fdb52dcd45ffaa68deb6c02c8754d9 # v1
- name: Get zizmor
run: cargo install zizmor
- name: Run zizmor 🌈 - name: Run zizmor 🌈
run: zizmor --format sarif . > results.sarif uses: zizmorcore/zizmor-action@1c7106082dbc1753372e3924b7da1b9417011a21
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3
with:
sarif_file: results.sarif
category: zizmor

View File

@ -1,6 +1,39 @@
Changelog Changelog
========= =========
Changes in Version 4.13.2 (2025/06/17)
--------------------------------------
Version 4.13.2 is a bug fix release.
- Fixed a bug where ``AsyncMongoClient`` would block the event loop while creating new connections,
potentially significantly increasing latency for ongoing operations.
- Fixed a bug that resulted in confusing error messages after hostname verification errors when using PyOpenSSL.
Issues Resolved
...............
See the `PyMongo 4.13.2 release notes in JIRA`_ for the list of resolved issues
in this release.
.. _PyMongo 4.13.2 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43937
Changes in Version 4.13.1 (2025/06/10)
--------------------------------------
Version 4.13.1 is a bug fix release.
- Fixed a bug that could raise ``ServerSelectionTimeoutError`` when using timeouts with ``AsyncMongoClient``.
- Fixed a bug that could raise ``NetworkTimeout`` errors on Windows.
Issues Resolved
...............
See the `PyMongo 4.13.1 release notes in JIRA`_ for the list of resolved issues
in this release.
.. _PyMongo 4.13.1 release notes in JIRA: https://jira.mongodb.org/secure/ReleaseNote.jspa?projectId=10004&version=43924
Changes in Version 4.13.0 (2025/05/14) Changes in Version 4.13.0 (2025/05/14)
-------------------------------------- --------------------------------------

View File

@ -32,6 +32,12 @@ RTT: ContextVar[float] = ContextVar("RTT", default=0.0)
DEADLINE: ContextVar[float] = ContextVar("DEADLINE", default=float("inf")) DEADLINE: ContextVar[float] = ContextVar("DEADLINE", default=float("inf"))
def reset_all() -> None:
TIMEOUT.set(None)
RTT.set(0.0)
DEADLINE.set(float("inf"))
def get_timeout() -> Optional[float]: def get_timeout() -> Optional[float]:
return TIMEOUT.get(None) return TIMEOUT.get(None)

View File

@ -18,7 +18,7 @@ from __future__ import annotations
import re import re
from typing import List, Tuple, Union from typing import List, Tuple, Union
__version__ = "4.13.0" __version__ = "4.13.3.dev0"
def get_version_tuple(version: str) -> Tuple[Union[int, str], ...]: def get_version_tuple(version: str) -> Tuple[Union[int, str], ...]:

View File

@ -286,6 +286,7 @@ async def _async_socket_receive(
_PYPY = "PyPy" in sys.version _PYPY = "PyPy" in sys.version
_WINDOWS = sys.platform == "win32"
def wait_for_read(conn: Connection, deadline: Optional[float]) -> None: def wait_for_read(conn: Connection, deadline: Optional[float]) -> None:
@ -337,7 +338,8 @@ def receive_data(conn: Connection, length: int, deadline: Optional[float]) -> me
while bytes_read < length: while bytes_read < length:
try: try:
# Use the legacy wait_for_read cancellation approach on PyPy due to PYTHON-5011. # Use the legacy wait_for_read cancellation approach on PyPy due to PYTHON-5011.
if _PYPY: # also use it on Windows due to PYTHON-5405
if _PYPY or _WINDOWS:
wait_for_read(conn, deadline) wait_for_read(conn, deadline)
if _csot.get_timeout() and deadline is not None: if _csot.get_timeout() and deadline is not None:
conn.set_conn_timeout(max(deadline - time.monotonic(), 0)) conn.set_conn_timeout(max(deadline - time.monotonic(), 0))
@ -359,6 +361,7 @@ def receive_data(conn: Connection, length: int, deadline: Optional[float]) -> me
raise _OperationCancelled("operation cancelled") from None raise _OperationCancelled("operation cancelled") from None
if ( if (
_PYPY _PYPY
or _WINDOWS
or not conn.is_sdam or not conn.is_sdam
and deadline is not None and deadline is not None
and deadline - time.monotonic() < 0 and deadline - time.monotonic() < 0

View File

@ -23,6 +23,7 @@ import time
import weakref import weakref
from typing import Any, Optional from typing import Any, Optional
from pymongo import _csot
from pymongo._asyncio_task import create_task from pymongo._asyncio_task import create_task
from pymongo.lock import _create_lock from pymongo.lock import _create_lock
@ -93,6 +94,8 @@ class AsyncPeriodicExecutor:
self._skip_sleep = True self._skip_sleep = True
async def _run(self) -> None: async def _run(self) -> None:
# The CSOT contextvars must be cleared inside the executor task before execution begins
_csot.reset_all()
while not self._stopped: while not self._stopped:
if self._task and self._task.cancelling(): # type: ignore[unused-ignore, attr-defined] if self._task and self._task.cancelling(): # type: ignore[unused-ignore, attr-defined]
raise asyncio.CancelledError raise asyncio.CancelledError

View File

@ -206,7 +206,8 @@ async def _async_create_connection(address: _Address, options: PoolOptions) -> s
# SOCK_CLOEXEC not supported for Unix sockets. # SOCK_CLOEXEC not supported for Unix sockets.
_set_non_inheritable_non_atomic(sock.fileno()) _set_non_inheritable_non_atomic(sock.fileno())
try: try:
sock.connect(host) sock.setblocking(False)
await asyncio.get_running_loop().sock_connect(sock, host)
return sock return sock
except OSError: except OSError:
sock.close() sock.close()
@ -241,14 +242,22 @@ async def _async_create_connection(address: _Address, options: PoolOptions) -> s
timeout = options.connect_timeout timeout = options.connect_timeout
elif timeout <= 0: elif timeout <= 0:
raise socket.timeout("timed out") raise socket.timeout("timed out")
sock.settimeout(timeout)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True) sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True)
_set_keepalive_times(sock) _set_keepalive_times(sock)
sock.connect(sa) # Socket needs to be non-blocking during connection to not block the event loop
sock.setblocking(False)
await asyncio.wait_for(
asyncio.get_running_loop().sock_connect(sock, sa), timeout=timeout
)
sock.settimeout(timeout)
return sock return sock
except OSError as e: except asyncio.TimeoutError as e:
err = e
sock.close() sock.close()
err = socket.timeout("timed out")
err.__cause__ = e
except OSError as e:
sock.close()
err = e # type: ignore[assignment]
if err is not None: if err is not None:
raise err raise err

View File

@ -420,9 +420,9 @@ class SSLContext:
pyopenssl.verify_ip_address(ssl_conn, server_hostname) pyopenssl.verify_ip_address(ssl_conn, server_hostname)
else: else:
pyopenssl.verify_hostname(ssl_conn, server_hostname) pyopenssl.verify_hostname(ssl_conn, server_hostname)
except ( # type:ignore[misc] except (
service_identity.SICertificateError, service_identity.CertificateError,
service_identity.SIVerificationError, service_identity.VerificationError,
) as exc: ) as exc:
raise _CertificateError(str(exc)) from None raise _CertificateError(str(exc)) from None
return ssl_conn return ssl_conn

View File

@ -0,0 +1,41 @@
# Copyright 2025-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test that AsyncPeriodicExecutors do not copy ContextVars from their parents."""
from __future__ import annotations
import asyncio
import sys
from test.asynchronous.utils import async_get_pool
from test.utils_shared import delay, one
sys.path[0:0] = [""]
from test.asynchronous import AsyncIntegrationTest
class TestAsyncContextVarsReset(AsyncIntegrationTest):
async def test_context_vars_are_reset_in_executor(self):
if sys.version_info < (3, 12):
self.skipTest("Test requires asyncio.Task.get_context (added in Python 3.12)")
await self.client.db.test.insert_one({"x": 1})
for server in self.client._topology._servers.values():
for context in [
c
for c in server._monitor._executor._task.get_context()
if c.name in ["TIMEOUT", "RTT", "DEADLINE"]
]:
self.assertIn(context.get(), [None, float("inf"), 0.0])
await self.client.db.test.delete_many({})

View File

@ -0,0 +1,56 @@
# Copyright 2025-present MongoDB, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test that the asynchronous API does not block the event loop."""
from __future__ import annotations
import asyncio
import time
from test.asynchronous import AsyncIntegrationTest
from pymongo.errors import ServerSelectionTimeoutError
class TestClientLoopUnblocked(AsyncIntegrationTest):
async def test_client_does_not_block_loop(self):
# Use an unreachable TEST-NET host to ensure that the client times out attempting to create a connection.
client = self.simple_client("192.0.2.1", serverSelectionTimeoutMS=500)
latencies = []
# If the loop is being blocked, at least one iteration will have a latency much more than 0.1 seconds
async def background_task():
start = time.monotonic()
try:
while True:
start = time.monotonic()
await asyncio.sleep(0.1)
latencies.append(time.monotonic() - start)
except asyncio.CancelledError:
latencies.append(time.monotonic() - start)
raise
t = asyncio.create_task(background_task())
with self.assertRaisesRegex(ServerSelectionTimeoutError, "No servers found yet"):
await client.admin.command("ping")
t.cancel()
with self.assertRaises(asyncio.CancelledError):
await t
self.assertLessEqual(
sorted(latencies, reverse=True)[0],
1.0,
"Background task was blocked from running",
)

View File

@ -323,7 +323,7 @@ class TestSSL(AsyncIntegrationTest):
response = await self.client.admin.command(HelloCompat.LEGACY_CMD) response = await self.client.admin.command(HelloCompat.LEGACY_CMD)
with self.assertRaises(ConnectionFailure): with self.assertRaises(ConnectionFailure) as cm:
await connected( await connected(
self.simple_client( self.simple_client(
"server", "server",
@ -335,6 +335,8 @@ class TestSSL(AsyncIntegrationTest):
**self.credentials, # type: ignore[arg-type] **self.credentials, # type: ignore[arg-type]
) )
) )
# PYTHON-5414 Check for "module service_identity has no attribute SICertificateError"
self.assertNotIn("has no attribute", str(cm.exception))
await connected( await connected(
self.simple_client( self.simple_client(

View File

@ -323,7 +323,7 @@ class TestSSL(IntegrationTest):
response = self.client.admin.command(HelloCompat.LEGACY_CMD) response = self.client.admin.command(HelloCompat.LEGACY_CMD)
with self.assertRaises(ConnectionFailure): with self.assertRaises(ConnectionFailure) as cm:
connected( connected(
self.simple_client( self.simple_client(
"server", "server",
@ -335,6 +335,8 @@ class TestSSL(IntegrationTest):
**self.credentials, # type: ignore[arg-type] **self.credentials, # type: ignore[arg-type]
) )
) )
# PYTHON-5414 Check for "module service_identity has no attribute SICertificateError"
self.assertNotIn("has no attribute", str(cm.exception))
connected( connected(
self.simple_client( self.simple_client(

View File

@ -185,6 +185,8 @@ def async_only_test(f: str) -> bool:
"test_concurrency.py", "test_concurrency.py",
"test_async_cancellation.py", "test_async_cancellation.py",
"test_async_loop_safety.py", "test_async_loop_safety.py",
"test_async_contextvars_reset.py",
"test_async_loop_unblocked.py",
] ]