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 6.0.0 release #83

Merged
merged 61 commits into from
May 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
c5c0b81
change EventSerializer to UserFilter since we'll be scrubbing events …
eli-darkly Mar 20, 2018
b9d73a1
preserve flag eval variation index for events
eli-darkly Mar 20, 2018
3543019
set variation to None when value is default
eli-darkly Mar 20, 2018
3c8e353
copy new flag properties into event
eli-darkly Mar 20, 2018
ae65f95
add new config properties
eli-darkly Mar 20, 2018
2870207
implement event summarizer
eli-darkly Mar 20, 2018
ab48ef8
add pylru dependency
eli-darkly Mar 20, 2018
57f5511
rename EventConsumer to EventProcessor; don't expose its internal que…
eli-darkly Mar 21, 2018
2e9ef50
don't need to create a FeatureRequestor if we're using a custom updat…
eli-darkly Mar 21, 2018
d32adaa
implement summary events in DefaultEventProcessor
eli-darkly Mar 22, 2018
ca7baf6
rm debugging
eli-darkly Mar 22, 2018
e0ff329
add flush_interval to Config, remove obsolete property
eli-darkly Mar 22, 2018
3708ef2
consumer->processor
eli-darkly Mar 22, 2018
3b07e87
consumer->processor
eli-darkly Mar 22, 2018
2231978
use timer thread objects to avoid race condition
eli-darkly Mar 23, 2018
971d7fe
rm spurious underscore
eli-darkly Mar 24, 2018
8a45a53
use namedtuple
eli-darkly Mar 24, 2018
d0284ed
make timer threads daemons
eli-darkly Mar 24, 2018
9b3632f
break up DefaultEventProcessor a bit and don't inherit from Thread
eli-darkly Mar 24, 2018
c73e0b9
break out the output event generation logic
eli-darkly Mar 24, 2018
5a32403
break up the event processor code some more
eli-darkly Mar 24, 2018
3b1b159
fix user filtering for index event
eli-darkly Mar 24, 2018
a745739
fix creation date in custom event
eli-darkly Mar 24, 2018
a67c7b3
don't create a task object unless we're actually going to send some e…
eli-darkly Mar 26, 2018
bd147c3
misc fixes
eli-darkly Mar 26, 2018
e8b2998
don't use stop() internally; make it safe to call stop() more than once
eli-darkly Mar 26, 2018
4a53a0c
break out user deduplicator into a separate class
eli-darkly Mar 26, 2018
00ef4e2
Merge branch 'summary-events' into eb/ch12903/summary-events
eli-darkly Mar 27, 2018
6108512
remove Python 2.6 support
eli-darkly Mar 29, 2018
279ae64
Merge branch 'summary-events' into eb/ch12903/summary-events
eli-darkly Mar 29, 2018
1fec687
Merge branch 'summary-events' into eb/ch12903/summary-events
eli-darkly Mar 30, 2018
d556e2d
Merge branch 'master' into eb/ch15531/drop-python-2.6
eli-darkly Mar 30, 2018
d5d0a38
fix setup.py
eli-darkly Mar 30, 2018
5a31bd8
misc fixes
eli-darkly Apr 4, 2018
43784e0
make flushes async, but ensure everything's been sent when shutting down
eli-darkly Apr 4, 2018
06cc4a1
put limit on concurrent flush tasks; put user dedup logic back into E…
eli-darkly Apr 4, 2018
1702241
don't need to keep reference to dispatcher
eli-darkly Apr 4, 2018
3b32885
fix time calculation
eli-darkly Apr 4, 2018
3f62e01
flush threads should be daemons
eli-darkly Apr 4, 2018
072f6cb
add thread pool class + misc fixes
eli-darkly Apr 4, 2018
710e1fb
fix overly broad exception catching
eli-darkly Apr 4, 2018
dc201e6
debugging
eli-darkly Apr 4, 2018
b4372d3
debugging
eli-darkly Apr 4, 2018
72112ec
tolerate being closed more than once
eli-darkly Apr 4, 2018
4397b2b
don't close session unless we created it
eli-darkly Apr 4, 2018
e3c1189
Merge pull request #40 from launchdarkly/eb/ch12903/summary-events
eli-darkly Apr 4, 2018
108bf10
Merge pull request #47 from launchdarkly/eb/ch15531/drop-python-2.6
eli-darkly Apr 6, 2018
821f4db
fix behavior of debug & index events
eli-darkly Apr 9, 2018
00fd2ef
try to clarify flow in _process_event
eli-darkly Apr 11, 2018
08b57eb
Merge pull request #48 from launchdarkly/eb/summary-events-debug-index
eli-darkly Apr 11, 2018
83f2a03
set schema header in event payload
eli-darkly Apr 18, 2018
d9277dd
Merge branch 'master' into summary-events
eli-darkly Apr 18, 2018
4ce4e37
Merge branch 'summary-events' into eb/event-schema
eli-darkly Apr 18, 2018
836bbe0
Merge pull request #50 from launchdarkly/eb/event-schema
eli-darkly Apr 19, 2018
91bc3d8
fix all_flags method + more unit tests
eli-darkly Apr 23, 2018
60d5d42
re-add redundant key property in identify event
eli-darkly Apr 23, 2018
d102924
Merge pull request #51 from launchdarkly/eb/fix-identify-event
eli-darkly Apr 23, 2018
1243778
send as much of a feature event as possible even if user is invalid
eli-darkly Apr 25, 2018
90f3bf6
Merge pull request #52 from launchdarkly/eb/events-for-bad-users
eli-darkly Apr 25, 2018
49e4a80
include variation index in events and bump schema version to 3
eli-darkly Apr 30, 2018
d671deb
Merge pull request #53 from launchdarkly/eb/var-index-and-schema-version
eli-darkly Apr 30, 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
7 changes: 2 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ Your first feature flag
else:
# the code to run if the feature is off

