Skip to content

Commit 9e88a18

Browse files
committed
Merge branch 'master' of github.com:mongodb/mongo-python-driver into cleanup_task_combos
2 parents 53ded6f + c3e3373 commit 9e88a18

29 files changed

+478
-150
lines changed

.evergreen/generated_configs/variants.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -626,17 +626,19 @@ buildvariants:
626626
- macos-14
627627
batchtime: 10080
628628
expansions:
629+
TEST_NAME: default
629630
SUB_TEST_NAME: pyopenssl
630631
PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3
631632
- name: pyopenssl-rhel8-python3.10
632633
tasks:
633-
- name: .replica_set .auth .ssl .sync
634-
- name: .7.0 .auth .ssl .sync
634+
- name: .replica_set .auth .ssl .sync_async
635+
- name: .7.0 .auth .ssl .sync_async
635636
display_name: PyOpenSSL RHEL8 Python3.10
636637
run_on:
637638
- rhel87-small
638639
batchtime: 10080
639640
expansions:
641+
TEST_NAME: default
640642
SUB_TEST_NAME: pyopenssl
641643
PYTHON_BINARY: /opt/python/3.10/bin/python3
642644
- name: pyopenssl-rhel8-python3.11
@@ -648,6 +650,7 @@ buildvariants:
648650
- rhel87-small
649651
batchtime: 10080
650652
expansions:
653+
TEST_NAME: default
651654
SUB_TEST_NAME: pyopenssl
652655
PYTHON_BINARY: /opt/python/3.11/bin/python3
653656
- name: pyopenssl-rhel8-python3.12
@@ -659,17 +662,19 @@ buildvariants:
659662
- rhel87-small
660663
batchtime: 10080
661664
expansions:
665+
TEST_NAME: default
662666
SUB_TEST_NAME: pyopenssl
663667
PYTHON_BINARY: /opt/python/3.12/bin/python3
664668
- name: pyopenssl-win64-python3.13
665669
tasks:
666-
- name: .replica_set .auth .ssl .sync
667-
- name: .7.0 .auth .ssl .sync
670+
- name: .replica_set .auth .ssl .sync_async
671+
- name: .7.0 .auth .ssl .sync_async
668672
display_name: PyOpenSSL Win64 Python3.13
669673
run_on:
670674
- windows-64-vsMulti-small
671675
batchtime: 10080
672676
expansions:
677+
TEST_NAME: default
673678
SUB_TEST_NAME: pyopenssl
674679
PYTHON_BINARY: C:/python/Python313/python.exe
675680
- name: pyopenssl-rhel8-pypy3.10
@@ -681,6 +686,7 @@ buildvariants:
681686
- rhel87-small
682687
batchtime: 10080
683688
expansions:
689+
TEST_NAME: default
684690
SUB_TEST_NAME: pyopenssl
685691
PYTHON_BINARY: /opt/python/pypy3.10/bin/python3
686692

.evergreen/scripts/generate_config.py

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ def create_enterprise_auth_variants():
253253
def create_pyopenssl_variants():
254254
base_name = "PyOpenSSL"
255255
batchtime = BATCHTIME_WEEK
256-
expansions = dict(SUB_TEST_NAME="pyopenssl")
256+
expansions = dict(TEST_NAME="default", SUB_TEST_NAME="pyopenssl")
257257
variants = []
258258

259259
for python in ALL_PYTHONS:
@@ -268,14 +268,25 @@ def create_pyopenssl_variants():
268268
host = DEFAULT_HOST
269269

270270
display_name = get_variant_name(base_name, host, python=python)
271-
variant = create_variant(
272-
[f".replica_set .{auth} .{ssl} .sync", f".7.0 .{auth} .{ssl} .sync"],
273-
display_name,
274-
python=python,
275-
host=host,
276-
expansions=expansions,
277-
batchtime=batchtime,
278-
)
271+
# only need to run some on async
272+
if python in (CPYTHONS[1], CPYTHONS[-1]):
273+
variant = create_variant(
274+
[f".replica_set .{auth} .{ssl} .sync_async", f".7.0 .{auth} .{ssl} .sync_async"],
275+
display_name,
276+
python=python,
277+
host=host,
278+
expansions=expansions,
279+
batchtime=batchtime,
280+
)
281+
else:
282+
variant = create_variant(
283+
[f".replica_set .{auth} .{ssl} .sync", f".7.0 .{auth} .{ssl} .sync"],
284+
display_name,
285+
python=python,
286+
host=host,
287+
expansions=expansions,
288+
batchtime=batchtime,
289+
)
279290
variants.append(variant)
280291

