def redis_get(): r = None try: r = yield self._get_connection() """ :type: RedisClient """ get_result = yield r.hget(self._features_key, key) if not get_result: log.warn("Didn't get response from redis for key: " + key + " Returning None.") defer.returnValue(None) f_json = get_result.get(key) if f_json is None or f_json is "": log.warn( "TwistedRedisFeatureStore: feature flag with key: " + key + " not found in Redis. Returning None.") defer.returnValue(None) f = json.loads(f_json.decode('utf-8')) if f.get('deleted', False) is True: log.warn( "TwistedRedisFeatureStore: get returned deleted flag from Redis. Returning None." ) defer.returnValue(None) self._cache[key] = f defer.returnValue(f) except Exception as e: log.error("Could not connect to Redis using url: " + self._url + " with error message: " + e.message) defer.returnValue(None) finally: if r: r.quit() defer.returnValue(None)
def run(self): if not self._running: log.info("Starting PollingUpdateProcessor with request interval: " + str(self._config.poll_interval)) self._running = True while self._running: start_time = time.time() try: all_data = self._requester.get_all_data() self._store.init(all_data) if not self._ready.is_set() is True and self._store.initialized is True: log.info("PollingUpdateProcessor initialized ok") self._ready.set() except UnsuccessfulResponseException as e: log.error(http_error_message(e.status, "polling request")) if not is_http_error_recoverable(e.status): self._ready.set() # if client is initializing, make it stop waiting; has no effect if already inited self.stop() break except Exception as e: log.exception( 'Error: Exception encountered when updating flags. %s' % e) elapsed = time.time() - start_time if elapsed < self._config.poll_interval: time.sleep(self._config.poll_interval - elapsed)
def redis_get_all(): r = None try: r = yield self._get_connection() """ :type: RedisClient """ all_features = yield r.hgetall(self._features_key) if all_features is None or all_features is "": log.warn( "TwistedRedisFeatureStore: call to get all flags returned no results. Returning None." ) defer.returnValue(None) results = {} for k, f_json in all_features.items() or {}: f = json.loads(f_json.decode('utf-8')) if 'deleted' in f and f['deleted'] is False: results[f['key']] = f defer.returnValue(results) except Exception as e: log.error("Could not connect to Redis using url: " + self._url + " with error message: " + e.message) defer.returnValue(None) finally: if r: r.quit() defer.returnValue(None)
def run(self): log.info("Starting StreamingUpdateProcessor connecting to uri: " + self._uri) self._running = True while self._running: try: messages = self._connect() for msg in messages: if not self._running: break message_ok = self.process_message(self._store, self._requester, msg) if message_ok is True and self._ready.is_set() is False: log.info("StreamingUpdateProcessor initialized ok.") self._ready.set() except HTTPError as e: log.error("Received unexpected status code %d for stream connection" % e.response.status_code) if e.response.status_code == 401: log.error("Received 401 error, no further streaming connection will be made since SDK key is invalid") self.stop() break else: log.warning("Restarting stream connection after one second.") except Exception: log.warning("Caught exception. Restarting stream connection after one second.", exc_info=True) time.sleep(1)
def run(self): if not self._running: log.info("Starting PollingUpdateProcessor with request interval: " + str(self._config.poll_interval)) self._running = True while self._running: start_time = time.time() try: all_data = self._requester.get_all_data() self._store.init(all_data) if not self._ready.is_set() is True and self._store.initialized is True: log.info("PollingUpdateProcessor initialized ok") self._ready.set() except HTTPError as e: log.error('Received unexpected status code %d from polling request' % e.response.status_code) if e.response.status_code == 401: log.error('Received 401 error, no further polling requests will be made since SDK key is invalid') self.stop() break except: log.exception( 'Error: Exception encountered when updating flags.') elapsed = time.time() - start_time if elapsed < self._config.poll_interval: time.sleep(self._config.poll_interval - elapsed)
def _run_main_loop(self): log.info("Starting event processor") while True: try: message = self._inbox.get(block=True) if message.type == 'event': self._process_event(message.param) elif message.type == 'flush': self._trigger_flush() elif message.type == 'flush_users': self._user_keys.clear() elif message.type == 'diagnostic': self._send_and_reset_diagnostics() elif message.type == 'test_sync': self._flush_workers.wait() if self._diagnostic_accumulator is not None: self._diagnostic_flush_workers.wait() message.param.set() elif message.type == 'stop': self._do_shutdown() message.param.set() return except Exception: log.error('Unhandled exception in event processor', exc_info=True)
def run(self): log.info("Starting MobileStreamingUpdateProcessor connecting to uri: " + self._uri) self._running = True while self._running: try: init_start = time.time() messages = self._connect() for msg in messages: if not self._running: break message_ok = self.process_message(self._store, self._requester, msg) if message_ok is True and self._ready.is_set() is False: log.info("MobileStreamingUpdateProcessor initialized ok.") init_duration = int((time.time() - init_start) * 1000) request_success.fire(request_type="ld:init", name="mobile", response_time=init_duration, response_length=0) self._ready.set() except UnsuccessfulResponseException as e: log.error(http_error_message(e.status, "stream connection")) init_duration = int((time.time() - init_start) * 1000) request_failure.fire(request_type="ld:init", name="mobile", response_time=init_duration, response_length=0, exception=e) if not is_http_error_recoverable(e.status): self._ready.set() # if client is initializing, make it stop waiting; has no effect if already inited self.stop() break except Exception as e: init_duration = int((time.time() - init_start) * 1000) request_failure.fire(request_type="ld:init", name="mobile", response_time=init_duration, response_length=0, exception=e) log.warning("Caught exception. Restarting stream connection after one second. %s" % e) # no stacktrace here because, for a typical connection error, it'll just be a lengthy tour of urllib3 internals time.sleep(1)
def cb(all_flags): try: return self._evaluate_multi(user, all_flags) except Exception as e: log.error("Exception caught in all_flags: " + e.message + " for user: " + str(user)) return {}
def __init__(self, sdk_key, config=None, start_wait=5): check_uwsgi() self._sdk_key = sdk_key self._config = config or Config.default() self._session = CacheControl(requests.Session()) self._queue = queue.Queue(self._config.events_max_pending) self._event_consumer = None self._lock = Lock() self._store = self._config.feature_store """ :type: FeatureStore """ if self._config.offline: self._config.events_enabled = False log.info("Started LaunchDarkly Client in offline mode") return if self._config.events_enabled: self._event_consumer = self._config.event_consumer_class( self._queue, self._sdk_key, self._config) self._event_consumer.start() if self._config.use_ldd: if self._store.__class__ == "RedisFeatureStore": log.info("Started LaunchDarkly Client in LDD mode") return log.error("LDD mode requires a RedisFeatureStore.") return if self._config.feature_requester_class: self._feature_requester = self._config.feature_requester_class( sdk_key, self._config) else: self._feature_requester = FeatureRequesterImpl(sdk_key, self._config) """ :type: FeatureRequester """ update_processor_ready = threading.Event() if self._config.update_processor_class: self._update_processor = self._config.update_processor_class( sdk_key, self._config, self._feature_requester, self._store, update_processor_ready) else: if self._config.stream: self._update_processor = StreamingUpdateProcessor( sdk_key, self._config, self._feature_requester, self._store, update_processor_ready) else: self._update_processor = PollingUpdateProcessor( sdk_key, self._config, self._feature_requester, self._store, update_processor_ready) """ :type: UpdateProcessor """ self._update_processor.start() log.info("Waiting up to " + str(start_wait) + " seconds for LaunchDarkly client to initialize...") update_processor_ready.wait(start_wait) if self._update_processor.initialized: log.info("Started LaunchDarkly Client: OK") else: log.info("Initialization timeout exceeded for LaunchDarkly Client. Feature Flags may not yet be available.")
def on_connect_error(self, ignored): """ :type ignored: twisted.python.Failure """ from twisted.internet import reactor ignored.printTraceback() log.error("error connecting to endpoint {}: {}".format( self.url, ignored.getTraceback())) reactor.callLater(self.on_error_retry, self.connect)
def _evaluate_internal(self, key, user, default, include_reasons_in_events): default = self._config.get_default(key, default) if self._config.offline: return EvaluationDetail(default, None, error_reason('CLIENT_NOT_READY')) if user is not None: self._sanitize_user(user) def send_event(value, variation=None, flag=None, reason=None): self._send_event({'kind': 'feature', 'key': key, 'user': user, 'value': value, 'variation': variation, 'default': default, 'version': flag.get('version') if flag else None, 'trackEvents': flag.get('trackEvents') if flag else None, 'debugEventsUntilDate': flag.get('debugEventsUntilDate') if flag else None, 'reason': reason if include_reasons_in_events else None}) if not self.is_initialized(): if self._store.initialized: log.warn("Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key) else: log.warn("Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: " + str(default) + " for feature key: " + key) reason = error_reason('CLIENT_NOT_READY') send_event(default, None, None, reason) return EvaluationDetail(default, None, reason) 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.") flag = self._store.get(FEATURES, key, lambda x: x) if not flag: reason = error_reason('FLAG_NOT_FOUND') send_event(default, None, None, reason) return EvaluationDetail(default, None, reason) else: if user is None or user.get('key') is None: reason = error_reason('USER_NOT_SPECIFIED') send_event(default, None, flag, reason) return EvaluationDetail(default, None, reason) try: result = evaluate(flag, user, self._store, include_reasons_in_events) for event in result.events or []: self._send_event(event) detail = result.detail if detail.is_default_value(): detail = EvaluationDetail(default, None, detail.reason) send_event(detail.value, detail.variation_index, flag, detail.reason) return detail except Exception as e: log.error("Unexpected error while evaluating feature flag \"%s\": %s" % (key, e)) log.debug(traceback.format_exc()) reason = error_reason('EXCEPTION') send_event(default, None, flag, reason) return EvaluationDetail(default, None, reason)
def on_response(self, response): from twisted.internet import reactor if response.code != 200: log.error("non 200 response received: %d" % response.code) reactor.callLater(self.on_error_retry, self.connect) else: finished = Deferred() protocol = EventSourceProtocol(self.on_event, finished) finished.addBoth(self.reconnect) response.deliverBody(protocol) return finished
def all_flags_state(self, user, **kwargs): """Returns an object that encapsulates the state of all feature flags for a given user, including the flag values and also metadata that can be used on the front end. This method does not send analytics events back to LaunchDarkly. :param dict user: the end user requesting the feature flags :param kwargs: optional parameters affecting how the state is computed: set `client_side_only=True` to limit it to only flags that are marked for use with the client-side SDK (by default, all flags are included); set `with_reasons=True` to include evaluation reasons in the state (see `variation_detail`) :return: a FeatureFlagsState object (will never be None; its 'valid' property will be False if the client is offline, has not been initialized, or the user is None or has no key) :rtype: FeatureFlagsState """ if self._config.offline: log.warn("all_flags_state() called, but client is in offline mode. Returning empty state") return FeatureFlagsState(False) if not self.is_initialized(): if self._store.initialized: log.warn("all_flags_state() called before client has finished initializing! Using last known values from feature store") else: log.warn("all_flags_state() called before client has finished initializing! Feature store unavailable - returning empty state") return FeatureFlagsState(False) if user is None or user.get('key') is None: log.warn("User or user key is None when calling all_flags_state(). Returning empty state.") return FeatureFlagsState(False) state = FeatureFlagsState(True) client_only = kwargs.get('client_side_only', False) with_reasons = kwargs.get('with_reasons', False) try: flags_map = self._store.all(FEATURES, lambda x: x) except Exception as e: log.error("Unable to read flags for all_flag_state: %s" % e) return FeatureFlagsState(False) for key, flag in flags_map.items(): if client_only and not flag.get('clientSide', False): continue try: detail = evaluate(flag, user, self._store, False).detail state.add_flag(flag, detail.value, detail.variation_index, detail.reason if with_reasons else None) except Exception as e: log.error("Error evaluating flag \"%s\" in all_flags_state: %s" % (key, e)) log.debug(traceback.format_exc()) reason = {'kind': 'ERROR', 'errorKind': 'EXCEPTION'} state.add_flag(flag, None, None, reason if with_reasons else None) return state
def _load_all(self): all_data = {FEATURES: {}, SEGMENTS: {}} for path in self._paths: try: self._load_file(path, all_data) except Exception as e: log.error('Unable to load flag data from "%s": %s' % (path, repr(e))) traceback.print_exc() return self._store.init(all_data) self._inited = True
def _handle_response(self, r): server_date_str = r.getheader('Date') if server_date_str is not None: server_date = parsedate(server_date_str) if server_date is not None: timestamp = int(time.mktime(server_date) * 1000) self._last_known_past_time = timestamp if r.status > 299: log.error(http_error_message(r.status, "event delivery", "some events were dropped")) if not is_http_error_recoverable(r.status): self._disabled = True return
def cb(flag): try: if not flag: log.warn("Feature Flag key: " + key + " not found in Feature Store. Returning default.") send_event(default) return default return self._evaluate_and_send_events(flag, user, default) except Exception as e: log.error("Exception caught in variation: " + e.message + " for flag key: " + key + " and user: " + str(user)) return default
def run(self): log.info("Starting StreamingUpdateProcessor connecting to uri: " + self._config.stream_uri) self._running = True hdrs = _stream_headers(self._sdk_key) uri = self._config.stream_uri while self._running: try: messages = SSEClient(uri, verify=self._config.verify_ssl, headers=hdrs) for msg in messages: if not self._running: break self.process_message(self._store, self._requester, msg, self._ready) except Exception as e: log.error("Could not connect to LaunchDarkly stream: " + str(e.message) + " waiting 1 second before trying again.") time.sleep(1)
def _run_main_loop(self): log.info("Starting event processor") while True: try: message = self._queue.get(block=True) if message.type == 'event': self._process_event(message.param) elif message.type == 'flush': self._trigger_flush() elif message.type == 'flush_users': self._user_keys.clear() elif message.type == 'test_sync': self._flush_workers.wait() message.param.set() elif message.type == 'stop': self._do_shutdown() message.param.set() return except Exception: log.error('Unhandled exception in event processor', exc_info=True)
def run(self): log.info("Starting StreamingUpdateProcessor connecting to uri: " + self._uri) self._running = True attempts = 0 while self._running: if attempts > 0: delay = self._retry_delay.next_retry_delay(time.time()) log.info("Will reconnect after delay of %fs" % delay) time.sleep(delay) attempts += 1 try: self._es_started = int(time.time() * 1000) messages = self._connect() for msg in messages: if not self._running: break self._retry_delay.set_good_since(time.time()) message_ok = self.process_message(self._store, msg) if message_ok: self._record_stream_init(False) self._es_started = None if message_ok is True and self._ready.is_set() is False: log.info("StreamingUpdateProcessor initialized ok.") self._ready.set() except UnsuccessfulResponseException as e: log.error(http_error_message(e.status, "stream connection")) self._record_stream_init(True) self._es_started = None if not is_http_error_recoverable(e.status): self._ready.set( ) # if client is initializing, make it stop waiting; has no effect if already inited self.stop() break except Exception as e: log.warning( "Unexpected error on stream connection: %s, will retry" % e) self._record_stream_init(True) self._es_started = None
def run(self): log.info("Starting StreamingUpdateProcessor connecting to uri: " + self._uri) self._running = True while self._running: try: messages = self._connect() for msg in messages: if not self._running: break message_ok = self.process_message(self._store, self._requester, msg) if message_ok is True and self._ready.is_set() is False: log.info("StreamingUpdateProcessor initialized ok.") self._ready.set() except UnsuccessfulResponseException as e: log.error(http_error_message(e.status, "stream connection")) if not is_http_error_recoverable(e.status): self._ready.set() # if client is initializing, make it stop waiting; has no effect if already inited self.stop() break except Exception as e: log.warning("Caught exception. Restarting stream connection after one second. %s" % e) # no stacktrace here because, for a typical connection error, it'll just be a lengthy tour of urllib3 internals time.sleep(1)
def run(self): if not self._running: log.info("Starting PollingUpdateProcessor with request interval: " + str(self._config.poll_interval)) self._running = True while self._running: start_time = time.time() try: all_data = self._requester.get_all_data() self._store.init(all_data) if not self._ready.is_set() is True and self._store.initialized is True: log.info("PollingUpdateProcessor initialized ok") self._ready.set() except UnsuccessfulResponseException as e: log.error(http_error_message(e.status, "polling request")) if not is_http_error_recoverable(e.status): self._ready.set() # if client is initializing, make it stop waiting; has no effect if already inited self.stop() except Exception as e: log.exception( 'Error: Exception encountered when updating flags. %s' % e) elapsed = time.time() - start_time if elapsed < self._config.poll_interval: time.sleep(self._config.poll_interval - elapsed)
def do_send(should_retry): # noinspection PyBroadException try: json_body = self._serializer.serialize_events(events) log.debug('Sending events payload: ' + json_body) hdrs = _headers(self._config.sdk_key) uri = self._config.events_uri r = self._session.post(uri, headers=hdrs, timeout=(self._config.connect_timeout, self._config.read_timeout), data=json_body) if r.status_code == 401: log.error( 'Received 401 error, no further events will be posted since SDK key is invalid' ) self.stop() return r.raise_for_status() except ProtocolError as e: if e.args is not None and len( e.args) > 1 and e.args[1] is not None: inner = e.args[1] if inner.errno is not None and inner.errno == errno.ECONNRESET and should_retry: log.warning( 'ProtocolError exception caught while sending events. Retrying.' ) do_send(False) else: log.warning( 'Unhandled exception in event consumer. Analytics events were not processed.', exc_info=True) except: log.warning( 'Unhandled exception in event consumer. Analytics events were not processed.', exc_info=True)
def run(self): log.info("Starting StreamingUpdateProcessor connecting to uri: " + self._uri) self._running = True while self._running: try: self._es_started = int(time.time() * 1000) messages = self._connect() for msg in messages: if not self._running: break message_ok = self.process_message(self._store, self._requester, msg) if message_ok: self._record_stream_init(False) self._es_started = None if message_ok is True and self._ready.is_set() is False: log.info("StreamingUpdateProcessor initialized ok.") self._ready.set() except UnsuccessfulResponseException as e: log.error(http_error_message(e.status, "stream connection")) self._record_stream_init(True) self._es_started = None if not is_http_error_recoverable(e.status): self._ready.set( ) # if client is initializing, make it stop waiting; has no effect if already inited self.stop() break except Exception as e: log.warning( "Caught exception. Restarting stream connection after one second. %s" % e) self._record_stream_init(True) self._es_started = None # no stacktrace here because, for a typical connection error, it'll just be a lengthy tour of urllib3 internals time.sleep(1)
def all_flags_state(self, user, **kwargs): """Returns an object that encapsulates the state of all feature flags for a given user, including the flag values and also metadata that can be used on the front end. See the JavaScript SDK Reference Guide on `Bootstrapping <https://docs.launchdarkly.com/docs/js-sdk-reference#section-bootstrapping>`_. This method does not send analytics events back to LaunchDarkly. :param dict user: the end user requesting the feature flags :param kwargs: optional parameters affecting how the state is computed - see below :Keyword Arguments: * **client_side_only** (*boolean*) -- set to True to limit it to only flags that are marked for use with the client-side SDK (by default, all flags are included) * **with_reasons** (*boolean*) -- set to True to include evaluation reasons in the state (see :func:`variation_detail()`) * **details_only_for_tracked_flags** (*boolean*) -- set to True to omit any metadata that is normally only used for event generation, such as flag versions and evaluation reasons, unless the flag has event tracking or debugging turned on :return: a FeatureFlagsState object (will never be None; its ``valid`` property will be False if the client is offline, has not been initialized, or the user is None or has no key) :rtype: FeatureFlagsState """ if self._config.offline: log.warn( "all_flags_state() called, but client is in offline mode. Returning empty state" ) return FeatureFlagsState(False) if not self.is_initialized(): if self._store.initialized: log.warn( "all_flags_state() called before client has finished initializing! Using last known values from feature store" ) else: log.warn( "all_flags_state() called before client has finished initializing! Feature store unavailable - returning empty state" ) return FeatureFlagsState(False) if user is None or user.get('key') is None: log.warn( "User or user key is None when calling all_flags_state(). Returning empty state." ) return FeatureFlagsState(False) state = FeatureFlagsState(True) client_only = kwargs.get('client_side_only', False) with_reasons = kwargs.get('with_reasons', False) details_only_if_tracked = kwargs.get('details_only_for_tracked_flags', False) try: flags_map = self._store.all(FEATURES, lambda x: x) if flags_map is None: raise ValueError("feature store error") except Exception as e: log.error("Unable to read flags for all_flag_state: %s" % repr(e)) return FeatureFlagsState(False) for key, flag in flags_map.items(): if client_only and not flag.get('clientSide', False): continue try: detail = evaluate(flag, user, self._store, False).detail state.add_flag(flag, detail.value, detail.variation_index, detail.reason if with_reasons else None, details_only_if_tracked) except Exception as e: log.error( "Error evaluating flag \"%s\" in all_flags_state: %s" % (key, repr(e))) log.debug(traceback.format_exc()) reason = {'kind': 'ERROR', 'errorKind': 'EXCEPTION'} state.add_flag(flag, None, None, reason if with_reasons else None, details_only_if_tracked) return state
def _evaluate_internal(self, key, user, default, event_factory): default = self._config.get_default(key, default) if self._config.offline: return EvaluationDetail(default, None, error_reason('CLIENT_NOT_READY')) if not self.is_initialized(): if self._store.initialized: log.warning( "Feature Flag evaluation attempted before client has initialized - using last known values from feature store for feature key: " + key) else: log.warning( "Feature Flag evaluation attempted before client has initialized! Feature store unavailable - returning default: " + str(default) + " for feature key: " + key) reason = error_reason('CLIENT_NOT_READY') self._send_event( event_factory.new_unknown_flag_event( key, user, default, reason)) return EvaluationDetail(default, None, reason) if user is not None and user.get('key', "") == "": log.warning( "User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly." ) try: flag = self._store.get(FEATURES, key, lambda x: x) except Exception as e: log.error( "Unexpected error while retrieving feature flag \"%s\": %s" % (key, repr(e))) log.debug(traceback.format_exc()) reason = error_reason('EXCEPTION') self._send_event( event_factory.new_unknown_flag_event(key, user, default, reason)) return EvaluationDetail(default, None, reason) if not flag: reason = error_reason('FLAG_NOT_FOUND') self._send_event( event_factory.new_unknown_flag_event(key, user, default, reason)) return EvaluationDetail(default, None, reason) else: if user is None or user.get('key') is None: reason = error_reason('USER_NOT_SPECIFIED') self._send_event( event_factory.new_default_event(flag, user, default, reason)) return EvaluationDetail(default, None, reason) try: result = evaluate(flag, user, self._store, event_factory) for event in result.events or []: self._send_event(event) detail = result.detail if detail.is_default_value(): detail = EvaluationDetail(default, None, detail.reason) self._send_event( event_factory.new_eval_event(flag, user, detail, default)) return detail except Exception as e: log.error( "Unexpected error while evaluating feature flag \"%s\": %s" % (key, repr(e))) log.debug(traceback.format_exc()) reason = error_reason('EXCEPTION') self._send_event( event_factory.new_default_event(flag, user, default, reason)) return EvaluationDetail(default, None, reason)
def log_backoff_message(props): log.error("Streaming connection failed, will attempt to restart") log.info("Will reconnect after delay of %fs", props['wait'])