Skip to content
This repository was archived by the owner on Mar 24, 2021. It is now read-only.

Commit 1c4a8b0

Browse files
committed
Merge remote-tracking branch 'parsely/master' into feature/rdkafka_extension
The producer-futures feature was backed out of master, which means the expected interface for RdKafkaProducer._produce() has changed back, too. I've addressed all merge conflicts here - the change in the _produce() interface will be addressed in the next commit. * parsely/master: (26 commits) changelog updates for 2.0.3, dev version increment version Catch IOError in recvall_into util. Catch IOError during connection response. re-import weakref Revert 52ae7a1 Link #334 to README. drop autocommit logging to DEBUG level. fixes #337 update after socket error in offset manager discovery remove unused condition unconditionally update partition leaders on update Load all topic values on values method. fix typo causing interpreter error in reset_offsets` fix outdenting error clarify functools import catch all exceptions when removing from zookeeper be very specific about the error we expect producer: minimal changes for gc'ability balancedconsumer: minimal changes for gc'ability (RFC) add logging, fix some retry/reconnect/update logic in simpleconsumer ... Signed-off-by: Yung-Chin Oei <[email protected]> Conflicts: .travis.yml pykafka/simpleconsumer.py tests/pykafka/test_producer.py
2 parents b43d645 + 8ad614e commit 1c4a8b0

17 files changed

+310
-274
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ notifications:
2323
2424

2525
install:
26-
- pip install codecov kazoo tox testinstances futures
26+
- pip install codecov kazoo tox testinstances
2727
- wget https://github.com/edenhill/librdkafka/archive/0.8.6.tar.gz
2828
- tar -xzf 0.8.6.tar.gz
2929
- cd librdkafka-0.8.6/ && ./configure --prefix=$HOME

CHANGES.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,32 @@
11
Changelog
22
=========
33

4+
2.0.3 (2015-11-10)
5+
------------------
6+
7+
`Compare 2.0.3`_
8+
9+
.. _Compare 2.0.3: https://github.com/Parsely/pykafka/compare/2.0.2...bd844cd66e79b3e0a56dd92a2aae4579a9046e8e
10+
11+
Features
12+
********
13+
14+
* Raise exceptions from worker threads to the main thread in `BalancedConsumer`
15+
* Call `stop()` when `BalancedConsumer` is finalized to minimize zombie threads
16+
17+
Bug Fixes
18+
*********
19+
20+
* Use weak references in `BalancedConsumer` workers to avoid zombie threads creating
21+
memory leaks
22+
* Stabilize `BalancedConsumer.start()`
23+
* Fix a bug in `TopicDict.values()` causing topics to be listed as `None`
24+
* Handle `IOError` in `BrokerConnection` and `socket.recvall_into`
25+
* Unconditionally update partitions' leaders after metadata requests
26+
* Fix thread-related memory leaks in `Producer`
27+
* Handle connection errors during offset commits
28+
* Fix an interpreter error in `SimpleConsumer`
29+
430
2.0.2 (2015-10-29)
531
------------------
632

README.rst

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -58,44 +58,10 @@ producing messages.
5858

5959
.. sourcecode:: python
6060

61-
>>> with topic.get_sync_producer() as producer:
61+
>>> with topic.get_producer() as producer:
6262
... for i in range(4):
6363
... producer.produce('test message ' + i ** 2)
6464

