Skip to content

Commit e3fdf70

Browse files
author
Joe Gershenson
committed
Join daemon thread on interpreter exit
This allows the message sending queue to clean up nicely and prevents an error as the interpreter is destroyed. If you are exiting intentionally you should still call flush() to ensure all your messages are delivered -- this doesn't change that behavior. We also change the blocking semantics on the delivery thread here to deliver messages in a more efficient manner. Fixes #46. Fixes #69.
1 parent 7c32b91 commit e3fdf70

2 files changed

Lines changed: 20 additions & 21 deletions

File tree

analytics/client.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from uuid import uuid4
33
import logging
44
import numbers
5+
import atexit
56

67
from dateutil.tz import tzutc
78
from six import string_types
@@ -34,6 +35,12 @@ def __init__(self, write_key=None, debug=False, max_queue_size=10000,
3435
self.debug = debug
3536
self.send = send
3637

38+
# On program exit, allow the consumer thread to exit cleanly.
39+
# This prevents exceptions and a messy shutdown when the interpreter is
40+
# destroyed before the daemon thread finishes execution. However, it
41+
# is *not* the same as flushing the queue! To guarantee all
42+
atexit.register(self.join)
43+
3744
if debug:
3845
self.log.setLevel(logging.DEBUG)
3946

@@ -215,7 +222,8 @@ def flush(self):
215222
queue = self.queue
216223
size = queue.qsize()
217224
queue.join()
218-
self.log.debug('successfully flushed %s items.', size)
225+
# Note that this message may not be precise, because of threading.
226+
self.log.debug('successfully flushed about %s items.', size)
219227

220228
def join(self):
221229
"""Ends the consumer thread once the queue is empty. Blocks execution until finished"""

analytics/consumer.py

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
from threading import Thread
21
import logging
2+
from threading import Thread
33

44
from analytics.version import VERSION
55
from analytics.request import post
66

7+
try:
8+
from queue import Empty
9+
except:
10+
from Queue import Empty
711

812
class Consumer(Thread):
913
"""Consumes the messages from the client's queue."""
@@ -22,7 +26,6 @@ def __init__(self, queue, write_key, upload_size=100, on_error=None):
2226
def run(self):
2327
"""Runs the consumer."""
2428
self.log.debug('consumer is running...')
25-
2629
self.running = True
2730
while self.running:
2831
self.upload()
@@ -49,37 +52,25 @@ def upload(self):
4952
if self.on_error:
5053
self.on_error(e, batch)
5154
finally:
52-
# cleanup
55+
# mark items as acknowledged from queue
5356
for item in batch:
5457
self.queue.task_done()
55-
5658
return success
5759

5860
def next(self):
5961
"""Return the next batch of items to upload."""
6062
queue = self.queue
6163
items = []
62-
item = self.next_item()
63-
if item is None:
64-
return items
6564

66-
items.append(item)
67-
while len(items) < self.upload_size and not queue.empty():
68-
item = self.next_item()
69-
if item:
65+
while len(items) < self.upload_size or self.queue.empty():
66+
try:
67+
item = queue.get(block=True, timeout=0.5)
7068
items.append(item)
69+
except Empty:
70+
break
7171

7272
return items
7373

74-
def next_item(self):
75-
"""Get a single item from the queue."""
76-
queue = self.queue
77-
try:
78-
item = queue.get(block=True, timeout=5)
79-
return item
80-
except Exception:
81-
return None
82-
8374
def request(self, batch, attempt=0):
8475
"""Attempt to upload the batch and retry before raising an error """
8576
try:

0 commit comments

Comments
 (0)