diff --git a/.evergreen/scripts/resync-all-specs.sh b/.evergreen/scripts/resync-all-specs.sh index 0f7ae2ccd..4bcf2cd23 100755 --- a/.evergreen/scripts/resync-all-specs.sh +++ b/.evergreen/scripts/resync-all-specs.sh @@ -33,7 +33,7 @@ then # we're running locally python3 ./.evergreen/scripts/resync-all-specs.py else - /opt/devtools/bin/python3.11 ./.evergreen/scripts/resync-all-specs.py "$PR_DESC" + /opt/devtools/bin/python3.11 ./.evergreen/scripts/resync-all-specs.py --filename "$PR_DESC" if [[ -f $PR_DESC ]]; then # changes were made -> call scrypt to create PR for us .evergreen/scripts/create-spec-pr.sh "$PR_DESC" diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index fb28f2476..27bf7511d 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -25,7 +25,7 @@ jobs: - name: Install just uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 - name: Install uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v5 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v5 with: enable-cache: true python-version: "3.9" @@ -65,7 +65,7 @@ jobs: - name: Install just uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 - name: Install uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v5 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v5 with: enable-cache: true python-version: ${{ matrix.python-version }} @@ -88,7 +88,7 @@ jobs: - name: Install just uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 - name: Install uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v5 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v5 with: enable-cache: true python-version: "3.9" @@ -111,7 +111,7 @@ jobs: with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v5 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v5 with: enable-cache: true python-version: "3.9" @@ -130,7 +130,7 @@ jobs: with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v5 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v5 with: enable-cache: true python-version: "3.9" @@ -152,7 +152,7 @@ jobs: with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v5 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v5 with: enable-cache: true python-version: "${{matrix.python}}" @@ -231,7 +231,7 @@ jobs: with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v5 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v5 with: python-version: '3.9' - name: Start MongoDB @@ -257,7 +257,7 @@ jobs: with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v5 + uses: astral-sh/setup-uv@7edac99f961f18b581bbd960d59d049f04c0002f # v5 with: python-version: '3.9' - name: Start MongoDB diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 6d9506776..1d58c0d5f 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -18,4 +18,4 @@ jobs: with: persist-credentials: false - name: Run zizmor 🌈 - uses: zizmorcore/zizmor-action@0f0557ab4a0b31211d42435e42df31cbd63fdd59 + uses: zizmorcore/zizmor-action@1c7106082dbc1753372e3924b7da1b9417011a21 diff --git a/doc/changelog.rst b/doc/changelog.rst index e4da11209..2e56b2c01 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -10,6 +10,7 @@ PyMongo 4.14 brings a number of changes including: - Added :meth:`pymongo.asynchronous.mongo_client.AsyncMongoClient.append_metadata` and :meth:`pymongo.mongo_client.MongoClient.append_metadata` to allow instantiated MongoClients to send client metadata on-demand +- Improved performance of selecting a server with the Primary selector. - Introduces a minor breaking change. When encoding :class:`bson.binary.BinaryVector`, a ``ValueError`` will be raised if the 'padding' metadata field is < 0 or > 7, or non-zero for any type other than PACKED_BIT. diff --git a/pymongo/topology_description.py b/pymongo/topology_description.py index 29293b231..e226992b4 100644 --- a/pymongo/topology_description.py +++ b/pymongo/topology_description.py @@ -34,7 +34,7 @@ from bson.min_key import MinKey from bson.objectid import ObjectId from pymongo import common from pymongo.errors import ConfigurationError, PyMongoError -from pymongo.read_preferences import ReadPreference, _AggWritePref, _ServerMode +from pymongo.read_preferences import Primary, ReadPreference, _AggWritePref, _ServerMode from pymongo.server_description import ServerDescription from pymongo.server_selectors import Selection from pymongo.server_type import SERVER_TYPE @@ -324,6 +324,17 @@ class TopologyDescription: description = self.server_descriptions().get(address) return [description] if description else [] + # Primary selection fast path. + if self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary and type(selector) is Primary: + for sd in self._server_descriptions.values(): + if sd.server_type == SERVER_TYPE.RSPrimary: + sds = [sd] + if custom_selector: + sds = custom_selector(sds) + return sds + # No primary found, return an empty list. + return [] + selection = Selection.from_topology_description(self) # Ignore read preference for sharded clusters. if self.topology_type != TOPOLOGY_TYPE.Sharded: diff --git a/requirements/docs.txt b/requirements/docs.txt index 7d52c1cb3..5543a6269 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -3,4 +3,4 @@ sphinx_rtd_theme>=2,<4 readthedocs-sphinx-search~=0.3 sphinxcontrib-shellcheck>=1,<2 sphinx-autobuild>=2020.9.1 -furo==2024.8.6 +furo==2025.7.19 diff --git a/test/asynchronous/test_server_selection.py b/test/asynchronous/test_server_selection.py index f98a05ee9..f570662b8 100644 --- a/test/asynchronous/test_server_selection.py +++ b/test/asynchronous/test_server_selection.py @@ -130,12 +130,12 @@ class TestCustomServerSelectorFunction(AsyncIntegrationTest): test_collection = mongo_client.testdb.test_collection self.addAsyncCleanup(mongo_client.drop_database, "testdb") - # Do N operations and test selector is called at least N times. + # Do N operations and test selector is called at least N-1 times due to fast path. await test_collection.insert_one({"age": 20, "name": "John"}) await test_collection.insert_one({"age": 31, "name": "Jane"}) await test_collection.update_one({"name": "Jane"}, {"$set": {"age": 21}}) await test_collection.find_one({"name": "Roe"}) - self.assertGreaterEqual(selector.call_count, 4) + self.assertGreaterEqual(selector.call_count, 3) @async_client_context.require_replica_set async def test_latency_threshold_application(self): diff --git a/test/test_server_selection.py b/test/test_server_selection.py index aec8e2e47..4384deac2 100644 --- a/test/test_server_selection.py +++ b/test/test_server_selection.py @@ -130,12 +130,12 @@ class TestCustomServerSelectorFunction(IntegrationTest): test_collection = mongo_client.testdb.test_collection self.addCleanup(mongo_client.drop_database, "testdb") - # Do N operations and test selector is called at least N times. + # Do N operations and test selector is called at least N-1 times due to fast path. test_collection.insert_one({"age": 20, "name": "John"}) test_collection.insert_one({"age": 31, "name": "Jane"}) test_collection.update_one({"name": "Jane"}, {"$set": {"age": 21}}) test_collection.find_one({"name": "Roe"}) - self.assertGreaterEqual(selector.call_count, 4) + self.assertGreaterEqual(selector.call_count, 3) @client_context.require_replica_set def test_latency_threshold_application(self): diff --git a/test/test_topology.py b/test/test_topology.py index 837cf25c6..d3bbcd906 100644 --- a/test/test_topology.py +++ b/test/test_topology.py @@ -30,7 +30,7 @@ from bson.objectid import ObjectId from pymongo import common from pymongo.errors import AutoReconnect, ConfigurationError, ConnectionFailure from pymongo.hello import Hello, HelloCompat -from pymongo.read_preferences import ReadPreference, Secondary +from pymongo.read_preferences import Primary, ReadPreference, Secondary from pymongo.server_description import ServerDescription from pymongo.server_selectors import any_server_selector, writable_server_selector from pymongo.server_type import SERVER_TYPE @@ -51,7 +51,10 @@ address = ("a", 27017) def create_mock_topology( - seeds=None, replica_set_name=None, monitor_class=DummyMonitor, direct_connection=False + seeds=None, + replica_set_name=None, + monitor_class=DummyMonitor, + direct_connection=False, ): partitioned_seeds = list(map(common.partition_node, seeds or ["a"])) topology_settings = TopologySettings( @@ -123,6 +126,25 @@ class TestTopologyConfiguration(TopologyTest): # The monitor, not its pool, is responsible for calling hello. self.assertTrue(monitor._pool.is_sdam) + def test_selector_fast_path(self): + topology = create_mock_topology(seeds=["a", "b:27018"], replica_set_name="foo") + description = topology.description + description._topology_type = TOPOLOGY_TYPE.ReplicaSetWithPrimary + + # There is no primary yet, so it should give an empty list. + self.assertEqual(description.apply_selector(Primary()), []) + + # If we set a primary server, we should get it back. + sd = list(description._server_descriptions.values())[0] + sd._server_type = SERVER_TYPE.RSPrimary + self.assertEqual(description.apply_selector(Primary()), [sd]) + + # If there is a custom selector, it should be applied. + def custom_selector(servers): + return [] + + self.assertEqual(description.apply_selector(Primary(), custom_selector=custom_selector), []) + class TestSingleServerTopology(TopologyTest): def test_direct_connection(self):