281292
return variants

.github/workflows/codeql.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ jobs:
5454
queries: security-extended
5555
config: |
5656
paths-ignore:
57-
- '.github/**'
5857
- 'doc/**'
5958
- 'tools/**'
6059
- 'test/**'

doc/changelog.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Version 4.12.1 is a bug fix release.
1616
errors such as: "NotImplementedError: Database objects do not implement truth value testing or bool()".
1717
- Removed Eventlet testing against Python versions newer than 3.9 since
1818
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.
19+
- Fixed a bug where MongoDB cluster topology changes could cause asynchronous operations to take much longer to complete
20+
due to holding the Topology lock while closing stale connections.
21+
- Fixed a bug that would cause AsyncMongoClient to attempt to use PyOpenSSL when available, resulting in errors such as
22+
"pymongo.errors.ServerSelectionTimeoutError: 'SSLContext' object has no attribute 'wrap_bio'".
1923

2024
Issues Resolved
2125
...............

pymongo/asynchronous/encryption.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
from pymongo.results import BulkWriteResult, DeleteResult
8888
from pymongo.ssl_support import BLOCKING_IO_ERRORS, get_ssl_context
8989
from pymongo.typings import _DocumentType, _DocumentTypeArg
90-
from pymongo.uri_parser_shared import parse_host
90+
from pymongo.uri_parser_shared import _parse_kms_tls_options, parse_host
9191
from pymongo.write_concern import WriteConcern
9292

