Delayed is a simple but robust task queue inspired by rq.
- Robust: all the enqueued tasks will run exactly once, even if the worker got killed at any time.
- Clean: finished tasks (including failed) take no space of your Redis.
- Distributed: workers as more as needed can run in the same time without further config.
- Portable: its Go and Python version can call each other.
- Python 3.7 or later, tested on CPython 3.7 - 3.12. Versions before 1.0 have also been tested on CPython 2.7, PyPy and PyPy3.
- To gracefully stop the workers, Unix-like systems (with Unix signal) are required, tested on Ubuntu 22.04 and macOS Monterey 12.
- Redis 2.6.0 or later (with Lua scripts).
Run a redis server:
$ redis-server
Install delayed:
$ pip install delayed
Create a task queue:
importredisfromdelayed.queueimportQueueconn=redis.Redis() queue=Queue(name='default', conn=conn)
Enqueue tasks:
Four ways to enqueue Python tasks:
Define a task function and enqueue it:
fromdelayed.delayimportdelayeddelayed=delayed(queue) i=0@delayeddefdelayed_add(a, b): returna+b@delayed(retry=3)defretry_div(x): globalii+=1returnx/ (i-1) delayed_add.delay(1, 2) # enqueue delayed_adddelayed_add.delay(1, b=2) # same as abovedelayed_add(1, 2) # call it immediatelyretry_div.delay(1) # enqueue retry_div
Directly enqueue a function:
fromdelayed.delayimportdelayeddelayed=delayed(queue) defadd(a, b): returna+bdelayed(add).delay(1, 2) delayed(add).delay(1, b=2) # same as abovedelayed(retry=3)(add).delay(1, b=2) delayed(add, retry=3).delay(1, b=2) # same as above
Create a task and enqueue it:
fromdelayed.taskimportPyTaskdefadd(a, b): returna+btask=PyTask(func=add, args=(1,), kwargs={'b': 2}, retry=1) queue.enqueue(task)
Enqueue a predefined task function without importing it (the fastest and lightest way):
fromdelayed.taskimportPyTasktask=PyTask(func='test:add', args=(1,), kwargs={'b': 2}, retry=1) queue.enqueue(task)
Enqueue Go tasks:
fromdelayed.taskimportGoTasktask=GoTask(func_path='syscall.Kill', args=(0, 1)) queue.enqueue(task) task=GoTask(func_path='fmt.Printf', args=('%d %s\n', [1, 'test'])) # the variadic argument needs to be a list or tuplequeue.enqueue(task) task=GoTask('fmt.Println', (1, 'test')) # if the variadic argument is the only argument, it's not required to wrap it with a list or tuplequeue.enqueue(task)
Enqueue tasks asynchronously:
fromdelayed.queueimportAsyncQueuefromdelayed.taskimportGoTask, PyTaskfromredis.asyncioimportRedisconn=Redis() queue=AsyncQueue(name='default', conn=conn) asyncdefenqueue(): task=PyTask(func='test:add', args=(1,), kwargs={'b': 2}, retry=1) awaitqueue.enqueue(task) task=GoTask(func_path='syscall.Kill', args=(0, 1)) awaitqueue.enqueue(task)
Run a task worker (or more) in a separated process:
importredisfromdelayed.queueimportQueuefromdelayed.workerimportWorkerconn=redis.Redis() queue=Queue(name='default', conn=conn) worker=Worker(queue=queue) worker.run()
Run a task sweeper in a separated process to recovery lost tasks (mainly due to the worker got killed):
importredisfromdelayed.queueimportQueuefromdelayed.sweeperimportSweeperconn=redis.Redis() queue=Queue(name='default', conn=conn) sweeper=Sweeper(queues=[queue]) sweeper.run()
See examples.
```bash
$ redis-server &
$ pip install delayed
$ python -m examples.sweeper &
$ python -m examples.worker &
$ python -m examples.caller
```
Q: What's the limitation on a task function?
A: A Python task function should be defined in module level (except the__main__module). Itsargsandkwargsshould be serializable by MessagePack. After deserializing, the type ofargsandkwargspassed to the task function might be changed (tuple -> list), so it should take care of this change.Q: What's the
nameparam of a queue?
A: It's the key used to store the tasks of the queue. A queue with name "default" will use those keys:- default: list, enqueued tasks.
- default_noti: list, the same length as enqueued tasks.
- default_processing: hash, the processing task of workers.
Q: What's lost tasks?
A: There are 2 situations a task might get lost:- a worker popped a task notification, then got killed before dequeueing the task.
- a worker dequeued a task, then got killed before releasing the task.
Q: How to recovery lost tasks?
A: Runs a sweeper. It dose two things:- it keeps the task notification length the same as the task queue.
- it checks the processing list, if the worker is dead, moves the processing task back to the task queue.
Q: How to turn on the debug logs?
A: Adds alogging.DEBUGlevel handler todelayed.logger.logger. The simplest way is to calldelayed.logger.setup_logger():fromdelayed.loggerimportsetup_loggersetup_logger()
**Q: Why isn't there an async version of
delayed()?
A: Because it will convert a sync function to an async function, which breaks the signature of the function.
1.3:
- Implements
AsyncQueuefor enqueueing tasks asynchronously.
- Implements
1.2:
- Adds
retryparam to functions wrapped bydelayed(). - Adds
retryparam toTask(). - Adds
releaseparam toQueue.enqueue(). - The
Workerwon't retry a failed task infinitely by default now. You can setretry=-1toTask()instead. (BREAKING CHANGE)
- Adds
1.1:
- Adds
log_levelparam todelayed.logger.setup_logger(). - Prevents different online workers have the same id.
- Adds
1.0:
- Python 2.7 is not supported anymore. (BREAKING CHANGE)
- Supports Go, adds
GoTask. - Use MessagePack instead of pickle to serialize / deserialize tasks. (BREAKING CHANGE)
- Removes
ForkedWorkerandPreforkedWorker. You can useWorkerinstead. (BREAKING CHANGE) - Changes params of
Queue(), removesdefault_timeout,requeue_timeoutandbusy_len, addsdequeue_timeoutandkeep_alive_timeout. (BREAKING CHANGE) - Rename
TasktoPyTask. (BREAKING CHANGE) - Removes those properties of
PyTask:id,func_path,argsandkwargs. (BREAKING CHANGE) - Removes those params of
PyTask():id,timeout,prioranderror_handler_path. (BREAKING CHANGE) - Removes
PyTask.create(). You can usePyTask()instead. (BREAKING CHANGE) - Rename
func_pathparam ofPyTask()tofunc, it accepts bothcallableandstr. (BREAKING CHANGE) - Removes
delayed.delay(). Removes params ofdelayed.delayed(). (BREAKING CHANGE)
0.11:
- Sleeps random time when a
Workerfails to pop ataskbefore retrying.
- Sleeps random time when a
0.10:
- The
Sweepercan handle multiple queues now. Itsqueueparam has been changed toqueues. (BREAKING CHANGE) - Changes the separator between
module_pathandfunc_namefrom.to:. (BREAKING CHANGE)
- The
0.9:
- Adds
prioranderror_handlerparams todelayed(), removes itstimeout()method. (BREAKING CHANGE) - Adds examples.
- Adds
0.8:
- The
Taskstruct has been changed, it's not compatible with older versions. (BREAKING CHANGE)- Removes
module_nameandfunc_namefromTask, addsfunc_pathinstead. - Adds
error_handler_pathtoTask.
- Removes
- Removes
success_handleranderror_handlerfromWorker. (BREAKING CHANGE)
- The
0.7:
- Implements prior task.
0.6:
- Adds
dequeued_len()andindextoQueue.
- Adds
0.5:
- Adds
delayed.task.set_pickle_protocol_version().
- Adds
0.4:
- Refactories and fixes bugs.
0.3:
- Changes param
secondtotimeoutfordelayed(). (BREAKING CHANGE) - Adds debug log.
- Changes param
0.2:
- Adds
timeout()todelayed().
- Adds
0.1:
- Init version.