Skip to content

Commit 07b971c

Browse files
committed
[IMP] queue_job: add error_handler
1 parent 2413ef6 commit 07b971c

File tree

11 files changed

+151
-1
lines changed

11 files changed

+151
-1
lines changed

queue_job/README.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Features:
7474
description, number of retries
7575
* Related Actions: link an action on the job view, such as open the record
7676
concerned by the job
77+
* Error Handler: trigger a method when job fails, such as calling a webhook
7778

7879
**Table of contents**
7980

@@ -429,6 +430,21 @@ Based on this configuration, we can tell that:
429430
* retries 10 to 15 postponed 30 seconds later
430431
* all subsequent retries postponed 5 minutes later
431432

433+
**Job function: Error Handler**
434+
435+
The *Error Handler* is a method executed whenever the job fails
436+
437+
It's configured similarly to Related Action
438+
439+
There is an OOTB handler: _call_webhook, which calls a webhook with configurable information.
440+
441+
Example of using _call_webhook to call a webhook to Slack:
442+
443+
.. code-block:: xml
444+
445+
<field name="error_handler" eval='{"func_name": "_call_webhook", "kwargs": {"webhook_url": "XXX", "only_if_max_retries_reached":True, "payload": {"text": "Hello World!"}}}' />
446+
447+
432448
**Job Context**
433449

434450
The context of the recordset of the job, or any recordset passed in arguments of
@@ -687,6 +703,7 @@ Contributors
687703
* Souheil Bejaoui <souheil.bejaoui@acsone.eu>
688704
* Eric Antones <eantones@nuobit.com>
689705
* Simone Orsi <simone.orsi@camptocamp.com>
706+
* Tris Doan <tridm@trobz.com>
690707
691708
Maintainers
692709
~~~~~~~~~~~

queue_job/controllers/main.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,14 @@ def _try_perform_job(self, env, job):
3333
env.cr.commit()
3434
_logger.debug("%s started", job)
3535

36-
job.perform()
36+
try:
37+
job.perform()
38+
except Exception as exc:
39+
with registry(job.env.cr.dbname).cursor() as new_cr:
40+
job.env = job.env(cr=new_cr)
41+
job.error_handler(exc)
42+
raise
43+
3744
# Triggers any stored computed fields before calling 'set_done'
3845
# so that will be part of the 'exec_time'
3946
env.flush_all()

queue_job/job.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,26 @@ def related_action(self):
891891
action_kwargs = self.job_config.related_action_kwargs
892892
return action(**action_kwargs)
893893

894+
def error_handler(self, exc):
895+
record = self.db_record()
896+
funcname = self.job_config.error_handler_func_name
897+
if not self.job_config.error_handler_enable or not funcname:
898+
return None
899+
900+
if not isinstance(funcname, str):
901+
raise ValueError(
902+
"error_handler must be the name of the method on queue.job as string"
903+
)
904+
action = getattr(record, funcname)
905+
_logger.info("Job %s fails due to %s, execute %s", self.uuid, exc, action)
906+
action_kwargs = self.job_config.error_handler_kwargs
907+
action_kwargs["job"] = self
908+
action_kwargs["job"] = self
909+
try:
910+
return action(**action_kwargs)
911+
except Exception as exc:
912+
_logger.warning("Error handler failed: %s", exc)
913+
894914

