2009-10-20 10:52:23 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2006-12-07 13:41:40 +00:00
|
|
|
##############################################################################
|
|
|
|
#
|
2010-09-06 15:29:27 +00:00
|
|
|
# OpenERP, Open Source Management Solution
|
2010-12-08 14:17:07 +00:00
|
|
|
# Copyright (C) 2004-TODAY OpenERP S.A. <http://www.openerp.com>
|
2008-06-16 11:00:21 +00:00
|
|
|
#
|
2008-11-03 18:27:16 +00:00
|
|
|
# This program is free software: you can redistribute it and/or modify
|
2010-12-08 14:17:07 +00:00
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
2006-12-07 13:41:40 +00:00
|
|
|
#
|
2008-11-03 18:27:16 +00:00
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
2010-12-08 14:17:07 +00:00
|
|
|
# GNU Affero General Public License for more details.
|
2006-12-07 13:41:40 +00:00
|
|
|
#
|
2010-12-08 14:17:07 +00:00
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
2008-11-03 18:27:16 +00:00
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2006-12-07 13:41:40 +00:00
|
|
|
#
|
|
|
|
##############################################################################
|
2011-05-11 16:07:07 +00:00
|
|
|
import logging
|
2013-03-01 12:07:44 +00:00
|
|
|
import threading
|
|
|
|
import time
|
2011-07-05 17:00:53 +00:00
|
|
|
import psycopg2
|
2009-12-14 15:21:04 +00:00
|
|
|
from datetime import datetime
|
2009-12-14 12:23:55 +00:00
|
|
|
from dateutil.relativedelta import relativedelta
|
2014-08-08 09:46:41 +00:00
|
|
|
import pytz
|
2011-09-28 22:44:29 +00:00
|
|
|
|
|
|
|
import openerp
|
2014-07-06 14:44:26 +00:00
|
|
|
from openerp import SUPERUSER_ID, netsvc, api
|
2012-12-10 15:27:23 +00:00
|
|
|
from openerp.osv import fields, osv
|
|
|
|
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
|
|
|
from openerp.tools.safe_eval import safe_eval as eval
|
|
|
|
from openerp.tools.translate import _
|
2013-04-18 01:04:10 +00:00
|
|
|
from openerp.modules import load_information_from_description_file
|
2006-12-07 13:41:40 +00:00
|
|
|
|
2012-01-24 11:47:30 +00:00
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
2013-04-18 01:04:10 +00:00
|
|
|
BASE_VERSION = load_information_from_description_file('base')['version']
|
|
|
|
|
2009-10-02 16:00:07 +00:00
|
|
|
def str2tuple(s):
|
2009-11-04 10:51:34 +00:00
|
|
|
return eval('tuple(%s)' % (s or ''))
|
2009-10-02 16:00:07 +00:00
|
|
|
|
2006-12-07 13:41:40 +00:00
|
|
|
_intervalTypes = {
|
2009-12-13 18:16:01 +00:00
|
|
|
'work_days': lambda interval: relativedelta(days=interval),
|
|
|
|
'days': lambda interval: relativedelta(days=interval),
|
|
|
|
'hours': lambda interval: relativedelta(hours=interval),
|
|
|
|
'weeks': lambda interval: relativedelta(days=7*interval),
|
|
|
|
'months': lambda interval: relativedelta(months=interval),
|
|
|
|
'minutes': lambda interval: relativedelta(minutes=interval),
|
2006-12-07 13:41:40 +00:00
|
|
|
}
|
|
|
|
|
2011-07-14 14:32:09 +00:00
|
|
|
class ir_cron(osv.osv):
|
2011-08-08 14:59:31 +00:00
|
|
|
""" Model describing cron jobs (also called actions or tasks).
|
2011-01-17 21:31:08 +00:00
|
|
|
"""
|
2011-09-28 22:44:29 +00:00
|
|
|
|
|
|
|
# TODO: perhaps in the future we could consider a flag on ir.cron jobs
|
|
|
|
# that would cause database wake-up even if the database has not been
|
|
|
|
# loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something)
|
|
|
|
# See also openerp.cron
|
|
|
|
|
2008-07-22 14:24:36 +00:00
|
|
|
_name = "ir.cron"
|
2010-12-10 22:42:58 +00:00
|
|
|
_order = 'name'
|
2008-07-22 14:24:36 +00:00
|
|
|
_columns = {
|
2014-05-21 09:52:05 +00:00
|
|
|
'name': fields.char('Name', required=True),
|
2008-07-22 14:24:36 +00:00
|
|
|
'user_id': fields.many2one('res.users', 'User', required=True),
|
|
|
|
'active': fields.boolean('Active'),
|
2010-09-29 10:37:26 +00:00
|
|
|
'interval_number': fields.integer('Interval Number',help="Repeat every x."),
|
2008-07-22 14:24:36 +00:00
|
|
|
'interval_type': fields.selection( [('minutes', 'Minutes'),
|
|
|
|
('hours', 'Hours'), ('work_days','Work Days'), ('days', 'Days'),('weeks', 'Weeks'), ('months', 'Months')], 'Interval Unit'),
|
2011-08-08 14:59:31 +00:00
|
|
|
'numbercall': fields.integer('Number of Calls', help='How many times the method is called,\na negative number indicates no limit.'),
|
|
|
|
'doall' : fields.boolean('Repeat Missed', help="Specify if missed occurrences should be executed when the server restarts."),
|
|
|
|
'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this job."),
|
2014-05-21 09:52:05 +00:00
|
|
|
'model': fields.char('Object', help="Model name on which the method to be called is located, e.g. 'res.partner'."),
|
|
|
|
'function': fields.char('Method', help="Name of the method to be called when this job is processed."),
|
2011-08-08 14:59:31 +00:00
|
|
|
'args': fields.text('Arguments', help="Arguments to be passed to the method, e.g. (uid,)."),
|
|
|
|
'priority': fields.integer('Priority', help='The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.')
|
2008-07-22 14:24:36 +00:00
|
|
|
}
|
2006-12-07 13:41:40 +00:00
|
|
|
|
2008-07-22 14:24:36 +00:00
|
|
|
_defaults = {
|
2011-09-28 22:44:29 +00:00
|
|
|
'nextcall' : lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
2012-11-02 09:47:05 +00:00
|
|
|
'priority' : 5,
|
2008-07-22 14:24:36 +00:00
|
|
|
'user_id' : lambda obj,cr,uid,context: uid,
|
2012-11-02 09:47:05 +00:00
|
|
|
'interval_number' : 1,
|
|
|
|
'interval_type' : 'months',
|
|
|
|
'numbercall' : 1,
|
|
|
|
'active' : 1,
|
|
|
|
'doall' : 1
|
2008-07-22 14:24:36 +00:00
|
|
|
}
|
2006-12-07 13:41:40 +00:00
|
|
|
|
2009-10-02 16:00:07 +00:00
|
|
|
def _check_args(self, cr, uid, ids, context=None):
|
|
|
|
try:
|
|
|
|
for this in self.browse(cr, uid, ids, context):
|
|
|
|
str2tuple(this.args)
|
2011-01-17 21:31:08 +00:00
|
|
|
except Exception:
|
2009-10-02 16:00:07 +00:00
|
|
|
return False
|
|
|
|
return True
|
2010-09-06 15:29:27 +00:00
|
|
|
|
2009-10-08 19:50:28 +00:00
|
|
|
_constraints = [
|
2009-10-02 16:00:07 +00:00
|
|
|
(_check_args, 'Invalid arguments', ['args']),
|
|
|
|
]
|
|
|
|
|
2011-08-08 14:59:31 +00:00
|
|
|
def _handle_callback_exception(self, cr, uid, model_name, method_name, args, job_id, job_exception):
|
|
|
|
""" Method called when an exception is raised by a job.
|
|
|
|
|
|
|
|
Simply logs the exception and rollback the transaction.
|
|
|
|
|
|
|
|
:param model_name: model name on which the job method is located.
|
|
|
|
:param method_name: name of the method to call when this job is processed.
|
|
|
|
:param args: arguments of the method (without the usual self, cr, uid).
|
|
|
|
:param job_id: job id.
|
|
|
|
:param job_exception: exception raised by the job.
|
|
|
|
|
|
|
|
"""
|
2011-05-06 10:01:35 +00:00
|
|
|
cr.rollback()
|
2012-01-24 11:47:30 +00:00
|
|
|
_logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model_name, method_name, args, job_id))
|
2011-08-08 14:59:31 +00:00
|
|
|
|
|
|
|
def _callback(self, cr, uid, model_name, method_name, args, job_id):
|
|
|
|
""" Run the method associated to a given job
|
|
|
|
|
|
|
|
It takes care of logging and exception handling.
|
2011-05-06 10:01:35 +00:00
|
|
|
|
2011-08-08 14:59:31 +00:00
|
|
|
:param model_name: model name on which the job method is located.
|
|
|
|
:param method_name: name of the method to call when this job is processed.
|
|
|
|
:param args: arguments of the method (without the usual self, cr, uid).
|
|
|
|
:param job_id: job id.
|
|
|
|
"""
|
2013-02-19 14:04:39 +00:00
|
|
|
try:
|
|
|
|
args = str2tuple(args)
|
|
|
|
openerp.modules.registry.RegistryManager.check_registry_signaling(cr.dbname)
|
2013-03-27 11:10:14 +00:00
|
|
|
registry = openerp.registry(cr.dbname)
|
2013-03-29 14:07:23 +00:00
|
|
|
if model_name in registry:
|
|
|
|
model = registry[model_name]
|
|
|
|
if hasattr(model, method_name):
|
|
|
|
log_depth = (None if _logger.isEnabledFor(logging.DEBUG) else 1)
|
|
|
|
netsvc.log(_logger, logging.DEBUG, 'cron.object.execute', (cr.dbname,uid,'*',model_name,method_name)+tuple(args), depth=log_depth)
|
|
|
|
if _logger.isEnabledFor(logging.DEBUG):
|
|
|
|
start_time = time.time()
|
|
|
|
getattr(model, method_name)(cr, uid, *args)
|
|
|
|
if _logger.isEnabledFor(logging.DEBUG):
|
|
|
|
end_time = time.time()
|
|
|
|
_logger.debug('%.3fs (%s, %s)' % (end_time - start_time, model_name, method_name))
|
|
|
|
openerp.modules.registry.RegistryManager.signal_caches_change(cr.dbname)
|
|
|
|
else:
|
|
|
|
msg = "Method `%s.%s` does not exist." % (model_name, method_name)
|
|
|
|
_logger.warning(msg)
|
2013-02-19 14:04:39 +00:00
|
|
|
else:
|
2013-03-29 14:07:23 +00:00
|
|
|
msg = "Model `%s` does not exist." % model_name
|
2013-02-19 14:04:39 +00:00
|
|
|
_logger.warning(msg)
|
|
|
|
except Exception, e:
|
|
|
|
self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e)
|
2009-10-02 16:00:07 +00:00
|
|
|
|
2014-08-11 14:51:42 +00:00
|
|
|
def _process_job(self, job_cr, job, cron_cr):
|
2012-09-22 10:51:07 +00:00
|
|
|
""" Run a given job taking care of the repetition.
|
|
|
|
|
2014-08-11 14:51:42 +00:00
|
|
|
:param job_cr: cursor to use to execute the job, safe to commit/rollback
|
2012-09-22 10:51:07 +00:00
|
|
|
:param job: job to be run (as a dictionary).
|
2012-12-22 01:13:55 +00:00
|
|
|
:param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date,
|
|
|
|
must not be committed/rolled back!
|
2012-09-22 10:51:07 +00:00
|
|
|
"""
|
|
|
|
try:
|
2014-07-06 14:44:26 +00:00
|
|
|
with api.Environment.manage():
|
2014-08-11 13:52:15 +00:00
|
|
|
now = fields.datetime.context_timestamp(job_cr, SUPERUSER_ID, datetime.now())
|
|
|
|
nextcall = fields.datetime.context_timestamp(job_cr, SUPERUSER_ID, datetime.strptime(job['nextcall'], DEFAULT_SERVER_DATETIME_FORMAT))
|
2014-07-06 14:44:26 +00:00
|
|
|
numbercall = job['numbercall']
|
|
|
|
|
|
|
|
ok = False
|
|
|
|
while nextcall < now and numbercall:
|
|
|
|
if numbercall > 0:
|
|
|
|
numbercall -= 1
|
|
|
|
if not ok or job['doall']:
|
2014-08-11 14:51:42 +00:00
|
|
|
self._callback(job_cr, job['user_id'], job['model'], job['function'], job['args'], job['id'])
|
2014-07-06 14:44:26 +00:00
|
|
|
if numbercall:
|
|
|
|
nextcall += _intervalTypes[job['interval_type']](job['interval_number'])
|
|
|
|
ok = True
|
|
|
|
addsql = ''
|
|
|
|
if not numbercall:
|
|
|
|
addsql = ', active=False'
|
|
|
|
cron_cr.execute("UPDATE ir_cron SET nextcall=%s, numbercall=%s"+addsql+" WHERE id=%s",
|
2014-08-11 13:52:15 +00:00
|
|
|
(nextcall.astimezone(pytz.UTC).strftime(DEFAULT_SERVER_DATETIME_FORMAT), numbercall, job['id']))
|
2014-08-11 15:37:46 +00:00
|
|
|
self.invalidate_cache(job_cr, SUPERUSER_ID)
|
2012-09-22 10:51:07 +00:00
|
|
|
|
|
|
|
finally:
|
2014-08-11 14:51:42 +00:00
|
|
|
job_cr.commit()
|
2012-12-22 01:13:55 +00:00
|
|
|
cron_cr.commit()
|
2012-09-22 10:51:07 +00:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _acquire_job(cls, db_name):
|
|
|
|
# TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py
|
|
|
|
""" Try to process one cron job.
|
|
|
|
|
|
|
|
This selects in database all the jobs that should be processed. It then
|
|
|
|
tries to lock each of them and, if it succeeds, run the cron job (if it
|
|
|
|
doesn't succeed, it means the job was already locked to be taken care
|
|
|
|
of by another thread) and return.
|
|
|
|
|
|
|
|
If a job was processed, returns True, otherwise returns False.
|
|
|
|
"""
|
|
|
|
db = openerp.sql_db.db_connect(db_name)
|
2013-03-01 12:07:44 +00:00
|
|
|
threading.current_thread().dbname = db_name
|
2012-09-22 10:51:07 +00:00
|
|
|
cr = db.cursor()
|
2012-12-22 01:13:55 +00:00
|
|
|
jobs = []
|
2012-09-22 10:51:07 +00:00
|
|
|
try:
|
2013-04-18 01:04:10 +00:00
|
|
|
# Make sure the database we poll has the same version as the code of base
|
|
|
|
cr.execute("SELECT 1 FROM ir_module_module WHERE name=%s AND latest_version=%s", ('base', BASE_VERSION))
|
|
|
|
if cr.fetchone():
|
|
|
|
# Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
|
|
|
|
cr.execute("""SELECT * FROM ir_cron
|
|
|
|
WHERE numbercall != 0
|
|
|
|
AND active AND nextcall <= (now() at time zone 'UTC')
|
|
|
|
ORDER BY priority""")
|
|
|
|
jobs = cr.dictfetchall()
|
|
|
|
else:
|
|
|
|
_logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION)
|
2012-09-22 10:51:07 +00:00
|
|
|
except psycopg2.ProgrammingError, e:
|
|
|
|
if e.pgcode == '42P01':
|
|
|
|
# Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table
|
|
|
|
# The table ir_cron does not exist; this is probably not an OpenERP database.
|
|
|
|
_logger.warning('Tried to poll an undefined table on database %s.', db_name)
|
|
|
|
else:
|
|
|
|
raise
|
2012-12-14 13:25:33 +00:00
|
|
|
except Exception:
|
2012-09-22 10:51:07 +00:00
|
|
|
_logger.warning('Exception in cron:', exc_info=True)
|
|
|
|
finally:
|
|
|
|
cr.close()
|
|
|
|
|
2012-12-22 01:13:55 +00:00
|
|
|
for job in jobs:
|
|
|
|
lock_cr = db.cursor()
|
|
|
|
try:
|
|
|
|
# Try to grab an exclusive lock on the job row from within the task transaction
|
2014-06-05 16:59:03 +00:00
|
|
|
# Restrict to the same conditions as for the search since the job may have already
|
|
|
|
# been run by an other thread when cron is running in multi thread
|
2012-12-22 01:13:55 +00:00
|
|
|
lock_cr.execute("""SELECT *
|
|
|
|
FROM ir_cron
|
2014-06-05 16:59:03 +00:00
|
|
|
WHERE numbercall != 0
|
|
|
|
AND active
|
|
|
|
AND nextcall <= (now() at time zone 'UTC')
|
|
|
|
AND id=%s
|
2012-12-22 01:13:55 +00:00
|
|
|
FOR UPDATE NOWAIT""",
|
|
|
|
(job['id'],), log_exceptions=False)
|
|
|
|
|
2014-06-05 16:59:03 +00:00
|
|
|
locked_job = lock_cr.fetchone()
|
|
|
|
if not locked_job:
|
|
|
|
_logger.debug("Job `%s` already executed by another process/thread. skipping it", job['name'])
|
|
|
|
continue
|
2012-12-22 01:13:55 +00:00
|
|
|
# Got the lock on the job row, run its code
|
|
|
|
_logger.debug('Starting job `%s`.', job['name'])
|
|
|
|
job_cr = db.cursor()
|
|
|
|
try:
|
2013-03-27 11:10:14 +00:00
|
|
|
registry = openerp.registry(db_name)
|
2012-12-22 01:13:55 +00:00
|
|
|
registry[cls._name]._process_job(job_cr, job, lock_cr)
|
|
|
|
except Exception:
|
|
|
|
_logger.exception('Unexpected exception while processing cron job %r', job)
|
|
|
|
finally:
|
|
|
|
job_cr.close()
|
|
|
|
|
|
|
|
except psycopg2.OperationalError, e:
|
|
|
|
if e.pgcode == '55P03':
|
|
|
|
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
|
|
|
|
_logger.debug('Another process/thread is already busy executing job `%s`, skipping it.', job['name'])
|
|
|
|
continue
|
|
|
|
else:
|
|
|
|
# Unexpected OperationalError
|
|
|
|
raise
|
|
|
|
finally:
|
|
|
|
# we're exiting due to an exception while acquiring the lock
|
|
|
|
lock_cr.close()
|
2012-09-22 10:51:07 +00:00
|
|
|
|
2013-03-01 12:07:44 +00:00
|
|
|
if hasattr(threading.current_thread(), 'dbname'): # cron job could have removed it as side-effect
|
|
|
|
del threading.current_thread().dbname
|
|
|
|
|
2011-09-29 00:18:58 +00:00
|
|
|
def _try_lock(self, cr, uid, ids, context=None):
|
|
|
|
"""Try to grab a dummy exclusive write-lock to the rows with the given ids,
|
|
|
|
to make sure a following write() or unlink() will not block due
|
|
|
|
to a process currently executing those cron tasks"""
|
|
|
|
try:
|
|
|
|
cr.execute("""SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table,
|
|
|
|
(tuple(ids),), log_exceptions=False)
|
|
|
|
except psycopg2.OperationalError:
|
|
|
|
cr.rollback() # early rollback to allow translations to work for the user feedback
|
|
|
|
raise osv.except_osv(_("Record cannot be modified right now"),
|
|
|
|
_("This cron task is currently being executed and may not be modified, "
|
|
|
|
"please try again in a few minutes"))
|
|
|
|
|
2009-09-16 09:20:36 +00:00
|
|
|
def create(self, cr, uid, vals, context=None):
|
2010-09-06 15:29:27 +00:00
|
|
|
res = super(ir_cron, self).create(cr, uid, vals, context=context)
|
2009-09-16 09:20:36 +00:00
|
|
|
return res
|
2010-09-06 15:29:27 +00:00
|
|
|
|
2011-09-29 00:18:58 +00:00
|
|
|
def write(self, cr, uid, ids, vals, context=None):
|
|
|
|
self._try_lock(cr, uid, ids, context)
|
|
|
|
res = super(ir_cron, self).write(cr, uid, ids, vals, context=context)
|
2009-09-16 09:20:36 +00:00
|
|
|
return res
|
2010-09-06 15:29:27 +00:00
|
|
|
|
2009-09-16 09:20:36 +00:00
|
|
|
def unlink(self, cr, uid, ids, context=None):
|
2011-09-29 00:18:58 +00:00
|
|
|
self._try_lock(cr, uid, ids, context)
|
2009-09-16 09:20:36 +00:00
|
|
|
res = super(ir_cron, self).unlink(cr, uid, ids, context=context)
|
|
|
|
return res
|
2008-07-23 15:01:27 +00:00
|
|
|
|
|
|
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|