Skip to content

Commit 5f16e33

Browse files
committed
PYTHON-1150 - Add maxStalenessMS to $readPreference
1 parent 1a45a0f commit 5f16e33

File tree

5 files changed

+137
-14
lines changed

5 files changed

+137
-14
lines changed

pymongo/message.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,16 @@ def _maybe_add_read_preference(spec, read_preference):
7373
"""Add $readPreference to spec when appropriate."""
7474
mode = read_preference.mode
7575
tag_sets = read_preference.tag_sets
76+
max_staleness = read_preference.max_staleness
7677
# Only add $readPreference if it's something other than primary to avoid
7778
# problems with mongos versions that don't support read preferences. Also,
7879
# for maximum backwards compatibility, don't add $readPreference for
79-
# secondaryPreferred unless tags are in use (setting the slaveOkay bit
80-
# has the same effect).
80+
# secondaryPreferred unless tags or maxStalenessMS are in use (setting the
81+
# slaveOkay bit has the same effect).
8182
if mode and (
82-
mode != ReadPreference.SECONDARY_PREFERRED.mode or tag_sets != [{}]):
83+
mode != ReadPreference.SECONDARY_PREFERRED.mode
84+
or tag_sets != [{}]
85+
or max_staleness):
8386

8487
if "$query" not in spec:
8588
spec = SON([("$query", spec)])

pymongo/read_preferences.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,22 @@ def _validate_tag_sets(tag_sets):
6363

6464

6565
def _validate_max_staleness(max_staleness):
66-
"""Validate maxStalenessMS."""
66+
"""Validate max_staleness."""
6767
if max_staleness is None:
6868
return 0.0
6969

70-
errmsg = "maxStalenessMS must be an integer or float"
70+
errmsg = "max_staleness must be an integer or float"
7171
try:
7272
max_staleness = float(max_staleness)
7373
except ValueError:
7474
raise ValueError(errmsg)
7575
except TypeError:
7676
raise TypeError(errmsg)
7777

78+
if not 0 < max_staleness < 1e9:
79+
raise ValueError(
80+
"max_staleness must be greater than 0 and less than one billion")
81+
7882
return max_staleness
7983

8084

@@ -100,9 +104,12 @@ def name(self):
100104
def document(self):
101105
"""Read preference as a document.
102106
"""
103-
if self.__tag_sets in (None, [{}]):
104-
return {'mode': self.__mongos_mode}
105-
return {'mode': self.__mongos_mode, 'tags': self.__tag_sets}
107+
doc = {'mode': self.__mongos_mode}
108+
if self.__tag_sets not in (None, [{}]):
109+
doc['tags'] = self.__tag_sets
110+
if self.__max_staleness:
111+
doc['maxStalenessMS'] = int(self.__max_staleness * 1000)
112+
return doc
106113

107114
@property
108115
def mode(self):
@@ -130,6 +137,8 @@ def max_staleness(self):
130137
"""The maximum estimated length of time (in seconds) a replica set
131138
secondary can fall behind the primary in replication before it will
132139
no longer be selected for operations."""
140+
if not self.__max_staleness:
141+
return None
133142
return self.__max_staleness
134143

135144
@property
@@ -146,8 +155,8 @@ def min_wire_version(self):
146155
return 5 if self.__max_staleness else 0
147156

148157
def __repr__(self):
149-
return "%s(tag_sets=%r)" % (
150-
self.name, self.__tag_sets)
158+
return "%s(tag_sets=%r, max_staleness=%r)" % (
159+
self.name, self.__tag_sets, self.max_staleness)
151160

152161
def __eq__(self, other):
153162
if isinstance(other, _ServerMode):

test/test_max_staleness.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ def run_scenario(self):
159159
mode_string = mode_string[:1].lower() + mode_string[1:]
160160
mode = read_preferences.read_pref_mode_from_name(mode_string)
161161
max_staleness = pref_def.get('maxStalenessMS', 0) / 1000.0
162+
if not max_staleness:
163+
max_staleness = None
162164
tag_sets = pref_def.get('tag_sets')
163165

164166
if scenario_def.get('error'):

