[NEW] ir.async: Schedule asynchronous jobs

Task: https://www.odoo.com/web#id=2081201&model=project.task
Branch: https://github.com/odoo-dev/odoo/tree/master-2081201-job_queue-juc
Pull Request: https://github.com/odoo/odoo/pull/41535

The problem

Odoo historically only supported two ways of processing tasks: sporadic HTTP
request and periodic CRON tasks. The first is the preferred way to exchange
information with the web application while the later is used to perform some
regular background batch operations.

Several models need to create sporadic background tasks assured to be processed
as soon as possible. The current strategy of doing so is to save the task in
some kind of queue later processed by a CRON. This strategy lacks reactivity due
to low frequency of CRON calls. e.g. (e)mail or marketing "send/start now"
button, payment transactions. Another strategy is to start the processing in a
brand new thread.

Those tasks are fine (to an extend) using a CRON because they do not need to
send back anything to the user. The user basically just fire-and-forget the

When it is required to communicate something back to the user, the CRONs become
useless because they lack a standard way to communicate back to the web
application. There is at the moment no other way then to do the heavy operation
right in the current HTTP Worker, blocking the UI of the user until it finishes
computing... or the process reaper kills the HTTP Worker because the time limit
was reached.

As a solution to the above problems, we introduce a new API to schedule and
process asynchronous tasks with optional web notification.

A solution, ir_async, a Job Queue.

A new ir_async model has been created in the ORM, its job is to expose a (we
hope) simple backend-only API to synchronously create and asynchronously
execute background jobs on demand.

An example is worth a thousand docs, so here is how you create a background task:

class Foo(models.Model):
    _name = 'foo'

    def async_foo(self):

    def foo(self):

That's it, just pass a method bound to a recordset to the call static method
of the ir_async model, you have the guarantee the async_foo method will be
executed soon with the same environment (recordset, user and context). Here
the created task is anonymous, web notification are disabled. In other words
this is a pure backend only task similar to a CRON job.

The method signature is as follow:

def call(self, target, args=None, kwargs=None):
    partial-like API to create asynchronous jobs.

    The method will be called later as ``target(*args, **kwargs)`` in a
    new transaction using a copy of the current environment (user, context,
    recordset).  Arguments must be serializable in JSON.

    The job's return value is stored in JSON in the ``payload`` field.

Please note, beside the 5 common letters, the ir.async model in Odoo has
nothing to do with the asyncio standard library or the async/await

call sequence diagram

To enable web-notification, use the call_notify static method. It behaves the
same as the above function apart that a message is sent via the longpolling bus
every time the job status changes. It also take an additional "description"
argument used to list the job in the systray.

def call_notify(self, description, method, *args, **kwargs):
    partial-like API to create asynchronous jobs with web notification.

Notifications are:

An example of a succeeded task is an export. When the export completes, the
XLSX file is saved in an attachment and the result is an ir.actions.act_url
to download the file. The frontend is expected to call the complete method
when the file has been successfully downloaded by the user.

class Foo(models.Model):
    _name = 'foo'

    def async_foo(self):

    def foo(self):
        job = self.env['ir_async'].call_notify("foo", self.async_foo)
        return {'asyncJobId': job.id}

In the frontend, you get back the jobId and start listening for events.

// const { bus } = require('web.core');
this.jobs = [];
bus.on('async_job', this, (job) => {
    if (this.jobs.contains(job.id)) {
        this.do_action(job.payload.result).then(() => {
            this._rpc({model: 'ir.async', method: 'complete', args: [job.id]});
this._rpc({model: 'foo', method: 'foo'}).then(({asyncJobId}) => {

Alternatively, you may use the asyncRpc method of the new async_job service
if you don't need much control on the job. It takes the same arguments as a
normal rpc would do. The notable difference is the promise doesn't resolve with
the HTTP request but when the async task finishes.

this.call('async_job', 'asyncRpc', {
    model: 'foo', method: 'foo'
}).then((job) => {
    this.do_action(job.payload.result).then(() => {
        this._rpc({model: 'ir.async', method: 'complete', args: [job.id]});

call_notify sequence diagram

When the additional description is truthy, the task is listed in a new
systray menu. You may hide the task from the list but still get the notification
by setting a falsy value.

self.env['ir.async'].call("Employee XLSX export", self.async_foo)

async systray

Technically, how it works ?

You may read the source-code directly before it gets complicated with hackish
optimizations, rusty bugfixes and useless features. Just reading the three
commits bellow should give you a good gasp of what's inside the black box.

full sequence diagram

The solution is built on :

I have a project that may use the job queue, where are the docs ?

There is no sphinx documentation yet, this mail, the task, the commit messages
and the source code are the documentation for now. Please consider the current
API is in a alpha-stage, it is going to change as I gather feedback.

Just reach me if you have any question. I'm Julien#4157 on discord, hiding in
the #rd-framework-py channel.

I suggest you increase the verbosity of a few loggers as the postgres
notifications are sometime hard of hearing (some notification don't wake up the
async workers). At worst you'll experience a 1-minute delay.

Here is the few arguments you can inject in your odoo-bin:

--log-handler odoo.service.server:DEBUG
--log-handler odoo.addons.base.models.ir_async:DEBUG


I would like to thanks both my team, the SaaS, the framework and the discuss
teams for their patience, help and reviews. If there are some architecture
non-sense or nasty stuff in the resulting source code, blame them :D

~The lazer guy