Skip to content

Commit 06872f7

Browse files
authored
PYTHON-4780 Implement fast path for server selection with Primary (#2416)
1 parent 5a640da commit 06872f7

File tree

6 files changed

+42
-8
lines changed

6 files changed

+42
-8
lines changed

.github/workflows/zizmor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ jobs:
1818
with:
1919
persist-credentials: false
2020
- name: Run zizmor 🌈
21-
uses: zizmorcore/zizmor-action@0f0557ab4a0b31211d42435e42df31cbd63fdd59
21+
uses: zizmorcore/zizmor-action@1c7106082dbc1753372e3924b7da1b9417011a21

doc/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ PyMongo 4.14 brings a number of changes including:
1010
- Added :meth:`pymongo.asynchronous.mongo_client.AsyncMongoClient.append_metadata` and
1111
:meth:`pymongo.mongo_client.MongoClient.append_metadata` to allow instantiated MongoClients to send client metadata
1212
on-demand
13+
- Improved performance of selecting a server with the Primary selector.
1314

1415
- Introduces a minor breaking change. When encoding :class:`bson.binary.BinaryVector`, a ``ValueError`` will be raised
1516
if the 'padding' metadata field is < 0 or > 7, or non-zero for any type other than PACKED_BIT.

pymongo/topology_description.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from bson.objectid import ObjectId
3535
from pymongo import common
3636
from pymongo.errors import ConfigurationError, PyMongoError
37-
from pymongo.read_preferences import ReadPreference, _AggWritePref, _ServerMode
37+
from pymongo.read_preferences import Primary, ReadPreference, _AggWritePref, _ServerMode
3838
from pymongo.server_description import ServerDescription
3939
from pymongo.server_selectors import Selection
4040
from pymongo.server_type import SERVER_TYPE
@@ -324,6 +324,17 @@ def apply_selector(
324324
description = self.server_descriptions().get(address)
325325
return [description] if description else []
326326

327+
# Primary selection fast path.
328+
if self.topology_type == TOPOLOGY_TYPE.ReplicaSetWithPrimary and type(selector) is Primary:
329+
for sd in self._server_descriptions.values():
330+
if sd.server_type == SERVER_TYPE.RSPrimary:
331+
sds = [sd]
332+
if custom_selector:
333+
sds = custom_selector(sds)
334+
return sds
335+
# No primary found, return an empty list.
336+
return []
337+
327338
selection = Selection.from_topology_description(self)
328339
# Ignore read preference for sharded clusters.
329340
if self.topology_type != TOPOLOGY_TYPE.Sharded:

test/asynchronous/test_server_selection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,12 @@ async def test_selector_called(self):
130130
test_collection = mongo_client.testdb.test_collection
131131
self.addAsyncCleanup(mongo_client.drop_database, "testdb")
132132

133-
# Do N operations and test selector is called at least N times.
133+
# Do N operations and test selector is called at least N-1 times due to fast path.
134134
await test_collection.insert_one({"age": 20, "name": "John"})
135135
await test_collection.insert_one({"age": 31, "name": "Jane"})
136136
await test_collection.update_one({"name": "Jane"}, {"$set": {"age": 21}})
137137
await test_collection.find_one({"name": "Roe"})
138-
self.assertGreaterEqual(selector.call_count, 4)
138+
self.assertGreaterEqual(selector.call_count, 3)
139139

140140
@async_client_context.require_replica_set
141141
async def test_latency_threshold_application(self):

test/test_server_selection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,12 @@ def test_selector_called(self):
130130
test_collection = mongo_client.testdb.test_collection
131131
self.addCleanup(mongo_client.drop_database, "testdb")
132132

133-
# Do N operations and test selector is called at least N times.
133+
# Do N operations and test selector is called at least N-1 times due to fast path.
134134
test_collection.insert_one({"age": 20, "name": "John"})
135135
test_collection.insert_one({"age": 31, "name": "Jane"})
136136
test_collection.update_one({"name": "Jane"}, {"$set": {"age": 21}})
137137
test_collection.find_one({"name": "Roe"})
138-
self.assertGreaterEqual(selector.call_count, 4)
138+
self.assertGreaterEqual(selector.call_count, 3)
139139

140140
@client_context.require_replica_set
141141
def test_latency_threshold_application(self):

test/test_topology.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from pymongo import common
3131
from pymongo.errors import AutoReconnect, ConfigurationError, ConnectionFailure
3232
from pymongo.hello import Hello, HelloCompat
33-
from pymongo.read_preferences import ReadPreference, Secondary
33+
from pymongo.read_preferences import Primary, ReadPreference, Secondary
3434
from pymongo.server_description import ServerDescription
3535
from pymongo.server_selectors import any_server_selector, writable_server_selector
3636
from pymongo.server_type import SERVER_TYPE
@@ -51,7 +51,10 @@ def get_topology_type(self):
5151

5252

5353
def create_mock_topology(
54-
seeds=None, replica_set_name=None, monitor_class=DummyMonitor, direct_connection=False
54+
seeds=None,
55+
replica_set_name=None,
56+
monitor_class=DummyMonitor,
57+
direct_connection=False,
5558
):
5659
partitioned_seeds = list(map(common.partition_node, seeds or ["a"]))
5760
topology_settings = TopologySettings(
@@ -123,6 +126,25 @@ def test_timeout_configuration(self):
123126
# The monitor, not its pool, is responsible for calling hello.
124127
self.assertTrue(monitor._pool.is_sdam)
125128

129+
def test_selector_fast_path(self):
130+
topology = create_mock_topology(seeds=["a", "b:27018"], replica_set_name="foo")
131+
description = topology.description
132+
description._topology_type = TOPOLOGY_TYPE.ReplicaSetWithPrimary
133+
134+
# There is no primary yet, so it should give an empty list.
135+
self.assertEqual(description.apply_selector(Primary()), [])
136+
137+
# If we set a primary server, we should get it back.
138+
sd = list(description._server_descriptions.values())[0]
139+
sd._server_type = SERVER_TYPE.RSPrimary
140+
self.assertEqual(description.apply_selector(Primary()), [sd])
141+
142+
# If there is a custom selector, it should be applied.
143+
def custom_selector(servers):
144+
return []
145+
146+
self.assertEqual(description.apply_selector(Primary(), custom_selector=custom_selector), [])
147+
126148

127149
class TestSingleServerTopology(TopologyTest):
128150
def test_direct_connection(self):

0 commit comments

Comments
 (0)