details:   https://code.tryton.org/tryton/commit/532babdb004e
branch:    default
user:      Cédric Krier <[email protected]>
date:      Mon Jul 13 17:36:10 2026 +0200
description:
        Retry unfinished queued tasks

        Closes #14949
diffstat:

 doc/migration.rst            |  12 ++++++++++++
 trytond/CHANGELOG            |   1 +
 trytond/trytond/ir/cron.py   |   1 +
 trytond/trytond/ir/error.py  |  16 +++++++++++-----
 trytond/trytond/ir/queue.xml |   6 ++++++
 trytond/trytond/ir/queue_.py |  37 +++++++++++++++++++++++++++++++++++++
 trytond/trytond/worker.py    |  35 +++++++++++++++++++++++++++++++----
 7 files changed, 99 insertions(+), 9 deletions(-)

diffs (216 lines):

diff -r 51728c97c24f -r 532babdb004e doc/migration.rst
--- a/doc/migration.rst Wed Jul 08 22:58:48 2026 +0200
+++ b/doc/migration.rst Mon Jul 13 17:36:10 2026 +0200
@@ -19,6 +19,18 @@
 8.2
 ---
 
+.. _migration-8.2-before:
+
+Before
+~~~~~~
+
+* If the ``trytond-worker`` is configured, stop it and finish all dequeued
+  tasks:
+
+   .. code-block:: SQL
+
+      UPDATE "ir_queue" SET finished_at = CURRENT_TIMESTAMP WHERE dequeued_at 
IS NOT NULL AND finished_at IS NULL;
+
 .. _migration-8.2-after:
 
 After
diff -r 51728c97c24f -r 532babdb004e trytond/CHANGELOG
--- a/trytond/CHANGELOG Wed Jul 08 22:58:48 2026 +0200
+++ b/trytond/CHANGELOG Mon Jul 13 17:36:10 2026 +0200
@@ -1,3 +1,4 @@
+* Retry unfinished queued tasks
 * Add a timer to prevent requests from lasting longer than their timeout
 * Apply mixin to Database and TableHandler from configuration
 * Restrict protocols allowed for weasyprint to http(s) (issue14947)