Python 2.6
Supported Python versions
----------
Python 2.6 requires an extra dependency. Here's how to set it up:

1. Use the `python2.6` extra in your requirements.txt:
`ldclient-py[python2.6]`
The SDK is tested with the most recent patch releases of Python 2.7, 3.3, 3.4, 3.5, and 3.6. Python 2.6 is no longer supported.

Learn more
-----------
Expand Down
88 changes: 44 additions & 44 deletions ldclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import hashlib
import hmac
import threading
import time

import requests
from builtins import object

from ldclient.config import Config as Config
from ldclient.event_processor import NullEventProcessor
from ldclient.feature_requester import FeatureRequesterImpl
from ldclient.flag import evaluate
from ldclient.polling import PollingUpdateProcessor
Expand All @@ -21,7 +21,7 @@
import queue
except:
# noinspection PyUnresolvedReferences,PyPep8Naming
import Queue as queue
import Queue as queue # Python 3

from cachecontrol import CacheControl
from threading import Lock
Expand All @@ -43,46 +43,46 @@ def __init__(self, sdk_key=None, config=None, start_wait=5):
self._config._validate()

self._session = CacheControl(requests.Session())
self._queue = queue.Queue(self._config.events_max_pending)
self._event_consumer = None
self._event_processor = None
self._lock = Lock()

self._store = self._config.feature_store
""" :type: FeatureStore """

if self._config.offline or not self._config.send_events:
self._event_processor = NullEventProcessor()
else:
self._event_processor = self._config.event_processor_class(self._config)

if self._config.offline:
log.info("Started LaunchDarkly Client in offline mode")
return

if self._config.send_events:
self._event_consumer = self._config.event_consumer_class(self._queue, self._config)
self._event_consumer.start()

if self._config.use_ldd:
log.info("Started LaunchDarkly Client in LDD mode")
return

if self._config.feature_requester_class:
self._feature_requester = self._config.feature_requester_class(self._config)
else:
self._feature_requester = FeatureRequesterImpl(self._config)
""" :type: FeatureRequester """

update_processor_ready = threading.Event()

if self._config.update_processor_class:
log.info("Using user-specified update processor: " + str(self._config.update_processor_class))
self._update_processor = self._config.update_processor_class(
self._config, self._feature_requester, self._store, update_processor_ready)
self._config, self._store, update_processor_ready)
else:
if self._config.feature_requester_class:
feature_requester = self._config.feature_requester_class(self._config)
else:
feature_requester = FeatureRequesterImpl(self._config)
""" :type: FeatureRequester """

if self._config.stream:
self._update_processor = StreamingUpdateProcessor(
self._config, self._feature_requester, self._store, update_processor_ready)
self._config, feature_requester, self._store, update_processor_ready)
else:
log.info("Disabling streaming API")
log.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support")
self._update_processor = PollingUpdateProcessor(
self._config, self._feature_requester, self._store, update_processor_ready)
self._config, feature_requester, self._store, update_processor_ready)
""" :type: UpdateProcessor """

self._update_processor.start()
Expand All @@ -102,19 +102,13 @@ def close(self):
log.info("Closing LaunchDarkly client..")
if self.is_offline():
return
if self._event_consumer and self._event_consumer.is_alive():
self._event_consumer.stop()
if self._event_processor:
self._event_processor.stop()
if self._update_processor and self._update_processor.is_alive():
self._update_processor.stop()

def _send_event(self, event):
if self._config.offline or not self._config.send_events:
return
event['creationDate'] = int(time.time() * 1000)
if self._queue.full():
log.warning("Event queue is full-- dropped an event")
else:
self._queue.put(event)
self._event_processor.send_event(event)

