Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
e21b2c9
[DOC] describe how to write queue.job.function in case of function de…
legalsylvain Jul 19, 2021
dc51586
Change technical fields to read-only
Feb 1, 2021
369bdd7
Optimize queue.job creation
Feb 2, 2021
e1f8262
Remove initial create notification and follower
Feb 1, 2021
e026133
Add model in search view / group by
Feb 10, 2021
271d83e
queue_job: add exec time to view some stats
simahawk Feb 8, 2021
fad22b1
Fix missing rollback on retried jobs
wpichler Nov 19, 2020
5befe80
Fix date_done set when state changes back to pending
Feb 10, 2021
a94ef24
queue_job: close buffer when done
simahawk Mar 29, 2021
f60765a
queue_job: improve filtering and grouping
simahawk Mar 29, 2021
94a0ae1
queue_job: add hook to customize stored values
simahawk Mar 29, 2021
8529226
queue_job: migration step to store exception data
simahawk Mar 29, 2021
10b492c
Fix display on exec_time on tree view as seconds
guewen May 31, 2021
2e01ad0
[IMP] queue_job: black, isort, prettier
dzungtran89 Nov 23, 2021
8a94b68
Forward migration scripts from #309 #328
dzungtran89 Nov 23, 2021
cfdcdea
queue_job: store exception name and message
simahawk Mar 29, 2021
5cc31ab
[FIX] queue_job: Migrations raising errors with OpenUpgrade
etobella Jun 22, 2021
8299ab0
[IMP] queue_job: Add cancelled state to queue.job
hbrunn May 14, 2021
695b0d9
[IMP] queue_job: tests for wizards
hbrunn May 21, 2021
69c9872
[IMP] queue_job: use a widget in eta field in queue job tree view
fernandahf Nov 9, 2021
ff20e21
queue_job: use parent channel if configured
simahawk Mar 11, 2022
6bfadde
queue_job: update contributors
simahawk Mar 11, 2022
ca4253f
[UPD] Update queue_job.pot
Oct 31, 2022
0fa94b2
[UPD] README.rst
OCA-git-bot Oct 31, 2022
840a5f2
Update translation files
weblate Oct 31, 2022
42b4645
Store dependencies
Jul 2, 2019
4b05438
Add wait dependencies state
Jul 2, 2019
b027782
Enqueue waiting jobs when parent jobs are done
Jul 3, 2019
64b2371
Optimize and make enqueue of waiting jobs more robust
Jul 4, 2019
802c47b
Adapt views for state wait_dependencies
Jul 5, 2019
8702ff6
Add API for Delayables
Jul 5, 2019
7773a90
Fix tests failing when test_queue_job is installed
Jul 6, 2019
a04200d
Add widget to show job dependencies on UI
Jul 9, 2019
ae2a1e2
Show the dependency widget in a tab
Jul 11, 2019
4ff1352
Add documentation on 'base' model public methods
Oct 4, 2019
0798d1f
Improve loading of dependencies using batch read
Oct 4, 2019
5ae57de
Use Delayable in DelayableRecordset
Oct 4, 2019
78ce4bc
Add documentation
guewen May 24, 2021
95ec8e2
Add a graph UUID
guewen May 26, 2021
c374e0a
Hide some technical fields
guewen May 26, 2021
c1ff4f5
Ignore requeues on dependency jobs waiting on parent jobs
guewen May 26, 2021
90a35ef
Fix lint
guewen May 27, 2021
034820f
Update vis-network js
guewen May 27, 2021
3b07e04
Improve display of jobs graph widget
guewen May 27, 2021
9db828c
Add powerful context manager for running tests on jobs
guewen May 28, 2021
85c08a7
Set graph_uuid only once in DelayableGraph
guewen May 30, 2021
ab4ff42
Add docstrings on the new delayable classes
guewen May 30, 2021
2c2caa7
Add option to generate a graph in create_test_job controller
guewen Jul 1, 2021
91b507e
Improve graph widget
guewen Jul 1, 2021
64e2b83
Add a smart button to open all the jobs of a graph
guewen Jul 1, 2021
57fccd2
ix duplicate label
guewen Jul 1, 2021
6827482
Fix graph widget now showing title as HTML
guewen Jul 1, 2021
2aec248
Improve graph widget performance
guewen Jul 1, 2021
a18233d
Escape strings passed to the graph js widget
guewen Jul 21, 2021
d3d6178
Fix things required by odoo 14.0 or python 3.9
guewen Oct 26, 2022
919821c
Add documentation about handling of failures in a graph
guewen Jan 29, 2022
9d76841
Fix equality of enqueued jobs
guewen Feb 2, 2022
20847eb
Use a python3.6 compatible data class
guewen Feb 2, 2022
e34e4ca
Rename mock_jobs() to trap_jobs()
guewen Feb 3, 2022
c014717
Rename dependency method done() to on_done()
guewen Feb 3, 2022
7d92172
Migrate queue job graph/dependencies to 15.0
guewen Oct 24, 2022
94bd236
Apply pre-commit on migration of jobs graph
guewen Oct 24, 2022
f76df74
[UPD] Update queue_job.pot
Nov 15, 2022
5620196
[UPD] README.rst
OCA-git-bot Nov 15, 2022
d2a47fb
Update translation files
weblate Nov 15, 2022
1cc2f8c
Remove initial create notification and follower
Feb 1, 2021
70bab16
queue_job: add exec time to view some stats
simahawk Feb 8, 2021
0b2f48f
queue_job: add hook to customize stored values
simahawk Mar 29, 2021
5fb0a9a
queue_job: store exception name and message
simahawk Mar 29, 2021
076fd7e
[UPD] Update test_queue_job.pot
Oct 31, 2022
e32e219
Update translation files
weblate Oct 31, 2022
8a22667
Store dependencies
Jul 2, 2019
3b8ad12
Add wait dependencies state
Jul 2, 2019
67555d8
Add API for Delayables
Jul 5, 2019
8f1dc77
Add a graph UUID
guewen May 26, 2021
4230c85
Ignore requeues on dependency jobs waiting on parent jobs
guewen May 26, 2021
4276b1c
Fix warnings in tests
guewen May 27, 2021
5d7a9eb
Improve display of jobs graph widget
guewen May 27, 2021
bccc555
Add powerful context manager for running tests on jobs
guewen May 28, 2021
2723daf
Set graph_uuid only once in DelayableGraph
guewen May 30, 2021
b2d5995
Add docstrings on the new delayable classes
guewen May 30, 2021
82a6cd5
Fix things required by odoo 14.0 or python 3.9
guewen Oct 26, 2022
f6c0e7e
Rename mock_jobs() to trap_jobs()
guewen Feb 3, 2022
50a8d12
Rename dependency method done() to on_done()
guewen Feb 3, 2022
b3352e1
Apply pre-commit on migration of jobs graph
guewen Oct 24, 2022
844b1fd
Use new TransactionCase instead of deprecated SavepointCase
guewen Oct 24, 2022
2a2be55
[MIG] queue_job, test_queue_job: Forward port queue from 15.0
lmignon Nov 16, 2022
1cd4942
[FIX] queue_job: fix invalid po files
lmignon Nov 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 265 additions & 1 deletion queue_job/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,124 @@ To use this module, you need to:
Developers
~~~~~~~~~~

