This addon adds an integrated Job Queue to Odoo.
@@ -928,21 +928,7 @@
@@ -1008,8 +994,8 @@
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
-
Current maintainer:
-

+
Current maintainers:
+

This module is part of the OCA/queue project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/queue_job/tests/test_wizards.py b/queue_job/tests/test_wizards.py
index 2ac162d313..7738836d2f 100644
--- a/queue_job/tests/test_wizards.py
+++ b/queue_job/tests/test_wizards.py
@@ -46,3 +46,60 @@ def test_03_done(self):
wizard = self._wizard("queue.jobs.to.done")
wizard.set_done()
self.assertEqual(self.job.state, "done")
+
+ def test_04_requeue_forbidden(self):
+ wizard = self._wizard("queue.requeue.job")
+
+ # State WAIT_DEPENDENCIES is not requeued
+ self.job.state = "wait_dependencies"
+ wizard.requeue()
+ self.assertEqual(self.job.state, "wait_dependencies")
+
+ # State PENDING, ENQUEUED or STARTED are ignored too
+ for test_state in ("pending", "enqueued", "started"):
+ self.job.state = test_state
+ wizard.requeue()
+ self.assertEqual(self.job.state, test_state)
+
+ # States CANCELLED, DONE or FAILED will change status
+ self.job.state = "cancelled"
+ wizard.requeue()
+ self.assertEqual(self.job.state, "pending")
+
+ def test_05_cancel_forbidden(self):
+ wizard = self._wizard("queue.jobs.to.cancelled")
+
+ # State WAIT_DEPENDENCIES is not cancelled
+ self.job.state = "wait_dependencies"
+ wizard.set_cancelled()
+ self.assertEqual(self.job.state, "wait_dependencies")
+
+ # State DONE is not cancelled
+ self.job.state = "done"
+ wizard.set_cancelled()
+ self.assertEqual(self.job.state, "done")
+
+ # State PENDING, ENQUEUED or FAILED will be cancelled
+ for test_state in ("pending", "enqueued"):
+ self.job.state = test_state
+ wizard.set_cancelled()
+ self.assertEqual(self.job.state, "cancelled")
+
+ def test_06_done_forbidden(self):
+ wizard = self._wizard("queue.jobs.to.done")
+
+ # State STARTED is not set DONE manually
+ self.job.state = "started"
+ wizard.set_done()
+ self.assertEqual(self.job.state, "started")
+
+ # State CANCELLED is not cancelled
+ self.job.state = "cancelled"
+ wizard.set_done()
+ self.assertEqual(self.job.state, "cancelled")
+
+ # State WAIT_DEPENDENCIES, PENDING, ENQUEUED or FAILED will be set to DONE
+ for test_state in ("wait_dependencies", "pending", "enqueued"):
+ self.job.state = test_state
+ wizard.set_done()
+ self.assertEqual(self.job.state, "done")
diff --git a/queue_job/wizards/queue_jobs_to_cancelled.py b/queue_job/wizards/queue_jobs_to_cancelled.py
index 9e73374ebd..bb9f831576 100644
--- a/queue_job/wizards/queue_jobs_to_cancelled.py
+++ b/queue_job/wizards/queue_jobs_to_cancelled.py
@@ -10,8 +10,8 @@ class SetJobsToCancelled(models.TransientModel):
_description = "Cancel all selected jobs"
def set_cancelled(self):
- jobs = self.job_ids.filtered(
- lambda x: x.state in ("pending", "failed", "enqueued")
- )
+ # Only jobs with state PENDING, FAILED, ENQUEUED
+ # will change to CANCELLED
+ jobs = self.job_ids
jobs.button_cancelled()
return {"type": "ir.actions.act_window_close"}
diff --git a/queue_job/wizards/queue_jobs_to_done.py b/queue_job/wizards/queue_jobs_to_done.py
index ff1366ffed..caf8129213 100644
--- a/queue_job/wizards/queue_jobs_to_done.py
+++ b/queue_job/wizards/queue_jobs_to_done.py
@@ -10,6 +10,8 @@ class SetJobsToDone(models.TransientModel):
_description = "Set all selected jobs to done"
def set_done(self):
+ # Only jobs with state WAIT_DEPENDENCIES, PENDING, ENQUEUED or FAILED
+ # will change to DONE
jobs = self.job_ids
jobs.button_done()
return {"type": "ir.actions.act_window_close"}
diff --git a/queue_job/wizards/queue_requeue_job.py b/queue_job/wizards/queue_requeue_job.py
index 67d2ffcbdc..a88256300f 100644
--- a/queue_job/wizards/queue_requeue_job.py
+++ b/queue_job/wizards/queue_requeue_job.py
@@ -20,6 +20,7 @@ def _default_job_ids(self):
)
def requeue(self):
+ # Only jobs with state FAILED, DONE or CANCELLED will change to PENDING
jobs = self.job_ids
jobs.requeue()
return {"type": "ir.actions.act_window_close"}
diff --git a/test_queue_job/__manifest__.py b/test_queue_job/__manifest__.py
index 3cf7243aa7..0d5eabc0f9 100644
--- a/test_queue_job/__manifest__.py
+++ b/test_queue_job/__manifest__.py
@@ -3,7 +3,7 @@
{
"name": "Queue Job Tests",
- "version": "19.0.1.0.0",
+ "version": "19.0.1.0.1",
"author": "Camptocamp,Odoo Community Association (OCA)",
"license": "LGPL-3",
"category": "Generic Modules",
@@ -15,5 +15,6 @@
"security/ir.model.access.csv",
"data/queue_job_test_job.xml",
],
+ "maintainers": ["sbidoul"],
"installable": True,
}
diff --git a/test_queue_job/tests/__init__.py b/test_queue_job/tests/__init__.py
index 62347148e5..0cfacebdf3 100644
--- a/test_queue_job/tests/__init__.py
+++ b/test_queue_job/tests/__init__.py
@@ -1,3 +1,4 @@
+from . import test_acquire_job
from . import test_autovacuum
from . import test_delayable
from . import test_dependencies
diff --git a/test_queue_job/tests/common.py b/test_queue_job/tests/common.py
index 335c072625..d3173a2198 100644
--- a/test_queue_job/tests/common.py
+++ b/test_queue_job/tests/common.py
@@ -20,3 +20,13 @@ def _create_job(self):
stored = Job.db_records_from_uuids(self.env, [test_job.uuid])
self.assertEqual(len(stored), 1)
return stored
+
+ def _get_demo_job(self, uuid):
+ # job created during load of demo data
+ job = self.env["queue.job"].search([("uuid", "=", uuid)], limit=1)
+ self.assertTrue(
+ job,
+ f"Demo data queue job {uuid!r} should be loaded in order "
+ "to make this test work",
+ )
+ return job
diff --git a/test_queue_job/tests/test_acquire_job.py b/test_queue_job/tests/test_acquire_job.py
new file mode 100644
index 0000000000..3f0c92a2be
--- /dev/null
+++ b/test_queue_job/tests/test_acquire_job.py
@@ -0,0 +1,51 @@
+# Copyright 2026 ACSONE SA/NV
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+import logging
+from unittest import mock
+
+from odoo.tests import tagged
+
+from odoo.addons.queue_job.controllers.main import RunJobController
+
+from .common import JobCommonCase
+
+
+@tagged("post_install", "-at_install")
+class TestRequeueDeadJob(JobCommonCase):
+ def test_acquire_enqueued_job(self):
+ job_record = self._get_demo_job(uuid="test_enqueued_job")
+ self.assertFalse(
+ self.env["queue.job.lock"].search(
+ [("queue_job_id", "=", job_record.id)],
+ ),
+ "A job lock record should not exist at this point",
+ )
+ with mock.patch.object(
+ self.env.cr, "commit", mock.Mock(side_effect=self.env.flush_all)
+ ) as mock_commit:
+ job = RunJobController._acquire_job(self.env, job_uuid="test_enqueued_job")
+ mock_commit.assert_called_once()
+ self.assertIsNotNone(job)
+ self.assertEqual(job.uuid, "test_enqueued_job")
+ self.assertEqual(job.state, "started")
+ self.assertTrue(
+ self.env["queue.job.lock"].search(
+ [("queue_job_id", "=", job_record.id)]
+ ),
+ "A job lock record should exist at this point",
+ )
+
+ def test_acquire_started_job(self):
+ with (
+ mock.patch.object(
+ self.env.cr, "commit", mock.Mock(side_effect=self.env.flush_all)
+ ) as mock_commit,
+ self.assertLogs(level=logging.WARNING) as logs,
+ ):
+ job = RunJobController._acquire_job(self.env, "test_started_job")
+ mock_commit.assert_not_called()
+ self.assertIsNone(job)
+ self.assertIn(
+ "was requested to run job test_started_job, but it does not exist",
+ logs.output[0],
+ )
diff --git a/test_queue_job/tests/test_requeue_dead_job.py b/test_queue_job/tests/test_requeue_dead_job.py
index a6328fed76..a267c43c87 100644
--- a/test_queue_job/tests/test_requeue_dead_job.py
+++ b/test_queue_job/tests/test_requeue_dead_job.py
@@ -13,23 +13,6 @@
@tagged("post_install", "-at_install")
class TestRequeueDeadJob(JobCommonCase):
- def _get_demo_job(self, uuid):
- # job created during load of demo data
- job = self.env["queue.job"].search(
- [
- ("uuid", "=", uuid),
- ],
- limit=1,
- )
-
- self.assertTrue(
- job,
- f"Demo data queue job {uuid} should be loaded in order"
- " to make this tests work",
- )
-
- return job
-
def get_locks(self, uuid, cr=None):
"""
Retrieve lock rows
@@ -52,7 +35,7 @@ def get_locks(self, uuid, cr=None):
WHERE
uuid = %s
)
- FOR UPDATE SKIP LOCKED
+ FOR NO KEY UPDATE SKIP LOCKED
""",
[uuid],
)
@@ -99,3 +82,19 @@ def test_requeue_dead_jobs(self):
uuids_requeued = self.env.cr.fetchall()
self.assertTrue(queue_job.uuid in j[0] for j in uuids_requeued)
+
+ def test_requeue_orphaned_jobs(self):
+ queue_job = self._get_demo_job("test_enqueued_job")
+ job_obj = Job.load(self.env, queue_job.uuid)
+
+ # Only enqueued job, don't set it to started to simulate the scenario
+ # that system shutdown before job is starting
+ job_obj.set_enqueued()
+ job_obj.date_enqueued = datetime.now() - timedelta(minutes=1)
+ job_obj.store()
+
+ # job is now picked up by the requeue query (which includes orphaned jobs)
+ query = Database(self.env.cr.dbname)._query_requeue_dead_jobs()
+ self.env.cr.execute(query)
+ uuids_requeued = self.env.cr.fetchall()
+ self.assertTrue(queue_job.uuid in j[0] for j in uuids_requeued)