Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 19 additions & 5 deletions pymongo/asynchronous/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
24 changes: 19 additions & 5 deletions pymongo/synchronous/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
38 changes: 29 additions & 9 deletions test/mockupdb/test_handshake.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Loading