Package: delayed
delayedDelayed is a simple but robust task queue inspired byrq.FeaturesRobust: all the enqueued tasks will run exactly once, even if the worker got killed at any time.Clean: finished tasks (including failed) won't take the space of your Redis.Distributed: workers as more as needed can run in the same time without further config.RequirementsPython 2.7 or later, tested on Python 2.7, 3.3 - 3.9, PyPy and PyPy3.UNIX-like systems (with os.fork() implemented), tested on Ubuntu and macOS.Redis 2.6.0 or later.Keeps syncing time among all the machines of each task queue.Getting startedRun a redis server:$redis-serverInstall delayed:$pipinstalldelayedCreate a task queue:importredisfromdelayed.queueimportQueueconn=redis.Redis()queue=Queue(name='default',conn=conn)Four ways to enqueue a task:Define a task function and enqueue it:fromdelayed.delayimportdelayeddelayed=delayed(queue)@delayed()defdelayed_add(a,b):returna+bdelayed_add.delay(1,2)# enqueue delayed_adddelayed_add.delay(1,b=2)# same as abovedelayed_add(1,2)# call it immediatelyDirectly enqueue a function:fromdelayed.delayimportdelay,delayeddelay=delay(queue)delayed=delayed(queue)defadd(a,b):returna+bdelay(add)(1,2)delay(add)(1,b=2)# same as abovedelayed()(add).delay(1,2)delayed()(add).delay(1,b=2)# same as aboveCreate a task and enqueue it:fromdelayed.taskimportTaskdefadd(a,b):returna+btask=Task.create(func=add,args=(1,),kwargs={'b':2})queue.enqueue(task)Enqueue a predefined task function without importing it:fromdelayed.taskimportTasktask=Task(id=None,func_path='test:add',args=(1,),kwargs={'b':2})queue.enqueue(task)Run a task worker (or more) in a separated process:importredisfromdelayed.queueimportQueuefromdelayed.workerimportForkedWorkerconn=redis.Redis()queue=Queue(name='default',conn=conn)worker=ForkedWorker(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()ExamplesSeeexamples.```bash
$ redis-server &
$ pip install delayed
$ python -m examples.sweeper &
$ python -m examples.forked_worker &  # or python -m examples.preforked_worker &
$ python -m examples.caller
```QAQ: What's the limitation on a task function?A: A task function should be defined in module level (except the__main__module). Itsargsandkwargsshould be picklable.Q: What's thenameparam 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_id: str, the next task id.default_noti: list, the same length as enqueued tasks.default_enqueued: sorted set, enqueued tasks with their timeouts.default_dequeued: sorted set, dequeued tasks with their dequeued timestamps.Q: Why the worker is slow?A: TheForkedWorkerforks a new process for each new task. So all the tasks are isolated and you won't leak memory.To reduce the overhead of forking processes and importing modules, if your task function code won't be changed in the worker's lifetime, you can switch toPreforkedWorker:importredisfromdelayed.queueimportQueuefromdelayed.workerimportPreforkedWorkerconn=redis.Redis()queue=Queue(name='default',conn=conn)worker=PreforkedWorker(queue=queue)worker.run()Q: How does aForkedWorkerrun?A: It runs such a loop:It dequeues a task from the queue periodically.It forks a child process to run the task.It kills the child process if the child runs out of time.When the child process exits, it releases the task.Q: How does aPreforkedWorkerrun?A: It runs such a loop:It dequeues a task from the queue periodically.If it has no child process, it forks a new one.It sends the task through a pipe to the child.It kills the child process if the child runs out of time.When the child process exits or it received result from the pipe, it releases the task.Q: How does the child process of a worker run?A: The child of aForkedWorkerjust runs the task, unmarks the task as dequeued, then exits.
The child of aPreforkedWorkerruns such a loop:It tries to receive a task from the pipe.If the pipe has been closed, it exits.It runs the task.It sends the task result to the pipe.It releases the task.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 both the monitor and its child process got killed before they 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 moves the timeout dequeued tasks back to the task queue.Q: How to set the timeout of tasks?A: You can setdefault_timeoutof a queue ortimeoutof a task:fromdelayed.delayimportdelay_with_paramsqueue=Queue('default',conn,default_timeout=60)delayed_add.timeout(10)(1,2)delay_with_params(queue)(timeout=10)(add)(1,2)Q: How to enqueue a task in front of the queue?A: You can setpriorof the task toTrue:task=Task(id=None,func_path='test:add',args=(1,2),prior=True)queue.enqueue(task)Q: How to handle the failed tasks?A: Sets theerror_handlerof the task. The handlers would be called in a forked process, except the forked process got killed or the monitor process raised an exception.fromdelayed.delayimportdelay_with_paramsdeferror_handler(task,kill_signal,exc_info):ifkill_signal:logging.error('task%dgot killed by signal%d',task.id,kill_signal)else:logging.exception('task%dfailed',task.id,exc_info=exc_info)@delayed_with_param(queue)(error_handler=error_handler)deferror():raiseExceptiondeferror2():raiseExceptiontask=Task.create(func_path='test:error2',error_handler=error_handler)Q: Why does sometimes theerror_handlernot be called for a failed task?A: If both the child process and the monitor process got killed at the same time, there is no chance to call theerror_handler.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: Can I enqueue and dequeue tasks in different Python versions?A:delayeduses thepicklemodule to serialize and deserialize tasks.
Ifpickle.HIGHEST_PROTOCOLis equal among all your Python runtimes, you can use it without any configurations.
Otherwise you have to choose the lowestpickle.HIGHEST_PROTOCOLof all your Python runtime as the pickle protocol.
eg: If you want to enqueue a task in Python 3.7 and dequeue it in Python 2.7. Theirpickle.HIGHEST_PROTOCOLare4and2, so you need to set the version to2:fromdelayed.taskimportset_pickle_protocol_versionset_pickle_protocol_version(2)Q: Why not use JSON or MessagePack to serialize tasks?A: These serializations may confuse some types (eg:bytes/str,list/tuple).Q: What will happen if I changed the pipe capacity?A:delayedassumes the pipe capacity is 65536 bytes (the default value on Linux and macOS).
To reduce syscalls, it won't check whether the pipe is writable if the length of data to be written is less than 65536.
If your system has a lower pipe capacity, thePreforkedWorkermay not working well for some large tasks.
To fix it, you can set a lower value todelayed.constants.BUF_SIZE:importdelayed.constantsdelayed.constants.BUF_SIZE=1024Release notes0.11:Sleeps random time when aWorkerfails to pop ataskbefore retrying.0.10:TheSweepercan handle multiple queues now. Itsqueueparam has been changed toqueues. (BREAKING CHANGE)Changes the separator betweenmodule_pathandfunc_namefrom.to:. (BREAKING CHANGE)0.9:Addsprioranderror_handlerparams todeleyed.delayed(), removes itstimeout()method. (BREAKING CHANGE)Addsexamples.0.8:TheTaskstruct has been changed, it's not compatible with older versions. (BREAKING CHANGE)Removesmodule_nameandfunc_namefromTask, addsfunc_pathinstead.Addserror_handler_pathtoTask.Removessuccess_handleranderror_handlerfromWorker. (BREAKING CHANGE)0.7:Implements prior task.0.6:Addsdequeued_len()andindextoQueue.0.5:Addsdelayed.task.set_pickle_protocol_version().0.4:Refactories and fixes bugs.0.3:Changes paramsecondtotimeoutfordelayed.delayed(). (BREAKING CHANGE)Adds debug log.0.2:Addstimeout()todelayed.delayed().0.1:Init version.