def track(self, event_name, user, data=None):
self._sanitize_user(user)
Expand All @@ -135,24 +129,26 @@ def is_initialized(self):
return self.is_offline() or self._config.use_ldd or self._update_processor.initialized()

def flush(self):
if self._config.offline or not self._config.send_events:
if self._config.offline:
return
return self._event_consumer.flush()
return self._event_processor.flush()

def toggle(self, key, user, default):
log.warn("Deprecated method: toggle() called. Use variation() instead.")
return self.variation(key, user, default)

def variation(self, key, user, default):
default = self._config.get_default(key, default)
self._sanitize_user(user)
if user is not None:
self._sanitize_user(user)

if self._config.offline:
return default

def send_event(value, version=None):
self._send_event({'kind': 'feature', 'key': key,
'user': user, 'value': value, 'default': default, 'version': version})
self._send_event({'kind': 'feature', 'key': key, 'user': user, 'variation': None,
'value': value, 'default': default, 'version': version,
'trackEvents': False, 'debugEventsUntilDate': None})

if not self.is_initialized():
if self._store.initialized:
Expand All @@ -163,12 +159,7 @@ def send_event(value, version=None):
send_event(default)
return default

if user is None or user.get('key') is None:
log.warn("Missing user or user key when evaluating Feature Flag key: " + key + ". Returning default.")
send_event(default)
return default

if user.get('key', "") == "":
if user is not None and user.get('key', "") == "":
log.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly.")

def cb(flag):
Expand All @@ -182,6 +173,7 @@ def cb(flag):

except Exception as e:
log.error("Exception caught in variation: " + e.message + " for flag key: " + key + " and user: " + str(user))
send_event(default)

return default

Expand All @@ -191,14 +183,22 @@ def _evaluate(self, flag, user):
return evaluate(flag, user, self._store)

def _evaluate_and_send_events(self, flag, user, default):
value, events = self._evaluate(flag, user)
for event in events or []:
self._send_event(event)

if value is None:
if user is None or user.get('key') is None:
log.warn("Missing user or user key when evaluating Feature Flag key: " + flag.get('key') + ". Returning default.")
value = default
variation = None
else:
result = evaluate(flag, user, self._store)
for event in result.events or []:
self._send_event(event)
value = default if result.value is None else result.value
variation = result.variation

self._send_event({'kind': 'feature', 'key': flag.get('key'),
'user': user, 'value': value, 'default': default, 'version': flag.get('version')})
'user': user, 'variation': variation, 'value': value,
'default': default, 'version': flag.get('version'),
'trackEvents': flag.get('trackEvents'),
'debugEventsUntilDate': flag.get('debugEventsUntilDate')})
return value

def all_flags(self, user):
Expand Down Expand Up @@ -227,7 +227,7 @@ def cb(all_flags):
return self._store.all(FEATURES, cb)

def _evaluate_multi(self, user, flags):
return dict([(k, self._evaluate(v, user)[0]) for k, v in flags.items() or {}])
return dict([(k, self._evaluate(v, user).value) for k, v in flags.items() or {}])

def secure_mode_hash(self, user):
if user.get('key') is None or self._config.sdk_key is None:
Expand Down
64 changes: 47 additions & 17 deletions ldclient/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ldclient.event_consumer import EventConsumerImpl
from ldclient.event_processor import DefaultEventProcessor
from ldclient.feature_store import InMemoryFeatureStore
from ldclient.util import log

