Skip to content

Commit b4b0359

Browse files
authored
prepare 6.1.0 release (#90)
1 parent 3d9a264 commit b4b0359

13 files changed

+388
-217
lines changed

ldclient/client.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import hmac
55
import threading
66

7-
import requests
87
from builtins import object
98

109
from ldclient.config import Config as Config
@@ -42,7 +41,6 @@ def __init__(self, sdk_key=None, config=None, start_wait=5):
4241
self._config = config or Config.default()
4342
self._config._validate()
4443

45-
self._session = CacheControl(requests.Session())
4644
self._event_processor = None
4745
self._lock = Lock()
4846

ldclient/event_processor.py

Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import errno
66
import jsonpickle
77
from threading import Event, Lock, Thread
8+
import six
89
import time
10+
import urllib3
911

1012
# noinspection PyBroadException
1113
try:
@@ -14,19 +16,17 @@
1416
# noinspection PyUnresolvedReferences,PyPep8Naming
1517
import Queue as queue
1618

17-
import requests
18-
from requests.packages.urllib3.exceptions import ProtocolError
19-
20-
import six
21-
2219
from ldclient.event_summarizer import EventSummarizer
2320
from ldclient.fixed_thread_pool import FixedThreadPool
2421
from ldclient.lru_cache import SimpleLRUCache
2522
from ldclient.user_filter import UserFilter
2623
from ldclient.interfaces import EventProcessor
2724
from ldclient.repeating_timer import RepeatingTimer
25+
from ldclient.util import UnsuccessfulResponseException
2826
from ldclient.util import _headers
27+
from ldclient.util import create_http_pool_manager
2928
from ldclient.util import log
29+
from ldclient.util import http_error_message, is_http_error_recoverable, throw_if_unsuccessful_response
3030

3131

3232
__MAX_FLUSH_THREADS__ = 5
@@ -144,8 +144,8 @@ def make_summary_event(self, summary):
144144

145145

146146
class EventPayloadSendTask(object):
147-
def __init__(self, session, config, formatter, payload, response_fn):
148-
self._session = session
147+
def __init__(self, http, config, formatter, payload, response_fn):
148+
self._http = http
149149
self._config = config
150150
self._formatter = formatter
151151
self._payload = payload
@@ -154,43 +154,30 @@ def __init__(self, session, config, formatter, payload, response_fn):
154154
def run(self):
155155
try:
156156
output_events = self._formatter.make_output_events(self._payload.events, self._payload.summary)
157-
resp = self._do_send(output_events, True)
158-
if resp is not None:
159-
self._response_fn(resp)
157+
resp = self._do_send(output_events)
160158
except Exception:
161159
log.warning(
162160
'Unhandled exception in event processor. Analytics events were not processed.',
163161
exc_info=True)
164162

165-
def _do_send(self, output_events, should_retry):
163+
def _do_send(self, output_events):
166164
# noinspection PyBroadException
167165
try:
168166
json_body = jsonpickle.encode(output_events, unpicklable=False)
169167
log.debug('Sending events payload: ' + json_body)
170168
hdrs = _headers(self._config.sdk_key)
171169
hdrs['X-LaunchDarkly-Event-Schema'] = str(__CURRENT_EVENT_SCHEMA__)
172170
uri = self._config.events_uri
173-
r = self._session.post(uri,
171+
r = self._http.request('POST', uri,
174172
headers=hdrs,
175-
timeout=(self._config.connect_timeout, self._config.read_timeout),
176-
data=json_body)
177-
r.raise_for_status()
173+
timeout=urllib3.Timeout(connect=self._config.connect_timeout, read=self._config.read_timeout),
174+
body=json_body,
175+
retries=1)
176+
self._response_fn(r)
178177
return r
179-
except ProtocolError as e:
180-
if e.args is not None and len(e.args) > 1 and e.args[1] is not None:
181-
inner = e.args[1]
182-
if inner.errno is not None and inner.errno == errno.ECONNRESET and should_retry:
183-
log.warning(
184-
'ProtocolError exception caught while sending events. Retrying.')
185-
self._do_send(output_events, False)
186-
else:
187-
log.warning(
188-
'Unhandled exception in event processor. Analytics events were not processed.',
189-
exc_info=True)
190-
except Exception:
178+
except Exception as e:
191179
log.warning(
192-
'Unhandled exception in event processor. Analytics events were not processed.',
193-
exc_info=True)
180+
'Unhandled exception in event processor. Analytics events were not processed. [%s]', e)
194181

