Skip to content

Commit b0e0640

Browse files
committed
Add flags to check memory usage and track leaks. Move mrq_workers to mongodb_jobs
1 parent 4fa5a64 commit b0e0640

11 files changed

Lines changed: 198 additions & 25 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,6 @@ venv
3838
pypy
3939
.DS_Store
4040

41-
mrq-config.py
41+
mrq-config.py
42+
dump.rdb
43+
supervisor.pid

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ RUN echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen
66
RUN apt-get update && echo "Updated on 2014-01-15"
77
RUN apt-get upgrade -y
88

9-
RUN apt-get install -y gcc make g++ build-essential libc6-dev tcl curl adduser mongodb-10gen python python-pip python-dev strace git software-properties-common libev-dev nginx
9+
RUN apt-get install -y gcc make g++ build-essential libc6-dev tcl curl adduser mongodb-10gen python python-pip python-dev strace git software-properties-common libev-dev nginx graphviz
1010

1111
# Then add PPAs (after software-properties-common is installed)
1212
# RUN add-apt-repository -y ppa:pypy/ppa

mrq/basetasks/tests/general.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import urllib2
55

66

7+
78
class Add(Task):
89

910
def run(self, params):
@@ -36,6 +37,21 @@ def run(self, params):
3637
return len(t)
3738

3839

40+
LEAKS = []
41+
42+
43+
class Leak(Task):
44+
def run(self, params):
45+
46+
if params.get("size", 0) > 0:
47+
LEAKS.append("x" * params.get("size", 0))
48+
49+
if params.get("sleep", 0) > 0:
50+
sleep(params.get("sleep", 0))
51+
52+
return params.get("return")
53+
54+
3955
class Retry(Task):
4056

4157
def run(self, params):

mrq/config.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,26 @@ def get_config(sources=("file", "env", "args"), env_prefix="MRQ_", defaults=None
2929
parser.add_argument('--trace_greenlets', action='store_true', default=False,
3030
help='Collect stats about each greenlet execution time and switches.')
3131

32+
parser.add_argument('--trace_memory', action='store_true', default=False,
33+
help='Collect stats about memory for each task. Incompatible with gevent > 1')
34+
35+
parser.add_argument('--trace_memory_type', action='store', default="",
36+
help='Create a .png object graph in trace_memory_output_dir with a random object of this type.')
37+
38+
parser.add_argument('--trace_memory_output_dir', action='store', default="memory_traces",
39+
help='Directory where to output .pngs with object graphs')
40+
3241
parser.add_argument('--profile', action='store_true', default=False,
3342
help='Run profiling on the whole worker')
3443

3544
parser.add_argument('--mongodb_jobs', action='store', default="mongodb://127.0.0.1:27017/mrq",
36-
help='MongoDB URI for the jobs database')
45+
help='MongoDB URI for the jobs, scheduled_jobs & workers database')
3746

3847
parser.add_argument('--mongodb_logs', action='store', default="mongodb://127.0.0.1:27017/mrq",
39-
help='MongoDB URI for the logs database')
48+
help='MongoDB URI for the logs database. If set to "0", will disable remote logs.')
4049

4150
parser.add_argument('--mongodb_logs_size', action='store', default=16 * 1024 * 1024, type=int,
42-
help='If provided, sets the log collection to capped to that amount of bytes')
51+
help='If provided, sets the log collection to capped to that amount of bytes.')
4352

4453
parser.add_argument('--redis', action='store', default="redis://127.0.0.1:6379",
4554
help='Redis URI')

mrq/dashboard/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ def api_datatables(unit):
9191
if unit == "workers":
9292
fields = None
9393
query = {"status": {"$nin": ["stop"]}}
94-
collection = connections.mongodb_logs.mrq_workers
94+
collection = connections.mongodb_jobs.mrq_workers
9595
sort = [("datestarted", -1)]
9696

9797
if request.args.get("showstopped"):

mrq/job.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .queue import Queue
88
from .context import get_current_worker, log, connections, get_current_config
99
import gevent
10+
import gc
1011

1112

1213
class Job(object):
@@ -232,3 +233,46 @@ def wait(self, poll_interval=1, timeout=None, full_data=False):
232233
time.sleep(poll_interval)
233234