**Configure default options for jobs**
Delaying jobs
-------------

The fast way to enqueue a job for a method is to use ``with_delay()`` on a record
or model:


.. code-block:: python

def button_done(self):
self.with_delay().print_confirmation_document(self.state)
self.write({"state": "done"})
return True

Here, the method ``print_confirmation_document()`` will be executed asynchronously
as a job. ``with_delay()`` can take several parameters to define more precisely how
the job is executed (priority, ...).

All the arguments passed to the method being delayed are stored in the job and
passed to the method when it is executed asynchronously, including ``self``, so
the current record is maintained during the job execution (warning: the context
is not kept).

Dependencies can be expressed between jobs. To start a graph of jobs, use ``delayable()``
on a record or model. The following is the equivalent of ``with_delay()`` but using the
long form:

.. code-block:: python

def button_done(self):
delayable = self.delayable()
delayable.print_confirmation_document(self.state)
delayable.delay()
self.write({"state": "done"})
return True

Methods of Delayable objects return itself, so it can be used as a builder pattern,
which in some cases allow to build the jobs dynamically:

.. code-block:: python

def button_generate_simple_with_delayable(self):
self.ensure_one()
# Introduction of a delayable object, using a builder pattern
# allowing to chain jobs or set properties. The delay() method
# on the delayable object actually stores the delayable objects
# in the queue_job table
(
self.delayable()
.generate_thumbnail((50, 50))
.set(priority=30)
.set(description=_("generate xxx"))
.delay()
)

The simplest way to define a dependency is to use ``.on_done(job)`` on a Delayable:

.. code-block:: python

def button_chain_done(self):
self.ensure_one()
job1 = self.browse(1).delayable().generate_thumbnail((50, 50))
job2 = self.browse(1).delayable().generate_thumbnail((50, 50))
job3 = self.browse(1).delayable().generate_thumbnail((50, 50))
# job 3 is executed when job 2 is done which is executed when job 1 is done
job1.on_done(job2.on_done(job3)).delay()

Delayables can be chained to form more complex graphs using the ``chain()`` and
``group()`` primitives.
A chain represents a sequence of jobs to execute in order, a group represents
jobs which can be executed in parallel. Using ``chain()`` has the same effect as
using several nested ``on_done()`` but is more readable. Both can be combined to
form a graph, for instance we can group [A] of jobs, which blocks another group
[B] of jobs. When and only when all the jobs of the group [A] are executed, the
jobs of the group [B] are executed. The code would look like:

.. code-block:: python

from odoo.addons.queue_job.delay import group, chain

def button_done(self):
group_a = group(self.delayable().method_foo(), self.delayable().method_bar())
group_b = group(self.delayable().method_baz(1), self.delayable().method_baz(2))
chain(group_a, group_b).delay()
self.write({"state": "done"})
return True

When a failure happens in a graph of jobs, the execution of the jobs that depend on the
failed job stops. They remain in a state ``wait_dependencies`` until their "parent" job is
successful. This can happen in two ways: either the parent job retries and is successful
on a second try, either the parent job is manually "set to done" by a user. In these two
cases, the dependency is resolved and the graph will continue to be processed. Alternatively,
the failed job and all its dependent jobs can be canceled by a user. The other jobs of the
graph that do not depend on the failed job continue their execution in any case.

Note: ``delay()`` must be called on the delayable, chain, or group which is at the top
of the graph. In the example above, if it was called on ``group_a``, then ``group_b``
would never be delayed (but a warning would be shown).


Enqueing Job Options
--------------------

* priority: default is 10, the closest it is to 0, the faster it will be
executed
* eta: Estimated Time of Arrival of the job. It will not be executed before this
date/time
* max_retries: default is 5, maximum number of retries before giving up and set
the job state to 'failed'. A value of 0 means infinite retries.
* description: human description of the job. If not set, description is computed
from the function doc or method name
* channel: the complete name of the channel to use to process the function. If
specified it overrides the one defined on the function
* identity_key: key uniquely identifying the job, if specified and a job with
the same key has not yet been run, the new job will not be created

Configure default options for jobs
----------------------------------

In earlier versions, jobs could be configured using the ``@job`` decorator.
This is now obsolete, they can be configured using optional ``queue.job.function``
Expand Down Expand Up @@ -177,6 +294,13 @@ they have different xmlids. On uninstall, the merged record is deleted when all
the modules using it are uninstalled.


**Job function: model**

If the function is defined in an abstract model, you can not write
``<field name="model_id" ref="xml_id_of_the_abstract_model"</field>``
but you have to define a function for each model that inherits from the abstract model.


**Job function: channel**

The channel where the job will be delayed. The default channel is ``root``.
Expand Down Expand Up @@ -287,6 +411,145 @@ Tip: you can do this at test case level like this
Then all your tests execute the job methods synchronously
without delaying any jobs.

Testing
-------

**Asserting enqueued jobs**

The recommended way to test jobs, rather than running them directly and synchronously is to
split the tests in two parts:

* one test where the job is mocked (trap jobs with ``trap_jobs()`` and the test
only verifies that the job has been delayed with the expected arguments
* one test that only calls the method of the job synchronously, to validate the
proper behavior of this method only

Proceeding this way means that you can prove that jobs will be enqueued properly
at runtime, and it ensures your code does not have a different behavior in tests
and in production (because running your jobs synchronously may have a different
behavior as they are in the same transaction / in the middle of the method).
Additionally, it gives more control on the arguments you want to pass when
calling the job's method (synchronously, this time, in the second type of
tests), and it makes tests smaller.

The best way to run such assertions on the enqueued jobs is to use
``odoo.addons.queue_job.tests.common.trap_jobs()``.

A very small example (more details in ``tests/common.py``):

.. code-block:: python

