From 68d6bc0afb67d9e490c807557d5eb10703df5c87 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 28 Oct 2020 16:47:41 +0100 Subject: [PATCH] Add job_auto_delay decorator This is a new decorator, to use instead of @job on methods, that transforms a method to a method automatically delayed as job when called. 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. --- queue_job/job.py | 75 +++++++++++++++++++++ test_queue_job/models/test_models.py | 15 ++++- test_queue_job/tests/__init__.py | 1 + test_queue_job/tests/test_job_auto_delay.py | 37 ++++++++++ 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 test_queue_job/tests/test_job_auto_delay.py diff --git a/queue_job/job.py b/queue_job/job.py index a7b7912981..5d60c723e4 100644 --- a/queue_job/job.py +++ b/queue_job/job.py @@ -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), + ) diff --git a/test_queue_job/models/test_models.py b/test_queue_job/models/test_models.py index 0478318caa..374545ceba 100644 --- a/test_queue_job/models/test_models.py +++ b/test_queue_job/models/test_models.py @@ -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): @@ -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): diff --git a/test_queue_job/tests/__init__.py b/test_queue_job/tests/__init__.py index 9af8df15a0..502a0752fd 100644 --- a/test_queue_job/tests/__init__.py +++ b/test_queue_job/tests/__init__.py @@ -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 diff --git a/test_queue_job/tests/test_job_auto_delay.py b/test_queue_job/tests/test_job_auto_delay.py new file mode 100644 index 0000000000..72b4973330 --- /dev/null +++ b/test_queue_job/tests/test_job_auto_delay.py @@ -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 _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))