195182

196183
FlushPayload = namedtuple('FlushPayload', ['events', 'summary'])
@@ -224,11 +211,11 @@ def clear(self):
224211

225212

226213
class EventDispatcher(object):
227-
def __init__(self, queue, config, session):
214+
def __init__(self, queue, config, http_client):
228215
self._queue = queue
229216
self._config = config
230-
self._session = requests.Session() if session is None else session
231-
self._close_session = (session is None) # so we know whether to close it later
217+
self._http = create_http_pool_manager(num_pools=1, verify_ssl=config.verify_ssl) if http_client is None else http_client
218+
self._close_http = (http_client is None) # so we know whether to close it later
232219
self._disabled = False
233220
self._buffer = EventBuffer(config.events_max_pending)
234221
self._user_keys = SimpleLRUCache(config.user_keys_capacity)
@@ -261,7 +248,6 @@ def _run_main_loop(self):
261248
return
262249
except Exception:
263250
log.error('Unhandled exception in event processor', exc_info=True)
264-
self._session.close()
265251

266252
def _process_event(self, event):
267253
if self._disabled:
@@ -320,7 +306,7 @@ def _trigger_flush(self):
320306
return
321307
payload = self._buffer.get_payload()
322308
if len(payload.events) > 0 or len(payload.summary.counters) > 0:
323-
task = EventPayloadSendTask(self._session, self._config, self._formatter, payload,
309+
task = EventPayloadSendTask(self._http, self._config, self._formatter, payload,
324310
self._handle_response)
325311
if self._flush_workers.execute(task.run):
326312
# The events have been handed off to a flush worker; clear them from our buffer.
@@ -330,34 +316,35 @@ def _trigger_flush(self):
330316
pass
331317

332318
def _handle_response(self, r):
333-
server_date_str = r.headers.get('Date')
319+
server_date_str = r.getheader('Date')
334320
if server_date_str is not None:
335321
server_date = parsedate(server_date_str)
336322
if server_date is not None:
337323
timestamp = int(time.mktime(server_date) * 1000)
338324
self._last_known_past_time = timestamp
339-
if r.status_code == 401:
340-
log.error('Received 401 error, no further events will be posted since SDK key is invalid')
341-
self._disabled = True
342-
return
325+
if r.status > 299:
326+
log.error(http_error_message(r.status, "event delivery", "some events were dropped"))
327+
if not is_http_error_recoverable(r.status):
328+
self._disabled = True
329+
return
343330

344331
def _do_shutdown(self):
345332
self._flush_workers.stop()
346333
self._flush_workers.wait()
347-
if self._close_session:
348-
self._session.close()
334+
if self._close_http:
335+
self._http.clear()
349336

350337

351338
class DefaultEventProcessor(EventProcessor):
352-
def __init__(self, config, session=None):
339+
def __init__(self, config, http=None):
353340
self._queue = queue.Queue(config.events_max_pending)
354341
self._flush_timer = RepeatingTimer(config.flush_interval, self.flush)
355342
self._users_flush_timer = RepeatingTimer(config.user_keys_flush_interval, self._flush_users)
356343
self._flush_timer.start()
357344
self._users_flush_timer.start()
358345
self._close_lock = Lock()
359346
self._closed = False
360-
EventDispatcher(self._queue, config, session)
347+
EventDispatcher(self._queue, config, http)
361348

362349
def send_event(self, event):
363350
event['creationDate'] = int(time.time() * 1000)

ldclient/feature_requester.py

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,61 @@
11
from __future__ import absolute_import
22

3-
import requests
4-
from cachecontrol import CacheControl
3+
from collections import namedtuple
4+
import json
5+
import urllib3
56

67
from ldclient.interfaces import FeatureRequester
8+
from ldclient.util import UnsuccessfulResponseException
79
from ldclient.util import _headers
10+
from ldclient.util import create_http_pool_manager
811
from ldclient.util import log
12+
from ldclient.util import throw_if_unsuccessful_response
913
from ldclient.versioned_data_kind import FEATURES, SEGMENTS
1014

1115

1216
LATEST_ALL_URI = '/sdk/latest-all'
1317

1418

19+
CacheEntry = namedtuple('CacheEntry', ['data', 'etag'])
20+
21+
1522
class FeatureRequesterImpl(FeatureRequester):
1623
def __init__(self, config):
17-
self._session_cache = CacheControl(requests.Session())
18-
self._session_no_cache = requests.Session()
24+
self._cache = dict()
25+
self._http = create_http_pool_manager(num_pools=1, verify_ssl=config.verify_ssl)
1926
self._config = config
2027

2128
def get_all_data(self):
22-
hdrs = _headers(self._config.sdk_key)
23-
uri = self._config.base_uri + LATEST_ALL_URI
24-
r = self._session_cache.get(uri,
25-
headers=hdrs,
26-
timeout=(
27-
self._config.connect_timeout,
28-
self._config.read_timeout))
29-
r.raise_for_status()
30-
all_data = r.json()
31-
log.debug("Get All flags response status:[%d] From cache?[%s] ETag:[%s]",
32-
r.status_code, r.from_cache, r.headers.get('ETag'))
29+
all_data = self._do_request(self._config.base_uri + LATEST_ALL_URI, True)
3330
return {
3431
FEATURES: all_data['flags'],
3532
SEGMENTS: all_data['segments']
3633
}
3734

3835
def get_one(self, kind, key):
36+
return self._do_request(kind.request_api_path + '/' + key, False)
37+
38+
def _do_request(self, uri, allow_cache):
3939
hdrs = _headers(self._config.sdk_key)
40-
path = kind.request_api_path + '/' + key
41-
uri = config.base_uri + path
42-
log.debug("Getting %s from %s using uri: %s", key, kind['namespace'], uri)
43-
r = self._session_no_cache.get(uri,
44-
headers=hdrs,
45-
timeout=(
46-
self._config.connect_timeout,
47-
self._config.read_timeout))
48-
r.raise_for_status()
49-
obj = r.json()
50-
log.debug("%s response status:[%d] key:[%s] version:[%d]",
51-
path, r.status_code, key, segment.get("version"))
52-
return obj
40+
if allow_cache:
41+
cache_entry = self._cache.get(uri)
42+
if cache_entry is not None:
43+
hdrs['If-None-Match'] = cache_entry.etag
44+
r = self._http.request('GET', uri,
45+
headers=hdrs,
46+
timeout=urllib3.Timeout(connect=self._config.connect_timeout, read=self._config.read_timeout),
47+
retries=1)
48+
throw_if_unsuccessful_response(r)
49+
if r.status == 304 and cache_entry is not None:
50+
data = cache_entry.data
51+
etag = cache_entry.etag
52+
from_cache = True
53+
else:
54+
data = json.loads(r.data.decode('UTF-8'))
55+
etag = r.getheader('ETag')
56+
from_cache = False
57+
if allow_cache and etag is not None:
58+
self._cache[uri] = CacheEntry(data=data, etag=etag)
59+
log.debug("%s response status:[%d] From cache? [%s] ETag:[%s]",
60+
uri, r.status, from_cache, etag)
61+
return data

ldclient/polling.py

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

33
from ldclient.interfaces import UpdateProcessor
44
from ldclient.util import log
5-
from requests import HTTPError
5+
from ldclient.util import UnsuccessfulResponseException, http_error_message, is_http_error_recoverable
6+
67
import time
78

89

@@ -28,15 +29,15 @@ def run(self):
2829
if not self._ready.is_set() is True and self._store.initialized is True:
2930
log.info("PollingUpdateProcessor initialized ok")
3031
self._ready.set()
31-
except HTTPError as e:
32-
log.error('Received unexpected status code %d from polling request' % e.response.status_code)
33-
if e.response.status_code == 401:
34-
log.error('Received 401 error, no further polling requests will be made since SDK key is invalid')
32+
except UnsuccessfulResponseException as e:
33+
log.error(http_error_message(e.status, "polling request"))
34+
if not is_http_error_recoverable(e.status):
35+
self._ready.set() # if client is initializing, make it stop waiting; has no effect if already inited
3536
self.stop()
3637
break
37-
except Exception:
38+
except Exception as e:
3839
log.exception(
39-
'Error: Exception encountered when updating flags.')
40+
'Error: Exception encountered when updating flags. %s' % e)
4041

