Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prepare 5.0.2 release #78

Merged
merged 39 commits into from
Mar 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
33fb7f5
support segments
eli-darkly Jan 18, 2018
64b0051
Merge branch 'segments' into eb/segments
eli-darkly Feb 1, 2018
91cd013
genericized feature store + misc fixes
eli-darkly Feb 1, 2018
7ae9b3a
unit tests, misc cleanup
eli-darkly Feb 1, 2018
6aaa7e8
undo renaming of modules
eli-darkly Feb 1, 2018
55baede
more test coverage
eli-darkly Feb 6, 2018
983ae60
misc cleanup
eli-darkly Feb 6, 2018
21b07ba
cleaner path-parsing logic
eli-darkly Feb 6, 2018
796a1fc
InMemoryFeatureStore should implement FeatureStore
eli-darkly Feb 6, 2018
745b3b9
add more unit test coverage of flag evals
eli-darkly Feb 6, 2018
f03aaa1
fix bug in flag evals - putting wrong flag in "prereqOf"
eli-darkly Feb 6, 2018
d245ef2
use namedtuple
eli-darkly Feb 6, 2018
8fdfd40
use namedtuple again
eli-darkly Feb 6, 2018
51853eb
misc cleanup
eli-darkly Feb 7, 2018
21389b6
use defaultdict
eli-darkly Feb 7, 2018
74beca3
change class name
eli-darkly Feb 7, 2018
79376e4
Merge branch 'segments' into eb/segments
eli-darkly Feb 7, 2018
3a691f6
Merge branch 'segments' into eb/segments
eli-darkly Feb 7, 2018
7e02fa2
fix merge
eli-darkly Feb 7, 2018
1d7dd3e
Merge branch 'segments' into eb/segments
eli-darkly Feb 7, 2018
830f2d1
Merge pull request #31 from launchdarkly/eb/segments
eli-darkly Feb 13, 2018
2018a25
fix & test edge case of weight=None
eli-darkly Feb 13, 2018
1602f10
Merge pull request #36 from launchdarkly/eb/more-segment-tests
eli-darkly Feb 13, 2018
29a05b6
remove all Twisted support
eli-darkly Feb 21, 2018
35c787a
update readme: we do support streaming for Python 2.6
eli-darkly Feb 21, 2018
c380f8a
Merge pull request #37 from launchdarkly/eb/remove-twisted
eli-darkly Feb 21, 2018
4778831
Merge branch 'master' of github.com:launchdarkly/python-client
eli-darkly Feb 21, 2018
739cf75
fix ridiculous mistakes that broke the stream
eli-darkly Feb 22, 2018
dde98bd
Merge pull request #38 from launchdarkly/eb/fix-streaming
eli-darkly Feb 22, 2018
d52ab9d
fix further breakage in StreamProcessor
eli-darkly Feb 22, 2018
4d2f6c7
Merge pull request #39 from launchdarkly/eb/fix-streaming-2
eli-darkly Feb 22, 2018
fa66014
Merge branch 'master' of github.com:launchdarkly/python-client
eli-darkly Feb 22, 2018
9a73a16
fix Redis store to use optimistic locking and retry as needed
eli-darkly Mar 26, 2018
aee7606
make parameter name explicit
eli-darkly Mar 26, 2018
57255c7
narrower try block
eli-darkly Mar 26, 2018
243bf5b
use break/continue
eli-darkly Mar 26, 2018
eb17215
Merge pull request #41 from launchdarkly/eb/ch13390/redis-watch
eli-darkly Mar 26, 2018
8071ace
add debug logging for out-of-order update
eli-darkly Mar 26, 2018
8ed91ab
Merge pull request #43 from launchdarkly/eb/redis-debug-log
eli-darkly Mar 26, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 43 additions & 30 deletions ldclient/redis_feature_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,19 +79,20 @@ def all(self, kind, callback):
return callback(results)

def get(self, kind, key, callback=lambda x: x):
item = self._get_even_if_deleted(kind, key)
item = self._get_even_if_deleted(kind, key, check_cache=True)
if item is not None and item.get('deleted', False) is True:
log.debug("RedisFeatureStore: get returned deleted item %s in '%s'. Returning None.", key, kind.namespace)
return callback(None)
return callback(item)

def _get_even_if_deleted(self, kind, key):
def _get_even_if_deleted(self, kind, key, check_cache = True):
cacheKey = self._cache_key(kind, key)
item = self._cache.get(cacheKey)
if item is not None:
# reset ttl
self._cache[cacheKey] = item
return item
if check_cache:
item = self._cache.get(cacheKey)
if item is not None:
# reset ttl
self._cache[cacheKey] = item
return item

