diff --git a/README.md b/README.md index aa4e119ecb..a71cc330cf 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,15 @@ Available addons ---------------- addon | version | maintainers | summary --- | --- | --- | --- -[base_export_async](base_export_async/) | 16.0.1.1.0 | | Asynchronous export with job queue +[base_export_async](base_export_async/) | 16.0.1.2.0 | | Asynchronous export with job queue [base_import_async](base_import_async/) | 16.0.1.2.0 | | Import CSV files in the background -[queue_job](queue_job/) | 16.0.2.11.5 | guewen | Job Queue +[queue_job](queue_job/) | 16.0.2.12.0 | guewen | Job Queue [queue_job_batch](queue_job_batch/) | 16.0.1.0.1 | | Job Queue Batch [queue_job_cron](queue_job_cron/) | 16.0.2.1.0 | | Scheduled Actions as Queue Jobs [queue_job_cron_jobrunner](queue_job_cron_jobrunner/) | 16.0.1.1.0 | ivantodorovich | Run jobs without a dedicated JobRunner [queue_job_subscribe](queue_job_subscribe/) | 16.0.1.1.0 | | Control which users are subscribed to queue job notifications [queue_job_web_notify](queue_job_web_notify/) | 16.0.1.0.0 | | This module allows to display a notification to the related user of a failed job. It uses the web_notify notification feature. -[test_queue_job](test_queue_job/) | 16.0.2.3.0 | | Queue Job Tests +[test_queue_job](test_queue_job/) | 16.0.2.4.0 | | Queue Job Tests [test_queue_job_batch](test_queue_job_batch/) | 16.0.1.0.0 | | Test Job Queue Batch diff --git a/base_export_async/README.rst b/base_export_async/README.rst index 0863ca6273..3cf70f08d2 100644 --- a/base_export_async/README.rst +++ b/base_export_async/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ================= Base Export Async ================= @@ -7,13 +11,13 @@ Base Export Async !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:85ce335207ce3505a7676a8a78743155f9f8c01d1c6f777fe2308c5e77f00e5a + !! source digest: sha256:da52a05130a89cd0000a39c9dd6315821387de2c310f9cf7cc1f7e6e8541fe55 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fqueue-lightgray.png?logo=github diff --git a/base_export_async/__manifest__.py b/base_export_async/__manifest__.py index 04975d5bf3..f43ed29632 100644 --- a/base_export_async/__manifest__.py +++ b/base_export_async/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Base Export Async", "summary": "Asynchronous export with job queue", - "version": "16.0.1.1.0", + "version": "16.0.1.2.0", "license": "AGPL-3", "author": "ACSONE SA/NV, Odoo Community Association (OCA)", "website": "https://github.com/OCA/queue", diff --git a/base_export_async/models/delay_export.py b/base_export_async/models/delay_export.py index 549d44a1e1..ee0ae25ab0 100644 --- a/base_export_async/models/delay_export.py +++ b/base_export_async/models/delay_export.py @@ -109,6 +109,10 @@ def export(self, params): attachment.name, ) + if any(user.has_group("base.group_portal") for user in users): + attachment.generate_access_token() + url += f"&access_token={attachment.access_token}" + time_to_live = ( self.env["ir.config_parameter"].sudo().get_param("attachment.ttl", 7) ) diff --git a/base_export_async/static/description/index.html b/base_export_async/static/description/index.html index 6aa89b8a93..147c0c8c2b 100644 --- a/base_export_async/static/description/index.html +++ b/base_export_async/static/description/index.html @@ -3,7 +3,7 @@ -Base Export Async +README.rst -
-

Base Export Async

+
+ + +Odoo Community Association + +
+

Base Export Async

-

Beta License: AGPL-3 OCA/queue Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/queue Translate me on Weblate Try me on Runboat

Standard Export can be delayed in asynchronous jobs executed in the background and then send by email to the user.

Table of contents

@@ -385,7 +390,7 @@

Base Export Async

-

Usage

+

Usage

The user is presented with a new checkbox “Asynchronous export” in the export screen. When selected, the export is delayed in a background job.

@@ -393,7 +398,7 @@

Usage

to the user who execute the export.

-

Bug Tracker

+