65-
The example above would produce to kafka synchronously, that is, the call only
66-
returns after we have confirmation that the message made it to the cluster.
67-
68-
To achieve higher throughput however, we recommend using the ``Producer`` in
69-
asynchronous mode. In that configuration, ``produce()`` calls will return a
70-
``concurrent.futures.Future`` (`docs`_), which you may evaluate later (or, if
71-
reliable delivery is not a concern, you're free to discard it unevaluated).
72-
Here's a rough usage example:
73-
74-
.. sourcecode:: python
75-
76-
>>> with topic.get_producer() as producer:
77-
... count = 0
78-
... pending = []
79-
... while True:
80-
... count += 1
81-
... future = producer.produce('test message',
82-
... partition_key='{}'.format(count))
83-
... pending.append(future)
84-
... if count % 10**5 == 0: # adjust this or bring lots of RAM ;)
85-
... done, not_done = concurrent.futures.wait(pending,
86-
timeout=.001)
87-
... for future in done:
88-
... message_key = future.kafka_msg.partition_key
89-
... if future.exception() is not None:
90-
... print 'Failed to deliver message {}: {}'.format(
91-
... message_key, repr(future.exception()))
92-
... else:
93-
... print 'Successfully delivered message {}'.format(
94-
... message_key)
95-
... pending = list(not_done)
96-
97-
.. _docs: https://pythonhosted.org/futures/#future-objects
98-
9965
You can also consume messages from this topic using a `Consumer` instance.
10066

10167
.. sourcecode:: python
@@ -179,6 +145,12 @@ previous versions will always be available in this repo.
179145

180146
.. _PyPI package: https://pypi.python.org/pypi/samsa/0.3.11
181147

148+
pykafka or kafka-python?
149+
------------------------
150+
151+
These are two different projects.
152+
See `the discussion here <https://github.com/Parsely/pykafka/issues/334>`_.
153+
182154
Support
183155
-------
184156

pykafka/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .client import KafkaClient
88
from .balancedconsumer import BalancedConsumer
99

10-
__version__ = '2.0.3-dev'
10+
__version__ = '2.0.4-dev'
1111

1212

1313
__all__ = ["Broker", "SimpleConsumer", "Cluster", "Partition", "Producer",

pykafka/balancedconsumer.py

Lines changed: 79 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
limitations under the License.
1919
"""
2020
__all__ = ["BalancedConsumer"]
21+
import functools
2122
import itertools
2223
import logging
2324
import socket
25+
import sys
2426
import time
27+
import traceback
2528
from uuid import uuid4
2629
import weakref
2730

@@ -44,6 +47,18 @@
4447
log = logging.getLogger(__name__)
4548

4649

50+
def _catch_thread_exception(fn):
51+
"""Sets self._worker_exception when fn raises an exception"""
52+
def wrapped(self, *args, **kwargs):
53+
try:
54+
ret = fn(self, *args, **kwargs)
55+
except Exception:
56+
self._worker_exception = sys.exc_info()
57+
else:
58+
return ret
59+
return wrapped
60+
61+
4762
class BalancedConsumer():
4863
"""
4964
A self-balancing consumer for Kafka that uses ZooKeeper to communicate
@@ -173,6 +188,8 @@ def __init__(self,
173188
self._zookeeper_connection_timeout_ms = zookeeper_connection_timeout_ms
174189
self._reset_offset_on_start = reset_offset_on_start
175190
self._running = False
191+
self._worker_exception = None
192+
self._worker_trace_logged = False
176193

177194
if not rdkafka and use_rdkafka:
178195
raise ImportError("use_rdkafka requires rdkafka to be installed")
@@ -200,6 +217,10 @@ def __init__(self,
200217
if auto_start is True:
201218
self.start()
202219

220+
def __del__(self):
221+
log.debug("Finalising {}".format(self))
222+
self.stop()
223+
203224
def __repr__(self):
204225
return "<{module}.{name} at {id_} (consumer_group={group})>".format(
205226
module=self.__class__.__module__,
@@ -208,15 +229,33 @@ def __repr__(self):
208229
group=self._consumer_group
209230
)
210231

232+
def _raise_worker_exceptions(self):
233+
"""Raises exceptions encountered on worker threads"""
234+
if self._worker_exception is not None:
235+
_, ex, tb = self._worker_exception
236+
if not self._worker_trace_logged:
237+
self._worker_trace_logged = True
238+
log.error("Exception encountered in worker thread:\n%s",
239+
"".join(traceback.format_tb(tb)))
240+
raise ex
241+
211242
def _setup_checker_worker(self):
212243
"""Start the zookeeper partition checker thread"""
244+
self = weakref.proxy(self)
245+
213246
def checker():
214247
while True:
215-
if not self._running:
248+
try:
249+
if not self._running:
250+
break
251+
time.sleep(120)
252+
if not self._check_held_partitions():
253+
self._rebalance()
254+
except Exception as e:
255+
if not isinstance(e, ReferenceError):
256+
# surface all exceptions to the main thread
257+
self._worker_exception = sys.exc_info()
216258
break
217-
time.sleep(120)
218-
if not self._check_held_partitions():
219-
self._rebalance()
220259
log.debug("Checker thread exiting")
221260
log.debug("Starting checker thread")
222261
return self._cluster.handler.spawn(checker)
@@ -242,17 +281,21 @@ def held_offsets(self):
242281

243282
def start(self):
244283
"""Open connections and join a cluster."""
245-
if self._zookeeper is None:
246-
self._setup_zookeeper(self._zookeeper_connect,
247-
self._zookeeper_connection_timeout_ms)
248-
self._zookeeper.ensure_path(self._topic_path)
249-
self._zk_state_listener = self._get_zk_state_listener()
250-
self._zookeeper.add_listener(self._zk_state_listener)
251-
self._add_self()
252-
self._running = True
253-
self._set_watches()
254-
self._rebalance()
255-
self._setup_checker_worker()
284+
try:
285+
if self._zookeeper is None:
286+
self._setup_zookeeper(self._zookeeper_connect,
287+
self._zookeeper_connection_timeout_ms)
288+
self._zookeeper.ensure_path(self._topic_path)
289+
self._zk_state_listener = self._get_zk_state_listener()
290+
self._zookeeper.add_listener(self._zk_state_listener)
291+
self._add_self()
292+
self._running = True
293+
self._set_watches()
294+
self._rebalance()
295+
self._setup_checker_worker()
296+
except Exception:
297+
log.error("Stopping consumer in response to error")
298+
self.stop()
256299

257300
def stop(self):
258301
"""Close the zookeeper connection and stop consuming.
@@ -273,10 +316,13 @@ def stop(self):
273316
self._zookeeper.stop()
274317
else:
275318
self._remove_partitions(self._get_held_partitions())
276-
self._zookeeper.delete(self._path_self)
277-
# additionally we'd want to remove watches here, but there are no
278-
# facilities for that in ChildrenWatch - as a workaround we check
279-
# self._running in the watcher callbacks (see further down)
319+
try:
320+
self._zookeeper.delete(self._path_self)
321+
except:
322+
pass
323+
# additionally we'd want to remove watches here, but there are no
324+
# facilities for that in ChildrenWatch - as a workaround we check
325+
# self._running in the watcher callbacks (see further down)
280326

281327
def _setup_zookeeper(self, zookeeper_connect, timeout):
282328
"""Open a connection to a ZooKeeper host.
@@ -411,13 +457,18 @@ def _set_watches(self):
411457
consumer group remains up-to-date with the current state of the
412458
cluster.
413459
"""
460+
proxy = weakref.proxy(self)
461+
_brokers_changed = functools.partial(BalancedConsumer._brokers_changed, proxy)
462+
_topics_changed = functools.partial(BalancedConsumer._topics_changed, proxy)
463+
_consumers_changed = functools.partial(BalancedConsumer._consumers_changed, proxy)
464+
414465
self._setting_watches = True
415466
# Set all our watches and then rebalance
416467
broker_path = '/brokers/ids'
417468
try:
418469
self._broker_watcher = ChildrenWatch(
419470
self._zookeeper, broker_path,
420-
self._brokers_changed
471+
_brokers_changed
421472
)
422473
except NoNodeException:
423474
raise Exception(
@@ -428,12 +479,12 @@ def _set_watches(self):
428479
self._topics_watcher = ChildrenWatch(
429480
self._zookeeper,
430481
'/brokers/topics',
431-
self._topics_changed
482+
_topics_changed
432483
)
433484

434485
self._consumer_watcher = ChildrenWatch(
435486
self._zookeeper, self._consumer_id_path,
436-
self._consumers_changed
487+
_consumers_changed
437488
)
438489
self._setting_watches = False
439490

@@ -602,6 +653,7 @@ def listener(zk_state):
602653

603654
return listener
604655

656+
@_catch_thread_exception
605657
def _brokers_changed(self, brokers):
606658
if not self._running:
607659
return False # `False` tells ChildrenWatch to disable this watch
@@ -611,6 +663,7 @@ def _brokers_changed(self, brokers):
611663
self._consumer_id))
612664
self._rebalance()
613665

666+
@_catch_thread_exception
614667
def _consumers_changed(self, consumers):
615668
if not self._running:
616669
return False # `False` tells ChildrenWatch to disable this watch
@@ -620,6 +673,7 @@ def _consumers_changed(self, consumers):
620673
self._consumer_id))
621674
self._rebalance()
622675

676+
@_catch_thread_exception
623677
def _topics_changed(self, topics):
624678
if not self._running:
625679
return False # `False` tells ChildrenWatch to disable this watch
@@ -641,6 +695,7 @@ def reset_offsets(self, partition_offsets=None):
641695
:type partition_offsets: Iterable of
642696
(:class:`pykafka.partition.Partition`, int)
643697
"""
698+
self._raise_worker_exceptions()
644699
if not self._consumer:
645700
raise ConsumerStoppedException("Internal consumer is stopped")
646701
self._consumer.reset_offsets(partition_offsets=partition_offsets)
@@ -663,6 +718,7 @@ def consumer_timed_out():
663718
message = None
664719
self._last_message_time = time.time()
665720
while message is None and not consumer_timed_out():
721+
self._raise_worker_exceptions()
666722
try:
667723
message = self._consumer.consume(block=block)
668724
except ConsumerStoppedException:
@@ -690,4 +746,5 @@ def commit_offsets(self):
690746
691747
Uses the offset commit/fetch API
692748
"""
749+
self._raise_worker_exceptions()
693750
return self._consumer.commit_offsets()

pykafka/cluster.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def __init__(self, cluster, exclude_internal_topics, *args, **kwargs):
4444
self._cluster = weakref.ref(cluster)
4545
self._exclude_internal_topics = exclude_internal_topics
4646

47+
def values(self):
48+
return [self[key] for key in self]
49+
4750
def __getitem__(self, key):
4851
if self._should_exclude_topic(key):
4952
raise KeyError("You have configured KafkaClient/Cluster to hide "
@@ -259,10 +262,13 @@ def _update_brokers(self, broker_metadata):
259262
try:
260263
self._brokers[id_].connect()
261264
except socket.error:
262-
log.info('Failed to re-establish connection with broker id %s: %s:%s', id_, meta.host, meta.port)
265+
log.info('Failed to re-establish connection with broker id %s: %s:%s',
266+
id_, meta.host, meta.port)
263267
else:
264268
broker = self._brokers[id_]
265269
if meta.host == broker.host and meta.port == broker.port:
270+
log.info('Broker %s:%s metadata unchanged. Continuing.',
271+
broker.host, broker.port)
266272
continue # no changes
267273
# TODO: Can brokers update? Seems like a problem if so.
268274
# Figure out and implement update/disconnect/reconnect if
@@ -298,9 +304,10 @@ def get_offset_manager(self, consumer_group):
298304
if i == MAX_RETRIES - 1:
299305
raise
300306
except SocketDisconnectedError:
301-
raise KafkaException("Socket disconnected during offset manager "
302-
"discovery. This can happen when using PyKafka "
303-
"with a Kafka version lower than 0.8.2.")
307+
log.error("Socket disconnected during offset manager "
308+
"discovery. This can happen when using PyKafka "
309+
"with a Kafka version lower than 0.8.2.")
310+
self.update()
304311
else:
305312
coordinator = self.brokers.get(res.coordinator_id, None)
306313
if coordinator is None:

pykafka/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class Message(object):
3333
:ivar key: (optional) Message key
3434
:ivar offset: Message offset
3535
"""
36-
__slots__ = []
36+
pass
3737

3838

3939
class CompressionType(object):

pykafka/connection.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,11 @@ def response(self):
112112
size = bytes()
113113
expected_len = 4 # Size => int32
114114
while len(size) != expected_len:
115-
r = self._socket.recv(expected_len - len(size))
116-
if len(r) == 0:
115+
try:
116+
r = self._socket.recv(expected_len - len(size))
117+
except IOError:
118+
r = None
119+
if r is None or len(r) == 0:
117120
# Happens when broker has shut down
118121
self.disconnect()
119122
raise SocketDisconnectedError

0 commit comments

Comments
 (0)