Skip to content

Commit a44055d

Browse files
authored
Merge pull request #55 from launchdarkly/dr/twistedv2
Add Twisted support for LDD mode
2 parents 8c18433 + 9ce49fe commit a44055d

16 files changed

+425
-62
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes to the LaunchDarkly Python SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
44

5+
## [3.0.0] - 2016-08-22
6+
### Added
7+
- Twisted support for LDD mode only.
8+
9+
### Changed
10+
- FeatureStore interface get() and all() methods now take an additional callback parameter.
11+
512
## [2.0.0] - 2016-08-10
613
### Added
714
- Support for multivariate feature flags. `variation` replaces `toggle` and can return a string, number, dict, or boolean value depending on how the flag is defined.

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
include requirements.txt
22
include README.txt
33
include test-requirements.txt
4+
include twisted-requirements.txt
45
include redis-requirements.txt

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,38 @@ Your first feature flag
2626
-----------------------
2727

2828
1. Create a new feature flag on your [dashboard](https://app.launchdarkly.com)
29-
2. In your application code, use the feature's key to check wthether the flag is on for each user:
29+
2. In your application code, use the feature's key to check whether the flag is on for each user:
3030

3131
if client.variation("your.flag.key", {"key": "[email protected]"}, False):
3232
# application code to show the feature
3333
else:
3434
# the code to run if the feature is off
3535

36+
Twisted
37+
-------
38+
Twisted is supported for LDD mode only. To run in Twisted/LDD mode,
39+
40+
1. Use this dependency:
41+
42+
```
43+
ldclient-py[twisted]==3.0.0
44+
```
45+
2. Configure the client:
46+
47+
```
48+
feature_store = TwistedRedisFeatureStore(url='YOUR_REDIS_URL', redis_prefix="ldd-restwrapper", expiration=0)
49+
ldclient.config.feature_store = feature_store
50+
51+
ldclient.config = ldclient.Config(
52+
use_ldd=use_ldd,
53+
event_consumer_class=TwistedEventConsumer,
54+
)
55+
ldclient.sdk_key = 'YOUR_SDK_KEY'
56+
```
57+
3. Get the client:
58+
59+
```client = ldclient.get()```
60+
3661
Learn more
3762
-----------
3863

ldclient/client.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,7 @@ def __init__(self, sdk_key, config=None, start_wait=5):
120120
self._event_consumer.start()
121121

122122
if self._config.use_ldd:
123-
if self._store.__class__ == "RedisFeatureStore":
124-
log.info("Started LaunchDarkly Client in LDD mode")
125-
return
126-
log.error("LDD mode requires a RedisFeatureStore.")
123+
log.info("Started LaunchDarkly Client in LDD mode")
127124
return
128125

129126
if self._config.feature_requester_class:
@@ -136,6 +133,7 @@ def __init__(self, sdk_key, config=None, start_wait=5):
136133
update_processor_ready = threading.Event()
137134

138135
if self._config.update_processor_class:
136+
log.info("Using user-specified update processor: " + str(self._config.update_processor_class))
139137
self._update_processor = self._config.update_processor_class(
140138
sdk_key, self._config, self._feature_requester, self._store, update_processor_ready)
141139
else:
@@ -230,23 +228,35 @@ def send_event(value, version=None):
230228
if user.get('key', "") == "":
231229
log.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly.")
232230

233-
flag = self._store.get(key)
234-
if not flag:
235-
log.warn("Feature Flag key: " + key + " not found in Feature Store. Returning default.")
236-
send_event(default)
231+
def cb(flag):
232+
try:
233+
if not flag:
234+
log.warn("Feature Flag key: " + key + " not found in Feature Store. Returning default.")
235+
send_event(default)
236+
return default
237+
238+
return self._evaluate_and_send_events(flag, user, default)
239+
240+
except Exception as e:
241+
log.error("Exception caught in variation: " + e.message + " for flag key: " + key + " and user: " + str(user))
242+
237243
return default
238244

239-
value, events = evaluate(flag, user, self._store)
245+
return self._store.get(key, cb)
246+
247+
def _evaluate(self, flag, user):
248+
return evaluate(flag, user, self._store)
249+
250+
def _evaluate_and_send_events(self, flag, user, default):
251+
value, events = self._evaluate(flag, user)
240252
for event in events or []:
241253
self._send_event(event)
242-
log.debug("Sending event: " + str(event))
243-
244-
if value is not None:
245-
send_event(value, flag.get('version'))
246-
return value
247254

248-
send_event(default, flag.get('version'))
249-
return default
255+
if value is None:
256+
value = default
257+
self._send_event({'kind': 'feature', 'key': flag.get('key'),
258+
'user': user, 'value': value, 'default': default, 'version': flag.get('version')})
259+
return value
250260

251261
def all_flags(self, user):
252262
if self._config.offline:
@@ -261,7 +271,17 @@ def all_flags(self, user):
261271
log.warn("User or user key is None when calling all_flags(). Returning None.")
262272
return None
263273

264-
return {k: evaluate(v, user, self._store)[0] for k, v in self._store.all().items() or {}}
274+
def cb(all_flags):
275+
try:
276+
return self._evaluate_multi(user, all_flags)
277+
except Exception as e:
278+
log.error("Exception caught in all_flags: " + e.message + " for user: " + str(user))
279+
return {}
280+
281+
return self._store.all(cb)
282+
283+
def _evaluate_multi(self, user, flags):
284+
return {k: self._evaluate(v, user)[0] for k, v in flags.items() or {}}
265285

266286
def secure_mode_hash(self, user):
267287
if user.get('key') is None:

ldclient/feature_store.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@ def __init__(self):
1010
self._initialized = False
1111
self._features = {}
1212

13-
def get(self, key):
13+
def get(self, key, callback):
1414
try:
1515
self._lock.rlock()
1616
f = self._features.get(key)
1717
if f is None:
1818
log.debug("Attempted to get missing feature: " + str(key) + " Returning None")
19-
return None
19+
return callback(None)
2020
if 'deleted' in f and f['deleted']:
2121
log.debug("Attempted to get deleted feature: " + str(key) + " Returning None")
22-
return None
23-
return f
22+
return callback(None)
23+
return callback(f)
2424
finally:
2525
self._lock.runlock()
2626

27-
def all(self):
27+
def all(self, callback):
2828
try:
2929
self._lock.rlock()
30-
return dict((k, f) for k, f in self._features.items() if ('deleted' not in f) or not f['deleted'])
30+
return callback(dict((k, f) for k, f in self._features.items() if ('deleted' not in f) or not f['deleted']))
3131
finally:
3232
self._lock.runlock()
3333

ldclient/flag.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,15 @@ def evaluate(flag, user, store):
2121
if value is not None:
2222
return value, prereq_events
2323

24-
if 'offVariation' in flag and flag['offVariation']:
25-
value = _get_variation(flag, flag['offVariation'])
26-
return value, prereq_events
27-
return None, prereq_events
24+
return _get_off_variation(flag), prereq_events
2825

2926

3027
def _evaluate(flag, user, store, prereq_events=None):
3128
events = prereq_events or []
3229
failed_prereq = None
3330
prereq_value = None
3431
for prereq in flag.get('prerequisites') or []:
35-
prereq_flag = store.get(prereq.get('key'))
32+
prereq_flag = store.get(prereq.get('key'), lambda x: x)
3633
if prereq_flag is None:
3734
log.warn("Missing prereq flag: " + prereq.get('key'))
3835
failed_prereq = prereq

ldclient/interfaces.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,23 @@ class FeatureStore(object):
88
__metaclass__ = ABCMeta
99

1010
@abstractmethod
11-
def get(self, key):
11+
def get(self, key, callback):
1212
"""
13-
Gets the data for a feature flag for evaluation
14-
15-
:param key: The feature flag key
13+
Gets a feature and calls the callback with the feature data to return the result
14+
:param key: The feature key
1615
:type key: str
17-
:return: The feature flag data
18-
:rtype: dict
16+
:param callback: The function that accepts the feature data and returns the feature value
17+
:type callback: Function that processes the feature flag once received.
18+
:return: The result of executing callback.
1919
"""
2020

2121
@abstractmethod
22-
def all(self):
22+
def all(self, callback):
2323
"""
2424
Returns all feature flags and their data
25-
26-
:rtype: dict[str, dict]
25+
:param callback: The function that accepts the feature data and returns the feature value
26+
:type callback: Function that processes the feature flags once received.
27+
:rtype: The result of executing callback.
2728
"""
2829

2930
@abstractmethod

ldclient/redis_feature_store.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,43 +40,44 @@ def init(self, features):
4040
pipe.hset(self._features_key, k, f_json)
4141
self._cache[k] = f
4242
pipe.execute()
43+
log.info("Initialized RedisFeatureStore with " + str(len(features)) + " feature flags")
4344

44-
def all(self):
45+
def all(self, callback):
4546
r = redis.Redis(connection_pool=self._pool)
4647
all_features = r.hgetall(self._features_key)
4748
if all_features is None or all_features is "":
4849
log.warn("RedisFeatureStore: call to get all flags returned no results. Returning None.")
49-
return None
50+
return callback(None)
5051

5152
results = {}
5253
for k, f_json in all_features.items() or {}:
5354
f = json.loads(f_json.decode('utf-8'))
5455
if 'deleted' in f and f['deleted'] is False:
5556
results[f['key']] = f
56-
return results
57+
return callback(results)
5758

58-
def get(self, key):
59+
def get(self, key, callback=lambda x: x):
5960
f = self._cache.get(key)
6061
if f is not None:
6162
# reset ttl
6263
self._cache[key] = f
6364
if f.get('deleted', False) is True:
6465
log.warn("RedisFeatureStore: get returned deleted flag from in-memory cache. Returning None.")
65-
return None
66-
return f
66+
return callback(None)
67+
return callback(f)
6768

6869
r = redis.Redis(connection_pool=self._pool)
6970
f_json = r.hget(self._features_key, key)
7071
if f_json is None or f_json is "":
7172
log.warn("RedisFeatureStore: feature flag with key: " + key + " not found in Redis. Returning None.")
72-
return None
73+
return callback(None)
7374

7475
f = json.loads(f_json.decode('utf-8'))
7576
if f.get('deleted', False) is True:
7677
log.warn("RedisFeatureStore: get returned deleted flag from Redis. Returning None.")
77-
return None
78+
return callback(None)
7879
self._cache[key] = f
79-
return f
80+
return callback(f)
8081

8182
def delete(self, key, version):
8283
r = redis.Redis(connection_pool=self._pool)

ldclient/streaming.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010

1111
class StreamingUpdateProcessor(Thread, UpdateProcessor):
12-
1312
def __init__(self, sdk_key, config, requester, store, ready):
1413
Thread.__init__(self)
1514
self.daemon = True
@@ -31,7 +30,8 @@ def run(self):
3130
for msg in messages:
3231
if not self._running:
3332
break
34-
self.process_message(self._store, self._requester, msg, self._ready)
33+
if self.process_message(self._store, self._requester, msg, self._ready) is True:
34+
self._ready.set()
3535
except Exception as e:
3636
log.error("Could not connect to LaunchDarkly stream: " + str(e.message) +
3737
" waiting 1 second before trying again.")
@@ -51,8 +51,8 @@ def process_message(store, requester, msg, ready):
5151
if msg.event == 'put':
5252
store.init(payload)
5353
if not ready.is_set() and store.initialized:
54-
ready.set()
5554
log.info("StreamingUpdateProcessor initialized ok")
55+
return True
5656
elif msg.event == 'patch':
5757
key = payload['path'][1:]
5858
feature = payload['data']
@@ -64,12 +64,13 @@ def process_message(store, requester, msg, ready):
6464
elif msg.event == "indirect/put":
6565
store.init(requester.get_all())
6666
if not ready.is_set() and store.initialized:
67-
ready.set()
6867
log.info("StreamingUpdateProcessor initialized ok")
68+
return True
6969
elif msg.event == 'delete':
7070
key = payload['path'][1:]
7171
# noinspection PyShadowingNames
7272
version = payload['version']
7373
store.delete(key, version)
7474
else:
75-
log.warning('Unhandled event in stream processor: ' + msg.event)
75+
log.warning('Unhandled event in stream processor: ' + msg.event)
76+
return False

0 commit comments

Comments
 (0)