Skip to content

Commit b0aca1e

Browse files
author
Guewen Baconnier
committed
Add method to patch a method to be automatically delayed
This patch method has to be called in ``_register_hook``. When a method is patched, any call to the method will not directly execute the method's body, but will instead enqueue a job. When a ``context_key`` is set when calling ``_patch_job_auto_delay``, the patched method is automatically delayed only when this key is ``True`` in the caller's context. It is advised to patch the method with a ``context_key``, because making the automatic delay *in any case* can produce nasty and unexpected side effects (e.g. another module calls the method and expects it to be computed before doing something else, expecting a result, ...). 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. It relies on #274 that deprecates the `@job` decorator.
1 parent 14712ca commit b0aca1e

4 files changed

Lines changed: 171 additions & 0 deletions

File tree

queue_job/models/base.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright 2016 Camptocamp
22
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
33

4+
import functools
45
import inspect
56
import logging
67
import os
@@ -108,3 +109,94 @@ def with_delay(
108109
channel=channel,
109110
identity_key=identity_key,
110111
)
112+
113+
def _patch_job_auto_delay(self, method_name, context_key=None):
114+
"""Patch a method to be automatically delayed as job method when called
115+
116+
This patch method has to be called in ``_register_hook`` (example
117+
below).
118+
119+
When a method is patched, any call to the method will not directly
120+
execute the method's body, but will instead enqueue a job.
121+
122+
When a ``context_key`` is set when calling ``_patch_job_auto_delay``,
123+
the patched method is automatically delayed only when this key is
124+
``True`` in the caller's context. It is advised to patch the method
125+
with a ``context_key``, because making the automatic delay *in any
126+
case* can produce nasty and unexpected side effects (e.g. another
127+
module calls the method and expects it to be computed before doing
128+
something else, expecting a result, ...).
129+
130+
A typical use case is when a method in a module we don't control is
131+
called synchronously in the middle of another method, and we'd like all
132+
the calls to this method become asynchronous.
133+
134+
The options of the job usually passed to ``with_delay()`` (priority,
135+
description, identity_key, ...) can be returned in a dictionary by a
136+
method named after the name of the method suffixed by ``_job_options``
137+
which takes the same parameters as the initial method.
138+
139+
It is still possible to force synchronous execution of the method by
140+
setting a key ``_job_force_sync`` to True in the environment context.
141+
142+
Example patching the "foo" method to be automatically delayed as job
143+
(the job options method is optional):
144+
145+
.. code-block:: python
146+
147+
# original method:
148+
def foo(self, arg1):
149+
print("hello", arg1)
150+
151+
def large_method(self):
152+
# doing a lot of things
153+
self.foo("world)
154+
# doing a lot of other things
155+
156+
def button_x(self):
157+
self.with_context(auto_delay_foo=True).large_method()
158+
159+
# auto delay patch:
160+
def foo_job_options(self, arg1):
161+
return {
162+
"priority": 100,
163+
"description": "Saying hello to {}".format(arg1)
164+
}
165+
166+
def _register_hook(self):
167+
self._patch_method(
168+
"foo",
169+
self._patch_job_auto_delay("foo", context_key="auto_delay_foo")
170+
)
171+
return super()._register_hook()
172+
173+
The result when ``button_x`` is called, is that a new job for ``foo``
174+
is delayed.
175+
"""
176+
177+
def auto_delay_wrapper(self, *args, **kwargs):
178+
# when no context_key is set, we delay in any case (warning, can be
179+
# dangerous)
180+
context_delay = self.env.context.get(context_key) if context_key else True
181+
if (
182+
self.env.context.get("job_uuid")
183+
or not context_delay
184+
or self.env.context.get("_job_force_sync")
185+
or self.env.context.get("test_queue_job_no_delay")
186+
):
187+
# we are in the job execution
188+
return auto_delay_wrapper.origin(self, *args, **kwargs)
189+
else:
190+
# replace the synchronous call by a job on itself
191+
method_name = auto_delay_wrapper.origin.__name__
192+
job_options_method = getattr(
193+
self, "{}_job_options".format(method_name), None
194+
)
195+
job_options = {}
196+
if job_options_method:
197+
job_options.update(job_options_method(*args, **kwargs))
198+
delayed = self.with_delay(**job_options)
199+
return getattr(delayed, method_name)(*args, **kwargs)
200+
201+
origin = getattr(self, method_name)
202+
return functools.update_wrapper(auto_delay_wrapper, origin)

