Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2a5ccb8
[19.0] queue_job: migrate + tests
Oct 1, 2025
ce67593
[pre-commit] update excluded addons (queue_job/test_queue_job now ins…
Oct 1, 2025
b283cd8
[pre-commit] apply hook updates + ruff fixes\n\n- Commit .pre-commit-…
Oct 1, 2025
df50b83
[lint] ruff fixes (E402, UP031, E501)
Oct 1, 2025
b7129d7
[pre-commit] ruff-format applied
Oct 1, 2025
ac812d7
[lint] satisfy pylint-odoo mandatory checks\n\n- Use http.request.env…
Oct 1, 2025
a7d4364
[pre-commit] ruff and format auto-fixes
Oct 1, 2025
4da9fae
[lint] tests: use odoo.tools.groupby instead of itertools.groupby
Oct 1, 2025
f494bcc
[pre-commit] reordered imports (ruff)
Oct 1, 2025
3b733f1
[lint] lazy translations + test groupby fix
Oct 1, 2025
ee67632
[fix] channel: enforce root deletion guard in ondelete hook
Oct 1, 2025
d827d58
style(queue): convert safe string formatting to f-strings and extract…
Oct 4, 2025
9aeebee
tests(doctest): reset optionflags to 0; remove no-op unlink override …
Oct 4, 2025
9ac6b25
style(queue): use PEP 604 unions in isinstance checks (Python 3.10+) …
Oct 4, 2025
99cc44f
style(queue): extract i18n raise messages into variables for consistency
Oct 4, 2025
211e467
autovacuum: restore original channel iteration form; keep linter sile…
Oct 4, 2025
620413a
tests(common): restore search([]) for readability and silence linter …
Oct 4, 2025
82d62a1
chore(queue): remove unnecessary migration comments in security and c…
Oct 4, 2025
81a1488
search: use dynamic datetime macros for date_created filters (now -1d…
Oct 4, 2025
6d330f2
security: set 'Job Queue' privilege sequence to 50 (under Settings › …
Oct 4, 2025
f685d12
pre-commit: all hooks pass locally
Oct 4, 2025
d27f83d
chore(queue): remove Odoo 19 migration comments
Oct 11, 2025
8dd4af0
tests(common): improve _format_job_call readability by using intermed…
Oct 11, 2025
231705d
jobrunner: read [queue_job] from odoo.conf when server_environment mi…
Oct 11, 2025
9ea78c5
models(queue_job): drop index_exists guards around create_index
Oct 11, 2025
038c334
models(base), tests(test_queue_job): remove _patch_method; patch in _…
Oct 11, 2025
96a9ada
jobrunner: support list-valued db_name
Nov 10, 2025
afc9c76
models(queue_job): compute graph count with _read_group
Nov 10, 2025
f947199
Update queue_job/tests/test_json_field.py
tishmen Nov 10, 2025
c30bf4b
tests: use Command API for m2m
Nov 10, 2025
123900c
pre-commit: apply autofixes (formatting, lint passes)
Nov 10, 2025
d25ff12
jobrunner: simplify get_db_names (no split, no parsing)
Nov 10, 2025
754e438
jobrunner: restore list/CSV handling in get_db_names (no comments)
Nov 10, 2025
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
2 changes: 0 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ exclude: |
(?x)
# NOT INSTALLABLE ADDONS
^base_import_async/|
^queue_job/|
^queue_job_batch/|
^queue_job_cron/|
^queue_job_cron_jobrunner/|
^queue_job_subscribe/|
^test_queue_job/|
^test_queue_job_batch/|
# END NOT INSTALLABLE ADDONS
# Files and folders generated by bots, to avoid loops
Expand Down
10 changes: 5 additions & 5 deletions queue_job/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ Job Queue
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fqueue-lightgray.png?logo=github
:target: https://github.com/OCA/queue/tree/18.0/queue_job
:target: https://github.com/OCA/queue/tree/19.0/queue_job
:alt: OCA/queue
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/queue-18-0/queue-18-0-queue_job
:target: https://translation.odoo-community.org/projects/queue-19-0/queue-19-0-queue_job
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=18.0
:target: https://runboat.odoo-community.org/builds?repo=OCA/queue&target_branch=19.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|
Expand Down Expand Up @@ -661,7 +661,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/queue/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/queue/issues/new?body=module:%20queue_job%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/queue/issues/new?body=module:%20queue_job%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Expand Down Expand Up @@ -720,6 +720,6 @@ Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-guewen|

This module is part of the `OCA/queue <https://github.com/OCA/queue/tree/18.0/queue_job>`_ project on GitHub.
This module is part of the `OCA/queue <https://github.com/OCA/queue/tree/19.0/queue_job>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
6 changes: 3 additions & 3 deletions queue_job/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

{
"name": "Job Queue",
"version": "18.0.2.0.2",
"version": "19.0.1.0.0",
"author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/queue",
"license": "LGPL-3",
Expand All @@ -24,10 +24,10 @@
],
"assets": {
"web.assets_backend": [
"/queue_job/static/src/views/**/*",
"queue_job/static/src/views/**/*",
],
},
"installable": False,
"installable": True,
"development_status": "Mature",
"maintainers": ["guewen"],
"post_init_hook": "post_init_hook",
Expand Down
6 changes: 3 additions & 3 deletions queue_job/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from psycopg2 import OperationalError, errorcodes
from werkzeug.exceptions import BadRequest, Forbidden

from odoo import SUPERUSER_ID, _, api, http
from odoo import SUPERUSER_ID, api, http
from odoo.modules.registry import Registry
from odoo.service.model import PG_CONCURRENCY_ERRORS_TO_RETRY

Expand Down Expand Up @@ -179,7 +179,7 @@ def create_test_job(
failure_rate=0,
):
if not http.request.env.user.has_group("base.group_erp_manager"):
raise Forbidden(_("Access Denied"))
raise Forbidden(http.request.env._("Access Denied"))

if failure_rate is not None:
try:
Expand Down Expand Up @@ -280,7 +280,7 @@ def _create_graph_test_jobs(
priority=priority,
max_retries=max_retries,
channel=channel,
description="%s #%d" % (description, current_count),
description=f"{description} #{current_count}",
)._test_job(failure_rate=failure_rate)
)

Expand Down
3 changes: 2 additions & 1 deletion queue_job/delay.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,8 @@ def __del__(self):
def _set_from_dict(self, properties):
for key, value in properties.items():
if key not in self._properties:
raise ValueError(f"No property {key}")
msg = f"No property {key}"
raise ValueError(msg)
setattr(self, key, value)

def set(self, *args, **kwargs):
Expand Down
6 changes: 4 additions & 2 deletions queue_job/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from odoo import fields, models
from odoo.tools.func import lazy
from odoo.tools.misc import SENTINEL


class JobSerialized(fields.Json):
Expand Down Expand Up @@ -38,13 +39,14 @@ class JobSerialized(fields.Json):
),
}

def __init__(self, string=fields.SENTINEL, base_type=fields.SENTINEL, **kwargs):
def __init__(self, string=SENTINEL, base_type=SENTINEL, **kwargs):
super().__init__(string=string, _base_type=base_type, **kwargs)

def _setup_attrs(self, model, name): # pylint: disable=missing-return
super()._setup_attrs(model, name)
if self._base_type not in self._default_json_mapping:
raise ValueError(f"{self._base_type} is not a supported base type")
msg = f"{self._base_type} is not a supported base type"
raise ValueError(msg)

def _base_type_default_json(self, env):
default_json = self._default_json_mapping.get(self._base_type)
Expand Down
12 changes: 6 additions & 6 deletions queue_job/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ def load(cls, env, job_uuid):
"""
stored = cls.db_records_from_uuids(env, [job_uuid])
if not stored:
raise NoSuchJobError(f"Job {job_uuid} does no longer exist in the storage.")
msg = f"Job {job_uuid} does no longer exist in the storage."
raise NoSuchJobError(msg)
return cls._load_from_db_record(stored)

@classmethod
Expand Down Expand Up @@ -505,7 +506,7 @@ def perform(self):
# traceback and message:
# http://blog.ianbicking.org/2007/09/12/re-raising-exceptions/
new_exc = FailedJobError(
"Max. retries (%d) reached: %s" % (self.max_retries, value or type_)
f"Max. retries ({self.max_retries}) reached: {value or type_}"
)
raise new_exc from err
raise
Expand Down Expand Up @@ -813,7 +814,7 @@ def set_failed(self, **kw):
setattr(self, k, v)

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

def _get_retry_seconds(self, seconds=None):
retry_pattern = self.job_config.retry_pattern
Expand All @@ -828,7 +829,7 @@ def _get_retry_seconds(self, seconds=None):
break
elif not seconds:
seconds = RETRY_INTERVAL
if isinstance(seconds, (list | tuple)):
if isinstance(seconds, list | tuple):
seconds = randint(seconds[0], seconds[1])
return seconds

Expand Down Expand Up @@ -856,8 +857,7 @@ def related_action(self):
funcname = record._default_related_action
if not isinstance(funcname, str):
raise ValueError(
"related_action must be the name of the "
"method on queue.job as string"
"related_action must be the name of the method on queue.job as string"
)
action = getattr(record, funcname)
action_kwargs = self.job_config.related_action_kwargs
Expand Down
37 changes: 33 additions & 4 deletions queue_job/jobrunner/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,46 @@
from odoo.tools import config

try:
# Preferred source when available: structured [queue_job] section provided
# by OCA's server_environment addon.
from odoo.addons.server_environment import serv_config

if serv_config.has_section("queue_job"):
queue_job_config = serv_config["queue_job"]
else:
queue_job_config = {}
except ImportError:
queue_job_config = config.misc.get("queue_job", {})


from .runner import QueueJobRunner, _channels
# Odoo 19: config.misc is no longer available. Build a minimal config
# from flat odoo.conf options so the runner works without server_environment.
queue_job_config = {}

# Merge flat odoo.conf options as a fallback (applies regardless of whether
# server_environment is installed). Precedence is enforced later where used:
# - Environment variables (highest) are read directly in runner functions
# - Then values coming from server_environment's [queue_job] section (above)
# - Finally flat odoo.conf options below (lowest)
#
# Supported flat options (under the [options] section in odoo.conf):
# queue_job_channels = root:2,mychan:1
# queue_job_jobrunner_db_host = localhost
# queue_job_jobrunner_db_port = 5432
# queue_job_jobrunner_db_user = odoo_queue
# queue_job_jobrunner_db_password = odoo_queue
_flat = {}
channels = config.get("queue_job_channels")
if channels:
_flat["channels"] = channels
for p in ("host", "port", "user", "password"):
v = config.get(f"queue_job_jobrunner_db_{p}")
if v:
_flat[f"jobrunner_db_{p}"] = v

# Do not override keys coming from server_environment if present
for k, v in _flat.items():
queue_job_config.setdefault(k, v)


from .runner import QueueJobRunner, _channels # noqa: E402

_logger = logging.getLogger(__name__)

Expand Down
15 changes: 6 additions & 9 deletions queue_job/jobrunner/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,12 +455,9 @@ def get_subchannel_by_name(self, subchannel_name):

def __str__(self):
capacity = "∞" if self.capacity is None else str(self.capacity)
return "%s(C:%s,Q:%d,R:%d,F:%d)" % (
self.fullname,
capacity,
len(self._queue),
len(self._running),
len(self._failed),
return (
f"{self.fullname}(C:{capacity},Q:{len(self._queue)},"
f"R:{len(self._running)},F:{len(self._failed)})"
)

def remove(self, job):
Expand Down Expand Up @@ -894,8 +891,7 @@ def parse_simple_config(cls, config_string):
)
if k in config:
raise ValueError(
f"Invalid channel config {config_string}: "
f"duplicate key {k}"
f"Invalid channel config {config_string}: duplicate key {k}"
)
config[k] = v
else:
Expand Down Expand Up @@ -996,7 +992,8 @@ def get_channel_by_name(
if channel_name in self._channels_by_name:
return self._channels_by_name[channel_name]
if not autocreate and not parent_fallback:
raise ChannelNotFound(f"Channel {channel_name} not found")
msg = f"Channel {channel_name} not found"
raise ChannelNotFound(msg)
parent = self._root_channel
if parent_fallback:
# Look for first direct parent w/ config.
Expand Down
21 changes: 19 additions & 2 deletions queue_job/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,26 @@ def _job_prepare_context_before_enqueue(self):

@classmethod
def _patch_method(cls, name, method):
"""Patch ``name`` with ``method`` preserving API metadata (Odoo 19).

Odoo 19 no longer exposes ``api.propagate``. We emulate the
propagation by using ``functools.update_wrapper`` and copying the
decorator metadata which Odoo relies on (see orm.decorators).
"""
origin = getattr(cls, name)
method.origin = origin
# propagate decorators from origin to method, and apply api decorator
wrapped = api.propagate(origin, method)
# carry over wrapper attributes (name, doc, etc.)
wrapped = functools.update_wrapper(method, origin)
# propagate common decorator metadata used by the framework
for attr in (
"_constrains",
"_depends",
"_onchange",
"_ondelete",
"_api_model",
"_api_private",
):
if hasattr(origin, attr):
setattr(wrapped, attr, getattr(origin, attr))

Choose a reason for hiding this comment

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

have you considered just reimplemented the same old propagate method instead?

Copy link
Author

Choose a reason for hiding this comment

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

In 19.0, api.propagate was removed as part of the API/ORM refactor (api now re‑exports from odoo.orm.*) and upstream uses functools.update_wrapper directly. To remain compatible and avoid reintroducing a removed internal API, we mirror the upstream approach: update_wrapper for name/doc and explicit propagation of the few decorator flags that Odoo actually reads (_constrains/_depends/_onchange/_ondelete/_api_model/_api_private). This keeps the behavior identical while minimizing reliance on non‑public helpers. If you prefer a local helper to reduce duplication, I can wrap this logic in a small internal function, but I’d avoid reviving the “propagate” API name.

Copy link
Author

Choose a reason for hiding this comment

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

@sbidoul can we have your input?

Choose a reason for hiding this comment

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

I don't know what the best to do here, but hard coding a list of apis here also don't work as a proper propagation. because if there is another api being added through customization, it will be skipped by this implementation I think 🤔

Copy link
Member

Choose a reason for hiding this comment

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

It looks like this _patch_method was introduced in queue_job in 17.0, as a copy of the method that was in upstream base in Odoo 16.0. I think this was not a good solution in the first place. I'm not sure what to do, but at least it should be renamed.

Copy link
Member

Choose a reason for hiding this comment

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

_patch_method is not actually used in queue_job. It is only mentioned in the comment of _patch_job_auto_delay . I'm inclined to remove _patch_method from queue_job.

So I'm inclined to remove it. If it is necessary to reintroduce it, that should happen in another standalone module. @guewen do you have an opinion on this?

Copy link
Author

Choose a reason for hiding this comment

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

@sbidoul @guewen @hoangtrann Addressed in: 038c334.

Please review.

wrapped.origin = origin
setattr(cls, name, wrapped)
Loading