234235
raise Exception("Waited for job result for %ss seconds, timeout." % timeout)
236+
237+
def trace_memory_start(self):
238+
""" Starts measuring memory consumption """
239+
import objgraph
240+
objgraph.show_growth(limit=10)
241+
#gc.collect() # - is done in show_growth
242+
self._memory_start = self.worker.get_memory()
243+
244+
def trace_memory_stop(self):
245+
""" Stops measuring memory consumption """
246+
247+
#gc.collect() - is done in show_growth
248+
import objgraph, random, gevent
249+
250+
gevent.sleep(0)
251+
252+
objgraph.show_growth(limit=10)
253+
254+
trace_type = get_current_config()["trace_memory_type"]
255+
if trace_type:
256+
257+
objgraph.show_chain(
258+
objgraph.find_backref_chain(
259+
random.choice(objgraph.by_type(trace_type)),
260+
objgraph.is_proper_module
261+
),
262+
filename='%s/%s-%s.png' % (get_current_config()["trace_memory_output_dir"], trace_type, self.id)
263+
)
264+
265+
self._memory_stop = self.worker.get_memory()
266+
267+
diff = self._memory_stop - self._memory_start
268+
269+
log.debug("Memory diff for job %s : %s" % (self.id, diff))
270+
271+
# We need to update it later than the results, we need them off memory already.
272+
self.collection.update({
273+
"_id": self.id
274+
}, {"$set": {
275+
"memory_diff": diff
276+
}}, w=1)
277+
278+

mrq/logger.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ def log(self, level, *args, **kwargs):
4242
if not self.quiet:
4343
print formatted
4444

45+
if self.collection is False:
46+
return
47+
4548
if worker is not None:
4649
self.buffer["workers"][worker].append(formatted)
4750
elif job is not None:
@@ -55,7 +58,7 @@ def log(self, level, *args, **kwargs):
5558
def flush(self, w=0):
5659

5760
# We may log some stuff before we are even connected to Mongo!
58-
if self.collection is None:
61+
if not self.collection:
5962
return
6063