diff -r 51728c97c24f -r 532babdb004e trytond/trytond/ir/cron.py
--- a/trytond/trytond/ir/cron.py        Wed Jul 08 22:58:48 2026 +0200
+++ b/trytond/trytond/ir/cron.py        Mon Jul 13 17:36:10 2026 +0200
@@ -93,6 +93,7 @@
         fields.Boolean("Running"), 'get_running')
     method = fields.Selection([
             ('ir.trigger|trigger_time', "Run On Time Triggers"),
+            ('ir.queue|retry', "Retry Queued Tasks"),
             ('ir.queue|clean', "Clean Task Queue"),
             ('ir.error|clean', "Clean Errors"),
             ('ir.cron.log|clean', "Clean Cron Logs"),
diff -r 51728c97c24f -r 532babdb004e trytond/trytond/ir/error.py
--- a/trytond/trytond/ir/error.py       Wed Jul 08 22:58:48 2026 +0200
+++ b/trytond/trytond/ir/error.py       Mon Jul 13 17:36:10 2026 +0200
@@ -81,11 +81,15 @@
         super().__setup__()
         table = cls.__table__()
 
-        cls._sql_indexes.add(
-            Index(
-                table,
-                (table.state, Index.Equality(cardinality='low')),
-                where=table.state.in_(['open', 'processing'])))
+        cls._sql_indexes.update({
+                Index(
+                    table,
+                    (table.state, Index.Equality(cardinality='low')),
+                    where=table.state.in_(['open', 'processing'])),
+                Index(
+                    table,
+                    (table.origin, Index.Equality(cardinality='high'))),
+                })
         cls._order = [
             ('create_date', 'DESC'),
             ('id', 'DESC'),
@@ -194,4 +198,6 @@
                 Cron.__queue__.run_once([error.origin])
             elif isinstance(error.origin, Queue):
                 task = error.origin
+                task.finished_at = dt.datetime.now()
+                task.save()
                 Queue.push(task.name, task.data)
diff -r 51728c97c24f -r 532babdb004e trytond/trytond/ir/queue.xml
--- a/trytond/trytond/ir/queue.xml      Wed Jul 08 22:58:48 2026 +0200
+++ b/trytond/trytond/ir/queue.xml      Mon Jul 13 17:36:10 2026 +0200
@@ -3,6 +3,12 @@
 this repository contains the full copyright notices and license terms. -->
 <tryton>
     <data noupdate="1">
+        <record model="ir.cron" id="cron_queue_retry">
+            <field name="method">ir.queue|retry</field>
+            <field name="interval_number" eval="1"/>
+            <field name="interval_type">hours</field>
+        </record>
+
         <record model="ir.cron" id="cron_queue_clean">
             <field name="method">ir.queue|clean</field>
             <field name="interval_number" eval="1"/>
diff -r 51728c97c24f -r 532babdb004e trytond/trytond/ir/queue_.py
--- a/trytond/trytond/ir/queue_.py      Wed Jul 08 22:58:48 2026 +0200
+++ b/trytond/trytond/ir/queue_.py      Mon Jul 13 17:36:10 2026 +0200
@@ -5,6 +5,7 @@
 from sql import Literal, Null, With
 from sql.aggregate import Min
 from sql.functions import CurrentTimestamp, Extract
+from sql.operators import Concat, Exists
 
 import trytond.config as config
 from trytond.model import Index, ModelSQL, fields
@@ -146,6 +147,7 @@
     def run(self):
         transaction = Transaction()
         Model = Pool().get(self.data['model'])
+        self.lock()
         with transaction.set_user(self.data['user']), \
                 transaction.set_context(
                     self.data['context'], _skip_warnings=True):
@@ -174,6 +176,41 @@
         self.save()
 
     @classmethod
+    def retry(cls):
+        pool = Pool()
+        Error = pool.get('ir.error')
+        queue = cls.__table__()
+        queue_s = cls.__table__()
+        error = Error.__table__()
+        transaction = Transaction()
+        database = transaction.database
+        cursor = transaction.connection.cursor()
+
+        selected = queue_s.select(
+            queue_s.id,
+            where=(queue_s.dequeued_at != Null)
+            & (queue_s.finished_at == Null)
+            & ~Exists(error.select(
+                    error.origin,
+                    where=error.origin == Concat('ir.queue,', queue_s.id))))
+        if database.has_select_for():
+            For = database.get_select_for_skip_locked()
+            selected.for_ = For('UPDATE')
+        update = queue.update(
+            [queue.dequeued_at],
+            [None],
+            where=queue.id.in_(selected))
+        if database.has_returning():
+            update.returning = [queue.id]
+        cursor.execute(*update)
+        if database.has_returning():
+            task_updated = bool(cursor.fetchone())
+        else:
+            task_updated = True
+        if task_updated and database.has_channel():
+            database.notify(transaction.connection, cls.__name__, '')
+
+    @classmethod
     def clean(cls, date=None):
         if date is None:
             date = (
diff -r 51728c97c24f -r 532babdb004e trytond/trytond/worker.py
--- a/trytond/trytond/worker.py Wed Jul 08 22:58:48 2026 +0200
+++ b/trytond/trytond/worker.py Mon Jul 13 17:36:10 2026 +0200
@@ -150,7 +150,11 @@
     retry = config.getint('database', 'retry')
     try:
         count = 0
-        transaction_extras = {}
+        transaction_extras = {
+            '_lock_records': {
+                Queue._table: [task_id],
+                }
+            }
         while True:
             if count:
                 time.sleep(0.02 * count)
@@ -188,11 +192,21 @@
         if not config.getboolean('queue', 'worker', default=False):
             time.sleep(0.02 * retry)
         try:
-            with Transaction().start(pool.database_name, 0) as transaction:
+            transaction_extras = {
+                '_lock_records': {
+                    Queue._table: [task_id],
+                    }
+                }
+            with Transaction().start(
+                    pool.database_name, 0,
+                    **transaction_extras) as transaction:
                 if not transaction.database.has_channel():
                     logger.critical('%s failed', name, exc_info=True)
                     return
                 task = Queue(task_id)
+                task.lock()
+                task.finished_at = dt.datetime.now()
+                task.save()
                 if task.scheduled_at and task.enqueued_at < task.scheduled_at:
                     duration = (task.scheduled_at - task.enqueued_at) * 2
                 else:
@@ -201,8 +215,9 @@
                 scheduled_at = dt.datetime.now() + duration * random.random()
                 Queue.push(task.name, task.data, scheduled_at=scheduled_at)
         except Exception:
-            logger.critical(
-                "rescheduling %s failed", name, exc_info=True)
+            logger.info(
+                "rescheduling %s failed", name,
+                exc_info=logger.isEnabledFor(logging.DEBUG))
     except (UserError, UserWarning):
         logger.info(
             "%s failed after %i ms", name, duration(),
@@ -210,3 +225,15 @@
     except Exception:
         logger.critical(
             "%s failed after %i ms", name, duration(), exc_info=True)
+        transaction_extras = {
+                '_lock_records': {
+                    Queue._table: [task_id],
+                    }
+                }
+        with Transaction().start(
+                pool.database_name, 0,
+                **transaction_extras) as transaction:
+            task = Queue(task_id)
+            task.lock()
+            # Avoid retrying tasks that fail unexpectedly
+            task.finished_at = dt.datetime.now()

Reply via email to