From 658735f5b4e07f2b0ceb585f9e38e6f5d2f525cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 7 Jun 2011 11:35:50 +0200 Subject: [PATCH 1/6] Keep user in context when browsing user It is needed to allow override of user values per user like the change of company by context. review30003 --- trytond/ir/rule.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/trytond/ir/rule.py b/trytond/ir/rule.py index 10e4d662f0..12e61f0846 100644 --- a/trytond/ir/rule.py +++ b/trytond/ir/rule.py @@ -163,6 +163,7 @@ class Rule(ModelSQL, ModelView): rule_group_user_obj = self.pool.get('ir.rule.group-res.user') rule_group_group_obj = self.pool.get('ir.rule.group-res.group') user_group_obj = self.pool.get('res.user-res.group') + user_obj = self.pool.get('res.user') cursor = Transaction().cursor cursor.execute('SELECT r.id FROM "' + self._table + '" r ' \ @@ -191,15 +192,17 @@ class Rule(ModelSQL, ModelView): clause_global = {} operand2query = self._operand_get('res.user', level=1, recur=['many2one'], root_tech='user', root='User') + user_id = Transaction().user + with Transaction().set_user(0, set_context=True): + user = user_obj.browse(user_id) # Use root user without context to prevent recursion - user = Transaction().user with contextlib.nested(Transaction().set_user(0), Transaction().set_context(user=0)): for rule in self.browse(ids): dom = safe_eval("[('%s', '%s', %s)]" % \ (rule.field.name, rule.operator, operand2query[rule.operand]), { - 'user': self.pool.get('res.user').browse(user), + 'user': user, 'time': time, }) @@ -226,7 +229,8 @@ class Rule(ModelSQL, ModelView): 'FROM "' + rule_group_group_obj._table + '" g_rel ' \ 'JOIN "' + user_group_obj._table + '" u_rel ' \ 'ON g_rel.group_id = u_rel.gid ' \ - 'WHERE u_rel.uid = %s))', (model_name, user, user)) + 'WHERE u_rel.uid = %s))', + (model_name, user_id, user_id)) fetchone = cursor.fetchone() if fetchone: group_id = fetchone[0] From ff5bfb26ba6b21db30de6e181aae705b625ff81e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 7 Jun 2011 11:58:55 +0200 Subject: [PATCH 2/6] Signal is a keyword for MySQL 5.5 issue2037 review30006 --- trytond/backend/mysql/init.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trytond/backend/mysql/init.sql b/trytond/backend/mysql/init.sql index cbb2a56bc4..bb500939fd 100644 --- a/trytond/backend/mysql/init.sql +++ b/trytond/backend/mysql/init.sql @@ -121,7 +121,7 @@ CREATE TABLE wkf_transition ( act_to BIGINT, `condition` VARCHAR(255), trigger_expr_id VARCHAR(255), - signal VARCHAR(255), + `signal` VARCHAR(255), `group` BIGINT, PRIMARY KEY(id), CONSTRAINT wkf_transition_act_from_fkey FOREIGN KEY (act_from) REFERENCES wkf_activity (id) ON DELETE CASCADE, From 0736573c2df00a4b3473b9b1bf9fae68827db82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20=C3=89vrard?= Date: Wed, 8 Jun 2011 14:41:36 +0200 Subject: [PATCH 3/6] Add missing colspan attribute for action --- trytond/ir/ui/board.rnc | 1 + trytond/ir/ui/board.rng | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/trytond/ir/ui/board.rnc b/trytond/ir/ui/board.rnc index 06662802c0..19fd9c112a 100644 --- a/trytond/ir/ui/board.rnc +++ b/trytond/ir/ui/board.rnc @@ -144,5 +144,6 @@ attlist.xpath &= attribute position { "inside" | "replace" | "replace_attributes" | "after" | "before" }? action = element action { attlist.action, empty } attlist.action &= attribute name { text } +attlist.action &= [ a:defaultValue = "1" ] attribute colspan { text }? label |= notAllowed start = data | board | label diff --git a/trytond/ir/ui/board.rng b/trytond/ir/ui/board.rng index a0c90e3b8d..06d68504d2 100644 --- a/trytond/ir/ui/board.rng +++ b/trytond/ir/ui/board.rng @@ -447,6 +447,11 @@ + + + + + From 1bb663298b2eafd2070502b77edba69b6030ea77 Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Wed, 8 Jun 2011 13:06:22 -0400 Subject: [PATCH 4/6] Change field type of timestamp_rounding to Float to allow higher precision timestamp in sequence fixes issue2049 --- trytond/ir/sequence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trytond/ir/sequence.py b/trytond/ir/sequence.py index aa13ef7d91..828f4b27ce 100644 --- a/trytond/ir/sequence.py +++ b/trytond/ir/sequence.py @@ -49,7 +49,7 @@ class Sequence(ModelSQL, ModelView): states={ 'invisible': Not(In(Eval('type'), ['incremental'])), }, depends=['type']) - timestamp_rounding = fields.Integer('Timestamp Rounding', required=True, + timestamp_rounding = fields.Float('Timestamp Rounding', required=True, states={ 'invisible': Not(In(Eval('type'), ['decimal timestamp', 'hexadecimal timestamp'])), From 641cb994403ec1449b58b67a1c6c2ddbd0c2f672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Tue, 14 Jun 2011 11:28:51 +0200 Subject: [PATCH 5/6] Improve cron - Fix racing condition with long job - Add user restriction for issue1817 - Remove unuseful fields: - priority (all cron must be runned) - running (never visible outside the transaction) - Rename some field according to their label - Remove "Work Days" interval type - Add cron option to activate or not the cron review38008 --- bin/trytond | 3 + etc/trytond.conf | 3 + trytond/config.py | 1 + trytond/ir/cron.py | 218 +++++++++++++++++++++----------------------- trytond/ir/cron.xml | 40 ++++---- trytond/res/ir.xml | 5 +- trytond/server.py | 24 +++-- 7 files changed, 148 insertions(+), 146 deletions(-) diff --git a/bin/trytond b/bin/trytond index ffd5d54750..a55071c781 100755 --- a/bin/trytond +++ b/bin/trytond @@ -37,6 +37,8 @@ def parse_commandline(): help="file where the server pid will be stored") parser.add_option("--logfile", dest="logfile", help="file where the server log will be stored") + parser.add_option("--disable-cron", dest="cron", + action="store_false", help="disable cron") (opt, _) = parser.parse_args() @@ -51,6 +53,7 @@ def parse_commandline(): 'debug_mode', 'pidfile', 'logfile', + 'cron', ): if getattr(opt, arg) is not None: options[arg] = getattr(opt, arg) diff --git a/etc/trytond.conf b/etc/trytond.conf index 90e74c0c97..894d201565 100644 --- a/etc/trytond.conf +++ b/etc/trytond.conf @@ -90,3 +90,6 @@ # Prevent database listing #prevent_dblist = False + +# Enable cron +# cron = True diff --git a/trytond/config.py b/trytond/config.py index 6d59bccdcb..ef587c22e5 100644 --- a/trytond/config.py +++ b/trytond/config.py @@ -51,6 +51,7 @@ class ConfigManager(object): 'prevent_dblist': False, 'init': {}, 'update': {}, + 'cron': True, } self.configfile = None diff --git a/trytond/ir/cron.py b/trytond/ir/cron.py index dfa81d2c45..9ed8151779 100644 --- a/trytond/ir/cron.py +++ b/trytond/ir/cron.py @@ -6,13 +6,14 @@ import datetime from dateutil.relativedelta import relativedelta import traceback import sys +import logging from trytond.backend import Database from trytond.model import ModelView, ModelSQL, fields from trytond.tools import safe_eval from trytond.transaction import Transaction +from trytond.backend import TableHandler _INTERVALTYPES = { - 'work_days': lambda interval: relativedelta(days=interval), 'days': lambda interval: relativedelta(days=interval), 'hours': lambda interval: relativedelta(hours=interval), 'weeks': lambda interval: relativedelta(weeks=interval), @@ -26,7 +27,8 @@ class Cron(ModelSQL, ModelView): _description = __doc__ name = fields.Char('Name', required=True, translate=True) user = fields.Many2One('res.user', 'Execution User', required=True, - help="The user used to execute this action") + 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") @@ -35,24 +37,20 @@ class Cron(ModelSQL, ModelView): interval_type = fields.Selection( [ ('minutes', 'Minutes'), ('hours', 'Hours'), - ('work_days', 'Work Days'), ('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months'), ], 'Interval Unit') - numbercall = fields.Integer('Number of calls', select=1, - help='Number of times the function is called,\n' \ - 'a negative number indicates that the function ' \ - 'will always be called') - doall = fields.Boolean('Repeat missed') - nextcall = fields.DateTime('Next call date', required=True, + number_calls = fields.Integer('Number of Calls', select=1, + 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=1) model = fields.Char('Model') function = fields.Char('Function') args = fields.Text('Arguments') - priority = fields.Integer('Priority', - help='0=Very Urgent\n10=Not urgent') - running = fields.Boolean('Running', readonly=True, select=1) def __init__(self): super(Cron, self).__init__() @@ -62,135 +60,131 @@ class Cron(ModelSQL, ModelView): "properly: \"%s\"\n Traceback: \n\n%s\n" }) - def default_nextcall(self): + def init(self, module_name): + cursor = Transaction().cursor + + # Migration from 2.0: rename numbercall, doall and nextcall + table = TableHandler(cursor, self, module_name) + table.column_rename('numbercall', 'number_calls') + table.column_rename('doall', 'repeat_missed') + table.column_rename('nextcall', 'next_call') + + super(Cron, self).init(module_name) + + # Migration from 2.0: work_days removed + cursor.execute('UPDATE "%s" ' + 'SET interval_type = %%s ' + 'WHERE interval_type = %%s' % self._table, + ('days', 'work_days')) + + def default_next_call(self): return datetime.datetime.now() - def default_priority(self): - return 5 - - def default_user(self): - return int(Transaction().user) - def default_interval_number(self): return 1 def default_interval_type(self): return 'months' - def default_numbercall(self): - return 1 + def default_number_calls(self): + return -1 def default_active(self): return True - def default_doall(self): + def default_repeat_missed(self): return True - def default_running(self): - return False - def check_xml_record(self, ids, values): return True + def get_delta(self, cron): + ''' + Return the relativedelta for the next call + ''' + return _INTERVALTYPES[cron.interval_type](cron.interval_number) + + def _get_request_values(self, cron): + tb_s = reduce(lambda x, y: x + y, + traceback.format_exception(*sys.exc_info())) + tb_s = tb_s.decode('utf-8', 'ignore') + name = self.raise_user_error('request_title', + raise_exception=False) + body = self.raise_user_error('request_body', (cron.name, tb_s), + raise_exception=False) + values = { + 'name': name, + 'priority': '2', + 'act_from': cron.user.id, + 'act_to': cron.request_user.id, + 'body': body, + 'date_sent': datetime.datetime.now(), + 'references': [ + ('create', { + 'reference': '%s,%s' % (self._name, cron.id), + }), + ], + 'state': 'waiting', + 'trigger_date': datetime.datetime.now(), + } + return values + def _callback(self, cron): try: - args = (cron['args'] or []) and safe_eval(cron['args']) - obj = self.pool.get(cron['model']) - if not obj and hasattr(obj, cron['function']): - return False - fct = getattr(obj, cron['function']) - with Transaction().set_user(cron['user']): - fct(*args) + args = (cron.args or []) and safe_eval(cron.args) + model_obj = self.pool.get(cron.model) + with Transaction().set_user(cron.user.id): + getattr(model_obj, cron.function)(*args) except Exception, error: Transaction().cursor.rollback() - tb_s = '' - for line in traceback.format_exception(*sys.exc_info()): - try: - line = line.encode('utf-8', 'ignore') - except Exception: - continue - tb_s += line - try: - tb_s += str(error).decode('utf-8', 'ignore') - except Exception: - pass - request_obj = self.pool.get('res.request') - try: - user_obj = self.pool.get('res.user') - req_user = user_obj.browse(cron['request_user']) - language = (req_user.language.code if req_user.language - else 'en_US') - with contextlib.nested(Transaction().set_user(cron['user']), - Transaction().set_context(language=language)): - rid = request_obj.create({ - 'name': self.raise_user_error('request_title', - raise_exception=False), - 'priority': '2', - 'act_from': cron['user'], - 'act_to': cron['request_user'], - 'body': self.raise_user_error('request_body', - (cron['name'], tb_s), raise_exception=False), - 'date_sent': datetime.datetime.now(), - 'references': [ - ('create', { - 'reference': "ir.cron,%s" % cron['id'], - }), - ], - 'state': 'waiting', - 'trigger_date': datetime.datetime.now(), - }) - Transaction().cursor.commit() - except Exception: - Transaction().cursor.rollback() + req_user = cron.request_user + language = (req_user.language.code if req_user.language + else 'en_US') + with contextlib.nested(Transaction().set_user(cron.user.id), + Transaction().set_context(language=language)): + values = self._get_request_values(cron) + rid = request_obj.create(values) + Transaction().cursor.commit() - def pool_jobs(self, db_name): + def run(self, db_name): now = datetime.datetime.now() with Transaction().start(db_name, 0) as transaction: - try: - transaction.cursor.lock(self._table) - transaction.cursor.execute('SELECT * FROM ir_cron ' - 'WHERE numbercall <> 0 ' - 'AND active ' - 'AND nextcall <= %s ' - 'AND NOT running ' - 'ORDER BY priority', (datetime.datetime.now(),)) - crons = transaction.cursor.dictfetchall() + transaction.cursor.lock(self._table) + cron_ids = self.search([ + ('number_calls', '!=', 0), + ('next_call', '<=', datetime.datetime.now()), + ]) + crons = self.browse(cron_ids) - for cron in crons: - transaction.cursor.execute('UPDATE ir_cron ' - 'SET running = %s ' - 'WHERE id = %s', (True, cron['id'])) - nextcall = cron['nextcall'] - numbercall = cron['numbercall'] - done = False - - while nextcall < now and numbercall: - if numbercall > 0: - numbercall -= 1 - if not done or cron['doall']: + for cron in crons: + 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: self._callback(cron) - if numbercall: - nextcall += _INTERVALTYPES[cron['interval_type']]( - cron['interval_number']) - done = True + next_call += self.get_delta(cron) + if number_calls > 0: + number_calls -= 1 + first = False - addsql = '' - addsql_param = [] - if not numbercall: - addsql = ', active = %s' - addsql_param = [False] - transaction.cursor.execute("UPDATE ir_cron " - "SET nextcall = %s, " - "running = %s, " - "numbercall = %s" + addsql + " " - "WHERE id = %s", - [nextcall, False, numbercall] + addsql_param + - [cron['id']]) + values = { + 'next_call': next_call, + 'number_calls': number_calls, + } + if not number_calls: + values['active'] = False + self.write(cron.id, values) transaction.cursor.commit() - except Exception: - transaction.cursor.rollback() - raise + except Exception: + transaction.cursor.rollback() + tb_s = reduce(lambda x, y: x + y, + traceback.format_exception(*sys.exc_info())) + tb_s = tb_s.decode('utf-8', 'ignore') + logger = logging.getLogger('cron') + logger.error('Exception:\n%s' % tb_s) Cron() diff --git a/trytond/ir/cron.xml b/trytond/ir/cron.xml index ac7842a8e9..cef16614b4 100644 --- a/trytond/ir/cron.xml +++ b/trytond/ir/cron.xml @@ -12,16 +12,14 @@ this repository contains the full copyright notices and license terms. --> - + - - + - ]]> @@ -31,37 +29,33 @@ this repository contains the full copyright notices and license terms. --> form +