Best Practices
Do NOT Block the Event Loop!
Mognet leverages Python's asyncio
to run itself, the tasks, and for managing connections to the Task Broker and Result Backend, and, by design, it doesn't use processes or threads to run tasks or background work (unless you define a @task
that isn't an async def
function).
This results in a simpler architecture, but the result is that the code must not block for long periods of time (ideally, less than 15s).
This is because, async def
s run pseudo-concurrently. If a function is designed in such a way that it blocks the CPU for too long (a CPU-bound intensive computation, for example), even if it is async def
, it will block the event loop, and prevent other coroutines from running.
If done for too long, it will prevent background tasks from running, which, in case of an AMQP-based Task Broker, stops connection keep-alives from being sent, which will result in disconnects.
If the task must do CPU-bound work for too long, consider using run_in_executor()
from asyncio
, which allows executing such code in a ThreadPoolExecutor
or ProcessPoolExecutor
.
Also, prefer using asyncio
-friendly libraries. See https://github.com/timofurrer/awesome-asyncio for some examples.
If your task function code is fully CPU bound, consider making it a non async def
function. This will make it run in the Event Loop's default executor.
AVOID Long Running Tasks
If you have a job that runs for a long period of time (think: hours), you may run into issues like RabbitMQ disconnects. These tasks can also be considered to be more fragile to run, because they will have to restart fully from scratch, in the event of a Broker Disconnect, Node reboot (e.g., when running under Kubernetes). Long running tasks, especially if they don't yield to wait for other tasks, will also hold resources for longer periods of time, which can cause other tasks to not run, if the system is under pressure.
Therefore, it is a good idea to split the work into smaller chunks, and have each chunk be processed separately. If there is still a need to have a task orchestrating them all, you can use State variables to create "checkpoints", which you can then use to know where your task should resume from.
It is also a good idea to, when possible, make use of the Pause
exception to pause a task's function. This will cause the task message to be put back into the queue, so that it can be resumed at a later stage. See Pause a Task.
Note: Currently it is not possible to "delay" the re-run of the task, though that is something to consider, provided there's a need for it.
DO Set the Task's Name Yourself
When using the @task
decorator, you can choose to omit name
. In that case, the function's full name (i.e., __module__.__name__
) will be used instead. That is, if you have a function my_task
in a module named demo
, the default name would be demo.my_task
.
While convenient, this is not something we recommend doing because, if you change the function's name, or move it to another file, especially during a rolling upgrade, tasks may break. That is because the name that is set on the decorator is also the name used for the Request
object that is created.
Therefore, we recommend always setting the task's name manually to prevent breaking changes when the functions are moved around or renamed.