Skip to content

Commit e7e2700

Browse files
author
Edward J M Easton
committed
When deleting symbols, we assert that the symbol is actually deleted
using has_symbol(). This assertion can fail when reading from a secondary cluser member. This commit adds the allow_secondary argument to the has_symbol() method, and a little refactoring of the code to determine the mongo read preference.
1 parent dfbaa0f commit e7e2700

File tree

2 files changed

+63
-17
lines changed

2 files changed

+63
-17
lines changed

arctic/store/version_store.py

+32-7
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ def __str__(self):
105105

106106
def __repr__(self):
107107
return str(self)
108+
109+
def _read_preference(self, allow_secondary):
110+
""" Return the mongo read preference given an 'allow_secondary' argument
111+
"""
112+
allow_secondary = self._allow_secondary if allow_secondary is None else allow_secondary
113+
return ReadPreference.NEAREST if allow_secondary else ReadPreference.PRIMARY
108114

109115
@mongo_retry
110116
def list_symbols(self, all_symbols=False, snapshot=None, regex=None, **kwargs):
@@ -166,7 +172,7 @@ def list_symbols(self, all_symbols=False, snapshot=None, regex=None, **kwargs):
166172
return sorted([x['symbol'] for x in results])
167173

168174
@mongo_retry
169-
def has_symbol(self, symbol, as_of=None):
175+
def has_symbol(self, symbol, as_of=None, allow_secondary=None):
170176
"""
171177
Return True if the 'symbol' exists in this library AND the symbol
172178
isn't deleted in the specified as_of.
@@ -177,9 +183,19 @@ def has_symbol(self, symbol, as_of=None):
177183
----------
178184
symbol : `str`
179185
symbol name for the item
186+
as_of : `str` or int or `datetime.datetime`
187+
Return the data as it was as_of the point in time.
188+
`int` : specific version number
189+
`str` : snapshot name which contains the version
190+
`datetime.datetime` : the version of the data that existed as_of the requested point in time
191+
allow_secondary : `bool` or `None`
192+
Override the default behavior for allowing reads from secondary members of a cluster:
193+
`None` : use the settings from the top-level `Arctic` object used to query this version store.
194+
`True` : allow reads from secondary members
195+
`False` : only allow reads from primary members
180196
"""
181197
try:
182-
self._read_metadata(symbol, as_of=as_of)
198+
self._read_metadata(symbol, as_of=as_of, read_preference=self._read_preference(allow_secondary))
183199
return True
184200
except NoDataFoundException:
185201
return False
@@ -287,14 +303,18 @@ def read(self, symbol, as_of=None, from_version=None, allow_secondary=None, **kw
287303
`int` : specific version number
288304
`str` : snapshot name which contains the version
289305
`datetime.datetime` : the version of the data that existed as_of the requested point in time
306+
allow_secondary : `bool` or `None`
307+
Override the default behavior for allowing reads from secondary members of a cluster:
308+
`None` : use the settings from the top-level `Arctic` object used to query this version store.
309+
`True` : allow reads from secondary members
310+
`False` : only allow reads from primary members
290311
291312
Returns
292313
-------
293314
VersionedItem namedtuple which contains a .data and .metadata element
294315
"""
295-
allow_secondary = self._allow_secondary if allow_secondary is None else allow_secondary
296316
try:
297-
read_preference = ReadPreference.NEAREST if allow_secondary else ReadPreference.PRIMARY
317+
read_preference = self._read_preference(allow_secondary)
298318
_version = self._read_metadata(symbol, as_of=as_of, read_preference=read_preference)
299319
return self._do_read(symbol, _version, from_version, read_preference=read_preference, **kwargs)
300320
except (OperationFailure, AutoReconnect) as e:
@@ -347,7 +367,7 @@ def _do_read(self, symbol, version, from_version=None, **kwargs):
347367
_do_read_retry = mongo_retry(_do_read)
348368

349369
@mongo_retry
350-
def read_metadata(self, symbol, as_of=None):
370+
def read_metadata(self, symbol, as_of=None, allow_secondary=None):
351371
"""
352372
Return the metadata saved for a symbol. This method is fast as it doesn't
353373
actually load the data.
@@ -361,8 +381,13 @@ def read_metadata(self, symbol, as_of=None):
361381
`int` : specific version number
362382
`str` : snapshot name which contains the version
363383
`datetime.datetime` : the version of the data that existed as_of the requested point in time
384+
allow_secondary : `bool` or `None`
385+
Override the default behavior for allowing reads from secondary members of a cluster:
386+
`None` : use the settings from the top-level `Arctic` object used to query this version store.
387+
`True` : allow reads from secondary members
388+
`False` : only allow reads from primary members
364389
"""
365-
_version = self._read_metadata(symbol, as_of=as_of)
390+
_version = self._read_metadata(symbol, as_of=as_of, read_preference=self._read_preference(allow_secondary))
366391
return VersionedItem(symbol=symbol, library=self._arctic_lib.get_name(), version=_version['version'],
367392
metadata=_version.pop('metadata', None), data=None)
368393

@@ -642,7 +667,7 @@ def delete(self, symbol):
642667
'metadata.deleted': {'$ne': True}})
643668
if not snapped_version:
644669
self._delete_version(symbol, sentinel.version)
645-
assert not self.has_symbol(symbol)
670+
assert not self.has_symbol(symbol, allow_secondary=False)
646671

647672
def _write_audit(self, user, message, changed_version):
648673
"""

tests/unit/store/test_version_store.py

+31-10
Original file line numberDiff line numberDiff line change
@@ -44,31 +44,52 @@ def test_list_versions_localTime():
4444
'snapshots': 'snap'}
4545

