Skip to content

Commit 3de1053

Browse files
committed
Add an "abort" status. Fixes #66
1 parent 946b8af commit 3de1053

14 files changed

Lines changed: 118 additions & 29 deletions

File tree

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ TASKS = {
3131
# Seconds before a job in retry status is requeued again
3232
"retry_delay": 600,
3333

34-
# Keep jobs with status in ("success", "cancel") that many seconds
34+
# Keep jobs with status in ("success", "cancel", "abort") that many seconds
3535
# in MongoDB
3636
"result_ttl": 7 * 24 * 3600,
3737

docs/jobs.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ When everything goes fine, a job will go through 3 statuses:
1919
However, to be reliable a task queue needs to prepare for everything that can go wrong. These statuses will help you manage those cases:
2020

2121
* ```failed```: A Python Exception was raised during the execution of the job. It can be an Exception you raised yourself or an error in a module you are using. MRQ's Dashboard features a view where you can have a look at the tracebacks to debug these exceptions.
22-
* ```cancel```: The job was cancelled. This will happen mainly when you cancel jobs from the Dashboard before they run. Be careful, cancelling jobs when they are `started` won't interrupt the job.
22+
* ```cancel```: The job was cancelled. This will happen mainly when you cancel jobs from the Dashboard before they run. Be careful, cancelling jobs when they are `started` won't interrupt the currently running job.
23+
* ```abort```: The job was aborted. This happens while the job is running and `abort_current_job()` is called. Often this will be the result of an unrecoverable error that you don't want to retry but still want logged in the Dashboard for some time (see `result_ttl`)
2324
* ```interrupt```: While running this job, the worker was interrupted and had the time to save this status. This happens when the worker process receives the UNIX signal SIGTERM or two SIGINTs (which can happen by sending Ctrl-C two times). This status won't be set if the process is interrupted with a SIGKILL or any other abrupt means like a power off, and the task will stay in `started` state until requeued or cancelled by a maintenance job.
2425
* ```timeout```: The job took too long to finish and was interrupted by the worker. Timeouts can be set globally or for each task.
2526
* ```retry```: The method `task.retry()` was called to interrupt the job but mark it for being retried later. This may be useful when calling unreliable 3rd-party services.
@@ -70,6 +71,10 @@ The `max_retries` parameter defaults to the value of `max_retries` in the task c
7071

7172
If the `queue` parameter is supplied, the job will be enventually requeued on that queue. If not, it will stay on its original queue.
7273

74+
* `abort_current_job()`
75+
76+
Stops the execution of the current job (by raising an `AbortInterrupt`) and mark it with the status `abort`. It will stay visible in the Dashboard for `result_ttl` seconds.
77+
7378
* `get_current_job()`
7479

7580
Returns the Job instance currently being executed. If None, you are outside of a Job context. This can only happen when calling code from your tasks outside of `mrq-run` or `mrq-worker`.

mrq/basetasks/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ def build_query(self):
4040
"dateretry",
4141
"exceptiontype"]:
4242
if self.params.get(k):
43-
query[k] = self.params.get(k)
43+
if isinstance(self.params[k], (list, tuple)):
44+
query[k] = {"$in": list(self.params[k])}
45+
else:
46+
query[k] = self.params[k]
4447

4548
if self.params.get("params"):
4649
params_dict = json.loads(self.params.get("params")) # pylint: disable=no-member

mrq/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def add_parser_args(parser, config_type):
142142
default=7 * 24 * 3600,
143143
action='store',
144144
type=int,
145-
help='Seconds the results are kept in MongoDB when status in ("success", "cancel")')
145+
help='Seconds the results are kept in MongoDB when status in ("success", "cancel", "abort")')
146146

147147
parser.add_argument(
148148
'--default_job_timeout',

mrq/context.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ def retry_current_job(delay=None, max_retries=None, queue=None):
8282
current_job.retry(delay=delay, max_retries=max_retries, queue=queue)
8383

8484

85+
def abort_current_job():
86+
current_job = get_current_job()
87+
if current_job:
88+
current_job.abort()
89+
90+
8591
def _connections_factory(attr):
8692

8793
config = get_current_config()

mrq/dashboard/app.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,15 @@ def build_api_datatables_query(req):
125125
req.args.get("redisqueue")).list_job_ids(limit=1000)]}
126126
else:
127127

128-
for param in ["queue", "path", "status", "exceptiontype"]:
128+
for param in ["queue", "path", "exceptiontype"]:
129129
if req.args.get(param):
130130
query[param] = req.args.get(param)
131+
if req.args.get("status"):
132+
statuses = req.args.get(param).split("-")
133+
if len(statuses) == 1:
134+
query[param] = statuses[0]
135+
else:
136+
query[param] = {"$in": statuses}
131137
if req.args.get("id"):
132138
query["_id"] = ObjectId(req.args.get("id"))
133139
if req.args.get("worker"):
@@ -280,9 +286,11 @@ def api_job_traceback(job_id):
280286
@app.route('/api/jobaction', methods=["POST"])
281287
@requires_auth
282288
def api_job_action():
289+
params = {k: v for k, v in request.form.iteritems()}
290+
if params.get("status") and "-" in params.get("status"):
291+
params["status"] = params.get("status").split("-")
283292
return jsonify({"job_id": queue_job("mrq.basetasks.utils.JobAction",
284-
{k: v for k,
285-
v in request.form.iteritems()},
293+
params,
286294
queue=get_current_config()["dashboard_queue"])})
287295

