diff --git a/pymongo/asynchronous/pool.py b/pymongo/asynchronous/pool.py index a5d5b28990..12412da641 100644 --- a/pymongo/asynchronous/pool.py +++ b/pymongo/asynchronous/pool.py @@ -46,6 +46,8 @@ MAX_MESSAGE_SIZE, MAX_WIRE_VERSION, MAX_WRITE_BATCH_SIZE, + MIN_SUPPORTED_SERVER_VERSION, + MIN_SUPPORTED_WIRE_VERSION, ORDERED_TYPES, ) from pymongo.errors import ( # type:ignore[attr-defined] @@ -235,13 +237,12 @@ async def unpin(self) -> None: await self.close_conn(ConnectionClosedReason.STALE) def hello_cmd(self) -> dict[str, Any]: - # Handshake spec requires us to use OP_MSG+hello command for the - # initial handshake in load balanced or stable API mode. + # As of PYTHON-5713, always use OP_MSG for the handshake since all + # supported servers (MongoDB 4.2+, wire version >= 8) support it. + self.op_msg_enabled = True if self.opts.server_api or self.hello_ok or self.opts.load_balanced: - self.op_msg_enabled = True return {HelloCompat.CMD: 1} - else: - return {HelloCompat.LEGACY_CMD: 1, "helloOk": True} + return {HelloCompat.LEGACY_CMD: 1, "helloOk": True} async def hello(self) -> Hello[dict[str, Any]]: return await self._hello(None, None) @@ -291,6 +292,19 @@ async def _hello( if performing_handshake: self.connect_rtt = time.monotonic() - start hello = Hello(doc, awaitable=awaitable) + # OP_MSG requires wire version 6+. + if hello.max_wire_version < 6: + raise ConfigurationError( + "Server at %s:%d reports wire version %d, but this version of " + "PyMongo requires at least %d (MongoDB %s)." + % ( + self.address[0], + self.address[1] or 0, + hello.max_wire_version, + MIN_SUPPORTED_WIRE_VERSION, + MIN_SUPPORTED_SERVER_VERSION, + ) + ) self.is_writable = hello.is_writable self.max_wire_version = hello.max_wire_version self.max_bson_size = hello.max_bson_size diff --git a/pymongo/synchronous/pool.py b/pymongo/synchronous/pool.py index 25f2d08fe7..ca389df3d9 100644 --- a/pymongo/synchronous/pool.py +++ b/pymongo/synchronous/pool.py @@ -43,6 +43,8 @@ MAX_MESSAGE_SIZE, MAX_WIRE_VERSION, MAX_WRITE_BATCH_SIZE, + MIN_SUPPORTED_SERVER_VERSION, + MIN_SUPPORTED_WIRE_VERSION, ORDERED_TYPES, ) from pymongo.errors import ( # type:ignore[attr-defined] @@ -235,13 +237,12 @@ def unpin(self) -> None: self.close_conn(ConnectionClosedReason.STALE) def hello_cmd(self) -> dict[str, Any]: - # Handshake spec requires us to use OP_MSG+hello command for the - # initial handshake in load balanced or stable API mode. + # As of PYTHON-5713, always use OP_MSG for the handshake since all + # supported servers (MongoDB 4.2+, wire version >= 8) support it. + self.op_msg_enabled = True if self.opts.server_api or self.hello_ok or self.opts.load_balanced: - self.op_msg_enabled = True return {HelloCompat.CMD: 1} - else: - return {HelloCompat.LEGACY_CMD: 1, "helloOk": True} + return {HelloCompat.LEGACY_CMD: 1, "helloOk": True} def hello(self) -> Hello[dict[str, Any]]: return self._hello(None, None) @@ -291,6 +292,19 @@ def _hello( if performing_handshake: self.connect_rtt = time.monotonic() - start hello = Hello(doc, awaitable=awaitable) + # OP_MSG requires wire version 6+. + if hello.max_wire_version < 6: + raise ConfigurationError( + "Server at %s:%d reports wire version %d, but this version of " + "PyMongo requires at least %d (MongoDB %s)." + % ( + self.address[0], + self.address[1] or 0, + hello.max_wire_version, + MIN_SUPPORTED_WIRE_VERSION, + MIN_SUPPORTED_SERVER_VERSION, + ) + ) self.is_writable = hello.is_writable self.max_wire_version = hello.max_wire_version self.max_bson_size = hello.max_bson_size diff --git a/test/mockupdb/test_handshake.py b/test/mockupdb/test_handshake.py index c2c978c4ad..42803c8a83 100644 --- a/test/mockupdb/test_handshake.py +++ b/test/mockupdb/test_handshake.py @@ -13,12 +13,13 @@ # limitations under the License. from __future__ import annotations +import re import unittest import pytest try: - from mockupdb import Command, MockupDB, OpMsg, OpMsgReply, OpQuery, OpReply, absent, go + from mockupdb import Command, MockupDB, OpMsg, OpMsgReply, OpReply, absent, go _HAVE_MOCKUPDB = True except ImportError: @@ -28,8 +29,8 @@ from bson.objectid import ObjectId from pymongo import MongoClient, has_c from pymongo import version as pymongo_version -from pymongo.common import MIN_SUPPORTED_WIRE_VERSION -from pymongo.errors import OperationFailure +from pymongo.common import MIN_SUPPORTED_SERVER_VERSION, MIN_SUPPORTED_WIRE_VERSION +from pymongo.errors import ConfigurationError, OperationFailure, ServerSelectionTimeoutError from pymongo.server_api import ServerApi, ServerApiVersion pytestmark = pytest.mark.mockupdb @@ -53,7 +54,7 @@ def _check_handshake_data(request): class TestHandshake(unittest.TestCase): def hello_with_option_helper(self, protocol, **kwargs): - hello = "ismaster" if isinstance(protocol(), OpQuery) else "hello" + hello = "hello" if ("apiVersion" in kwargs or "loadBalanced" in kwargs) else "ismaster" # `db.command("hello"|"ismaster")` commands are the same for primaries and # secondaries, so we only need one server. primary = MockupDB() @@ -165,7 +166,7 @@ def test_client_handshake_data(self): future = go(client.db.command, "whatever") for request in primary: - if request.matches(Command("ismaster")): + if request.matches("ismaster"): if request.client_port == heartbeat.client_port: # This is the monitor again, keep going. request.ok(primary_response) @@ -242,11 +243,10 @@ def test_handshake_versioned_api(self): self.hello_with_option_helper(Command, apiVersion="1") def test_handshake_not_either(self): - # If we don't specify either option then it should be using - # OP_QUERY for the initial step of the handshake. - self.hello_with_option_helper(Command) + # As of PYTHON-5713, always use OP_MSG for the initial handshake. + self.hello_with_option_helper(OpMsg) with self.assertRaisesRegex(AssertionError, "does not match"): - self.hello_with_option_helper(OpMsg) + self.hello_with_option_helper(Command) def test_handshake_max_wire(self): server = MockupDB() @@ -292,6 +292,26 @@ def responder(request): self.found_auth_msg, "Could not find authentication command with correct protocol" ) + def test_handshake_op_msg_not_supported(self): + # If a server responds with maxWireVersion < 6 (no OP_MSG support), + # the wire version error must surface to the user. + server = MockupDB() + server.autoresponds("ismaster", ok=1, ismaster=True, minWireVersion=0, maxWireVersion=5) + server.run() + self.addCleanup(server.stop) + + client = MongoClient(server.uri, serverSelectionTimeoutMS=500) + self.addCleanup(client.close) + + # The ConfigurationError from _hello() is stored as the server's error + # and surfaces inside ServerSelectionTimeoutError. + expected = re.escape( + "reports wire version 5, but this version of PyMongo requires at least " + "%d (MongoDB %s)." % (MIN_SUPPORTED_WIRE_VERSION, MIN_SUPPORTED_SERVER_VERSION) + ) + with self.assertRaisesRegex(ServerSelectionTimeoutError, expected): + client.db.command("ping") + if __name__ == "__main__": unittest.main()