From ac12b55e05a4687894fe6291a4c819a7ded4cdbf Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 2 Jul 2025 08:21:15 -0500 Subject: [PATCH 1/9] PYTHON-4780 Implement fast path for server selection with Primary --- pymongo/topology_description.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pymongo/topology_description.py b/pymongo/topology_description.py index 29293b2314..34b340d0b7 100644 --- a/pymongo/topology_description.py +++ b/pymongo/topology_description.py @@ -324,6 +324,14 @@ def apply_selector( description = self.server_descriptions().get(address) return [description] if description else [] + # Primary selection fast path. + if self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary and isinstance( + selector, TOPOLOGY_TYPE.Primary + ): + for sd in self._server_descriptions.values(): + if sd.server_type == SERVER_TYPE.RSPrimary: + return [sd] + selection = Selection.from_topology_description(self) # Ignore read preference for sharded clusters. if self.topology_type != TOPOLOGY_TYPE.Sharded: From f32a3a62c75b981b51af19bb82a5c917ef542226 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 3 Jul 2025 09:31:01 -0500 Subject: [PATCH 2/9] PYTHON-4780 Implement fast path for server selection with Primary --- pymongo/topology_description.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymongo/topology_description.py b/pymongo/topology_description.py index 34b340d0b7..fefc752ea9 100644 --- a/pymongo/topology_description.py +++ b/pymongo/topology_description.py @@ -34,7 +34,7 @@ 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 @@ -326,7 +326,7 @@ def apply_selector( # Primary selection fast path. if self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary and isinstance( - selector, TOPOLOGY_TYPE.Primary + selector, Primary ): for sd in self._server_descriptions.values(): if sd.server_type == SERVER_TYPE.RSPrimary: From 07e0dca9a2fbdb0c86061bc2a21438f35c4361d7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 3 Jul 2025 09:38:49 -0500 Subject: [PATCH 3/9] fix zizmor --- .github/workflows/zizmor.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index c6237d2bda..785444be76 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -17,16 +17,5 @@ jobs: uses: actions/checkout@v4 with: persist-credentials: false - - name: Setup Rust - uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1 - - name: Get zizmor - run: cargo install zizmor - name: Run zizmor 🌈 - run: zizmor --format sarif . > results.sarif - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@39edc492dbe16b1465b0cafca41432d857bdb31a # v3 - with: - sarif_file: results.sarif - category: zizmor + uses: zizmorcore/zizmor-action@1cc8f934e6fad1414fbfc420bd02b0c325d9daab # v1.11.0 From 7dfc3f11962f5bad854af0171ed6cc2caf501a83 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 3 Jul 2025 09:40:42 -0500 Subject: [PATCH 4/9] fix zizmor --- .github/workflows/zizmor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 785444be76..1d58c0d5fb 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@1cc8f934e6fad1414fbfc420bd02b0c325d9daab # v1.11.0 + uses: zizmorcore/zizmor-action@1c7106082dbc1753372e3924b7da1b9417011a21 From 746e8d5b75a0b22b0b84876032da2c0d4a315997 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 3 Jul 2025 10:02:55 -0500 Subject: [PATCH 5/9] update test --- test/asynchronous/test_server_selection.py | 4 ++-- test/test_server_selection.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/asynchronous/test_server_selection.py b/test/asynchronous/test_server_selection.py index f98a05ee91..f570662b85 100644 --- a/test/asynchronous/test_server_selection.py +++ b/test/asynchronous/test_server_selection.py @@ -130,12 +130,12 @@ async def test_selector_called(self): 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 aec8e2e47a..4384deac2b 100644 --- a/test/test_server_selection.py +++ b/test/test_server_selection.py @@ -130,12 +130,12 @@ def test_selector_called(self): 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): From e5a0c846552d2828dae57ed4791ae47029a5b945 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 3 Jul 2025 10:03:46 -0500 Subject: [PATCH 6/9] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2acc0fc086..374fc3e4f3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ a native Python driver for MongoDB, offering both synchronous and asynchronous A [gridfs](https://github.com/mongodb/specifications/blob/master/source/gridfs/gridfs-spec.md/) implementation on top of `pymongo`. -PyMongo supports MongoDB 4.0, 4.2, 4.4, 5.0, 6.0, 7.0, and 8.0. PyMongo follows [semantic versioning](https://semver.org/spec/v2.0.0.html) for its releases. +PyMongo supports MongoDB 4.0, 4.2, 4.4, 5.0, 6.0, 7.0, and 8.0. PyMongo follows [semantic versioning](https://semver.org/spec/v2.0.0.html) for its releases. ## Support / Feedback From 40794fec9f2ca217f558e3f5f3c19df8b637c9bd Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Tue, 8 Jul 2025 13:11:37 -0500 Subject: [PATCH 7/9] address review --- doc/changelog.rst | 1 + pymongo/topology_description.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/changelog.rst b/doc/changelog.rst index 933e2922db..2d1447d505 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 232ca07bd1..5650ca8391 100644 --- a/pymongo/topology_description.py +++ b/pymongo/topology_description.py @@ -325,9 +325,7 @@ def apply_selector( return [description] if description else [] # Primary selection fast path. - if self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary and isinstance( - selector, Primary - ): + 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: return [sd] From ee955f1d11aaad0f333fb3b0c82fd753e7f29eaa Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 11 Jul 2025 08:34:40 -0500 Subject: [PATCH 8/9] address review --- pymongo/topology_description.py | 11 +++++++++-- test/test_topology.py | 26 ++++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/pymongo/topology_description.py b/pymongo/topology_description.py index 5650ca8391..d7c0d40183 100644 --- a/pymongo/topology_description.py +++ b/pymongo/topology_description.py @@ -325,10 +325,17 @@ def apply_selector( return [description] if description else [] # Primary selection fast path. - if self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary and type(selector) is Primary: + if ( + custom_selector is None + and 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: - return [sd] + sds = [sd] + if custom_selector: + sds = custom_selector(sds) + return sds # No primary found, return an empty list. return [] diff --git a/test/test_topology.py b/test/test_topology.py index 837cf25c62..d3bbcd9060 100644 --- a/test/test_topology.py +++ b/test/test_topology.py @@ -30,7 +30,7 @@ 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 @@ def get_topology_type(self): 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 @@ def test_timeout_configuration(self): # 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): From fd194f2235a7538b02a02f10407996ac7aadcbd7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 17 Jul 2025 10:42:46 -0500 Subject: [PATCH 9/9] address review --- pymongo/topology_description.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pymongo/topology_description.py b/pymongo/topology_description.py index d7c0d40183..e226992b45 100644 --- a/pymongo/topology_description.py +++ b/pymongo/topology_description.py @@ -325,11 +325,7 @@ def apply_selector( return [description] if description else [] # Primary selection fast path. - if ( - custom_selector is None - and self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary - and type(selector) is Primary - ): + 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]