changeset 32265209fd9e in trytond:default
details: https://hg.tryton.org/trytond?cmd=changeset;node=32265209fd9e
description:
Simplify cron
issue8201
issue6117
issue2286
diffstat:
CHANGELOG | 1 +
doc/topics/cron.rst | 47 ++++++++++
doc/topics/index.rst | 1 +
doc/topics/start_server.rst | 4 +-
trytond/ir/cron.py | 182 +++++++++++++----------------------------
trytond/ir/view/cron_form.xml | 40 ++++-----
trytond/ir/view/cron_list.xml | 5 +-
trytond/res/ir.xml | 19 +----
8 files changed, 129 insertions(+), 170 deletions(-)
diffs (432 lines):
diff -r 072daf920a7c -r 32265209fd9e CHANGELOG
--- a/CHANGELOG Fri Apr 12 13:08:01 2019 +0200
+++ b/CHANGELOG Fri Apr 12 15:59:25 2019 +0200
@@ -1,3 +1,4 @@
+* Simplify cron
* Add duration on Cache
* Add strip wildcards helpers
* Add list-form view
diff -r 072daf920a7c -r 32265209fd9e doc/topics/cron.rst
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/topics/cron.rst Fri Apr 12 15:59:25 2019 +0200
@@ -0,0 +1,47 @@
+.. _topics-cron:
+
+=================
+Scheduled Actions
+=================
+
+Tryton provides a scheduler (aka cron) which can execute methods of
+:ref:`models <topics-models>` periodically at set intervals.
+
+The planning is managed by ``ir.cron`` records which store the method to call
+and the interval of time between calls. The method must be a class method of a
+:class:`~trytond.model.Model` which can be called without any parameters.
+
+To register a new method with the scheduler, you must extend the ``ir.cron``
+model and append the new method to the
+:attr:`~trytond.model.fields.Selection.selection` attribute of the ``method``
+field in :meth:`~trytond.model.Model.__setup__()`. The name of the selection
+must be the model name and the method name joined together with a ``|`` between
+them.
+
+Example:
+
+.. highlight:: python
+
+::
+
+ from trytond.model import Model
+ from trytond.pool import PoolMeta
+
+ class Cron(metaclass=PoolMeta):
+ __name__ = 'ir.cron'
+
+ @classmethod
+ def __setup__(cls):
+ super().__setup__()
+ cls.method.selection.append(
+ ('my_model|my_method', "Run my method"),
+ )
+
+
+ class MyModel(Model):
+ "My Model"
+ __name__ = 'my_model'
+
+ @classmethod
+ def my_method(cls):
+ pass
diff -r 072daf920a7c -r 32265209fd9e doc/topics/index.rst
--- a/doc/topics/index.rst Fri Apr 12 13:08:01 2019 +0200
+++ b/doc/topics/index.rst Fri Apr 12 15:59:25 2019 +0200
@@ -30,6 +30,7 @@
reports/index
rpc
task_queue
+ cron
user_application
bus
modules/index
diff -r 072daf920a7c -r 32265209fd9e doc/topics/start_server.rst
--- a/doc/topics/start_server.rst Fri Apr 12 13:08:01 2019 +0200
+++ b/doc/topics/start_server.rst Fri Apr 12 15:59:25 2019 +0200
@@ -43,8 +43,8 @@
Cron service
============
-If you want to run some scheduled actions, you must also run the cron server
-with this command line::
+If you want to run some :ref:`scheduled actions <topics-cron>`, you must also
+run the cron server with this command line::
trytond-cron -c <config file> -d <database>
diff -r 072daf920a7c -r 32265209fd9e trytond/ir/cron.py
--- a/trytond/ir/cron.py Fri Apr 12 13:08:01 2019 +0200
+++ b/trytond/ir/cron.py Fri Apr 12 15:59:25 2019 +0200
@@ -2,46 +2,20 @@
# this repository contains the full copyright notices and license terms.
import datetime
from dateutil.relativedelta import relativedelta
-import traceback
-import sys
import logging
-from email.mime.text import MIMEText
-from email.header import Header
-from ast import literal_eval
-from trytond.i18n import gettext
from ..model import ModelView, ModelSQL, DeactivableMixin, fields, dualmethod
from ..transaction import Transaction
from ..pool import Pool
-from ..config import config
-from ..sendmail import sendmail
+from trytond.pyson import Eval
from trytond.worker import run_task
-__all__ = [
- 'Cron',
- ]
-
logger = logging.getLogger(__name__)
-_INTERVALTYPES = {
- 'days': lambda interval: relativedelta(days=interval),
- 'hours': lambda interval: relativedelta(hours=interval),
- 'weeks': lambda interval: relativedelta(weeks=interval),
- 'months': lambda interval: relativedelta(months=interval),
- 'minutes': lambda interval: relativedelta(minutes=interval),
-}
-
class Cron(DeactivableMixin, ModelSQL, ModelView):
"Cron"
__name__ = "ir.cron"
- name = fields.Char('Name', required=True, translate=True)
- user = fields.Many2One('res.user', 'Execution User', required=True,
- domain=[('active', '=', False)],
- help="The user used to execute this action")
- request_user = fields.Many2One(
- 'res.user', 'Request User', required=True,
- help="The user who will receive requests in case of failure")
interval_number = fields.Integer('Interval Number', required=True)
interval_type = fields.Selection([
('minutes', 'Minutes'),
@@ -49,17 +23,35 @@
('days', 'Days'),
('weeks', 'Weeks'),
('months', 'Months'),
- ], 'Interval Unit')
- number_calls = fields.Integer('Number of Calls', select=1, required=True,
- help=('Number of times the function is called, a negative '
- 'number indicates that the function will always be '
- 'called'))
- repeat_missed = fields.Boolean('Repeat Missed')
- next_call = fields.DateTime('Next Call', required=True,
- select=True)
- model = fields.Char('Model')
- function = fields.Char('Function')
- args = fields.Text('Arguments')
+ ], "Interval Type", required=True)
+ minute = fields.Integer("Minute",
+ states={
+ 'invisible': Eval('interval_type').in_(['minutes']),
+ },
+ depends=['interval_type'])
+ hour = fields.Integer("Hour",
+ states={
+ 'invisible': Eval('interval_type').in_(['minutes', 'hours']),
+ },
+ depends=['interval_type'])
+ weekday = fields.Many2One(
+ 'ir.calendar.day', "Day of Week",
+ states={
+ 'invisible': Eval('interval_type').in_(
+ ['minutes', 'hours', 'days']),
+ },
+ depends=['interval_type'])
+ day = fields.Integer("Day",
+ states={
+ 'invisible': Eval('interval_type').in_(
+ ['minutes', 'hours', 'days', 'weeks']),
+ },
+ depends=['interval_type'])
+
+ next_call = fields.DateTime("Next Call", select=True)
+ method = fields.Selection([
+ ('ir.trigger|trigger_time', "Run On Time Triggers"),
+ ], "Method", required=True)
@classmethod
def __setup__(cls):
@@ -70,84 +62,43 @@
},
})
- @staticmethod
- def default_next_call():
- return datetime.datetime.now()
+ @classmethod
+ def __register__(cls, module_name):
+ super().__register__(module_name)
- @staticmethod
- def default_interval_number():
- return 1
+ table_h = cls.__table_handler__(module_name)
- @staticmethod
- def default_interval_type():
- return 'months'
+ # Migration from 5.0: remove fields
+ for column in ['name', 'user', 'request_user', 'number_calls',
+ 'repeat_missed', 'model', 'function', 'args']:
+ table_h.drop_column(column)
- @staticmethod
- def default_number_calls():
- return -1
-
- @staticmethod
- def default_repeat_missed():
- return True
+ # Migration from 5.0: remove required on next_call
+ table_h.not_null_action('next_call', 'remove')
@staticmethod
def check_xml_record(crons, values):
return True
- @staticmethod
- def get_delta(cron):
- '''
- Return the relativedelta for the next call
- '''
- return _INTERVALTYPES[cron.interval_type](cron.interval_number)
-
- def send_error_message(self):
- pool = Pool()
- Config = pool.get('ir.configuration')
-
- if self.request_user.language:
- language = self.request_user.language.code
- else:
- language = Config.get_language()
-
- with Transaction().set_user(self.user.id), \
- Transaction().set_context(language=language):
- tb_s = ''.join(traceback.format_exception(*sys.exc_info()))
- # On Python3, the traceback is already a unicode
- if hasattr(tb_s, 'decode'):
- tb_s = tb_s.decode('utf-8', 'ignore')
- message = gettext(
- 'ir.msg_cron_email',
- name=self.name,
- url=self.__url__,
- traceback=tb_s).splitlines()
- subject = message[0]
- body = '\n'.join(message[1:])
-
- from_addr = config.get('email', 'from')
- to_addr = self.request_user.email
-
- msg = MIMEText(body, _charset='utf-8')
- msg['To'] = to_addr
- msg['From'] = from_addr
- msg['Subject'] = Header(subject, 'utf-8')
- if not to_addr:
- logger.error(msg.as_string())
- else:
- sendmail(from_addr, to_addr, msg)
+ def compute_next_call(self, now):
+ return (now
+ + relativedelta(**{self.interval_type: self.interval_number})
+ + relativedelta(
+ microsecond=0,
+ second=0,
+ minute=self.minute,
+ hour=self.hour,
+ day=self.day,
+ weekday=int(self.weekday.index) if self.weekday else None))
@dualmethod
@ModelView.button
def run_once(cls, crons):
pool = Pool()
for cron in crons:
- if cron.args:
- args = literal_eval(cron.args)
- else:
- args = []
- Model = pool.get(cron.model)
- with Transaction().set_user(cron.user.id):
- getattr(Model, cron.function)(*args)
+ model, method = cron.method.split('|')
+ Model = pool.get(model)
+ getattr(Model, method)()
@classmethod
def run(cls, db_name):
@@ -155,33 +106,16 @@
now = datetime.datetime.now()
with Transaction().start(db_name, 0) as transaction:
transaction.database.lock(transaction.connection, cls._table)
- crons = cls.search([
- ('number_calls', '!=', 0),
- ('next_call', '<=', datetime.datetime.now()),
+ crons = cls.search(['OR',
+ ('next_call', '<=', now),
+ ('next_call', '=', None),
])
for cron in crons:
logger.info("Run cron %s", cron.id)
try:
- next_call = cron.next_call
- number_calls = cron.number_calls
- first = True
- while next_call < now and number_calls != 0:
- if first or cron.repeat_missed:
- try:
- cron.run_once()
- except Exception:
- transaction.rollback()
- cron.send_error_message()
- next_call += cls.get_delta(cron)
- if number_calls > 0:
- number_calls -= 1
- first = False
-
- cron.next_call = next_call
- cron.number_calls = number_calls
- if not number_calls:
- cron.active = False
+ cron.run_once()
+ cron.next_call = cron.compute_next_call(now)
cron.save()
transaction.commit()
except Exception:
diff -r 072daf920a7c -r 32265209fd9e trytond/ir/view/cron_form.xml
--- a/trytond/ir/view/cron_form.xml Fri Apr 12 13:08:01 2019 +0200
+++ b/trytond/ir/view/cron_form.xml Fri Apr 12 15:59:25 2019 +0200
@@ -2,31 +2,27 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<form>
- <label name="name"/>
- <field name="name"/>
+ <label name="method"/>
+ <field name="method"/>
<label name="active"/>
<field name="active"/>
- <label name="user"/>
- <field name="user"/>
- <label name="request_user"/>
- <field name="request_user"/>
- <label name="interval_number"/>
- <field name="interval_number"/>
- <label name="interval_type"/>
- <field name="interval_type"/>
- <label name="number_calls"/>
- <field name="number_calls"/>
+ <label id="interval_label" string="Every"/>
+ <group id="interval" col="-1" colspan="3">
+ <field name="interval_number"/>
+ <field name="interval_type"/>
+ </group>
+ <label id="time_label" string="At"/>
+ <group id="time" col="-1" colspan="3">
+ <label name="minute"/>
+ <field name="minute" xexpand="0"/>
+ <label name="hour"/>
+ <field name="hour" xexpand="0"/>
+ <label name="day"/>
+ <field name="day" xexpand="0"/>
+ <label name="weekday"/>
+ <field name="weekday" xexpand="0" widget="selection"/>
+ </group>
<label name="next_call"/>
<field name="next_call"/>
- <label name="repeat_missed"/>
- <field name="repeat_missed"/>
<button name="run_once" colspan="2"/>
- <separator string="Action to trigger" colspan="4"
- id="action_trigger"/>
- <label name="model"/>
- <field name="model"/>
- <label name="function"/>
- <field name="function"/>
- <label name="args"/>
- <field name="args" colspan="3" widget="char"/>
</form>
diff -r 072daf920a7c -r 32265209fd9e trytond/ir/view/cron_list.xml
--- a/trytond/ir/view/cron_list.xml Fri Apr 12 13:08:01 2019 +0200
+++ b/trytond/ir/view/cron_list.xml Fri Apr 12 15:59:25 2019 +0200
@@ -2,13 +2,10 @@
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tree>
+ <field name="method"/>
<field name="next_call" widget="date"/>
<field name="next_call" widget="time"/>
- <field name="name"/>
- <field name="user"/>
- <field name="request_user"/>
<field name="interval_number"/>
<field name="interval_type"/>
- <field name="number_calls"/>
<button name="run_once" string="Run Once" tree_invisible="1"/>
</tree>
diff -r 072daf920a7c -r 32265209fd9e trytond/res/ir.xml
--- a/trytond/res/ir.xml Fri Apr 12 13:08:01 2019 +0200
+++ b/trytond/res/ir.xml Fri Apr 12 15:59:25 2019 +0200
@@ -792,27 +792,10 @@
<field name="rule_group" ref="rule_group_sequence_strict"/>
</record>
- <record model="res.user" id="user_trigger">
- <field name="login">user_cron_trigger</field>
- <field name="name">Cron Trigger</field>
- <field name="active" eval="False"/>
- </record>
- <record model="res.user-res.group" id="user_trigger_group_admin">
- <field name="user" ref="user_trigger"/>
- <field name="group" ref="group_admin"/>
- </record>
-
<record model="ir.cron" id="cron_trigger_time">
- <field name="name">Run On Time Triggers</field>
- <field name="request_user" ref="user_admin"/>
- <field name="user" ref="user_trigger"/>
- <field name="active" eval="True"/>
+ <field name="method">ir.trigger|trigger_time</field>
<field name="interval_number" eval="5"/>
<field name="interval_type">minutes</field>
- <field name="number_calls" eval="-1"/>
- <field name="repeat_missed" eval="False"/>
- <field name="model">ir.trigger</field>
- <field name="function">trigger_time</field>
</record>
<record model="ir.model.access" id="rule_default_view_tree_state">