6164
inserts = [{

mrq/worker.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ class Worker(object):
4444
"""
4545
status = "init"
4646

47+
mongodb_jobs = None
48+
mongodb_logs = None
49+
redis = None
50+
4751
def __init__(self, config):
4852

4953
self.config = config
@@ -99,9 +103,12 @@ def connect(self, force=False):
99103
# Accessing connections attributes will automatically connect
100104
self.redis = connections.redis
101105
self.mongodb_jobs = connections.mongodb_jobs
102-
self.mongodb_logs = connections.mongodb_logs
103106

104-
self.log_handler.set_collection(self.mongodb_logs.mrq_logs)
107+
if self.config["mongodb_logs"] == "0":
108+
self.log_handler.set_collection(False) # Disable
109+
else:
110+
self.mongodb_logs = connections.mongodb_logs
111+
self.log_handler.set_collection(self.mongodb_logs.mrq_logs)
105112

106113
self.connected = True
107114

@@ -110,18 +117,20 @@ def connect(self, force=False):
110117

111118
def ensure_indexes(self):
112119

113-
self.mongodb_logs.mrq_logs.ensure_index([("job", 1)], background=False)
114-
self.mongodb_logs.mrq_logs.ensure_index([("worker", 1)], background=False, sparse=True)
120+
if self.mongodb_logs:
115121

116-
if self.config["mongodb_logs_size"] > 0:
122+
self.mongodb_logs.mrq_logs.ensure_index([("job", 1)], background=False)
123+
self.mongodb_logs.mrq_logs.ensure_index([("worker", 1)], background=False, sparse=True)
117124

118-
try:
119-
self.mongodb_logs.command("convertToCapped", "mrq_logs", size=self.config["mongodb_logs_size"])
120-
except:
121-
pass
125+
if self.config["mongodb_logs_size"] > 0:
126+
127+
try:
128+
self.mongodb_logs.command("convertToCapped", "mrq_logs", size=self.config["mongodb_logs_size"])
129+
except:
130+
pass
122131

123-
self.mongodb_logs.mrq_workers.ensure_index([("status", 1)], background=False)
124-
self.mongodb_logs.mrq_workers.ensure_index([("datereported", 1)], background=False, expireAfterSeconds=3600)
132+
self.mongodb_jobs.mrq_workers.ensure_index([("status", 1)], background=False)
133+
self.mongodb_jobs.mrq_workers.ensure_index([("datereported", 1)], background=False, expireAfterSeconds=3600)
125134

126135
self.mongodb_jobs.mrq_jobs.ensure_index([("status", 1)], background=False)
127136
self.mongodb_jobs.mrq_jobs.ensure_index([("path", 1), ("status", 1)], background=False)
@@ -166,6 +175,9 @@ def greenlet_monitoring(self):
166175
self.flush_logs(w=0)
167176
time.sleep(int(self.config["report_interval"]))
168177

178+
def get_memory(self):
179+
return self.process.get_memory_info().rss
180+
169181
def get_worker_report(self):
170182

171183
greenlets = []
@@ -217,7 +229,7 @@ def get_worker_report(self):
217229
"percent": self.process.get_cpu_percent(0)
218230
},
219231
"mem": {
220-
"rss": self.process.get_memory_info().rss
232+
"rss": self.get_memory()
221233
}
222234
# https://code.google.com/p/psutil/wiki/Documentation
223235
# get_open_files
@@ -233,7 +245,7 @@ def get_worker_report(self):
233245
def report_worker(self, w=0):
234246

235247
try:
236-
self.mongodb_logs.mrq_workers.update({
248+
self.mongodb_jobs.mrq_workers.update({
237249
"_id": ObjectId(self.id)
238250
}, {"$set": self.get_worker_report()}, upsert=True, w=w)
239251
except pymongo.errors.AutoReconnect:
@@ -321,7 +333,14 @@ def work_loop(self):
321333
while True:
322334

323335
while True:
336+
337+
if self.config["trace_memory"]:
338+
# When debugging memory, intermediate psutils call like this one are
339+
# needed for some obscure reason. (tested in test_memoryleaks.py)
340+
self.get_memory()
341+
324342
free_pool_slots = self.gevent_pool.free_count()
343+
325344
if free_pool_slots > 0:
326345
self.status = "wait"
327346
break
@@ -387,6 +406,9 @@ def perform_job(self, job):
387406
This is the first call happening inside the greenlet.
388407
"""
389408

409+
if self.config["trace_memory"]:
410+
job.trace_memory_start()
411+
390412
set_current_job(job)
391413

392414
gevent_timeout = gevent.Timeout(job.timeout, JobTimeoutException(
@@ -437,6 +459,9 @@ def perform_job(self, job):
437459

438460
self.done_jobs += 1
439461

462+
if self.config["trace_memory"]:
463+
job.trace_memory_stop()
464+
440465
def shutdown_graceful(self):
441466
""" Graceful shutdown: waits for all the jobs to finish. """
442467

tests/conftest.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import time
1010
import re
1111
import json
12+
import urllib2
1213

1314
sys.path.append(os.getcwd())
1415

@@ -205,6 +206,13 @@ def send_task_cli(self, path, params, block=True, queue=None, **kwargs):
205206
return json.loads(out)
206207
return out
207208

209+
def get_report(self):
210+
wait_for_net_service("127.0.0.1", 20020)
211+
f = urllib2.urlopen("http://127.0.0.1:20020")
212+
data = json.load(f)
213+
f.close()
214+
return data
215+
208216

209217
class RedisFixture(ProcessFixture):
210218
def flush(self):
@@ -214,9 +222,10 @@ def flush(self):
214222
class MongoFixture(ProcessFixture):
215223
def flush(self):
216224
for mongodb in (connections.mongodb_jobs, connections.mongodb_logs):
217-
for c in mongodb.collection_names():
218-
if not c.startswith("system."):
219-
mongodb.drop_collection(c)
225+
if mongodb:
226+
for c in mongodb.collection_names():
227+
if not c.startswith("system."):
228+
mongodb.drop_collection(c)
220229

221230

222231
@pytest.fixture(scope="function")

tests/test_general.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def test_general_simple_task_one(worker):
1212

1313
time.sleep(0.1)
1414

15-
db_workers = list(worker.mongodb_logs.mrq_workers.find())
15+
db_workers = list(worker.mongodb_jobs.mrq_workers.find())
1616
assert len(db_workers) == 1
1717
assert db_workers[0]["status"] in ["full", "wait"]
1818

@@ -40,7 +40,7 @@ def test_general_simple_task_one(worker):
4040
assert db_jobs[0]["time"] < 0.1
4141
assert db_jobs[0]["switches"] >= 1
4242

43-
db_workers = list(worker.mongodb_logs.mrq_workers.find())
43+
db_workers = list(worker.mongodb_jobs.mrq_workers.find())
4444
assert len(db_workers) == 1
4545
assert db_workers[0]["_id"] == db_jobs[0]["worker"]
4646
assert db_workers[0]["status"] == "stop"

0 commit comments

Comments
 (0)