288296

mrq/dashboard/static/js/views/jobs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ define(["jquery", "underscore", "views/generic/datatablepage", "models"],functio
278278
'maxretries': "label-danger",
279279
'interrupt': "label-danger",
280280
'cancel': "label-warning",
281+
'abort': "label-warning",
281282
'retry': "label-warning"
282283
};
283284
var css_class = status_classes[source.status] || "label-info";

mrq/dashboard/templates/index.html

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,22 @@ <h4 class="modal-title"></h4>
318318
<div class="form-group">
319319
<select class="form-control input-sm js-datatable-filters-status" id="jobs-form-status">
320320
<option <%= filters.status==""?"selected='selected'":"" %> value="">-statuses-</option>
321-
<% _.each(["queued", "started", "success", "failed", "retry", "maxretries", "timeout", "interrupt", "cancel"], function(k) { %>
322-
<option <%= filters.status==k?"selected='selected'":"" %> value="<%= k %>"><%= k %></option>
321+
<% _.each({
322+
"queued": "queued",
323+
"started": "started",
324+
"success": "success",
325+
"failed": "failed",
326+
"retry": "retry",
327+
"maxretries": "maxretries",
328+
"timeout": "timeout",
329+
"interrupt": "interrupt",
330+
"cancel": "cancel",
331+
"abort": "abort",
332+
"queued-started-success": "{OK}",
333+
"failed-retry-maxretries-timeout-interrupt-cancel-abort": "{NOT OK}",
334+
"failed-maxretries-timeout-abort": "{ERROR}"
335+
}, function(k, i) { %>
336+
<option <%= filters.status==i?"selected='selected'":"" %> value="<%= i %>"><%= k %></option>
323337
<% }) %>
324338
</select>
325339
</div>

mrq/exceptions.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,41 @@
11
from gevent import GreenletExit
22
import traceback
3-
import sys
43

54

65
# Inherits from BaseException to avoid being caught when not intended.
7-
class TimeoutInterrupt(BaseException):
8-
pass
9-
6+
class _MrqInterrupt(BaseException):
107

11-
class RetryInterrupt(BaseException):
12-
delay = None
13-
queue = None
14-
retry_count = 0
158
original_exception = None
169

10+
def _get_exception_name(self):
11+
return self.__class__.__name__
12+
1713
def __str__(self):
18-
s = "<RetryInterrupt #%s: %s seconds, %s queue>" % (self.retry_count, self.delay, self.queue)
14+
s = self._get_exception_name()
1915
if self.original_exception is not None:
2016
s += "\n---- Original exception: -----\n%s" % ("".join(traceback.format_exception(*self.original_exception)))
2117

2218
return s
2319

2420

25-
class MaxRetriesInterrupt(BaseException):
21+
class TimeoutInterrupt(_MrqInterrupt):
22+
pass
23+
24+
25+
class AbortInterrupt(_MrqInterrupt):
26+
pass
27+
28+
29+
class RetryInterrupt(_MrqInterrupt):
30+
delay = None
31+
queue = None
32+
retry_count = 0
33+
34+
def _get_exception_name(self):
35+
return "%s #%s: %s seconds, %s queue" % (self.__class__.__name__, self.retry_count, self.delay, self.queue)
36+
37+
38+
class MaxRetriesInterrupt(_MrqInterrupt):
2639
pass
2740

2841

mrq/job.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import datetime
22
from bson import ObjectId
33
import time
4-
from .exceptions import RetryInterrupt, MaxRetriesInterrupt
4+
from .exceptions import RetryInterrupt, MaxRetriesInterrupt, AbortInterrupt
55
from .utils import load_class_by_path
66
from .queue import Queue
77
from .context import get_current_worker, log, connections, get_current_config, metric
@@ -85,7 +85,7 @@ def fetch(self, start=False, full_data=True):
8585
self.set_data(self.collection.find_and_modify(
8686
{
8787
"_id": self.id,
88-
"status": {"$nin": ["cancel"]}
88+
"status": {"$nin": ["cancel", "abort", "maxretries"]}
8989
},
9090
{"$set": {
9191
"status": "started",
@@ -171,6 +171,14 @@ def insert(cls, jobs_data, queue=None, return_jobs=True, w=1):
171171
else:
172172
return inserted
173173

174+
def _attach_original_exception(self, exc):
175+
""" Often, a retry will be raised inside an "except" block.
176+
This Keep track of the first exception for debugging purposes. """
177+
178+
original_exception = sys.exc_info()
179+
if original_exception[0] is not None:
180+
exc.original_exception = original_exception
181+
174182
def retry(self, queue=None, delay=None, max_retries=None):
175183
""" Marks the current job as needing to be retried. Interrupts it. """
176184

@@ -189,12 +197,14 @@ def retry(self, queue=None, delay=None, max_retries=None):
189197
if exc.delay is None:
190198
exc.delay = self.retry_delay
191199

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
200+
self._attach_original_exception(exc)
201+
202+
raise exc
197203

204+
def abort(self):
205+
""" Aborts the current task mid-excution. """
206+
exc = AbortInterrupt()
207+
self._attach_original_exception(exc)
198208
raise exc
199209

200210
def cancel(self):

0 commit comments

Comments
 (0)