Bug Tracker

Bugs are tracked on GitHub 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 @@ -401,22 +406,22 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • ACSONE SA/NV
-

Contributors

+

Contributors

  • Arnaud Pineux (ACSONE SA/NV) authored the initial prototype.
  • Guewen Baconnier (Camptocamp)
-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -429,5 +434,6 @@

Maintainers

+
diff --git a/base_export_async/tests/test_base_export_async.py b/base_export_async/tests/test_base_export_async.py index d10ca04281..4a2771d0c1 100644 --- a/base_export_async/tests/test_base_export_async.py +++ b/base_export_async/tests/test_base_export_async.py @@ -98,3 +98,22 @@ def test_cron_delete(self): # The attachment must be deleted self.assertFalse(new_attachment.exists()) + + def test_portal_export(self): + """Check that we make attachments externally accessible for portal users""" + portal_user = self.env["res.users"].create( + { + "login": "base_export_async_portal_user", + "name": "base_export_async_portal_user", + "groups_id": self.env.ref("base.group_portal").ids, + } + ) + params = json.loads(data_csv.get("data")) + params["user_ids"] = portal_user.ids + attachments = self.env["ir.attachment"].search([]) + mails = self.env["mail.mail"].search([]) + self.delay_export_obj.export(params) + new_attachment = self.env["ir.attachment"].search([]) - attachments + self.assertTrue(new_attachment.access_token) + new_mail = self.env["mail.mail"].search([]) - mails + self.assertIn("&access_token=", new_mail.body) diff --git a/queue_job/README.rst b/queue_job/README.rst index 29800622d8..f22fd7bc10 100644 --- a/queue_job/README.rst +++ b/queue_job/README.rst @@ -11,7 +11,7 @@ Job Queue !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:d14c52037a007ed26c3868531df1cef148fcc85fb5cee8b35e34f3522879cc0f + !! source digest: sha256:b92d06dbbf161572f2bf02e0c6a59282cea11cc5e903378094bead986f0125de !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png diff --git a/queue_job/__manifest__.py b/queue_job/__manifest__.py index c7d9138637..f32b20e2e2 100644 --- a/queue_job/__manifest__.py +++ b/queue_job/__manifest__.py @@ -2,7 +2,7 @@ { "name": "Job Queue", - "version": "16.0.2.11.5", + "version": "16.0.2.12.0", "author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/queue", "license": "LGPL-3", diff --git a/queue_job/controllers/main.py b/queue_job/controllers/main.py index 2cac106b35..4addf1be23 100644 --- a/queue_job/controllers/main.py +++ b/queue_job/controllers/main.py @@ -74,7 +74,13 @@ def _enqueue_dependent_jobs(self, env, job): else: break - @http.route("/queue_job/runjob", type="http", auth="none", save_session=False) + @http.route( + "/queue_job/runjob", + type="http", + auth="none", + save_session=False, + readonly=False, + ) def runjob(self, db, job_uuid, **kw): http.request.session.db = db env = http.request.env(user=SUPERUSER_ID) diff --git a/queue_job/i18n/ca.po b/queue_job/i18n/ca.po index 9d6362e6a9..1a4ae1c1f5 100644 --- a/queue_job/i18n/ca.po +++ b/queue_job/i18n/ca.po @@ -671,6 +671,7 @@ msgstr "Pendent" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Prioritat" @@ -904,6 +905,11 @@ msgid "Time required to execute this job in seconds. Average when grouped." msgstr "" "Temps necessari per executar el treball en segons. Mitjana quan s'agrupa." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/de.po b/queue_job/i18n/de.po index 761b5f46f1..2717a70447 100644 --- a/queue_job/i18n/de.po +++ b/queue_job/i18n/de.po @@ -683,6 +683,7 @@ msgstr "Ausstehend" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Priorität" @@ -929,6 +930,11 @@ msgstr "" "Benötigte Zeit zur Ausführung dieses Jobs in Sekunden. Bei Gruppierung wird " "der Durchschnitt ermittelt." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." @@ -951,8 +957,8 @@ msgid "" msgstr "" "Unerwartetes Format der zugehörigen Aktion für {}.\n" "Beispiel für ein gültiges Format:\n" -"{{\"enable\": True, \"func_name\": \"related_action_foo\", \"kwargs\" {{" -"\"limit\": 10}}}}" +"{{\"enable\": True, \"func_name\": \"related_action_foo\", " +"\"kwargs\" {{\"limit\": 10}}}}" #. module: queue_job #. odoo-python diff --git a/queue_job/i18n/es.po b/queue_job/i18n/es.po index 35547c9138..0580170ec6 100644 --- a/queue_job/i18n/es.po +++ b/queue_job/i18n/es.po @@ -679,6 +679,7 @@ msgstr "Pendiente" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Prioridad" @@ -923,6 +924,11 @@ msgstr "" "Tiempo requerido para ejecutar este trabajo en segundos. Promedio cuando se " "agrupa." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/fr.po b/queue_job/i18n/fr.po index ab7eeb1b6b..0a03d545ef 100644 --- a/queue_job/i18n/fr.po +++ b/queue_job/i18n/fr.po @@ -678,6 +678,7 @@ msgstr "En attente" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Priorité" @@ -923,6 +924,11 @@ msgstr "" "Temps requis pour exécuter cette tâche en seconde. Moyenne lors des " "regroupements." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/it.po b/queue_job/i18n/it.po index 25373a1f35..4f66a2f2a4 100644 --- a/queue_job/i18n/it.po +++ b/queue_job/i18n/it.po @@ -678,6 +678,7 @@ msgstr "In attesa" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Priorità" @@ -920,6 +921,11 @@ msgid "Time required to execute this job in seconds. Average when grouped." msgstr "" "Tempo in secondi richiesto per eseguire il lavoro. Medio quando raggruppati." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/queue_job.pot b/queue_job/i18n/queue_job.pot index a64310a5c6..3a3462af74 100644 --- a/queue_job/i18n/queue_job.pot +++ b/queue_job/i18n/queue_job.pot @@ -661,6 +661,7 @@ msgstr "" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "" @@ -887,6 +888,11 @@ msgstr "" msgid "Time required to execute this job in seconds. Average when grouped." msgstr "" +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/tr.po b/queue_job/i18n/tr.po index a76e2c1ac6..ba8e6bff69 100644 --- a/queue_job/i18n/tr.po +++ b/queue_job/i18n/tr.po @@ -678,6 +678,7 @@ msgstr "Beklemede" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "Öncelik" @@ -920,6 +921,11 @@ msgstr "" "Saniye cinsinden bu işi yapmak için gereken süre. Gruplandığında ortalaması " "alınır." +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/i18n/zh_CN.po b/queue_job/i18n/zh_CN.po index 610288b6d8..f75cca8176 100644 --- a/queue_job/i18n/zh_CN.po +++ b/queue_job/i18n/zh_CN.po @@ -670,6 +670,7 @@ msgstr "等待" #. module: queue_job #: model:ir.model.fields,field_description:queue_job.field_queue_job__priority +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search msgid "Priority" msgstr "优先级" @@ -906,6 +907,11 @@ msgstr "" msgid "Time required to execute this job in seconds. Average when grouped." msgstr "" +#. module: queue_job +#: model_terms:ir.ui.view,arch_db:queue_job.view_queue_job_search +msgid "Tried many times" +msgstr "" + #. module: queue_job #: model:ir.model.fields,help:queue_job.field_queue_job__activity_exception_decoration msgid "Type of the exception activity on record." diff --git a/queue_job/job.py b/queue_job/job.py index d8e50e5124..790e07d90e 100644 --- a/queue_job/job.py +++ b/queue_job/job.py @@ -9,7 +9,6 @@ import uuid import weakref from datetime import datetime, timedelta -from functools import total_ordering from random import randint import odoo @@ -104,7 +103,6 @@ def identity_exact_hasher(job_): return hasher -@total_ordering class Job: """A Job is a task to execute. It is the in-memory representation of a job. @@ -367,65 +365,6 @@ def job_record_with_same_identity_key(self): ) return existing - # TODO to deprecate (not called anymore) - @classmethod - def enqueue( - cls, - func, - args=None, - kwargs=None, - priority=None, - eta=None, - max_retries=None, - description=None, - channel=None, - identity_key=None, - ): - """Create a Job and enqueue it in the queue. Return the job uuid. - - This expects the arguments specific to the job to be already extracted - from the ones to pass to the job function. - - If the identity key is the same than the one in a pending job, - no job is created and the existing job is returned - - """ - new_job = cls( - func=func, - args=args, - kwargs=kwargs, - priority=priority, - eta=eta, - max_retries=max_retries, - description=description, - channel=channel, - identity_key=identity_key, - ) - return new_job._enqueue_job() - - # TODO to deprecate (not called anymore) - def _enqueue_job(self): - if self.identity_key: - existing = self.job_record_with_same_identity_key() - if existing: - _logger.debug( - "a job has not been enqueued due to having " - "the same identity key (%s) than job %s", - self.identity_key, - existing.uuid, - ) - return Job._load_from_db_record(existing) - self.store() - _logger.debug( - "enqueued %s:%s(*%r, **%r) with uuid: %s", - self.recordset, - self.method_name, - self.args, - self.kwargs, - self.uuid, - ) - return self - @staticmethod def db_record_from_uuid(env, job_uuid): # TODO remove in 15.0 or 16.0 @@ -749,16 +688,6 @@ def __eq__(self, other): def __hash__(self): return self.uuid.__hash__() - def sorting_key(self): - return self.eta, self.priority, self.date_created, self.seq - - def __lt__(self, other): - if self.eta and not other.eta: - return True - elif not self.eta and other.eta: - return False - return self.sorting_key() < other.sorting_key() - def db_record(self): return self.db_records_from_uuids(self.env, [self.uuid]) diff --git a/queue_job/jobrunner/channels.py b/queue_job/jobrunner/channels.py index 468fb5760d..7fcabbd07b 100644 --- a/queue_job/jobrunner/channels.py +++ b/queue_job/jobrunner/channels.py @@ -2,6 +2,7 @@ # Copyright 2015-2016 Camptocamp SA # License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) import logging +from collections import namedtuple from functools import total_ordering from heapq import heappop, heappush from weakref import WeakValueDictionary @@ -10,6 +11,7 @@ from ..job import CANCELLED, DONE, ENQUEUED, FAILED, PENDING, STARTED, WAIT_DEPENDENCIES NOT_DONE = (WAIT_DEPENDENCIES, PENDING, ENQUEUED, STARTED, FAILED) +JobSortingKey = namedtuple("SortingKey", "eta priority date_created seq") _logger = logging.getLogger(__name__) @@ -108,7 +110,7 @@ class ChannelJob: job that are necessary to prioritise them. Channel jobs are comparable according to the following rules: - * jobs with an eta come before all other jobs + * jobs with an eta cannot be compared with jobs without * then jobs with a smaller eta come first * then jobs with a smaller priority come first * then jobs with a smaller creation time come first @@ -135,14 +137,18 @@ class ChannelJob: >>> j3 < j1 True - j4 and j5 comes even before j3, because they have an eta + j4 and j5 have an eta, they cannot be compared with j3 >>> j4 = ChannelJob(None, None, 4, ... seq=0, date_created=4, priority=9, eta=9) >>> j5 = ChannelJob(None, None, 5, ... seq=0, date_created=5, priority=9, eta=9) - >>> j4 < j5 < j3 + >>> j4 < j5 True + >>> j4 < j3 + Traceback (most recent call last): + ... + TypeError: '<' not supported between instances of 'int' and 'NoneType' j6 has same date_created and priority as j5 but a smaller eta @@ -153,7 +159,7 @@ class ChannelJob: Here is the complete suite: - >>> j6 < j4 < j5 < j3 < j1 < j2 + >>> j6 < j4 < j5 and j3 < j1 < j2 True j0 has the same properties as j1 but they are not considered @@ -173,14 +179,13 @@ class ChannelJob: """ + __slots__ = ("db_name", "channel", "uuid", "_sorting_key", "__weakref__") + def __init__(self, db_name, channel, uuid, seq, date_created, priority, eta): self.db_name = db_name self.channel = channel self.uuid = uuid - self.seq = seq - self.date_created = date_created - self.priority = priority - self.eta = eta + self._sorting_key = JobSortingKey(eta, priority, date_created, seq) def __repr__(self): return "" % self.uuid @@ -191,18 +196,36 @@ def __eq__(self, other): def __hash__(self): return id(self) + def set_no_eta(self): + self._sorting_key = JobSortingKey(None, *self._sorting_key[1:]) + + @property + def seq(self): + return self._sorting_key.seq + + @property + def date_created(self): + return self._sorting_key.date_created + + @property + def priority(self): + return self._sorting_key.priority + + @property + def eta(self): + return self._sorting_key.eta + def sorting_key(self): - return self.eta, self.priority, self.date_created, self.seq + # DEPRECATED + return self._sorting_key def sorting_key_ignoring_eta(self): - return self.priority, self.date_created, self.seq + return self._sorting_key[1:] def __lt__(self, other): - if self.eta and not other.eta: - return True - elif not self.eta and other.eta: - return False - return self.sorting_key() < other.sorting_key() + # Do not compare job where ETA is set with job where it is not + # If one job 'eta' is set, and the other is None, it raises TypeError + return self._sorting_key < other._sorting_key class ChannelQueue: @@ -312,7 +335,7 @@ def remove(self, job): def pop(self, now): while self._eta_queue and self._eta_queue[0].eta <= now: eta_job = self._eta_queue.pop() - eta_job.eta = None + eta_job.set_no_eta() self._queue.add(eta_job) if self.sequential and self._eta_queue and self._queue: eta_job = self._eta_queue[0] diff --git a/queue_job/jobrunner/runner.py b/queue_job/jobrunner/runner.py index 4e662ebf4e..846682a666 100644 --- a/queue_job/jobrunner/runner.py +++ b/queue_job/jobrunner/runner.py @@ -434,6 +434,17 @@ def __init__( self._stop = False self._stop_pipe = os.pipe() + def __del__(self): + # pylint: disable=except-pass + try: + os.close(self._stop_pipe[0]) + except OSError: + pass + try: + os.close(self._stop_pipe[1]) + except OSError: + pass + @classmethod def from_environ_or_config(cls): scheme = os.environ.get("ODOO_QUEUE_JOB_SCHEME") or queue_job_config.get( diff --git a/queue_job/models/queue_job.py b/queue_job/models/queue_job.py index 075c8f0501..df33e2c7c5 100644 --- a/queue_job/models/queue_job.py +++ b/queue_job/models/queue_job.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from odoo import _, api, exceptions, fields, models -from odoo.tools import config, html_escape +from odoo.tools import config, html_escape, index_exists from odoo.addons.base_sparse_field.models.fields import Serialized @@ -91,7 +91,7 @@ class QueueJob(models.Model): func_string = fields.Char(string="Task", readonly=True) state = fields.Selection(STATES, readonly=True, required=True, index=True) - priority = fields.Integer() + priority = fields.Integer(group_operator=False) exc_name = fields.Char(string="Exception", readonly=True) exc_message = fields.Char(string="Exception Message", readonly=True, tracking=True) exc_info = fields.Text(string="Exception Info", readonly=True) @@ -130,16 +130,21 @@ class QueueJob(models.Model): worker_pid = fields.Integer(readonly=True) def init(self): - self._cr.execute( - "SELECT indexname FROM pg_indexes WHERE indexname = %s ", - ("queue_job_identity_key_state_partial_index",), - ) - if not self._cr.fetchone(): + index_1 = "queue_job_identity_key_state_partial_index" + index_2 = "queue_job_channel_date_done_date_created_index" + if not index_exists(self._cr, index_1): + # Used by Job.job_record_with_same_identity_key self._cr.execute( "CREATE INDEX queue_job_identity_key_state_partial_index " "ON queue_job (identity_key) WHERE state in ('pending', " "'enqueued', 'wait_dependencies') AND identity_key IS NOT NULL;" ) + if not index_exists(self._cr, index_2): + # Used by .autovacuum + self._cr.execute( + "CREATE INDEX queue_job_channel_date_done_date_created_index " + "ON queue_job (channel, date_done, date_created);" + ) @api.depends("records") def _compute_record_ids(self): @@ -408,6 +413,7 @@ def autovacuum(self): ("date_cancelled", "<=", deadline), ("channel", "=", channel.complete_name), ], + order="date_done, date_created", limit=1000, ) if jobs: diff --git a/queue_job/static/description/index.html b/queue_job/static/description/index.html index a194f69546..82bed11d0f 100644 --- a/queue_job/static/description/index.html +++ b/queue_job/static/description/index.html @@ -372,7 +372,7 @@

