Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
75 changes: 75 additions & 0 deletions queue_job/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,3 +899,78 @@ def decorate(func):
return func

return decorate


def job_auto_delay(func=None, default_channel="root", retry_pattern=None):
"""Decorator to automatically delay as job method when called

The decorator applies ``odoo.addons.queue_job.job`` at the same time,
so the decorated method is listed in job functions. The arguments
are the same, propagated to the ``job`` decorator.

When a method is decorated by ``job_auto_delay``, any call to the method
will not directly execute the method's body, but will instead enqueue a
job.

A typical use case is when a method in a module we don't control is called
synchronously in the middle of another method, and we'd like all the calls
to this method become asynchronous.

The options of the job usually passed to ``with_delay()`` (priority,
description, identity_key, ...) can be returned in a dictionary by a method
named after the name of the method suffixed by ``_job_options`` which takes
the same parameters as the initial method.

It is still possible to directly execute the method by setting a key
``_job_force_sync`` to True in the environment context.

Example:
.. code-block:: python
class ProductProduct(models.Model):
_inherit = 'product.product'

def foo_job_options(self, arg1):
return {
"priority": 100,
"description": "Saying hello to {}".format(arg1)
}

@job_auto_delay(default_channel="root.channel1")
def foo(self, arg1):
print("hello", arg1)

def button_x(self):
foo("world")

The result when ``button_x`` is called, is that a new job for ``foo`` is
delayed.
"""
if func is None:
return functools.partial(
job_auto_delay, default_channel=default_channel, retry_pattern=retry_pattern
)

def auto_delay(self, *args, **kwargs):
if (
self.env.context.get("job_uuid")
or self.env.context.get("_job_force_sync")
or self.env.context.get("test_queue_job_no_delay")
):
# we are in the job execution
return func(self, *args, **kwargs)
else:
# replace the synchronous call by a job on itself
method_name = func.__name__
job_options_method = getattr(
self, "{}_job_options".format(method_name), None
)
job_options = {}
if job_options_method:
job_options.update(job_options_method(*args, **kwargs))
delayed = self.with_delay(**job_options)
return getattr(delayed, method_name)(*args, **kwargs)

return functools.update_wrapper(
auto_delay,
job(func, default_channel=default_channel, retry_pattern=retry_pattern),
)
15 changes: 14 additions & 1 deletion test_queue_job/models/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from odoo import fields, models

from odoo.addons.queue_job.exception import RetryableJobError
from odoo.addons.queue_job.job import job, related_action
from odoo.addons.queue_job.job import job, job_auto_delay, related_action


class QueueJob(models.Model):
Expand Down Expand Up @@ -69,6 +69,19 @@ def job_alter_mutable(self, mutable_arg, mutable_kwarg=None):
mutable_kwarg["b"] = 2
return mutable_arg, mutable_kwarg

@job_auto_delay
def delay_me(self, arg, kwarg=None):
return arg, kwarg

def delay_me_options_job_options(self):
return {
"identity_key": "my_job_identity",
}

@job_auto_delay
def delay_me_options(self):
return "ok"


class TestQueueChannel(models.Model):

Expand Down
1 change: 1 addition & 0 deletions test_queue_job/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import test_autovacuum
from . import test_job
from . import test_job_auto_delay
from . import test_job_channels
from . import test_related_actions
37 changes: 37 additions & 0 deletions test_queue_job/tests/test_job_auto_delay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2020 Camptocamp SA
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)

from odoo.addons.queue_job.job import Job

from .common import JobCommonCase


class TestJobAutoDelay(JobCommonCase):
"""Test auto delay of jobs"""

def test_auto_delay(self):
"""method decorated by @job_auto_delay is automatically delayed"""
result = self.env["test.queue.job"].delay_me(1, kwarg=2)
self.assertTrue(isinstance(result, Job))
self.assertEqual(result.args, (1,))
self.assertEqual(result.kwargs, {"kwarg": 2})

def test_auto_delay_options(self):
"""method automatically delayed une <method>_job_options arguments"""
result = self.env["test.queue.job"].delay_me_options()
self.assertTrue(isinstance(result, Job))
self.assertEqual(result.identity_key, "my_job_identity")

def test_auto_delay_inside_job(self):
"""when a delayed job is processed, it must not delay itself"""
job_ = self.env["test.queue.job"].delay_me(1, kwarg=2)
self.assertTrue(job_.perform(), (1, 2))

def test_auto_delay_force_sync(self):
"""method forced to run synchronously"""
result = (
self.env["test.queue.job"]
.with_context(_job_force_sync=True)
.delay_me(1, kwarg=2)
)
self.assertTrue(result, (1, 2))