9393
if TYPE_CHECKING:
@@ -157,6 +157,7 @@ def __init__(
157157
self.mongocryptd_client = mongocryptd_client
158158
self.opts = opts
159159
self._spawned = False
160+
self._kms_ssl_contexts = opts._kms_ssl_contexts(_IS_SYNC)
160161

161162
async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
162163
"""Complete a KMS request.
@@ -168,7 +169,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
168169
endpoint = kms_context.endpoint
169170
message = kms_context.message
170171
provider = kms_context.kms_provider
171-
ctx = self.opts._kms_ssl_contexts.get(provider)
172+
ctx = self._kms_ssl_contexts.get(provider)
172173
if ctx is None:
173174
# Enable strict certificate verification, OCSP, match hostname, and
174175
# SNI using the system default CA certificates.
@@ -180,6 +181,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
180181
False, # allow_invalid_certificates
181182
False, # allow_invalid_hostnames
182183
False, # disable_ocsp_endpoint_check
184+
_IS_SYNC,
183185
)
184186
# CSOT: set timeout for socket creation.
185187
connect_timeout = max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0.001)
@@ -396,6 +398,8 @@ def __init__(self, client: AsyncMongoClient[_DocumentTypeArg], opts: AutoEncrypt
396398
encrypted_fields_map = _dict_to_bson(opts._encrypted_fields_map, False, _DATA_KEY_OPTS)
397399
self._bypass_auto_encryption = opts._bypass_auto_encryption
398400
self._internal_client = None
401+
# parsing kms_ssl_contexts here so that parsing errors will be raised before internal clients are created
402+
opts._kms_ssl_contexts(_IS_SYNC)
399403

400404
def _get_internal_client(
401405
encrypter: _Encrypter, mongo_client: AsyncMongoClient[_DocumentTypeArg]
@@ -675,6 +679,7 @@ def __init__(
675679
kms_tls_options=kms_tls_options,
676680
key_expiration_ms=key_expiration_ms,
677681
)
682+
self._kms_ssl_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)
678683
self._io_callbacks: Optional[_EncryptionIO] = _EncryptionIO(
679684
None, key_vault_coll, None, opts
680685
)

pymongo/asynchronous/pool.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from __future__ import annotations
1616

17+
import asyncio
1718
import collections
1819
import contextlib
1920
import logging
@@ -75,6 +76,7 @@
7576
from pymongo.network_layer import AsyncNetworkingInterface, async_receive_message, async_sendall
7677
from pymongo.pool_options import PoolOptions
7778
from pymongo.pool_shared import (
79+
SSLErrors,
7880
_CancellationContext,
7981
_configured_protocol_interface,
8082
_get_timeout_details,
@@ -85,7 +87,6 @@
8587
from pymongo.server_api import _add_to_command
8688
from pymongo.server_type import SERVER_TYPE
8789
from pymongo.socket_checker import SocketChecker
88-
from pymongo.ssl_support import SSLError
8990

9091
if TYPE_CHECKING:
9192
from bson import CodecOptions
@@ -637,7 +638,7 @@ async def _raise_connection_failure(self, error: BaseException) -> NoReturn:
637638
reason = ConnectionClosedReason.ERROR
638639
await self.close_conn(reason)
639640
# SSLError from PyOpenSSL inherits directly from Exception.
640-
if isinstance(error, (IOError, OSError, SSLError)):
641+
if isinstance(error, (IOError, OSError, *SSLErrors)):
641642
details = _get_timeout_details(self.opts)
642643
_raise_connection_failure(self.address, error, timeout_details=details)
643644
else:
@@ -860,8 +861,14 @@ async def _reset(
860861
# PoolClosedEvent but that reset() SHOULD close sockets *after*
861862
# publishing the PoolClearedEvent.
862863
if close:
863-
for conn in sockets:
864-
await conn.close_conn(ConnectionClosedReason.POOL_CLOSED)
864+
if not _IS_SYNC:
865+
await asyncio.gather(
866+
*[conn.close_conn(ConnectionClosedReason.POOL_CLOSED) for conn in sockets],
867+
return_exceptions=True,
868+
)
869+
else:
870+
for conn in sockets:
871+
await conn.close_conn(ConnectionClosedReason.POOL_CLOSED)
865872
if self.enabled_for_cmap:
866873
assert listeners is not None
867874
listeners.publish_pool_closed(self.address)
@@ -891,8 +898,14 @@ async def _reset(
891898
serverPort=self.address[1],
892899
serviceId=service_id,
893900
)
894-
for conn in sockets:
895-
await conn.close_conn(ConnectionClosedReason.STALE)
901+
if not _IS_SYNC:
902+
await asyncio.gather(
903+
*[conn.close_conn(ConnectionClosedReason.STALE) for conn in sockets],
904+
return_exceptions=True,
905+
)
906+
else:
907+
for conn in sockets:
908+
await conn.close_conn(ConnectionClosedReason.STALE)
896909

897910
async def update_is_writable(self, is_writable: Optional[bool]) -> None:
898911
"""Updates the is_writable attribute on all sockets currently in the
@@ -938,8 +951,14 @@ async def remove_stale_sockets(self, reference_generation: int) -> None:
938951
and self.conns[-1].idle_time_seconds() > self.opts.max_idle_time_seconds
939952
):
940953
close_conns.append(self.conns.pop())
941-
for conn in close_conns:
942-
await conn.close_conn(ConnectionClosedReason.IDLE)
954+
if not _IS_SYNC:
955+
await asyncio.gather(
956+
*[conn.close_conn(ConnectionClosedReason.IDLE) for conn in close_conns],
957+
return_exceptions=True,
958+
)
959+
else:
960+
for conn in close_conns:
961+
await conn.close_conn(ConnectionClosedReason.IDLE)
943962

944963
while True:
945964
async with self.size_cond:
@@ -1033,7 +1052,7 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
10331052
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
10341053
error=ConnectionClosedReason.ERROR,
10351054
)
1036-
if isinstance(error, (IOError, OSError, SSLError)):
1055+
if isinstance(error, (IOError, OSError, *SSLErrors)):
10371056
details = _get_timeout_details(self.opts)
10381057
_raise_connection_failure(self.address, error, timeout_details=details)
10391058