Job Queue

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:d14c52037a007ed26c3868531df1cef148fcc85fb5cee8b35e34f3522879cc0f +!! source digest: sha256:b92d06dbbf161572f2bf02e0c6a59282cea11cc5e903378094bead986f0125de !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Mature License: LGPL-3 OCA/queue Translate me on Weblate Try me on Runboat

This addon adds an integrated Job Queue to Odoo.

diff --git a/queue_job/tests/common.py b/queue_job/tests/common.py index 13f1f5f832..51e4fec832 100644 --- a/queue_job/tests/common.py +++ b/queue_job/tests/common.py @@ -256,6 +256,7 @@ def _add_job(self, *args, **kwargs): if not job.identity_key or all( j.identity_key != job.identity_key for j in self.enqueued_jobs ): + self._prepare_context(job) self.enqueued_jobs.append(job) patcher = mock.patch.object(job, "store") @@ -274,6 +275,13 @@ def _add_job(self, *args, **kwargs): ) return job + def _prepare_context(self, job): + # pylint: disable=context-overridden + job_model = job.job_model.with_context({}) + field_records = job_model._fields["records"] + # Filter the context to simulate store/load of the job + job.recordset = field_records.convert_to_write(job.recordset, job_model) + def __enter__(self): return self diff --git a/queue_job/tests/test_runner_runner.py b/queue_job/tests/test_runner_runner.py index c6486e27ef..131ce6322d 100644 --- a/queue_job/tests/test_runner_runner.py +++ b/queue_job/tests/test_runner_runner.py @@ -3,8 +3,57 @@ # pylint: disable=odoo-addons-relative-import # we are testing, we want to test as we were an external consumer of the API +import os + +from odoo.tests import BaseCase, tagged + from odoo.addons.queue_job.jobrunner import runner from .common import load_doctests load_tests = load_doctests(runner) + + +@tagged("-at_install", "post_install") +class TestRunner(BaseCase): + @classmethod + def _is_open_file_descriptor(cls, fd): + try: + os.fstat(fd) + return True + except OSError: + return False + + def test_runner_file_descriptor(self): + a_runner = runner.QueueJobRunner.from_environ_or_config() + + read_fd, write_fd = a_runner._stop_pipe + self.assertTrue(self._is_open_file_descriptor(read_fd)) + self.assertTrue(self._is_open_file_descriptor(write_fd)) + + del a_runner + + self.assertFalse(self._is_open_file_descriptor(read_fd)) + self.assertFalse(self._is_open_file_descriptor(write_fd)) + + def test_runner_file_closed_read_descriptor(self): + a_runner = runner.QueueJobRunner.from_environ_or_config() + + read_fd, write_fd = a_runner._stop_pipe + os.close(read_fd) + + del a_runner + + self.assertFalse(self._is_open_file_descriptor(read_fd)) + self.assertFalse(self._is_open_file_descriptor(write_fd)) + + def test_runner_file_closed_write_descriptor(self): + a_runner = runner.QueueJobRunner.from_environ_or_config() + + read_fd, write_fd = a_runner._stop_pipe + os.close(write_fd) + + del a_runner + + self.assertFalse(self._is_open_file_descriptor(read_fd)) + self.assertFalse(self._is_open_file_descriptor(write_fd)) diff --git a/queue_job/views/queue_job_views.xml b/queue_job/views/queue_job_views.xml index 1dc77c5e0b..7f374d8ba8 100644 --- a/queue_job/views/queue_job_views.xml +++ b/queue_job/views/queue_job_views.xml @@ -157,7 +157,7 @@ decoration-muted="state == 'done'" > - + - - - - - - + + + + + + + @@ -212,6 +213,7 @@ + @@ -253,6 +255,12 @@ domain="[('state', '=', 'cancelled')]" /> + + +