Simplify cron
issue8201 issue6117 issue2286
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
* Simplify cron
|
||||
* Add duration on Cache
|
||||
* Add strip wildcards helpers
|
||||
* Add list-form view
|
||||
|
||||
@@ -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
|
||||
@@ -30,6 +30,7 @@ Introduction to all the key parts of trytond:
|
||||
reports/index
|
||||
rpc
|
||||
task_queue
|
||||
cron
|
||||
user_application
|
||||
bus
|
||||
modules/index
|
||||
|
||||
@@ -43,8 +43,8 @@ You can start the server with coroutine using the option `--coroutine`.
|
||||
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>
|
||||
|
||||
|
||||
+58
-124
@@ -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 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
('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 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
},
|
||||
})
|
||||
|
||||
@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 @@ class Cron(DeactivableMixin, ModelSQL, ModelView):
|
||||
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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-18
@@ -792,27 +792,10 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user