test/test_read_preferences.py

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
from bson.py3compat import MAXSIZE
2626
from bson.son import SON
27-
from pymongo.errors import ConfigurationError
27+
from pymongo.errors import ConfigurationError, OperationFailure
2828
from pymongo.message import _maybe_add_read_preference
2929
from pymongo.mongo_client import MongoClient
3030
from pymongo.read_preferences import (ReadPreference, MovingAverage,
@@ -446,6 +446,77 @@ def test_moving_average(self):
446446

447447
class TestMongosAndReadPreference(unittest.TestCase):
448448

449+
def test_read_preference_document(self):
450+
451+
pref = Primary()
452+
self.assertEqual(
453+
pref.document,
454+
{'mode': 'primary'})
455+
456+
pref = PrimaryPreferred()
457+
self.assertEqual(
458+
pref.document,
459+
{'mode': 'primaryPreferred'})
460+
pref = PrimaryPreferred(tag_sets=[{'dc': 'sf'}])
461+
self.assertEqual(
462+
pref.document,
463+
{'mode': 'primaryPreferred', 'tags': [{'dc': 'sf'}]})
464+
pref = PrimaryPreferred(
465+
tag_sets=[{'dc': 'sf'}], max_staleness=30)
466+
self.assertEqual(
467+
pref.document,
468+
{'mode': 'primaryPreferred',
469+
'tags': [{'dc': 'sf'}],
470+
'maxStalenessMS': 30000})
471+
472+
pref = Secondary()
473+
self.assertEqual(
474+
pref.document,
475+
{'mode': 'secondary'})
476+
pref = Secondary(tag_sets=[{'dc': 'sf'}])
477+
self.assertEqual(
478+
pref.document,
479+
{'mode': 'secondary', 'tags': [{'dc': 'sf'}]})
480+
pref = Secondary(
481+
tag_sets=[{'dc': 'sf'}], max_staleness=30)
482+
self.assertEqual(
483+
pref.document,
484+
{'mode': 'secondary',
485+
'tags': [{'dc': 'sf'}],
486+
'maxStalenessMS': 30000})
487+
488+
pref = SecondaryPreferred()
489+
self.assertEqual(
490+
pref.document,
491+
{'mode': 'secondaryPreferred'})
492+
pref = SecondaryPreferred(tag_sets=[{'dc': 'sf'}])
493+
self.assertEqual(
494+
pref.document,
495+
{'mode': 'secondaryPreferred', 'tags': [{'dc': 'sf'}]})
496+
pref = SecondaryPreferred(
497+
tag_sets=[{'dc': 'sf'}], max_staleness=30)
498+
self.assertEqual(
499+
pref.document,
500+
{'mode': 'secondaryPreferred',
501+
'tags': [{'dc': 'sf'}],
502+
'maxStalenessMS': 30000})
503+
504+
pref = Nearest()
505+
self.assertEqual(
506+
pref.document,
507+
{'mode': 'nearest'})
508+
pref = Nearest(tag_sets=[{'dc': 'sf'}])
509+
self.assertEqual(
510+
pref.document,
511+
{'mode': 'nearest', 'tags': [{'dc': 'sf'}]})
512+
pref = Nearest(
513+
tag_sets=[{'dc': 'sf'}], max_staleness=30)
514+
self.assertEqual(
515+
pref.document,
516+
{'mode': 'nearest',
517+
'tags': [{'dc': 'sf'}],
518+
'maxStalenessMS': 30000})
519+
449520
def test_maybe_add_read_preference(self):
450521

451522
# Primary doesn't add $readPreference
@@ -470,12 +541,17 @@ def test_maybe_add_read_preference(self):
470541
self.assertEqual(
471542
out, SON([("$query", {}), ("$readPreference", pref.document)]))
472543

473-
# SecondaryPreferred without tag_sets doesn't add $readPreference
544+
# SecondaryPreferred without tag_sets or max_staleness doesn't add
545+
# $readPreference
474546
pref = SecondaryPreferred()
475547
out = _maybe_add_read_preference({}, pref)
476548
self.assertEqual(out, {})
477549
pref = SecondaryPreferred(tag_sets=[{'dc': 'nyc'}])
478550
out = _maybe_add_read_preference({}, pref)
551+
self.assertEqual(
552+
out, SON([("$query", {}), ("$readPreference", pref.document)]))
553+
pref = SecondaryPreferred(max_staleness=120)
554+
out = _maybe_add_read_preference({}, pref)
479555
self.assertEqual(
480556
out, SON([("$query", {}), ("$readPreference", pref.document)]))
481557

@@ -533,6 +609,39 @@ def test_mongos(self):
533609
self.assertEqual(first_id, results[-1]["_id"])
534610
self.assertEqual(last_id, results[0]["_id"])
535611

612+
@client_context.require_mongos
613+
@client_context.require_version_min(3, 3, 12)
614+
def test_mongos_max_staleness(self):
615+
# Sanity check that we're sending maxStalenessMS
616+
coll = client_context.client.pymongo_test.get_collection(
617+
"test", read_preference=SecondaryPreferred(max_staleness=120))
618+
# No error
619+
coll.find_one()
620+
621+
coll = client_context.client.pymongo_test.get_collection(
622+
"test", read_preference=SecondaryPreferred(max_staleness=10))
623+
try:
624+
coll.find_one()
625+
except OperationFailure as exc:
626+
self.assertEqual(160, exc.code)
627+
else:
628+
self.fail("mongos accepted invalid staleness")
629+
630+
coll = single_client(
631+
readPreference='secondaryPreferred',
632+
maxStalenessMS=120000).pymongo_test.test
633+
# No error
634+
coll.find_one()
635+
636+
coll = single_client(
637+
readPreference='secondaryPreferred',
638+
maxStalenessMS=10000).pymongo_test.test
639+
try:
640+
coll.find_one()
641+
except OperationFailure as exc:
642+
self.assertEqual(160, exc.code)
643+
else:
644+
self.fail("mongos accepted invalid staleness")
536645

537646
if __name__ == "__main__":
538647
unittest.main()

test/test_topology.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -710,12 +710,12 @@ def test_no_secondary(self):
710710

711711
self.assertMessage(
712712
'No replica set members match selector'
713-
' "Secondary(tag_sets=None)"',
713+
' "Secondary(tag_sets=None, max_staleness=None)"',
714714
t, ReadPreference.SECONDARY)
715715

716716
self.assertMessage(
717717
"No replica set members match selector"
718-
" \"Secondary(tag_sets=[{'dc': 'ny'}])\"",
718+
" \"Secondary(tag_sets=[{'dc': 'ny'}], max_staleness=None)\"",
719719
t, Secondary(tag_sets=[{'dc': 'ny'}]))
720720

721721
def test_bad_replica_set_name(self):

0 commit comments

Comments
 (0)