diff --git a/pymongo/asynchronous/pool.py b/pymongo/asynchronous/pool.py index 74b36d9d19..a170025ae9 100644 --- a/pymongo/asynchronous/pool.py +++ b/pymongo/asynchronous/pool.py @@ -287,7 +287,9 @@ async def _hello( hello.logical_session_timeout_minutes is not None and hello.is_readable ) self.logical_session_timeout_minutes: Optional[int] = hello.logical_session_timeout_minutes - self.hello_ok = hello.hello_ok + # hello_ok is set from helloOk, which is only returned from ismaster + # don't overwrite this when connection switches to hello + self.hello_ok = self.hello_ok or hello.hello_ok self.is_repl = hello.server_type in ( SERVER_TYPE.RSPrimary, SERVER_TYPE.RSSecondary, diff --git a/pymongo/synchronous/pool.py b/pymongo/synchronous/pool.py index 84ea207a64..8f55258be3 100644 --- a/pymongo/synchronous/pool.py +++ b/pymongo/synchronous/pool.py @@ -287,7 +287,9 @@ def _hello( hello.logical_session_timeout_minutes is not None and hello.is_readable ) self.logical_session_timeout_minutes: Optional[int] = hello.logical_session_timeout_minutes - self.hello_ok = hello.hello_ok + # hello_ok is set from helloOk, which is only returned from ismaster + # don't overwrite this when connection switches to hello + self.hello_ok = self.hello_ok or hello.hello_ok self.is_repl = hello.server_type in ( SERVER_TYPE.RSPrimary, SERVER_TYPE.RSSecondary, diff --git a/test/asynchronous/test_hello_latched.py b/test/asynchronous/test_hello_latched.py new file mode 100644 index 0000000000..20ad7cedff --- /dev/null +++ b/test/asynchronous/test_hello_latched.py @@ -0,0 +1,82 @@ +# Copyright 2026-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 connection continues to use hello after ismaster returns hello_ok""" + +from __future__ import annotations + +import unittest +from types import SimpleNamespace + +from pymongo.asynchronous.pool import AsyncConnection +from test.asynchronous import AsyncUnitTest + +_IS_SYNC = False + + +class TestHelloLatched(AsyncUnitTest): + def setUp(self): + self._sent = [] + + def create_connection(self) -> AsyncConnection: + """Returns a minimal connection object for _hello""" + conn = object.__new__(AsyncConnection) + conn.hello_ok = False + conn.performed_handshake = True + conn.opts = SimpleNamespace(server_api=None, load_balanced=False, _credentials=None) + + return conn + + async def test_hello_is_latched(self): + """ + Regression Test for PYTHON-5904 + Tests for connection hello_ok persistence when connection + Switches from ismaster to hello + """ + + async def mock_conn_command(db, cmd, **kwargs): + """Returns mocked hello and ismaster results for conn.command""" + self._sent.append(cmd.copy()) + if cmd.get("ismaster") == 1: + return {"ok": 1, "helloOk": True, "ismaster": True, "maxWireVersion": 25} + return {"ok": 1, "isWritablePrimary": True, "maxWireVersion": 25} + + conn = self.create_connection() + conn.command = mock_conn_command + + # First hello + await conn._hello(None, None) + # Verify hello_ok is True + self.assertTrue(conn.hello_ok) + # Verify command sent is ismaster + self.assertEqual(self._sent[0].get("ismaster"), 1) + self.assertEqual(self._sent[0].get("helloOk"), True) + + # Second hello + await conn._hello(None, None) + # Verify hello_ok has not changed + self.assertTrue(conn.hello_ok) + # Verify command sent is hello + self.assertEqual(self._sent[1].get("hello"), 1) + self.assertIsNone(self._sent[1].get("ismaster", None)) + + # Third hello + await conn._hello(None, None) + # Verify connection continues to use hello + self.assertEqual(self._sent[2].get("hello"), 1) + self.assertIsNone(self._sent[2].get("ismaster", None)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_hello_latched.py b/test/test_hello_latched.py new file mode 100644 index 0000000000..8bc8e995a6 --- /dev/null +++ b/test/test_hello_latched.py @@ -0,0 +1,82 @@ +# Copyright 2026-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 connection continues to use hello after ismaster returns hello_ok""" + +from __future__ import annotations + +import unittest +from types import SimpleNamespace + +from pymongo.synchronous.pool import Connection +from test import UnitTest + +_IS_SYNC = True + + +class TestHelloLatched(UnitTest): + def setUp(self): + self._sent = [] + + def create_connection(self) -> Connection: + """Returns a minimal connection object for _hello""" + conn = object.__new__(Connection) + conn.hello_ok = False + conn.performed_handshake = True + conn.opts = SimpleNamespace(server_api=None, load_balanced=False, _credentials=None) + + return conn + + def test_hello_is_latched(self): + """ + Regression Test for PYTHON-5904 + Tests for connection hello_ok persistence when connection + Switches from ismaster to hello + """ + + def mock_conn_command(db, cmd, **kwargs): + """Returns mocked hello and ismaster results for conn.command""" + self._sent.append(cmd.copy()) + if cmd.get("ismaster") == 1: + return {"ok": 1, "helloOk": True, "ismaster": True, "maxWireVersion": 25} + return {"ok": 1, "isWritablePrimary": True, "maxWireVersion": 25} + + conn = self.create_connection() + conn.command = mock_conn_command + + # First hello + conn._hello(None, None) + # Verify hello_ok is True + self.assertTrue(conn.hello_ok) + # Verify command sent is ismaster + self.assertEqual(self._sent[0].get("ismaster"), 1) + self.assertEqual(self._sent[0].get("helloOk"), True) + + # Second hello + conn._hello(None, None) + # Verify hello_ok has not changed + self.assertTrue(conn.hello_ok) + # Verify command sent is hello + self.assertEqual(self._sent[1].get("hello"), 1) + self.assertIsNone(self._sent[1].get("ismaster", None)) + + # Third hello + conn._hello(None, None) + # Verify connection continues to use hello + self.assertEqual(self._sent[2].get("hello"), 1) + self.assertIsNone(self._sent[2].get("ismaster", None)) + + +if __name__ == "__main__": + unittest.main()