Skip to content

PYTHON-5309 Async client no longer works on Atlas mongodb Cloud #2286

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 44 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
efe494d
add is_sync param
sleepyStick Apr 15, 2025
b29d1ba
update contributing
sleepyStick Apr 15, 2025
d9dfb99
add vars for pyopenssl and test
sleepyStick Apr 15, 2025
c847f25
update evergreen config to run this pyopenssl on async as well (not s…
sleepyStick Apr 15, 2025
ae8ecc4
fix typing
sleepyStick Apr 15, 2025
03f4ba1
fix tests
sleepyStick Apr 16, 2025
5349164
fix test pt2
sleepyStick Apr 16, 2025
67100fc
edit evergreen config
sleepyStick Apr 16, 2025
88ae345
fix test
sleepyStick Apr 16, 2025
e451ceb
fix test errors
sleepyStick Apr 18, 2025
0312acb
fix typo...
sleepyStick Apr 18, 2025
4e85024
fix typing
sleepyStick Apr 18, 2025
dccd96a
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 18, 2025
c86a85f
maybe this works?
sleepyStick Apr 21, 2025
12ef993
fix typing
sleepyStick Apr 21, 2025
bc76aae
fix tests
sleepyStick Apr 21, 2025
a9c63c8
fix typing again
sleepyStick Apr 21, 2025
67c6738
fix tests pt 2?
sleepyStick Apr 21, 2025
3ea4de7
fix test pt3
sleepyStick Apr 21, 2025
38ad677
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 21, 2025
c57aed2
add test_name
sleepyStick Apr 21, 2025
2591169
address review
sleepyStick Apr 22, 2025
06a710d
update changelog
sleepyStick Apr 22, 2025
ef4111e
undo whitespace changes
sleepyStick Apr 22, 2025
0b3c6bb
fix failures
sleepyStick Apr 22, 2025
9336f58
fix typing
sleepyStick Apr 22, 2025
23b7cbe
fix typing?
sleepyStick Apr 22, 2025
760fa97
fix error?
sleepyStick Apr 22, 2025
4b8a4ed
undo whitespace changes
sleepyStick Apr 22, 2025
5807ba1
more whitespace changes
sleepyStick Apr 22, 2025
683ba33
move kms_ssl_contexts
sleepyStick Apr 22, 2025
d007c5f
fix import
sleepyStick Apr 22, 2025
05c061a
fix test failures
sleepyStick Apr 22, 2025
350f103
fix test failure pt2
sleepyStick Apr 22, 2025
5fa117f
change changelog line ft noah's suggestion
sleepyStick Apr 23, 2025
56c9662
_ssl -> _stdssl and ssl_in_use -> _ssl
sleepyStick Apr 23, 2025
a7324e5
make combined error type
sleepyStick Apr 23, 2025
af83d81
jk cant do _stdssl
sleepyStick Apr 23, 2025
f6b17dd
_pysslConn back to _sslConn
sleepyStick Apr 23, 2025
74ca8be
fix _ssl and has_sni
sleepyStick Apr 23, 2025
536f189
fix test
sleepyStick Apr 23, 2025
24354b4
_ssl -> ssl
sleepyStick Apr 23, 2025
4178fcc
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 23, 2025
b2324e3
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .evergreen/generated_configs/variants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -620,17 +620,19 @@ buildvariants:
- macos-14
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3
- name: pyopenssl-rhel8-python3.10
tasks:
- name: .replica_set .auth .ssl .sync
- name: .7.0 .auth .ssl .sync
- name: .replica_set .auth .ssl .sync_async
- name: .7.0 .auth .ssl .sync_async
display_name: PyOpenSSL RHEL8 Python3.10
run_on:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.10/bin/python3
- name: pyopenssl-rhel8-python3.11
Expand All @@ -642,6 +644,7 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.11/bin/python3
- name: pyopenssl-rhel8-python3.12
Expand All @@ -653,17 +656,19 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.12/bin/python3
- name: pyopenssl-win64-python3.13
tasks:
- name: .replica_set .auth .ssl .sync
- name: .7.0 .auth .ssl .sync
- name: .replica_set .auth .ssl .sync_async
- name: .7.0 .auth .ssl .sync_async
display_name: PyOpenSSL Win64 Python3.13
run_on:
- windows-64-vsMulti-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: C:/python/Python313/python.exe
- name: pyopenssl-rhel8-pypy3.10
Expand All @@ -675,6 +680,7 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/pypy3.10/bin/python3

Expand Down
29 changes: 20 additions & 9 deletions .evergreen/scripts/generate_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def create_enterprise_auth_variants():
def create_pyopenssl_variants():
base_name = "PyOpenSSL"
batchtime = BATCHTIME_WEEK
expansions = dict(SUB_TEST_NAME="pyopenssl")
expansions = dict(TEST_NAME="default", SUB_TEST_NAME="pyopenssl")
variants = []

for python in ALL_PYTHONS:
Expand All @@ -265,14 +265,25 @@ def create_pyopenssl_variants():
host = DEFAULT_HOST

display_name = get_variant_name(base_name, host, python=python)
variant = create_variant(
[f".replica_set .{auth} .{ssl} .sync", f".7.0 .{auth} .{ssl} .sync"],
display_name,
python=python,
host=host,
expansions=expansions,
batchtime=batchtime,
)
# only need to run some on async
if python in (CPYTHONS[1], CPYTHONS[-1]):
variant = create_variant(
[f".replica_set .{auth} .{ssl} .sync_async", f".7.0 .{auth} .{ssl} .sync_async"],
display_name,
python=python,
host=host,
expansions=expansions,
batchtime=batchtime,
)
else:
variant = create_variant(
[f".replica_set .{auth} .{ssl} .sync", f".7.0 .{auth} .{ssl} .sync"],
display_name,
python=python,
host=host,
expansions=expansions,
batchtime=batchtime,
)
variants.append(variant)

return variants
Expand Down
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,21 @@ partially-converted asynchronous version of the same name to the `test/asynchron
Use this generated file as a starting point for the completed conversion.

The script is used like so: `python tools/convert_test_to_async.py [test_file.py]`

## Running PyMongo with SSL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for adding this section?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh uh, I was struggling to get it working and asked Noah a bunch of questions, so he suggested I add a section here.

Note that `AsyncMongoClient` does not support PyOpenSSL.
Assuming all required packages are installed, set the `tls` and `tlsAllowInvalidCertificates` flags in the URI to enable
the driver to connect with SSL, like so:
```python
from pymongo import MongoClient

client = MongoClient(
"mongodb://localhost:27017?tls=true&tlsAllowInvalidCertificates=true"
)
```
Another way of doing this would be to pass these options in as parameters to the MongoClient, like so:
```python
client = MongoClient(
"mongodb://localhost:27017", tls=True, tlsAllowInvalidCertificates=True
)
```
2 changes: 2 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Version 4.12.1 is a bug fix release.
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.
- Fixed a bug where MongoDB cluster topology changes could cause asynchronous operations to take much longer to complete
due to holding the Topology lock while closing stale connections.
- Fixed a bug that would cause AsyncMongoClient to attempt to use PyOpenSSL when available, resulting in errors such as
"pymongo.errors.ServerSelectionTimeoutError: 'SSLContext' object has no attribute 'wrap_bio'"

Issues Resolved
...............
Expand Down
7 changes: 5 additions & 2 deletions pymongo/asynchronous/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
from pymongo.results import BulkWriteResult, DeleteResult
from pymongo.ssl_support import BLOCKING_IO_ERRORS, get_ssl_context
from pymongo.typings import _DocumentType, _DocumentTypeArg
from pymongo.uri_parser_shared import parse_host
from pymongo.uri_parser_shared import _parse_kms_tls_options, parse_host
from pymongo.write_concern import WriteConcern

if TYPE_CHECKING:
Expand Down Expand Up @@ -157,6 +157,7 @@ def __init__(
self.mongocryptd_client = mongocryptd_client
self.opts = opts
self._spawned = False
self._kms_ssl_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)

async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
"""Complete a KMS request.
Expand All @@ -168,7 +169,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
endpoint = kms_context.endpoint
message = kms_context.message
provider = kms_context.kms_provider
ctx = self.opts._kms_ssl_contexts.get(provider)
ctx = self._kms_ssl_contexts.get(provider)
if ctx is None:
# Enable strict certificate verification, OCSP, match hostname, and
# SNI using the system default CA certificates.
Expand All @@ -180,6 +181,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
False, # allow_invalid_certificates
False, # allow_invalid_hostnames
False, # disable_ocsp_endpoint_check
_IS_SYNC,
)
# CSOT: set timeout for socket creation.
connect_timeout = max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0.001)
Expand Down Expand Up @@ -675,6 +677,7 @@ def __init__(
kms_tls_options=kms_tls_options,
key_expiration_ms=key_expiration_ms,
)
self._kms_ssl_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)
self._io_callbacks: Optional[_EncryptionIO] = _EncryptionIO(
None, key_vault_coll, None, opts
)
Expand Down
6 changes: 3 additions & 3 deletions pymongo/asynchronous/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
from pymongo.server_api import _add_to_command
from pymongo.server_type import SERVER_TYPE
from pymongo.socket_checker import SocketChecker
from pymongo.ssl_support import SSLError
from pymongo.ssl_support import PYSSLError, SSLError

if TYPE_CHECKING:
from bson import CodecOptions
Expand Down Expand Up @@ -638,7 +638,7 @@ async def _raise_connection_failure(self, error: BaseException) -> NoReturn:
reason = ConnectionClosedReason.ERROR
await self.close_conn(reason)
# SSLError from PyOpenSSL inherits directly from Exception.
if isinstance(error, (IOError, OSError, SSLError)):
if isinstance(error, (IOError, OSError, SSLError, PYSSLError)):
details = _get_timeout_details(self.opts)
_raise_connection_failure(self.address, error, timeout_details=details)
else:
Expand Down Expand Up @@ -1052,7 +1052,7 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
error=ConnectionClosedReason.ERROR,
)
if isinstance(error, (IOError, OSError, SSLError)):
if isinstance(error, (IOError, OSError, SSLError, PYSSLError)):
details = _get_timeout_details(self.opts)
_raise_connection_failure(self.address, error, timeout_details=details)

Expand Down
7 changes: 5 additions & 2 deletions pymongo/client_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ def _parse_read_concern(options: Mapping[str, Any]) -> ReadConcern:
return ReadConcern(concern)


def _parse_ssl_options(options: Mapping[str, Any]) -> tuple[Optional[SSLContext], bool]:
def _parse_ssl_options(
options: Mapping[str, Any], is_sync: bool
) -> tuple[Optional[SSLContext], bool]:
"""Parse ssl options."""
use_tls = options.get("tls")
if use_tls is not None:
Expand Down Expand Up @@ -138,6 +140,7 @@ def _parse_ssl_options(options: Mapping[str, Any]) -> tuple[Optional[SSLContext]
allow_invalid_certificates,
allow_invalid_hostnames,
disable_ocsp_endpoint_check,
is_sync,
)
return ctx, allow_invalid_hostnames
return None, allow_invalid_hostnames
Expand Down Expand Up @@ -167,7 +170,7 @@ def _parse_pool_options(
compression_settings = CompressionSettings(
options.get("compressors", []), options.get("zlibcompressionlevel", -1)
)
ssl_context, tls_allow_invalid_hostnames = _parse_ssl_options(options)
ssl_context, tls_allow_invalid_hostnames = _parse_ssl_options(options, is_sync)
load_balanced = options.get("loadbalanced")
max_connecting = options.get("maxconnecting", common.MAX_CONNECTING)
return PoolOptions(
Expand Down
3 changes: 1 addition & 2 deletions pymongo/encryption_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
from bson import int64
from pymongo.common import validate_is_mapping
from pymongo.errors import ConfigurationError
from pymongo.uri_parser_shared import _parse_kms_tls_options

if TYPE_CHECKING:
from pymongo.typings import _AgnosticMongoClient, _DocumentTypeArg
Expand Down Expand Up @@ -236,7 +235,7 @@ def __init__(
if not any("idleShutdownTimeoutSecs" in s for s in self._mongocryptd_spawn_args):
self._mongocryptd_spawn_args.append("--idleShutdownTimeoutSecs=60")
# Maps KMS provider name to a SSLContext.
self._kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options)
self._kms_tls_options = kms_tls_options
self._bypass_query_analysis = bypass_query_analysis
self._key_expiration_ms = key_expiration_ms

Expand Down
26 changes: 13 additions & 13 deletions pymongo/network_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,18 @@
_HAVE_SSL = False

try:
from pymongo.pyopenssl_context import (
BLOCKING_IO_LOOKUP_ERROR,
BLOCKING_IO_READ_ERROR,
BLOCKING_IO_WRITE_ERROR,
_sslConn,
)
from pymongo.pyopenssl_context import _sslConn

_HAVE_PYOPENSSL = True
except ImportError:
_HAVE_PYOPENSSL = False
_sslConn = SSLSocket # type: ignore
from pymongo.ssl_support import ( # type: ignore[assignment]
BLOCKING_IO_LOOKUP_ERROR,
BLOCKING_IO_READ_ERROR,
BLOCKING_IO_WRITE_ERROR,
)
_sslConn = SSLSocket # type: ignore[assignment, misc]

from pymongo.ssl_support import (
BLOCKING_IO_LOOKUP_ERROR,
BLOCKING_IO_READ_ERROR,
BLOCKING_IO_WRITE_ERROR,
)

if TYPE_CHECKING:
from pymongo.asynchronous.pool import AsyncConnection
Expand All @@ -71,7 +67,11 @@
_UNPACK_COMPRESSION_HEADER = struct.Struct("<iiB").unpack
_POLL_TIMEOUT = 0.5
# Errors raised by sockets (and TLS sockets) when in non-blocking mode.
BLOCKING_IO_ERRORS = (BlockingIOError, BLOCKING_IO_LOOKUP_ERROR, *ssl_support.BLOCKING_IO_ERRORS)
BLOCKING_IO_ERRORS = (
BlockingIOError,
*BLOCKING_IO_LOOKUP_ERROR,
*ssl_support.BLOCKING_IO_ERRORS,
)


# These socket-based I/O methods are for KMS requests and any other network operations that do not use
Expand Down
19 changes: 10 additions & 9 deletions pymongo/pool_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@
)
from pymongo.network_layer import AsyncNetworkingInterface, NetworkingInterface, PyMongoProtocol
from pymongo.pool_options import PoolOptions
from pymongo.ssl_support import HAS_SNI, SSLError
from pymongo.ssl_support import PYSSLError, SSLError, _has_sni

SSLErrors = (PYSSLError, SSLError)
if TYPE_CHECKING:
from pymongo.pyopenssl_context import _sslConn
from pymongo.typings import _Address
Expand Down Expand Up @@ -138,7 +139,7 @@ def _raise_connection_failure(
msg += format_timeout_details(timeout_details)
if isinstance(error, socket.timeout):
raise NetworkTimeout(msg) from error
elif isinstance(error, SSLError) and "timed out" in str(error):
elif isinstance(error, SSLErrors) and "timed out" in str(error):
# Eventlet does not distinguish TLS network timeouts from other
# SSLErrors (https://github.com/eventlet/eventlet/issues/692).
# Luckily, we can work around this limitation because the phrase
Expand Down Expand Up @@ -279,7 +280,7 @@ async def _async_configured_socket(
try:
# We have to pass hostname / ip address to wrap_socket
# to use SSLContext.check_hostname.
if HAS_SNI:
if _has_sni(False):
loop = asyncio.get_running_loop()
ssl_sock = await loop.run_in_executor(
None,
Expand All @@ -293,7 +294,7 @@ async def _async_configured_socket(
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, *SSLErrors) as exc:
sock.close()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
Expand Down Expand Up @@ -349,7 +350,7 @@ async def _configured_protocol_interface(
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, *SSLErrors) as exc:
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
# mismatch, will be turned into ServerSelectionTimeoutErrors later.
Expand Down Expand Up @@ -458,7 +459,7 @@ def _configured_socket(address: _Address, options: PoolOptions) -> Union[socket.
try:
# We have to pass hostname / ip address to wrap_socket
# to use SSLContext.check_hostname.
if HAS_SNI:
if _has_sni(True):
ssl_sock = ssl_context.wrap_socket(sock, server_hostname=host) # type: ignore[assignment, misc, unused-ignore]
else:
ssl_sock = ssl_context.wrap_socket(sock) # type: ignore[assignment, misc, unused-ignore]
Expand All @@ -467,7 +468,7 @@ def _configured_socket(address: _Address, options: PoolOptions) -> Union[socket.
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, *SSLErrors) as exc:
sock.close()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
Expand Down Expand Up @@ -507,7 +508,7 @@ def _configured_socket_interface(address: _Address, options: PoolOptions) -> Net
try:
# We have to pass hostname / ip address to wrap_socket
# to use SSLContext.check_hostname.
if HAS_SNI:
if _has_sni(True):
ssl_sock = ssl_context.wrap_socket(sock, server_hostname=host)
else:
ssl_sock = ssl_context.wrap_socket(sock)
Expand All @@ -516,7 +517,7 @@ def _configured_socket_interface(address: _Address, options: PoolOptions) -> Net
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, *SSLErrors) as exc:
sock.close()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
Expand Down
Loading
Loading