diff --git a/odoo/addons/base/ir/ir_cron.py b/odoo/addons/base/ir/ir_cron.py index bb070e4753c..c577ea221ac 100644 --- a/odoo/addons/base/ir/ir_cron.py +++ b/odoo/addons/base/ir/ir_cron.py @@ -18,6 +18,13 @@ _logger = logging.getLogger(__name__) BASE_VERSION = odoo.modules.load_information_from_description_file('base')['version'] +class BadVersion(Exception): + pass + +class BadModuleState(Exception): + pass + + def str2tuple(s): return safe_eval('tuple(%s)' % (s or '')) @@ -170,20 +177,19 @@ class ir_cron(models.Model): cron_cr.commit() @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. + def _process_jobs(cls, db_name): + """ Try to process all cron jobs. 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. + :raise BadVersion: if the version is different from the worker's + :raise BadModuleState: if modules are to install/upgrade/remove """ db = odoo.sql_db.db_connect(db_name) threading.current_thread().dbname = db_name - jobs = [] try: with db.cursor() as cr: # Make sure the database has the same version as the code of @@ -193,17 +199,81 @@ class ir_cron(models.Model): cr.execute("SELECT COUNT(*) FROM ir_module_module WHERE state LIKE %s", ['to %']) (changes,) = cr.fetchone() if not version or changes: - _logger.warning('Skipping database %s because of modules to install/upgrade/remove.', db_name) + raise BadModuleState() elif version != BASE_VERSION: - _logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION) - else: - # 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() - except psycopg2.ProgrammingError, e: + raise BadVersion() + # 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() + + for job in jobs: + lock_cr = db.cursor() + try: + # Try to grab an exclusive lock on the job row from within the task transaction + # 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 + lock_cr.execute("""SELECT * + FROM ir_cron + WHERE numbercall != 0 + AND active + AND nextcall <= (now() at time zone 'UTC') + AND id=%s + FOR UPDATE NOWAIT""", + (job['id'],), log_exceptions=False) + + locked_job = lock_cr.fetchone() + if not locked_job: + _logger.debug("Job `%s` already executed by another process/thread. skipping it", job['name']) + continue + # Got the lock on the job row, run its code + _logger.debug('Starting job `%s`.', job['name']) + job_cr = db.cursor() + try: + registry = odoo.registry(db_name) + 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() + + finally: + if hasattr(threading.current_thread(), 'dbname'): + del threading.current_thread().dbname + + @classmethod + def _acquire_job(cls, db_name): + """ Try to process all cron jobs. + + 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. + + This method hides most exceptions related to the database's version, the + modules' state, and such. + """ + try: + cls._process_jobs(db_name) + except BadVersion: + _logger.warning('Skipping database %s as its base version is not %s.', db_name, BASE_VERSION) + except BadModuleState: + _logger.warning('Skipping database %s because of modules to install/upgrade/remove.', db_name) + except psycopg2.ProgrammingError as 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. @@ -213,51 +283,6 @@ class ir_cron(models.Model): except Exception: _logger.warning('Exception in cron:', exc_info=True) - for job in jobs: - lock_cr = db.cursor() - try: - # Try to grab an exclusive lock on the job row from within the task transaction - # 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 - lock_cr.execute("""SELECT * - FROM ir_cron - WHERE numbercall != 0 - AND active - AND nextcall <= (now() at time zone 'UTC') - AND id=%s - FOR UPDATE NOWAIT""", - (job['id'],), log_exceptions=False) - - locked_job = lock_cr.fetchone() - if not locked_job: - _logger.debug("Job `%s` already executed by another process/thread. skipping it", job['name']) - continue - # Got the lock on the job row, run its code - _logger.debug('Starting job `%s`.', job['name']) - job_cr = db.cursor() - try: - registry = odoo.registry(db_name) - 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() - - if hasattr(threading.current_thread(), 'dbname'): # cron job could have removed it as side-effect - del threading.current_thread().dbname - @api.multi def _try_lock(self): """Try to grab a dummy exclusive write-lock to the rows with the given ids, diff --git a/odoo/service/server.py b/odoo/service/server.py index a702c99a339..67ea1dedba1 100644 --- a/odoo/service/server.py +++ b/odoo/service/server.py @@ -215,14 +215,11 @@ class ThreadedServer(CommonServer): registries = odoo.modules.registry.Registry.registries _logger.debug('cron%d polling for jobs', number) for db_name, registry in registries.iteritems(): - while registry.ready: + if registry.ready: try: - acquired = odoo.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name) - if not acquired: - break + odoo.addons.base.ir.ir_cron.ir_cron._acquire_job(db_name) except Exception: _logger.warning('cron%d encountered an Exception:', number, exc_info=True) - break def cron_spawn(self): """ Start the above runner function in a daemon thread.