4142
elapsed = time.time() - start_time
4243
if elapsed < self._config.poll_interval:

ldclient/sse_client.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
import six
88

9-
import requests
9+
import urllib3
10+
11+
from ldclient.util import create_http_pool_manager
12+
from ldclient.util import throw_if_unsuccessful_response
1013

1114
# Inspired by: https://bitbucket.org/btubbs/sseclient/src/a47a380a3d7182a205c0f1d5eb470013ce796b4d/sseclient.py?at=default&fileviewer=file-view-default
1215

@@ -16,18 +19,19 @@
1619

1720

1821
class SSEClient(object):
19-
def __init__(self, url, last_id=None, retry=3000, connect_timeout=10, read_timeout=300, chunk_size=10000, session=None, **kwargs):
22+
def __init__(self, url, last_id=None, retry=3000, connect_timeout=10, read_timeout=300, chunk_size=10000,
23+
verify_ssl=False, http=None, **kwargs):
2024
self.url = url
2125
self.last_id = last_id
2226
self.retry = retry
2327
self._connect_timeout = connect_timeout
2428
self._read_timeout = read_timeout
2529
self._chunk_size = chunk_size
2630

27-
# Optional support for passing in a requests.Session()
28-
self.session = session
31+
# Optional support for passing in an HTTP client
32+
self.http = create_http_pool_manager(num_pools=1, verify_ssl=verify_ssl)
2933