test_queue_job/models/test_models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,33 @@ def job_alter_mutable(self, mutable_arg, mutable_kwarg=None):
6161
mutable_kwarg["b"] = 2
6262
return mutable_arg, mutable_kwarg
6363

64+
def delay_me(self, arg, kwarg=None):
65+
return arg, kwarg
66+
67+
def delay_me_options_job_options(self):
68+
return {
69+
"identity_key": "my_job_identity",
70+
}
71+
72+
def delay_me_options(self):
73+
return "ok"
74+
75+
def delay_me_context_key(self):
76+
return "ok"
77+
78+
def _register_hook(self):
79+
self._patch_method("delay_me", self._patch_job_auto_delay("delay_me"))
80+
self._patch_method(
81+
"delay_me_options", self._patch_job_auto_delay("delay_me_options")
82+
)
83+
self._patch_method(
84+
"delay_me_context_key",
85+
self._patch_job_auto_delay(
86+
"delay_me_context_key", context_key="auto_delay_delay_me_context_key"
87+
),
88+
)
89+
return super()._register_hook()
90+
6491

6592
class TestQueueChannel(models.Model):
6693

test_queue_job/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from . import test_autovacuum
22
from . import test_job
3+
from . import test_job_auto_delay
34
from . import test_job_channels
45
from . import test_related_actions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright 2020 Camptocamp SA
2+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
3+
4+
from odoo.addons.queue_job.job import Job
5+
6+
from .common import JobCommonCase
7+
8+
9+
class TestJobAutoDelay(JobCommonCase):
10+
"""Test auto delay of jobs"""
11+
12+
def test_auto_delay(self):
13+
"""method decorated by @job_auto_delay is automatically delayed"""
14+
result = self.env["test.queue.job"].delay_me(1, kwarg=2)
15+
self.assertTrue(isinstance(result, Job))
16+
self.assertEqual(result.args, (1,))
17+
self.assertEqual(result.kwargs, {"kwarg": 2})
18+
19+
def test_auto_delay_options(self):
20+
"""method automatically delayed une <method>_job_options arguments"""
21+
result = self.env["test.queue.job"].delay_me_options()
22+
self.assertTrue(isinstance(result, Job))
23+
self.assertEqual(result.identity_key, "my_job_identity")
24+
25+
def test_auto_delay_inside_job(self):
26+
"""when a delayed job is processed, it must not delay itself"""
27+
job_ = self.env["test.queue.job"].delay_me(1, kwarg=2)
28+
self.assertTrue(job_.perform(), (1, 2))
29+
30+
def test_auto_delay_force_sync(self):
31+
"""method forced to run synchronously"""
32+
result = (
33+
self.env["test.queue.job"]
34+
.with_context(_job_force_sync=True)
35+
.delay_me(1, kwarg=2)
36+
)
37+
self.assertTrue(result, (1, 2))
38+
39+
def test_auto_delay_context_key_set(self):
40+
"""patched with context_key delays only if context keys is set"""
41+
result = (
42+
self.env["test.queue.job"]
43+
.with_context(auto_delay_delay_me_context_key=True)
44+
.delay_me_context_key()
45+
)
46+
self.assertTrue(isinstance(result, Job))
47+
48+
def test_auto_delay_context_key_unset(self):
49+
"""patched with context_key do not delay if context keys is not set"""
50+
result = self.env["test.queue.job"].delay_me_context_key()
51+
self.assertEqual(result, "ok")

0 commit comments

Comments
 (0)