4646

47+
def test__read_preference__allow_secondary_true():
48+
self = create_autospec(VersionStore)
49+
assert VersionStore._read_preference(self, True) == ReadPreference.NEAREST
50+
51+
52+
def test__read_preference__allow_secondary_false():
53+
self = create_autospec(VersionStore)
54+
assert VersionStore._read_preference(self, False) == ReadPreference.PRIMARY
55+
56+
57+
def test__read_preference__default_true():
58+
self = create_autospec(VersionStore, _allow_secondary=True)
59+
assert VersionStore._read_preference(self, None) == ReadPreference.NEAREST
60+
61+
62+
def test__read_preference__default_false():
63+
self = create_autospec(VersionStore, _allow_secondary=False)
64+
assert VersionStore._read_preference(self, None) == ReadPreference.PRIMARY
65+
66+
4767
def test_get_version_allow_secondary_True():
4868
vs = create_autospec(VersionStore, instance=True,
4969
_versions=Mock())
50-
vs._allow_secondary = True
70+
vs._read_preference.return_value = sentinel.read_preference
5171
vs._find_snapshots.return_value = 'snap'
5272
vs._versions.find.return_value = [{'_id': bson.ObjectId.from_datetime(dt(2013, 4, 1, 9, 0)),
5373
'symbol': 's', 'version': 10}]
5474

5575
VersionStore.read(vs, "symbol")
56-
assert vs._read_metadata.call_args_list == [call('symbol', as_of=None, read_preference=ReadPreference.NEAREST)]
57-
assert vs._do_read.call_args_list == [call('symbol', vs._read_metadata.return_value, None, read_preference=ReadPreference.NEAREST)]
76+
assert vs._read_metadata.call_args_list == [call('symbol', as_of=None, read_preference=sentinel.read_preference)]
77+
assert vs._do_read.call_args_list == [call('symbol', vs._read_metadata.return_value, None, read_preference=sentinel.read_preference)]
5878

5979

6080
def test_get_version_allow_secondary_user_override_False():
6181
"""Ensure user can override read preference when calling read"""
6282
vs = create_autospec(VersionStore, instance=True,
6383
_versions=Mock())
64-
vs._allow_secondary = True
84+
vs._read_preference.return_value = sentinel.read_preference
6585
vs._find_snapshots.return_value = 'snap'
6686
vs._versions.find.return_value = [{'_id': bson.ObjectId.from_datetime(dt(2013, 4, 1, 9, 0)),
6787
'symbol': 's', 'version': 10}]
6888

6989
VersionStore.read(vs, "symbol", allow_secondary=False)
70-
assert vs._read_metadata.call_args_list == [call('symbol', as_of=None, read_preference=ReadPreference.PRIMARY)]
71-
assert vs._do_read.call_args_list == [call('symbol', vs._read_metadata.return_value, None, read_preference=ReadPreference.PRIMARY)]
90+
assert vs._read_metadata.call_args_list == [call('symbol', as_of=None, read_preference=sentinel.read_preference)]
91+
assert vs._do_read.call_args_list == [call('symbol', vs._read_metadata.return_value, None, read_preference=sentinel.read_preference)]
92+
vs._read_preference.assert_called_once_with(False)
7293

7394

7495
def test_read_as_of_LondonTime():
@@ -176,17 +197,17 @@ def test_prune_previous_versions_0_timeout():
176197

177198

178199
def test_read_handles_operation_failure():
179-
self = create_autospec(VersionStore, _versions=Mock(), _arctic_lib=Mock(),
180-
_allow_secondary=True)
200+
self = create_autospec(VersionStore, _versions=Mock(), _arctic_lib=Mock())
201+
self._read_preference.return_value = sentinel.read_preference
181202
self._collection = create_autospec(Collection)
182203
self._read_metadata.side_effect = [sentinel.meta1, sentinel.meta2]
183204
self._read_metadata.__name__ = 'name'
184205
self._do_read.__name__ = 'name' # feh: mongo_retry decorator cares about this
185206
self._do_read.side_effect = [OperationFailure('error'), sentinel.read]
186207
VersionStore.read(self, sentinel.symbol, sentinel.as_of, sentinel.from_version)
187-
# Assert that, for the two read calls, the second uses the new metadata
208+
# Assert that, for the two read calls, the second uses the new metadata and forces a read from primary
188209
assert self._do_read.call_args_list == [call(sentinel.symbol, sentinel.meta1, sentinel.from_version,
189-
read_preference=ReadPreference.NEAREST)]
210+
read_preference=sentinel.read_preference)]
190211
assert self._do_read_retry.call_args_list == [call(sentinel.symbol, sentinel.meta2, sentinel.from_version,
191212
read_preference=ReadPreference.PRIMARY)]
192213

0 commit comments

Comments
 (0)