30-
# Any extra kwargs will be fed into the requests.get call later.
34+
# Any extra kwargs will be fed into the request call later.
3135
self.requests_kwargs = kwargs
3236

3337
# The SSE spec requires making requests with Cache-Control: nocache
@@ -48,21 +52,22 @@ def _connect(self):
4852
self.requests_kwargs['headers']['Last-Event-ID'] = self.last_id
4953

5054
# Use session if set. Otherwise fall back to requests module.
51-
requester = self.session or requests
52-
self.resp = requester.get(
55+
self.resp = self.http.request(
56+
'GET',
5357
self.url,
54-
stream=True,
55-
timeout=(self._connect_timeout, self._read_timeout),
58+
timeout=urllib3.Timeout(connect=self._connect_timeout, read=self._read_timeout),
59+
preload_content=False,
60+
retries=0, # caller is responsible for implementing appropriate retry semantics, e.g. backoff
5661
**self.requests_kwargs)
5762

5863
# Raw readlines doesn't work because we may be missing newline characters until the next chunk
5964
# For some reason, we also need to specify a chunk size because stream=True doesn't seem to guarantee
6065
# that we get the newlines in a timeline manner
61-
self.resp_file = self.resp.iter_content(chunk_size=self._chunk_size, decode_unicode=True)
66+
self.resp_file = self.resp.stream(amt=self._chunk_size)
6267

6368
# TODO: Ensure we're handling redirects. Might also stick the 'origin'
6469
# attribute on Events like the Javascript spec requires.
65-
self.resp.raise_for_status()
70+
throw_if_unsuccessful_response(self.resp)
6671

6772
def _event_complete(self):
6873
return re.search(end_of_field, self.buf[len(self.buf)-self._chunk_size-10:]) is not None # Just search the last chunk plus a bit
@@ -77,8 +82,8 @@ def __next__(self):
7782
# There are some bad cases where we don't always get a line: https://github.com/requests/requests/pull/2431
7883
if not nextline:
7984
raise EOFError()
80-
self.buf += nextline
81-
except (StopIteration, requests.RequestException, EOFError) as e:
85+
self.buf += nextline.decode("utf-8")
86+
except (StopIteration, EOFError) as e:
8287
time.sleep(self.retry / 1000.0)
8388
self._connect()
8489

0 commit comments

Comments
 (0)