# code
def my_job_method(self, name, count):
self.write({"name": " ".join([name] * count)

def method_to_test(self):
count = self.env["other.model"].search_count([])
self.with_delay(priority=15).my_job_method("Hi!", count=count)
return count

# tests
from odoo.addons.queue_job.tests.common import trap_jobs

# first test only check the expected behavior of the method and the proper
# enqueuing of jobs
def test_method_to_test(self):
with trap_jobs() as trap:
result = self.env["model"].method_to_test()
expected_count = 12

trap.assert_jobs_count(1, only=self.env["model"].my_job_method)
trap.assert_enqueued_job(
self.env["model"].my_job_method,
args=("Hi!",),
kwargs=dict(count=expected_count),
properties=dict(priority=15)
)
self.assertEqual(result, expected_count)


# second test to validate the behavior of the job unitarily
def test_my_job_method(self):
record = self.env["model"].browse(1)
record.my_job_method("Hi!", count=12)
self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!")

If you prefer, you can still test the whole thing in a single test, by calling
``jobs_tester.perform_enqueued_jobs()`` in your test.

.. code-block:: python

def test_method_to_test(self):
with trap_jobs() as trap:
result = self.env["model"].method_to_test()
expected_count = 12

trap.assert_jobs_count(1, only=self.env["model"].my_job_method)
trap.assert_enqueued_job(
self.env["model"].my_job_method,
args=("Hi!",),
kwargs=dict(count=expected_count),
properties=dict(priority=15)
)
self.assertEqual(result, expected_count)

trap.perform_enqueued_jobs()

record = self.env["model"].browse(1)
record.my_job_method("Hi!", count=12)
self.assertEqual(record.name, "Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi! Hi!")

**Execute jobs synchronously when running Odoo**

When you are developing (ie: connector modules) you might want
to bypass the queue job and run your code immediately.

To do so you can set ``TEST_QUEUE_JOB_NO_DELAY=1`` in your environment.

.. WARNING:: Do not do this in production

**Execute jobs synchronously in tests**

You should use ``trap_jobs``, really, but if for any reason you could not use it,
and still need to have job methods executed synchronously in your tests, you can
do so by setting ``test_queue_job_no_delay=True`` in the context.

Tip: you can do this at test case level like this

.. code-block:: python

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.env = cls.env(context=dict(
cls.env.context,
test_queue_job_no_delay=True, # no jobs thanks
))

Then all your tests execute the job methods synchronously without delaying any
jobs.

In tests you'll have to mute the logger like:

@mute_logger('odoo.addons.queue_job.models.base')

.. NOTE:: in graphs of jobs, the ``test_queue_job_no_delay`` context key must be in at
least one job's env of the graph for the whole graph to be executed synchronously


Tips and tricks
---------------

* **Idempotency** (https://www.restapitutorial.com/lessons/idempotency.html): The queue_job should be idempotent so they can be retried several times without impact on the data.
* **The job should test at the very beginning its relevance**: the moment the job will be executed is unknown by design. So the first task of a job should be to check if the related work is still relevant at the moment of the execution.

Patterns
--------
Through the time, two main patterns emerged:

1. For data exposed to users, a model should store the data and the model should be the creator of the job. The job is kept hidden from the users
2. For technical data, that are not exposed to the users, it is generally alright to create directly jobs with data passed as arguments to the job, without intermediary models.

Known issues / Roadmap
======================

Expand Down Expand Up @@ -364,6 +627,7 @@ Contributors
* Tatiana Deribina <tatiana.deribina@avoin.systems>
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
* Eric Antones <eantones@nuobit.com>
* Simone Orsi <simone.orsi@camptocamp.com>

Maintainers
~~~~~~~~~~~
Expand Down
9 changes: 7 additions & 2 deletions queue_job/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)


{
"name": "Job Queue",
"version": "16.0.1.1.0",
"author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/queue",
"license": "LGPL-3",
"category": "Generic Modules",
"depends": ["mail", "base_sparse_field"],
"depends": ["mail", "base_sparse_field", "web"],
"external_dependencies": {"python": ["requests"]},
"data": [
"security/security.xml",
Expand All @@ -17,11 +16,17 @@
"views/queue_job_channel_views.xml",
"views/queue_job_function_views.xml",
"wizards/queue_jobs_to_done_views.xml",
"wizards/queue_jobs_to_cancelled_views.xml",
"wizards/queue_requeue_job_views.xml",
"views/queue_job_menus.xml",
"data/queue_data.xml",
"data/queue_job_function_data.xml",
],
"assets": {
"web.assets_backend": [
"/queue_job/static/src/views/**/*",
],
},
"installable": True,
"development_status": "Mature",
"maintainers": ["guewen"],
Expand Down
Loading