Expand All @@ -13,8 +13,8 @@ def __init__(self,
events_uri='https://events.launchdarkly.com',
connect_timeout=10,
read_timeout=15,
events_upload_max_batch_size=100,
events_max_pending=10000,
flush_interval=5,
stream_uri='https://stream.launchdarkly.com',
stream=True,
verify_ssl=True,
Expand All @@ -26,10 +26,13 @@ def __init__(self,
use_ldd=False,
feature_store=InMemoryFeatureStore(),
feature_requester_class=None,
event_consumer_class=None,
event_processor_class=None,
private_attribute_names=(),
all_attributes_private=False,
offline=False):
offline=False,
user_keys_capacity=1000,
user_keys_flush_interval=300,
inline_users_in_events=False):
"""
:param string sdk_key: The SDK key for your LaunchDarkly account.
:param string base_uri: The base URL for the LaunchDarkly server. Most users should use the default
Expand All @@ -43,6 +46,8 @@ def __init__(self,
:param int events_max_pending: The capacity of the events buffer. The client buffers up to this many
events in memory before flushing. If the capacity is exceeded before the buffer is flushed, events
will be discarded.
: param float flush_interval: The number of seconds in between flushes of the events buffer. Decreasing
the flush interval means that the event buffer is less likely to reach capacity.
:param string stream_uri: The URL for the LaunchDarkly streaming events server. Most users should
use the default value.
:param bool stream: Whether or not the streaming API should be used to receive flag updates. By
Expand All @@ -66,10 +71,17 @@ def __init__(self,
private, not just the attributes specified in `private_attribute_names`.
:param feature_store: A FeatureStore implementation
:type feature_store: FeatureStore
:param int user_keys_capacity: The number of user keys that the event processor can remember at any
one time, so that duplicate user details will not be sent in analytics events.
:param float user_keys_flush_interval: The interval in seconds at which the event processor will
reset its set of known user keys.
:param bool inline_users_in_events: Whether to include full user details in every analytics event.
By default, events will only include the user key, except for one "index" event that provides the
full details for the user.
:param feature_requester_class: A factory for a FeatureRequester implementation taking the sdk key and config
:type feature_requester_class: (str, Config, FeatureStore) -> FeatureRequester
:param event_consumer_class: A factory for an EventConsumer implementation taking the event queue, sdk key, and config
:type event_consumer_class: (queue.Queue, str, Config) -> EventConsumer
:param event_processor_class: A factory for an EventProcessor implementation taking the config
:type event_processor_class: (Config) -> EventProcessor
:param update_processor_class: A factory for an UpdateProcessor implementation taking the sdk key,
config, and FeatureStore implementation
"""
Expand All @@ -86,12 +98,12 @@ def __init__(self,
self.__poll_interval = max(poll_interval, 30)
self.__use_ldd = use_ldd
self.__feature_store = InMemoryFeatureStore() if not feature_store else feature_store
self.__event_consumer_class = EventConsumerImpl if not event_consumer_class else event_consumer_class
self.__event_processor_class = DefaultEventProcessor if not event_processor_class else event_processor_class
self.__feature_requester_class = feature_requester_class
self.__connect_timeout = connect_timeout
self.__read_timeout = read_timeout
self.__events_upload_max_batch_size = events_upload_max_batch_size
self.__events_max_pending = events_max_pending
self.__flush_interval = flush_interval
self.__verify_ssl = verify_ssl
self.__defaults = defaults
if offline is True:
Expand All @@ -100,6 +112,9 @@ def __init__(self,
self.__private_attribute_names = private_attribute_names
self.__all_attributes_private = all_attributes_private
self.__offline = offline
self.__user_keys_capacity = user_keys_capacity
self.__user_keys_flush_interval = user_keys_flush_interval
self.__inline_users_in_events = inline_users_in_events

@classmethod
def default(cls):
Expand All @@ -111,8 +126,8 @@ def copy_with_new_sdk_key(self, new_sdk_key):
events_uri=self.__events_uri,
connect_timeout=self.__connect_timeout,
read_timeout=self.__read_timeout,
events_upload_max_batch_size=self.__events_upload_max_batch_size,
events_max_pending=self.__events_max_pending,
flush_interval=self.__flush_interval,
stream_uri=self.__stream_uri,
stream=self.__stream,
verify_ssl=self.__verify_ssl,
Expand All @@ -123,10 +138,13 @@ def copy_with_new_sdk_key(self, new_sdk_key):
use_ldd=self.__use_ldd,
feature_store=self.__feature_store,
feature_requester_class=self.__feature_requester_class,
event_consumer_class=self.__event_consumer_class,
event_processor_class=self.__event_processor_class,
private_attribute_names=self.__private_attribute_names,
all_attributes_private=self.__all_attributes_private,
offline=self.__offline)
offline=self.__offline,
user_keys_capacity=self.__user_keys_capacity,
user_keys_flush_interval=self.__user_keys_flush_interval,
inline_users_in_events=self.__inline_users_in_events)

def get_default(self, key, default):
return default if key not in self.__defaults else self.__defaults[key]
Expand Down Expand Up @@ -176,8 +194,8 @@ def feature_store(self):
return self.__feature_store

@property
def event_consumer_class(self):
return self.__event_consumer_class
def event_processor_class(self):
return self.__event_processor_class

@property
def feature_requester_class(self):
Expand All @@ -199,14 +217,14 @@ def events_enabled(self):
def send_events(self):
return self.__send_events

@property
def events_upload_max_batch_size(self):
return self.__events_upload_max_batch_size

@property
def events_max_pending(self):
return self.__events_max_pending

@property
def flush_interval(self):
return self.__flush_interval

@property
def verify_ssl(self):
return self.__verify_ssl
Expand All @@ -223,6 +241,18 @@ def all_attributes_private(self):
def offline(self):
return self.__offline

@property
def user_keys_capacity(self):
return self.__user_keys_capacity

@property
def user_keys_flush_interval(self):
return self.__user_keys_flush_interval

@property
def inline_users_in_events(self):
return self.__inline_users_in_events

def _validate(self):
if self.offline is False and self.sdk_key is None or self.sdk_key is '':
log.warn("Missing or blank sdk_key.")
Loading