[IMP] ir.cron: trigger on multiple moments

Often the business want to trigger the cron several time at different
moments.  The typical use case is the business holds a recordset and
needs to trigger the cron for every record.  The current solution is to
loop on each record to call `_trigger` times.

We now allow to invoke `_trigger` with an iterable of `datetime`
objects, and isolate the implementation in method `_trigger_list`.

We also removed the `delay` argument as it was not used.

closes odoo/odoo#65459

Task: 2452697
Signed-off-by: Julien Castiaux <Julien00859@users.noreply.github.com>
This commit is contained in:
Julien Castiaux
2021-02-04 08:55:23 +00:00
committed by Raphael Collet
parent c4b4eb699d
commit a29bb54633
2 changed files with 46 additions and 20 deletions
@@ -162,7 +162,10 @@ class TestEventNotifications(TransactionCase, MailCase):
triggers_after = self.env['ir.cron.trigger'].search([('cron_id', '=', cron_id)]) triggers_after = self.env['ir.cron.trigger'].search([('cron_id', '=', cron_id)])
new_triggers = triggers_after - triggers_before new_triggers = triggers_after - triggers_before
new_triggers.ensure_one() new_triggers.ensure_one()
self.assertEqual(new_triggers.call_at, now - relativedelta(minutes=5)) self.assertEqual(
new_triggers.call_at,
now.replace(second=0) - relativedelta(minutes=5),
)
with patch.object(fields.Datetime, 'now', lambda: now): with patch.object(fields.Datetime, 'now', lambda: now):
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], { with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
+42 -19
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details. # Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging import logging
import threading import threading
@@ -11,6 +10,7 @@ from dateutil.relativedelta import relativedelta
import odoo import odoo
from odoo import api, fields, models, _ from odoo import api, fields, models, _
from odoo.exceptions import UserError from odoo.exceptions import UserError
from odoo.tools import unique
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -393,36 +393,59 @@ class ir_cron(models.Model):
return self.try_write({'active': active}) return self.try_write({'active': active})
@api.model @api.model
def _trigger(self, delay=None, at=None): def _trigger(self, at=None):
""" """
Schedule a cron job to be executed soon independently of its Schedule a cron job to be executed soon independently of its
``nextcall`` field value. ``nextcall`` field value.
By default the cron is scheduled to be executed in the next By default the cron is scheduled to be executed in the next batch but
batch but optional arguments may be given to delay the execution the optional `at` argument may be given to delay the execution later
with a precision down to 1 minute. with a precision down to 1 minute.
:param datetime.timedelta delay: The method may be called with a datetime or an iterable of datetime.
Execute the cron later, after the delay expires. The actual implementation is in :meth:`~._trigger_list`, which is the
recommended method for overrides.
:param datetime.datetime at: :param Optional[Union[datetime.datetime, list[datetime.datetime]]] at:
Execute the cron later, at a precise moment in time. When to execute the cron, at one or several moments in time instead
of as soon as possible.
""" """
if at is None:
at_list = [datetime.utcnow()]
elif isinstance(at, datetime):
at_list = [at]
else:
at_list = list(at)
assert all(isinstance(at, datetime) for at in at_list)
self._trigger_list(at_list)
@api.model
def _trigger_list(self, at_list):
"""
Implementation of :meth:`~._trigger`.
:param list[datetime.datetime] at_list:
Execute the cron later, at precise moments in time.
"""
if not at_list:
return
self.ensure_one() self.ensure_one()
now = datetime.utcnow().replace(second=0, microsecond=0)
if delay and at: # compress the list to insert at most one trigger per minute
raise ValueError("Set either a delay either a moment, not both.") at_list = list(unique(at.replace(second=0, microsecond=0) for at in at_list))
now = datetime.utcnow() self.env['ir.cron.trigger'].sudo().create([
call_at = at or now {'cron_id': self.id, 'call_at': at}
if delay: for at in at_list
call_at += delay ])
if _logger.isEnabledFor(logging.DEBUG):
ats = ', '.join(map(str, at_list))
_logger.debug("will execute '%s' at %s", self.sudo().name, ats)
self.env['ir.cron.trigger'].sudo().create({'cron_id': self.id, 'call_at': call_at}) if min(at_list) <= now:
_logger.debug("will execute '%s' at %s",
self.name, call_at if call_at > now else "soon as possible")
if call_at <= now:
self._cr.postcommit.add(self._notifydb) self._cr.postcommit.add(self._notifydb)
def _notifydb(self): def _notifydb(self):