Skip to content

Commit 44f8b69

Browse files
committed
Add documentation on finding memory leaks with guppy, save original tracebacks when retrying
1 parent 1b9ee7d commit 44f8b69

3 files changed

Lines changed: 77 additions & 1 deletion

File tree

docs/memory-leaks.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,66 @@ When a worker has a steadily growing memory usage, here are the steps to find th
1010
* Start a dedicated worker with ```--trace_memory --greenlets 1``` on the same queue : This will start a worker doing one job at a time with memory profiling enabled. After each job you should see a report of leaked object types.
1111
* Find the most unique type in the list (usually not 'list' or 'dict') and restart the worker with ```--trace_memory --greenlets 1 --trace_memory_type=XXX --trace_memory_output_dir=memdbg``` (after creating the directory memdbg).
1212
* There you will find a graph for each task generated by [objgraph](https://mg.pov.lt/objgraph/) which is incredibly helpful to track down the leak.
13+
14+
# Using guppy
15+
16+
If you want to get an interactive debugging session to deal with high memory usage, you can use [guppy](http://guppy-pe.sourceforge.net/). Here is how:
17+
18+
First, initialize a REPL with MRQ configured and guppy loaded:
19+
20+
```
21+
$ pip install guppy
22+
$ python
23+
>>> from mrq import config
24+
>>> from mrq.context import set_current_config, run_task
25+
>>> set_current_config(config.get_config(sources=("file", "env"), config_type="run"))
26+
>>> from guppy import hpy
27+
>>> hp = hpy()
28+
```
29+
30+
Then, wrap your memory-intensive task or code around guppy calls
31+
32+
```
33+
>>> hp.setrelheap() # Used as reference point for memory usage
34+
>>> run_task("tasks.your.MemoryHungryTask", {"a": 1, "b": 2})
35+
>>> h = hp.heap()
36+
```
37+
38+
At this point `h` should contain all the infos you need. You can view an extended debugging session [here](http://smira.ru/wp-content/uploads/2011/08/heapy.html).
39+
40+
```
41+
>>> h
42+
Partition of a set of 300643 objects. Total size = 41626536 bytes.
43+
Index Count % Size % Cumulative % Kind (class / dict of class)
44+
0 130043 43 15682088 38 15682088 38 str
45+
1 76123 25 6978416 17 22660504 54 tuple
46+
2 1015 0 2794024 7 25454528 61 dict of module
47+
3 20181 7 2583168 6 28037696 67 types.CodeType
48+
4 20610 7 2473200 6 30510896 73 function
49+
5 2321 1 2095216 5 32606112 78 type
50+
6 2319 1 2045160 5 34651272 83 dict of type
51+
7 1277 0 1162808 3 35814080 86 dict (no owner)
52+
8 2890 1 918352 2 36732432 88 unicode
53+
9 494 0 440912 1 37173344 89 dict of class
54+
```
55+
56+
So our task added 41.6M of RAM to the current process. Let's see where it comes from, starting by these 15M of strings:
57+
58+
```
59+
>>> h[0].byvia
60+
Partition of a set of 130043 objects. Total size = 15682088 bytes.
61+
Index Count % Size % Cumulative % Referred Via:
62+
0 8208 6 3890208 25 3890208 25 '.func_doc', '[0]'
63+
1 20065 15 3239664 21 7129872 45 '.co_code'
64+
2 16673 13 1706224 11 8836096 56 '.co_filename'
65+
3 2398 2 1606864 10 10442960 67 "['__doc__']"
66+
4 19810 15 1109640 7 11552600 74 '.co_lnotab'
67+
5 419 0 308392 2 11860992 76 '.func_doc'
68+
6 4311 3 285232 2 12146224 77 '[1]'
69+
7 2788 2 167616 1 12313840 79 '[2]'
70+
8 2153 2 129136 1 12442976 79 '[3]'
71+
9 1006 1 109560 1 12552536 80 "['__file__']"
72+
<21212 more rows. Type e.g. '_.more' to view.>
73+
```
74+
75+
Here, it seems that surprisingly, most of the strings are actually docstrings. This can happen if you work with large Python modules like scipy or boto. One might consider stripping them manually or with Python's optimized mode.

mrq/exceptions.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from gevent import GreenletExit
2+
import traceback
3+
import sys
24

35

46
# Inherits from BaseException to avoid being caught when not intended.
@@ -10,9 +12,14 @@ class RetryInterrupt(BaseException):
1012
delay = None
1113
queue = None
1214
retry_count = 0
15+
original_exception = None
1316

1417
def __str__(self):
15-
return "<RetryInterrupt #%s: %s seconds, %s queue>" % (self.retry_count, self.delay, self.queue)
18+
s = "<RetryInterrupt #%s: %s seconds, %s queue>" % (self.retry_count, self.delay, self.queue)
19+
if self.original_exception is not None:
20+
s += "\n---- Original exception: -----\n%s" % ("".join(traceback.format_exception(*self.original_exception)))
21+
22+
return s
1623

1724

1825
class MaxRetriesInterrupt(BaseException):

mrq/job.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ def retry(self, queue=None, delay=None, max_retries=None):
189189
if exc.delay is None:
190190
exc.delay = self.retry_delay
191191

192+
# Often, a retry will be raised inside an "except" block.
193+
# Keep track of the first exception for debugging purposes.
194+
original_exception = sys.exc_info()
195+
if original_exception[0] is not None:
196+
exc.original_exception = original_exception
197+
192198
raise exc
193199

194200
def cancel(self):

0 commit comments

Comments
 (0)