mirror of https://github.com/rq/rq.git
Initial attempt at job timeouts.
This commit is contained in:
parent
fb587297f6
commit
8a856e79ea
10
rq/job.py
10
rq/job.py
|
@ -143,10 +143,10 @@ class Job(object):
|
||||||
"""
|
"""
|
||||||
key = self.key
|
key = self.key
|
||||||
properties = ['data', 'created_at', 'origin', 'description',
|
properties = ['data', 'created_at', 'origin', 'description',
|
||||||
'enqueued_at', 'ended_at', 'result', 'exc_info']
|
'enqueued_at', 'ended_at', 'result', 'exc_info', 'timeout']
|
||||||
data, created_at, origin, description, \
|
data, created_at, origin, description, \
|
||||||
enqueued_at, ended_at, result, \
|
enqueued_at, ended_at, result, \
|
||||||
exc_info = conn.hmget(key, properties)
|
exc_info, timeout = conn.hmget(key, properties)
|
||||||
if data is None:
|
if data is None:
|
||||||
raise NoSuchJobError('No such job: %s' % (key,))
|
raise NoSuchJobError('No such job: %s' % (key,))
|
||||||
|
|
||||||
|
@ -164,6 +164,10 @@ class Job(object):
|
||||||
self.ended_at = to_date(ended_at)
|
self.ended_at = to_date(ended_at)
|
||||||
self._result = result
|
self._result = result
|
||||||
self.exc_info = exc_info
|
self.exc_info = exc_info
|
||||||
|
if timeout is None:
|
||||||
|
self.timeout = None
|
||||||
|
else:
|
||||||
|
self.timeout = int(timeout)
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Persists the current job instance to its corresponding Redis key."""
|
"""Persists the current job instance to its corresponding Redis key."""
|
||||||
|
@ -186,6 +190,8 @@ class Job(object):
|
||||||
obj['result'] = self._result
|
obj['result'] = self._result
|
||||||
if self.exc_info is not None:
|
if self.exc_info is not None:
|
||||||
obj['exc_info'] = self.exc_info
|
obj['exc_info'] = self.exc_info
|
||||||
|
if self.timeout is not None:
|
||||||
|
obj['timeout'] = self.timeout
|
||||||
|
|
||||||
conn.hmset(key, obj)
|
conn.hmset(key, obj)
|
||||||
|
|
||||||
|
|
21
rq/queue.py
21
rq/queue.py
|
@ -32,10 +32,11 @@ class Queue(object):
|
||||||
name = queue_key[len(prefix):]
|
name = queue_key[len(prefix):]
|
||||||
return Queue(name)
|
return Queue(name)
|
||||||
|
|
||||||
def __init__(self, name='default'):
|
def __init__(self, name='default', default_timeout=None):
|
||||||
prefix = self.redis_queue_namespace_prefix
|
prefix = self.redis_queue_namespace_prefix
|
||||||
self.name = name
|
self.name = name
|
||||||
self._key = '%s%s' % (prefix, name)
|
self._key = '%s%s' % (prefix, name)
|
||||||
|
self._default_timeout = default_timeout
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self):
|
def key(self):
|
||||||
|
@ -99,24 +100,38 @@ class Queue(object):
|
||||||
|
|
||||||
Expects the function to call, along with the arguments and keyword
|
Expects the function to call, along with the arguments and keyword
|
||||||
arguments.
|
arguments.
|
||||||
|
|
||||||
|
The special keyword `timeout` is reserved for `enqueue()` itself and
|
||||||
|
it won't be passed to the actual job function.
|
||||||
"""
|
"""
|
||||||
if f.__module__ == '__main__':
|
if f.__module__ == '__main__':
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Functions from the __main__ module cannot be processed '
|
'Functions from the __main__ module cannot be processed '
|
||||||
'by workers.')
|
'by workers.')
|
||||||
|
|
||||||
|
timeout = kwargs.pop('timeout', None)
|
||||||
job = Job.create(f, *args, **kwargs)
|
job = Job.create(f, *args, **kwargs)
|
||||||
return self.enqueue_job(job)
|
return self.enqueue_job(job, timeout=timeout)
|
||||||
|
|
||||||
def enqueue_job(self, job, set_meta_data=True):
|
def enqueue_job(self, job, timeout=None, set_meta_data=True):
|
||||||
"""Enqueues a job for delayed execution.
|
"""Enqueues a job for delayed execution.
|
||||||
|
|
||||||
|
When the `timeout` argument is sent, it will overrides the default
|
||||||
|
timeout value of 180 seconds. `timeout` may either be a string or
|
||||||
|
integer.
|
||||||
|
|
||||||
If the `set_meta_data` argument is `True` (default), it will update
|
If the `set_meta_data` argument is `True` (default), it will update
|
||||||
the properties `origin` and `enqueued_at`.
|
the properties `origin` and `enqueued_at`.
|
||||||
"""
|
"""
|
||||||
if set_meta_data:
|
if set_meta_data:
|
||||||
job.origin = self.name
|
job.origin = self.name
|
||||||
job.enqueued_at = times.now()
|
job.enqueued_at = times.now()
|
||||||
|
|
||||||
|
if timeout:
|
||||||
|
job.timeout = timeout # _timeout_in_seconds(timeout)
|
||||||
|
else:
|
||||||
|
job.timeout = 180 # default
|
||||||
|
|
||||||
job.save()
|
job.save()
|
||||||
self.push_job_id(job.id)
|
self.push_job_id(job.id)
|
||||||
return job
|
return job
|
||||||
|
|
62
rq/worker.py
62
rq/worker.py
|
@ -1,4 +1,3 @@
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
import errno
|
import errno
|
||||||
import random
|
import random
|
||||||
|
@ -295,14 +294,14 @@ class Worker(object):
|
||||||
return did_perform_work
|
return did_perform_work
|
||||||
|
|
||||||
def fork_and_perform_job(self, job):
|
def fork_and_perform_job(self, job):
|
||||||
|
"""Spawns a work horse to perform the actual work and passes it a job.
|
||||||
|
The worker will wait for the work horse and make sure it executes
|
||||||
|
within the given timeout bounds, or will end the work horse with
|
||||||
|
SIGALRM.
|
||||||
|
"""
|
||||||
child_pid = os.fork()
|
child_pid = os.fork()
|
||||||
if child_pid == 0:
|
if child_pid == 0:
|
||||||
self._is_horse = True
|
self.main_work_horse(job)
|
||||||
random.seed()
|
|
||||||
self.log = Logger('horse')
|
|
||||||
|
|
||||||
success = self.perform_job(job)
|
|
||||||
sys.exit(int(not success))
|
|
||||||
else:
|
else:
|
||||||
self._horse_pid = child_pid
|
self._horse_pid = child_pid
|
||||||
self.procline('Forked %d at %d' % (child_pid, time.time()))
|
self.procline('Forked %d at %d' % (child_pid, time.time()))
|
||||||
|
@ -320,13 +319,62 @@ class Worker(object):
|
||||||
if e.errno != errno.EINTR:
|
if e.errno != errno.EINTR:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def main_work_horse(self, job):
|
||||||
|
"""This is the entry point of the newly spawned work horse."""
|
||||||
|
# After fork()'ing, always assure we are generating random sequences
|
||||||
|
# that are different from the worker.
|
||||||
|
random.seed()
|
||||||
|
self._is_horse = True
|
||||||
|
self.log = Logger('horse')
|
||||||
|
|
||||||
|
success = self.perform_job(job)
|
||||||
|
|
||||||
|
# os._exit() is the way to exit from childs after a fork(), in
|
||||||
|
# constrast to the regular sys.exit()
|
||||||
|
os._exit(int(not success))
|
||||||
|
|
||||||
|
def raise_death_penalty_after(self, timeout):
|
||||||
|
"""Sets up an alarm signal and a signal handler that raises
|
||||||
|
a JobTimeoutException after the given `timeout` amount (expressed
|
||||||
|
in seconds).
|
||||||
|
"""
|
||||||
|
|
||||||
|
class JobTimeoutException(Exception):
|
||||||
|
"""Raised when a job takes longer to complete than the allowed
|
||||||
|
maximum time.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Setup a timeout handler
|
||||||
|
def timeout_handler(signum, frame):
|
||||||
|
raise JobTimeoutException('Job exceeded maximum timeout '
|
||||||
|
'value (%d seconds).' % timeout)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGALRM, timeout_handler)
|
||||||
|
signal.alarm(timeout)
|
||||||
|
|
||||||
|
def cancel_death_penalty(self):
|
||||||
|
"""Removes the death penalty alarm and puts back the system into
|
||||||
|
default signal handling.
|
||||||
|
"""
|
||||||
|
signal.alarm(0)
|
||||||
|
signal.signal(signal.SIGALRM, signal.SIG_DFL)
|
||||||
|
|
||||||
def perform_job(self, job):
|
def perform_job(self, job):
|
||||||
|
"""Performs the actual work of a job. Will/should only be called
|
||||||
|
inside the work horse's process.
|
||||||
|
"""
|
||||||
self.procline('Processing %s from %s since %s' % (
|
self.procline('Processing %s from %s since %s' % (
|
||||||
job.func.__name__,
|
job.func.__name__,
|
||||||
job.origin, time.time()))
|
job.origin, time.time()))
|
||||||
|
|
||||||
|
# Set up death penalty
|
||||||
|
self.raise_death_penalty_after(job.timeout or 180)
|
||||||
try:
|
try:
|
||||||
rv = job.perform()
|
rv = job.perform()
|
||||||
|
self.cancel_death_penalty()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
self.cancel_death_penalty()
|
||||||
fq = self.failed_queue
|
fq = self.failed_queue
|
||||||
self.log.exception(red(str(e)))
|
self.log.exception(red(str(e)))
|
||||||
self.log.warning('Moving job to %s queue.' % fq.name)
|
self.log.warning('Moving job to %s queue.' % fq.name)
|
||||||
|
|
Loading…
Reference in New Issue