Skip to content
Open
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
1 change: 1 addition & 0 deletions queue_job/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ def retry_postpone(job, message, seconds=None):
vals = cls._get_failure_values(job, traceback_txt, orig_exception)
job.set_failed(**vals)
job.store()
job.on_fail_hook(vals)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we pass the exception obj too?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any added value to pass the raw exception instead of what's been extracted from it in _get_failure_values

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you can inspect the exception for additional info. Not a must have tho.

buff.close()
raise

Expand Down
7 changes: 7 additions & 0 deletions queue_job/delay.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ class Delayable:
"description",
"channel",
"identity_key",
"on_fail_method",
)
__slots__ = _properties + (
"recordset",
Expand All @@ -457,6 +458,7 @@ def __init__(
description=None,
channel=None,
identity_key=None,
on_fail_method=None,
):
self._graph = DelayableGraph()
self._graph.add_vertex(self)
Expand All @@ -469,6 +471,7 @@ def __init__(
self.description = description
self.channel = channel
self.identity_key = identity_key
self.on_fail_method = on_fail_method

self._job_method = None
self._job_args = ()
Expand Down Expand Up @@ -547,6 +550,7 @@ def split(self, size, chain=False):
description=self.description,
channel=self.channel,
identity_key=self.identity_key,
on_fail_method=self.on_fail_method,
)
# Update the __self__
delayable._job_method = getattr(recordset, self._job_method.__name__)
Expand Down Expand Up @@ -583,6 +587,7 @@ def _build_job(self):
description=self.description,
channel=self.channel,
identity_key=self.identity_key,
on_fail_method=self.on_fail_method,
)
return self._generated_job