pymongo/asynchronous/topology.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -529,12 +529,6 @@ async def _process_change(
529529
if not _IS_SYNC:
530530
self._monitor_tasks.append(self._srv_monitor)
531531

532-
# Clear the pool from a failed heartbeat.
533-
if reset_pool:
534-
server = self._servers.get(server_description.address)
535-
if server:
536-
await server.pool.reset(interrupt_connections=interrupt_connections)
537-
538532
# Wake anything waiting in select_servers().
539533
self._condition.notify_all()
540534

@@ -557,6 +551,11 @@ async def on_change(
557551
# that didn't include this server.
558552
if self._opened and self._description.has_server(server_description.address):
559553
await self._process_change(server_description, reset_pool, interrupt_connections)
554+
# Clear the pool from a failed heartbeat, done outside the lock to avoid blocking on connection close.
555+
if reset_pool:
556+
server = self._servers.get(server_description.address)
557+
if server:
558+
await server.pool.reset(interrupt_connections=interrupt_connections)
560559

561560
async def _process_srv_update(self, seedlist: list[tuple[str, Any]]) -> None:
562561
"""Process a new seedlist on an opened topology.

pymongo/client_options.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ def _parse_read_concern(options: Mapping[str, Any]) -> ReadConcern:
8484
return ReadConcern(concern)
8585

8686

87-
def _parse_ssl_options(options: Mapping[str, Any]) -> tuple[Optional[SSLContext], bool]:
87+
def _parse_ssl_options(
88+
options: Mapping[str, Any], is_sync: bool
89+
) -> tuple[Optional[SSLContext], bool]:
8890
"""Parse ssl options."""
8991
use_tls = options.get("tls")
9092
if use_tls is not None:
@@ -138,6 +140,7 @@ def _parse_ssl_options(options: Mapping[str, Any]) -> tuple[Optional[SSLContext]
138140
allow_invalid_certificates,
139141
allow_invalid_hostnames,
140142
disable_ocsp_endpoint_check,
143+
is_sync,
141144
)
142145
return ctx, allow_invalid_hostnames
143146
return None, allow_invalid_hostnames
@@ -167,7 +170,7 @@ def _parse_pool_options(
167170
compression_settings = CompressionSettings(
168171
options.get("compressors", []), options.get("zlibcompressionlevel", -1)
169172
)
170-
ssl_context, tls_allow_invalid_hostnames = _parse_ssl_options(options)
173+
ssl_context, tls_allow_invalid_hostnames = _parse_ssl_options(options, is_sync)
171174
load_balanced = options.get("loadbalanced")
172175
max_connecting = options.get("maxconnecting", common.MAX_CONNECTING)
173176
return PoolOptions(

pymongo/encryption_options.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
from typing import TYPE_CHECKING, Any, Mapping, Optional
2222

23+
from pymongo.uri_parser_shared import _parse_kms_tls_options
24+
2325
try:
2426
import pymongocrypt # type:ignore[import-untyped] # noqa: F401
2527

@@ -32,9 +34,9 @@
3234
from bson import int64
3335
from pymongo.common import validate_is_mapping
3436
from pymongo.errors import ConfigurationError
35-
from pymongo.uri_parser_shared import _parse_kms_tls_options
3637

3738
if TYPE_CHECKING:
39+
from pymongo.pyopenssl_context import SSLContext
3840
from pymongo.typings import _AgnosticMongoClient, _DocumentTypeArg
3941

4042

@@ -236,10 +238,22 @@ def __init__(
236238
if not any("idleShutdownTimeoutSecs" in s for s in self._mongocryptd_spawn_args):
237239
self._mongocryptd_spawn_args.append("--idleShutdownTimeoutSecs=60")
238240
# Maps KMS provider name to a SSLContext.
239-
self._kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options)
241+
self._kms_tls_options = kms_tls_options
242+
self._sync_kms_ssl_contexts: Optional[dict[str, SSLContext]] = None
243+
self._async_kms_ssl_contexts: Optional[dict[str, SSLContext]] = None
240244
self._bypass_query_analysis = bypass_query_analysis
241245
self._key_expiration_ms = key_expiration_ms
242246

247+
def _kms_ssl_contexts(self, is_sync: bool) -> dict[str, SSLContext]:
248+
if is_sync:
249+
if self._sync_kms_ssl_contexts is None:
250+
self._sync_kms_ssl_contexts = _parse_kms_tls_options(self._kms_tls_options, True)
251+
return self._sync_kms_ssl_contexts
252+
else:
253+
if self._async_kms_ssl_contexts is None:
254+
self._async_kms_ssl_contexts = _parse_kms_tls_options(self._kms_tls_options, False)
255+
return self._async_kms_ssl_contexts
256+
243257

244258
class RangeOpts:
245259
"""Options to configure encrypted queries using the range algorithm."""

0 commit comments

Comments
 (0)