Skip to content

Commit f6e84fd

Browse files
committed
PYTHON-1117 - Add TopologyDescription.has_readable/writable_server
1 parent 1355c5a commit f6e84fd

File tree

3 files changed

+151
-38
lines changed

3 files changed

+151
-38
lines changed

pymongo/topology.py

Lines changed: 6 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from pymongo.topology_description import (updated_topology_description,
3333
TOPOLOGY_TYPE,
3434
TopologyDescription)
35-
from pymongo.errors import ServerSelectionTimeoutError, ConfigurationError
35+
from pymongo.errors import ServerSelectionTimeoutError
3636
from pymongo.monotonic import time as _time
3737
from pymongo.server import Server
3838
from pymongo.server_selectors import (any_server_selector,
@@ -179,7 +179,8 @@ def select_servers(self,
179179

180180
now = _time()
181181
end_time = now + server_timeout
182-
server_descriptions = self._apply_selector(selector, address)
182+
server_descriptions = self._description.apply_selector(
183+
selector, address)
183184

184185
while not server_descriptions:
185186
# No suitable servers.
@@ -192,12 +193,13 @@ def select_servers(self,
192193

193194
# Release the lock and wait for the topology description to
194195
# change, or for a timeout. We won't miss any changes that
195-
# came after our most recent _apply_selector call, since we've
196+
# came after our most recent apply_selector call, since we've
196197
# held the lock until now.
197198
self._condition.wait(common.MIN_HEARTBEAT_INTERVAL)
198199
self._description.check_compatible()
199200
now = _time()
200-
server_descriptions = self._apply_selector(selector, address)
201+
server_descriptions = self._description.apply_selector(
202+
selector, address)
201203

202204
return [self.get_server_by_address(sd.address)
203205
for sd in server_descriptions]
@@ -411,39 +413,6 @@ def _request_check_all(self):
411413
for server in self._servers.values():
412414
server.request_check()
413415

414-
def _apply_selector(self, selector, address):
415-
if getattr(selector, 'min_wire_version', 0):
416-
common_wv = self._description.common_wire_version
417-
if common_wv and common_wv < selector.min_wire_version:
418-
raise ConfigurationError(
419-
"%s requires min wire version %d, but topology's min"
420-
" wire version is %d" % (selector,
421-
selector.min_wire_version,
422-
common_wv))
423-
424-
if self._description.topology_type == TOPOLOGY_TYPE.Single:
425-
# Ignore the selector.
426-
return self._description.known_servers
427-
elif address:
428-
sd = self._description.server_descriptions().get(address)
429-
return [sd] if sd else []
430-
elif self._description.topology_type == TOPOLOGY_TYPE.Sharded:
431-
# Ignore the read preference, but apply localThresholdMS.
432-
return self._apply_local_threshold(self._new_selection())
433-
else:
434-
return self._apply_local_threshold(selector(self._new_selection()))
435-
436-
def _apply_local_threshold(self, selection):
437-
"""Return list of servers from Selection that are in latency window."""
438-
if not selection:
439-
return []
440-
441-
# Round trip time in seconds.
442-
fastest = min(s.round_trip_time for s in selection.server_descriptions)
443-
threshold = self._settings.local_threshold_ms / 1000.0
444-
return [s for s in selection.server_descriptions
445-
if (s.round_trip_time - fastest) <= threshold]
446-
447416
def _update_servers(self):
448417
"""Sync our Servers from TopologyDescription.server_descriptions.
449418

pymongo/topology_description.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
from collections import namedtuple
1818

1919
from pymongo import common
20-
from pymongo.server_type import SERVER_TYPE
2120
from pymongo.errors import ConfigurationError
21+
from pymongo.read_preferences import ReadPreference
2222
from pymongo.server_description import ServerDescription
23+
from pymongo.server_selectors import Selection
24+
from pymongo.server_type import SERVER_TYPE
2325

2426

2527
TOPOLOGY_TYPE = namedtuple('TopologyType', ['Single', 'ReplicaSetNoPrimary',
@@ -160,6 +162,71 @@ def common_wire_version(self):
160162
def heartbeat_frequency(self):
161163
return self._topology_settings.heartbeat_frequency
162164

165+
def apply_selector(self, selector, address):
166+
167+
def apply_local_threshold(selection):
168+
if not selection:
169+
return []
170+
171+
settings = self._topology_settings
172+
173+
# Round trip time in seconds.
174+
fastest = min(
175+
s.round_trip_time for s in selection.server_descriptions)
176+
threshold = settings.local_threshold_ms / 1000.0
177+
return [s for s in selection.server_descriptions
178+
if (s.round_trip_time - fastest) <= threshold]
179+
180+
if getattr(selector, 'min_wire_version', 0):
181+
common_wv = self.common_wire_version
182+
if common_wv and common_wv < selector.min_wire_version:
183+
raise ConfigurationError(
184+
"%s requires min wire version %d, but topology's min"
185+
" wire version is %d" % (selector,
186+
selector.min_wire_version,
187+
common_wv))
188+
189+
if self.topology_type == TOPOLOGY_TYPE.Single:
190+
# Ignore the selector.
191+
return self.known_servers
192+
elif address:
193+
description = self.server_descriptions().get(address)
194+
return [description] if description else []
195+
elif self.topology_type == TOPOLOGY_TYPE.Sharded:
196+
# Ignore the read preference, but apply localThresholdMS.
197+
return apply_local_threshold(
198+
Selection.from_topology_description(self))
199+
else:
200+
return apply_local_threshold(
201+
selector(Selection.from_topology_description(self)))
202+
203+
def has_readable_server(self, read_preference=ReadPreference.PRIMARY):
204+
"""Does this topology have any readable servers available matching the
205+
given read preference?
206+
207+
:Parameters:
208+
- `read_preference`: an instance of a read preference from
209+
:mod:`~pymongo.read_preferences`. Defaults to
210+
:attr:`~pymongo.ReadPreference.PRIMARY`.
211+
212+
.. note:: When connected directly to a single server this method
213+
always returns ``True``.
214+
215+
.. versionadded:: 3.4
216+
"""
217+
common.validate_read_preference("read_preference", read_preference)
218+
return any(self.apply_selector(read_preference, None))
219+
220+
def has_writable_server(self):
221+
"""Does this topology have a writable server available?
222+
223+
.. note:: When connected directly to a single server this method
224+
always returns ``True``.
225+
226+
.. versionadded:: 3.4
227+
"""
228+
return self.has_readable_server(ReadPreference.PRIMARY)
229+
163230

164231
# If topology type is Unknown and we receive an ismaster response, what should
165232
# the new topology type be?

test/test_topology.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,14 @@ def test_direct_connection(self):
222222
s = t.select_server(writable_server_selector)
223223
self.assertEqual(server_type, s.description.server_type)
224224

225+
# Topology type single is always readable and writable regardless
226+
# of server type or state.
227+
self.assertTrue(t.description.has_writable_server())
228+
self.assertTrue(t.description.has_readable_server())
229+
self.assertTrue(t.description.has_readable_server(Secondary()))
230+
self.assertTrue(t.description.has_readable_server(
231+
Secondary(tag_sets=[{'tag': 'does-not-exist'}])))
232+
225233
def test_reopen(self):
226234
t = create_mock_topology()
227235

@@ -290,6 +298,75 @@ def new_average():
290298

291299

292300
class TestMultiServerTopology(TopologyTest):
301+
def test_readable_writable(self):
302+
t = create_mock_topology(replica_set_name='rs')
303+
got_ismaster(t, ('a', 27017), {
304+
'ok': 1,
305+
'ismaster': True,
306+
'setName': 'rs',
307+
'hosts': ['a', 'b']})
308+
309+
got_ismaster(t, ('b', 27017), {
310+
'ok': 1,
311+
'ismaster': False,
312+
'secondary': True,
313+
'setName': 'rs',
314+
'hosts': ['a', 'b']})
315+
316+
self.assertTrue(t.description.has_writable_server())
317+
self.assertTrue(t.description.has_readable_server())
318+
self.assertTrue(
319+
t.description.has_readable_server(Secondary()))
320+
self.assertFalse(
321+
t.description.has_readable_server(
322+
Secondary(tag_sets=[{'tag': 'exists'}])))
323+
324+
t = create_mock_topology(replica_set_name='rs')
325+
got_ismaster(t, ('a', 27017), {
326+
'ok': 1,
327+
'ismaster': False,
328+
'secondary': False,
329+
'setName': 'rs',
330+
'hosts': ['a', 'b']})
331+
332+
got_ismaster(t, ('b', 27017), {
333+
'ok': 1,
334+
'ismaster': False,
335+
'secondary': True,
336+
'setName': 'rs',
337+
'hosts': ['a', 'b']})
338+
339+
self.assertFalse(t.description.has_writable_server())
340+
self.assertFalse(t.description.has_readable_server())
341+
self.assertTrue(
342+
t.description.has_readable_server(Secondary()))
343+
self.assertFalse(
344+
t.description.has_readable_server(
345+
Secondary(tag_sets=[{'tag': 'exists'}])))
346+
347+
t = create_mock_topology(replica_set_name='rs')
348+
got_ismaster(t, ('a', 27017), {
349+
'ok': 1,
350+
'ismaster': True,
351+
'setName': 'rs',
352+
'hosts': ['a', 'b']})
353+
354+
got_ismaster(t, ('b', 27017), {
355+
'ok': 1,
356+
'ismaster': False,
357+
'secondary': True,
358+
'setName': 'rs',
359+
'hosts': ['a', 'b'],
360+
'tags': {'tag': 'exists'}})
361+
362+
self.assertTrue(t.description.has_writable_server())
363+
self.assertTrue(t.description.has_readable_server())
364+
self.assertTrue(
365+
t.description.has_readable_server(Secondary()))
366+
self.assertTrue(
367+
t.description.has_readable_server(
368+
Secondary(tag_sets=[{'tag': 'exists'}])))
369+
293370
def test_close(self):
294371
t = create_mock_topology(replica_set_name='rs')
295372
got_ismaster(t, ('a', 27017), {

0 commit comments

Comments
 (0)