895915
def _is_model_method(func):
896916
return inspect.ismethod(func) and isinstance(

queue_job/models/queue_job.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
# Copyright 2013-2020 Camptocamp SA
22
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
33

4+
import json
45
import logging
56
import random
67
from datetime import datetime, timedelta
78

9+
import requests
10+
811
from odoo import _, api, exceptions, fields, models
912
from odoo.osv import expression
1013
from odoo.tools import config, html_escape
@@ -506,3 +509,31 @@ def _test_job(self, failure_rate=0):
506509
_logger.info("Running test job.")
507510
if random.random() <= failure_rate:
508511
raise JobError("Job failed")
512+
513+
def _call_webhook(self, **kwargs):
514+
only_if_max_retries_reached = kwargs.get("only_if_max_retries_reached", False)
515+
job = kwargs.get("job")
516+
if only_if_max_retries_reached and (job and job.retry < job.max_retries):
517+
return
518+
519+
webhook_url = kwargs.get("webhook_url", None)
520+
if not webhook_url:
521+
return
522+
payload = kwargs.get("payload", None)
523+
json_values = json.dumps(payload, sort_keys=True, default=str)
524+
headers = kwargs.get("headers", {"Content-Type": "application/json"})
525+
# inspired by https://github.com/odoo/odoo/blob/18.0/odoo/addons/base
526+
# /models/ir_actions.py#L867
527+
try:
528+
response = requests.post(
529+
url=webhook_url, data=json_values, headers=headers, timeout=1
530+
)
531+
response.raise_for_status()
532+
except requests.exceptions.ReadTimeout:
533+
_logger.warning(
534+
"Webhook call timed out after 1s - it may or may not have failed. "
535+
"If this happens often, it may be a sign that the system you're "
536+
"trying to reach is slow or non-functional."
537+
)
538+
except requests.exceptions.RequestException as exc:
539+
_logger.warning("Webhook call failed: %s", exc)

queue_job/models/queue_job_function.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ class QueueJobFunction(models.Model):
2828
"related_action_enable "
2929
"related_action_func_name "
3030
"related_action_kwargs "
31+
"error_handler_enable "
32+
"error_handler_func_name "
33+
"error_handler_kwargs "
3134
"job_function_id ",
3235
)
3336

@@ -79,6 +82,33 @@ def _default_channel(self):
7982
"enable, func_name, kwargs.\n"
8083
"See the module description for details.",
8184
)
85+
error_handler = JobSerialized(base_type=dict)
86+
edit_error_handler = fields.Text(
87+
string="Error Handler",
88+
compute="_compute_edit_error_handler",
89+
inverse="_inverse_edit_error_handler",
90+
help="The handler is executed when the job fails. "
91+
"Configured as a dictionary with optional keys: "
92+
"enable, func_name, kwargs.\n"
93+
"See the module description for details.",
94+
)
95+
96+
@api.depends("error_handler")
97+
def _compute_edit_error_handler(self):
98+
for record in self:
99+
record.edit_error_handler = str(record.error_handler)
100+
101+
def _inverse_edit_error_handler(self):
102+
try:
103+
edited = (self.edit_error_handler or "").strip()
104+
if edited:
105+
self.error_handler = ast.literal_eval(edited)
106+
else:
107+
self.error_handler = {}
108+
except (ValueError, TypeError, SyntaxError) as ex:
109+
raise exceptions.UserError(
110+
self._error_handler_format_error_message()
111+
) from ex
82112

83113
@api.depends("model_id.model", "method")
84114
def _compute_name(self):
@@ -149,6 +179,9 @@ def job_default_config(self):
149179
related_action_func_name=None,
150180
related_action_kwargs={},
151181
job_function_id=None,
182+
error_handler_enable=True,
183+
error_handler_func_name=None,
184+
error_handler_kwargs={},
152185
)
153186

154187
def _parse_retry_pattern(self):
@@ -182,6 +215,9 @@ def job_config(self, name):
182215
related_action_func_name=config.related_action.get("func_name"),
183216
related_action_kwargs=config.related_action.get("kwargs", {}),
184217
job_function_id=config.id,
218+
error_handler_enable=config.error_handler.get("enable", True),
219+
error_handler_func_name=config.error_handler.get("func_name"),
220+
error_handler_kwargs=config.error_handler.get("kwargs", {}),
185221
)
186222

187223
def _retry_pattern_format_error_message(self):
@@ -215,6 +251,14 @@ def _related_action_format_error_message(self):
215251
' "kwargs" {{"limit": 10}}}}'
216252
).format(self.name)
217253

254+
def _error_handler_format_error_message(self):
255+
return _(
256+
"Unexpected format of Error Handler for {}.\n"
257+
"Example of valid format:\n"
258+
'{{"enable": True, "func_name": "_call_webhook",'
259+
' "kwargs" {"webhook_url": "XXX","payload": {"text":"Hello World!"}}}}'
260+
).format(self.name)
261+
218262
@api.constrains("related_action")
219263
def _check_related_action(self):
220264
valid_keys = ("enable", "func_name", "kwargs")

queue_job/readme/CONTRIBUTORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
* Souheil Bejaoui <[email protected]>
1111
* Eric Antones <[email protected]>
1212
* Simone Orsi <[email protected]>
13+
* Tris Doan <[email protected]>