try:
r = redis.Redis(connection_pool=self._pool)
Expand All @@ -110,17 +111,11 @@ def _get_even_if_deleted(self, kind, key):
return item

def delete(self, kind, key, version):
r = redis.Redis(connection_pool=self._pool)
baseKey = self._items_key(kind)
r.watch(baseKey)
item_json = r.hget(baseKey, key)
item = None if item_json is None else json.loads(item_json.decode('utf-8'))
if item is None or item['version'] < version:
deletedItem = { "deleted": True, "version": version }
item_json = json.dumps(deletedItem)
r.hset(baseKey, key, item_json)
self._cache[self._cache_key(kind, key)] = deletedItem
r.unwatch()
deleted_item = { "key": key, "version": version, "deleted": True }
self._update_with_versioning(kind, deleted_item)

def upsert(self, kind, item):
self._update_with_versioning(kind, item)

@property
def initialized(self):
Expand All @@ -130,18 +125,36 @@ def _query_init(self):
r = redis.Redis(connection_pool=self._pool)
return r.exists(self._items_key(FEATURES))

def upsert(self, kind, item):
def _update_with_versioning(self, kind, item):
r = redis.Redis(connection_pool=self._pool)
baseKey = self._items_key(kind)
base_key = self._items_key(kind)
key = item['key']
r.watch(baseKey)
old = self._get_even_if_deleted(kind, key)
if old:
if old['version'] >= item['version']:
r.unwatch()
return

item_json = json.dumps(item)
r.hset(baseKey, key, item_json)
self._cache[self._cache_key(kind, key)] = item
r.unwatch()

while True:
pipeline = r.pipeline()
pipeline.watch(base_key)
old = self._get_even_if_deleted(kind, key, check_cache=False)
self._before_update_transaction(base_key, key)
if old and old['version'] >= item['version']:
log.debug('RedisFeatureStore: Attempted to %s key: %s version %d with a version that is the same or older: %d in "%s"',
'delete' if item.get('deleted') else 'update',
key, old['version'], item['version'], kind.namespace)
pipeline.unwatch()
break
else:
pipeline.multi()
pipeline.hset(base_key, key, item_json)
try:
pipeline.execute()
# Unlike Redis implementations for other platforms, in redis-py a failed WATCH
# produces an exception rather than a null result from execute().
except redis.exceptions.WatchError:
log.debug("RedisFeatureStore: concurrent modification detected, retrying")
continue
self._cache[self._cache_key(kind, key)] = item
break

def _before_update_transaction(self, base_key, key):
# exposed for testing
pass
1 change: 1 addition & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mock>=2.0.0
pytest>=2.8
pytest-timeout>=1.0
redis>=2.10.5
Expand Down
42 changes: 42 additions & 0 deletions testing/test_feature_store.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import json
from mock import patch
import pytest
import redis

Expand Down Expand Up @@ -120,3 +122,43 @@ def test_upsert_older_version_after_delete(self, store):
old_ver = self.make_feature('foo', 9)
store.upsert(FEATURES, old_ver)
assert store.get(FEATURES, 'foo', lambda x: x) is None


class TestRedisFeatureStoreExtraTests:
@patch.object(RedisFeatureStore, '_before_update_transaction')
def test_upsert_race_condition_against_external_client_with_higher_version(self, mock_method):
other_client = redis.StrictRedis(host='localhost', port=6379, db=0)
store = RedisFeatureStore()
store.init({ FEATURES: {} })

other_version = {u'key': u'flagkey', u'version': 2}
def hook(base_key, key):
if other_version['version'] <= 4:
other_client.hset(base_key, key, json.dumps(other_version))
other_version['version'] = other_version['version'] + 1
mock_method.side_effect = hook

feature = { u'key': 'flagkey', u'version': 1 }

store.upsert(FEATURES, feature)
result = store.get(FEATURES, 'flagkey', lambda x: x)
assert result['version'] == 2

@patch.object(RedisFeatureStore, '_before_update_transaction')
def test_upsert_race_condition_against_external_client_with_lower_version(self, mock_method):
other_client = redis.StrictRedis(host='localhost', port=6379, db=0)
store = RedisFeatureStore()
store.init({ FEATURES: {} })

other_version = {u'key': u'flagkey', u'version': 2}
def hook(base_key, key):
if other_version['version'] <= 4:
other_client.hset(base_key, key, json.dumps(other_version))
other_version['version'] = other_version['version'] + 1
mock_method.side_effect = hook

feature = { u'key': 'flagkey', u'version': 5 }

store.upsert(FEATURES, feature)
result = store.get(FEATURES, 'flagkey', lambda x: x)
assert result['version'] == 5