Expand Down Expand Up @@ -633,6 +638,7 @@ def __init__(
description=None,
channel=None,
identity_key=None,
on_fail_method=None,
):
self.delayable = Delayable(
recordset,
Expand All @@ -642,6 +648,7 @@ def __init__(
description=description,
channel=channel,
identity_key=identity_key,
on_fail_method=on_fail_method,
)

@property
Expand Down
17 changes: 17 additions & 0 deletions queue_job/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ class Job:
be added to a channel if the existing job with the same key is not yet
started or executed.

.. attribute::on_fail_method

A function to be called if the job is failed and will not be retried.
"""

@classmethod
Expand Down Expand Up @@ -298,6 +301,9 @@ def _load_from_db_record(cls, job_db_record):
description=stored.name,
channel=stored.channel,
identity_key=stored.identity_key,
on_fail_method=getattr(recordset, stored.on_fail_method_name)
if stored.on_fail_method_name
else None,
)

if stored.date_created:
Expand Down Expand Up @@ -365,6 +371,7 @@ def __init__(
description=None,
channel=None,
identity_key=None,
on_fail_method=None,
):
"""Create a Job

Expand Down Expand Up @@ -407,6 +414,11 @@ def __init__(
self.method_name = func.__name__
self.recordset = recordset

if on_fail_method:
if not _is_model_method(on_fail_method):
raise TypeError("Job accepts only methods of Models")
self.on_fail_method_name = on_fail_method.__name__

self.job_config = (
self.env["queue.job.function"].sudo().job_config(self.job_function_name)
)
Expand Down Expand Up @@ -830,6 +842,11 @@ def set_failed(self, **kw):
if v is not None:
setattr(self, k, v)

def on_fail_hook(self, fail_vals):
on_fail_func = getattr(self.recordset, self.on_fail_method_name, None)
if on_fail_func:
on_fail_func(**fail_vals)

def __repr__(self):
return f"<Job {self.uuid}, priority:{self.priority}>"

Expand Down
4 changes: 4 additions & 0 deletions queue_job/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def with_delay(
description=None,
channel=None,
identity_key=None,
on_fail_method=None,
):
"""Return a ``DelayableRecordset``
Expand Down Expand Up @@ -59,6 +60,7 @@ def with_delay(
description=description,
channel=channel,
identity_key=identity_key,
on_fail_method=on_fail_method,
)

def delayable(
Expand All @@ -69,6 +71,7 @@ def delayable(
description=None,
channel=None,
identity_key=None,
on_fail_method=None,
):
"""Return a ``Delayable``
Expand Down Expand Up @@ -140,6 +143,7 @@ def delayable(
description=description,
channel=channel,
identity_key=identity_key,
on_fail_method=on_fail_method,
)

def _patch_job_auto_delay(self, method_name, context_key=None):
Expand Down
4 changes: 4 additions & 0 deletions queue_job/models/queue_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class QueueJob(models.Model):

model_name = fields.Char(string="Model", readonly=True)
method_name = fields.Char(readonly=True)
on_fail_method_name = fields.Char(readonly=True)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate consistency in naming: on_fail_hook + on_fail_method + on_fail_method_name is going to make some greps miss.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @amh-mw

What would you suggest?

As I take naming quite seriously, here was my rationale:

  • on_fail_method is a reference to the method (eg when call with_delay )
  • on_fail_method_name is the name of the method (which must be on the same model than the queue job)
  • on_fail_hook is the internal function of the job that will be calling the on_fail_method

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might rename all of them (plus on_fail_func that I just noticed) to on_fail. They're on different classes or are method arguments, so I am 85% confident they won't collide.

In honor of this pull request, I'm going to rename _on_done in #937 to check_done to reserve the on_ prefix for hooks.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on_fail instead of on_fail_hook make sense. For the rest, is true that func is the term that is already present. But not a blocker from my POV.

records = JobSerialized(
string="Record(s)",
readonly=True,
Expand Down Expand Up @@ -490,3 +491,6 @@ def _test_job(
time.sleep(job_duration)
if commit_within_job:
self.env.cr.commit() # pylint: disable=invalid-commit

def _test_on_fail_hook(self, **kw):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If only for testing,this is not needed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get AttributeError: <class 'odoo.addons.queue_job.models.queue_job.QueueJob'> does not have the attribute '_test_on_fail_hook' if I remove it

pass
30 changes: 30 additions & 0 deletions queue_job/tests/test_run_rob_controller.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from unittest.mock import patch

from odoo.tests.common import TransactionCase
from odoo.tools import mute_logger

from ..controllers.main import RunJobController
from ..exception import JobError
from ..job import Job


class TestRunJobController(TransactionCase):
def setUp(cls):
super().setUp()

def _clean_queue_job():
cls.env["queue.job"].search([]).unlink()

cls.addCleanup(_clean_queue_job)

def test_get_failure_values(self):
method = self.env["res.users"].mapped
job = Job(method)
Expand All @@ -21,3 +32,22 @@ def test_runjob_success(self):
RunJobController._runjob(self.env, job)
self.assertEqual(job.state, "done")
self.assertEqual(job.db_record().state, "done")

def test_runjob_on_fail_hook(self):
job = (
self.env["queue.job"]
.with_delay(on_fail_method=self.env["queue.job"]._test_on_fail_hook)
._test_job(failure_rate=1)
)
with (
self.assertRaises(JobError),
patch(
"odoo.addons.queue_job.models.queue_job.QueueJob._test_on_fail_hook"
) as mocked_hook,
patch("odoo.addons.queue_job.job.Job.in_temporary_env") as mocked_temp_env,
mute_logger("odoo.addons.queue_job.controllers.main"),
):
mocked_temp_env.return_value.__enter__.return_value = self.env
RunJobController._runjob(self.env, job)
self.assertEqual(job.state, "failed")
self.assertEqual(mocked_hook.call_count, 1)
1 change: 0 additions & 1 deletion test_queue_job/tests/test_autovacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ def test_autovacuum_multi_channel(self):
job_60days.write(
{"channel": channel_60days.complete_name, "date_done": date_done}
)

self.assertEqual(
len(self.env["queue.job"].search([("channel", "!=", False)])), 2
)
Expand Down
1 change: 1 addition & 0 deletions test_queue_job/tests/test_delay_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ def test_mock_with_delay(self):
"eta": 15,
"identity_key": identity_exact,
"max_retries": 1,
"on_fail_method": None,
"priority": 15,
},
)
Expand Down
Loading