queue_job/readme/DESCRIPTION.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,4 @@ Features:
4444
description, number of retries
4545
* Related Actions: link an action on the job view, such as open the record
4646
concerned by the job
47+
* Error Handler: trigger a method when job fails, such as calling a webhook

queue_job/readme/USAGE.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,21 @@ Based on this configuration, we can tell that:
274274
* retries 10 to 15 postponed 30 seconds later
275275
* all subsequent retries postponed 5 minutes later
276276

277+
**Job function: Error Handler**
278+
279+
The *Error Handler* is a method executed whenever the job fails
280+
281+
It's configured similarly to Related Action
282+
283+
There is an OOTB handler: _call_webhook, which calls a webhook with configurable information.
284+
285+
Example of using _call_webhook to call a webhook to Slack:
286+
287+
.. code-block:: xml
288+
289+
<field name="error_handler" eval='{"func_name": "_call_webhook", "kwargs": {"webhook_url": "XXX", "only_if_max_retries_reached":True, "payload": {"text": "Hello World!"}}}' />
290+
291+
277292
**Job Context**
278293

279294
The context of the recordset of the job, or any recordset passed in arguments of

queue_job/static/description/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ <h1 class="title">Job Queue</h1>
408408
description, number of retries</li>
409409
<li>Related Actions: link an action on the job view, such as open the record
410410
concerned by the job</li>
411+
<li>Error Handler: trigger a method when job fails, such as calling a webhook</li>
411412
</ul>
412413
<p><strong>Table of contents</strong></p>
413414
<div class="contents local topic" id="contents">
@@ -744,6 +745,14 @@ <h3><a class="toc-backref" href="#toc-entry-7">Configure default options for job
744745
<li>retries 10 to 15 postponed 30 seconds later</li>
745746
<li>all subsequent retries postponed 5 minutes later</li>
746747
</ul>
748+
<p><strong>Job function: Error Handler</strong></p>
749+
<p>The <em>Error Handler</em> is a method executed whenever the job fails</p>
750+
<p>It’s configured similarly to Related Action</p>
751+
<p>There is an OOTB handler: _call_webhook, which calls a webhook with configurable information.</p>
752+
<p>Example of using _call_webhook to call a webhook to Slack:</p>
753+
<pre class="code xml literal-block">
754+
<span class="nt">&lt;field</span><span class="w"> </span><span class="na">name=</span><span class="s">&quot;error_handler&quot;</span><span class="w"> </span><span class="na">eval=</span><span class="s">'{&quot;func_name&quot;: &quot;_call_webhook&quot;, &quot;kwargs&quot;: {&quot;webhook_url&quot;: &quot;XXX&quot;, &quot;only_if_max_retries_reached&quot;:True, &quot;payload&quot;: {&quot;text&quot;: &quot;Hello World!&quot;}}}'</span><span class="w"> </span><span class="nt">/&gt;</span>
755+
</pre>
747756
<p><strong>Job Context</strong></p>
748757
<p>The context of the recordset of the job, or any recordset passed in arguments of
749758
a job, is transferred to the job according to an allow-list.</p>
@@ -982,6 +991,7 @@ <h2><a class="toc-backref" href="#toc-entry-17">Contributors</a></h2>
982991
<li>Souheil Bejaoui &lt;<a class="reference external" href="mailto:souheil.bejaoui&#64;acsone.eu">souheil.bejaoui&#64;acsone.eu</a>&gt;</li>
983992
<li>Eric Antones &lt;<a class="reference external" href="mailto:eantones&#64;nuobit.com">eantones&#64;nuobit.com</a>&gt;</li>
984993
<li>Simone Orsi &lt;<a class="reference external" href="mailto:simone.orsi&#64;camptocamp.com">simone.orsi&#64;camptocamp.com</a>&gt;</li>
994+
<li>Tris Doan &lt;<a class="reference external" href="mailto:tridm&#64;trobz.com">tridm&#64;trobz.com</a>&gt;</li>
985995
</ul>
986996
</div>
987997
<div class="section" id="maintainers">

queue_job/tests/test_model_job_function.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ def test_function_job_config(self):
5252
related_action_enable=True,
5353
related_action_func_name="related_action_foo",
5454
related_action_kwargs={"b": 1},
55+
error_handler_enable=True,
56+
error_handler_func_name=None,
57+
error_handler_kwargs={},
5558
job_function_id=job_function.id,
5659
),
5760
)

0 commit comments

Comments
 (0)