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
#
##############################################################################
import time
2011-05-11 16:07:07 +00:00
import logging
2011-07-05 17:00:53 +00:00
import threading
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
2011-09-28 22:44:29 +00:00
2006-12-07 13:41:40 +00:00
import netsvc
2011-09-28 22:44:29 +00:00
import openerp
2006-12-07 13:41:40 +00:00
import pooler
2011-09-28 22:44:29 +00:00
import tools
from openerp . cron import WAKE_UP_NOW
2009-10-08 19:50:28 +00:00
from osv import fields , osv
2011-09-28 22:44:29 +00:00
from tools import DEFAULT_SERVER_DATETIME_FORMAT
from tools . safe_eval import safe_eval as eval
2006-12-07 13:41:40 +00:00
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 = {
' name ' : fields . char ( ' Name ' , size = 60 , required = True ) ,
' 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, \n a 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. " ) ,
' model ' : fields . char ( ' Object ' , size = 64 , help = " Model name on which the method to be called is located, e.g. ' res.partner ' . " ) ,
' function ' : fields . char ( ' Method ' , size = 64 , help = " Name of the method to be called when this job is processed. " ) ,
' 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 ) ,
2008-07-22 14:24:36 +00:00
' priority ' : lambda * a : 5 ,
' user_id ' : lambda obj , cr , uid , context : uid ,
' interval_number ' : lambda * a : 1 ,
' interval_type ' : lambda * a : ' months ' ,
' numbercall ' : lambda * a : 1 ,
' active ' : lambda * a : 1 ,
' doall ' : lambda * a : 1
}
2006-12-07 13:41:40 +00:00
2011-07-14 14:32:09 +00:00
_logger = logging . getLogger ( ' cron ' )
2011-07-07 13:58:43 +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 ( )
2011-08-08 14:59:31 +00:00
self . _logger . exception ( " Call of self.pool.get( ' %s ' ). %s (cr, uid, * %r ) failed in Job %s " % ( model_name , method_name , args , job_id ) )
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 .
"""
2009-10-02 16:00:07 +00:00
args = str2tuple ( args )
2011-08-08 14:59:31 +00:00
model = self . pool . get ( model_name )
if model and hasattr ( model , method_name ) :
method = getattr ( model , method_name )
2009-10-02 16:00:07 +00:00
try :
2011-08-08 14:59:31 +00:00
netsvc . log ( ' cron ' , ( cr . dbname , uid , ' * ' , model_name , method_name ) + tuple ( args ) , channel = logging . DEBUG ,
2011-05-13 13:35:14 +00:00
depth = ( None if self . _logger . isEnabledFor ( logging . DEBUG_RPC_ANSWER ) else 1 ) , fn = ' object.execute ' )
2011-06-20 15:26:15 +00:00
logger = logging . getLogger ( ' execution time ' )
2011-06-28 15:31:48 +00:00
if logger . isEnabledFor ( logging . DEBUG ) :
2011-06-20 15:26:15 +00:00
start_time = time . time ( )
2011-08-08 14:59:31 +00:00
method ( cr , uid , * args )
2011-06-28 15:31:48 +00:00
if logger . isEnabledFor ( logging . DEBUG ) :
2011-06-20 15:26:15 +00:00
end_time = time . time ( )
2011-08-08 14:59:31 +00:00
logger . log ( logging . DEBUG , ' %.3f s ( %s , %s ) ' % ( end_time - start_time , model_name , method_name ) )
2009-10-02 16:00:07 +00:00
except Exception , e :
2011-08-08 14:59:31 +00:00
self . _handle_callback_exception ( cr , uid , model_name , method_name , args , job_id , e )
2009-10-02 16:00:07 +00:00
2011-07-05 17:00:53 +00:00
def _run_job ( self , cr , job , now ) :
2011-08-08 14:59:31 +00:00
""" Run a given job taking care of the repetition.
2011-09-28 22:44:29 +00:00
The cursor has a lock on the job ( aquired by _run_jobs_multithread ( ) ) and this
method is run in a worker thread ( spawned by _run_jobs_multithread ( ) ) ) .
2011-08-08 14:59:31 +00:00
: param job : job to be run ( as a dictionary ) .
: param now : timestamp ( result of datetime . now ( ) , no need to call it multiple time ) .
"""
2011-07-05 17:00:53 +00:00
try :
2011-09-28 22:44:29 +00:00
nextcall = datetime . strptime ( job [ ' nextcall ' ] , DEFAULT_SERVER_DATETIME_FORMAT )
2011-07-05 17:00:53 +00:00
numbercall = job [ ' numbercall ' ]
ok = False
while nextcall < now and numbercall :
if numbercall > 0 :
numbercall - = 1
if not ok or job [ ' doall ' ] :
self . _callback ( cr , job [ ' user_id ' ] , job [ ' model ' ] , job [ ' function ' ] , job [ ' args ' ] , job [ ' id ' ] )
if numbercall :
nextcall + = _intervalTypes [ job [ ' interval_type ' ] ] ( job [ ' interval_number ' ] )
ok = True
addsql = ' '
if not numbercall :
addsql = ' , active=False '
2011-09-28 22:44:29 +00:00
cr . execute ( " UPDATE ir_cron SET nextcall= %s , numbercall= %s " + addsql + " WHERE id= %s " , ( nextcall . strftime ( DEFAULT_SERVER_DATETIME_FORMAT ) , numbercall , job [ ' id ' ] ) )
2011-07-07 13:58:43 +00:00
if numbercall :
# Reschedule our own main cron thread if necessary.
2011-08-08 13:05:02 +00:00
# This is really needed if this job runs longer than its rescheduling period.
2011-07-15 11:38:45 +00:00
nextcall = time . mktime ( nextcall . timetuple ( ) )
2011-09-28 22:44:29 +00:00
openerp . cron . schedule_wakeup ( nextcall , cr . dbname )
2011-07-05 17:00:53 +00:00
finally :
cr . commit ( )
cr . close ( )
2011-09-28 22:44:29 +00:00
openerp . cron . release_thread_slot ( )
2011-07-05 17:00:53 +00:00
2011-09-28 22:44:29 +00:00
def _run_jobs_multithread ( self ) :
2011-07-05 17:00:53 +00:00
# TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py
2011-07-07 13:58:43 +00:00
""" Process the cron jobs by spawning worker threads.
This selects in database all the jobs that should be processed . It then
2011-08-08 14:59:31 +00:00
tries to lock each of them and , if it succeeds , spawns a thread to run
the cron job ( if it doesn ' t succeed, it means the job was already
2011-08-08 15:29:08 +00:00
locked to be taken care of by another thread ) .
2011-07-07 13:58:43 +00:00
2011-08-08 14:59:31 +00:00
The cursor used to lock the job in database is given to the worker
thread ( which has to close it itself ) .
2011-07-07 13:58:43 +00:00
"""
2011-07-14 14:32:09 +00:00
db = self . pool . db
2009-11-23 17:07:34 +00:00
cr = db . cursor ( )
2011-07-14 14:32:09 +00:00
db_name = db . dbname
2009-10-02 16:00:07 +00:00
try :
2011-07-05 17:00:53 +00:00
jobs = { } # mapping job ids to jobs for all jobs being processed.
2011-09-28 22:44:29 +00:00
now = datetime . now ( )
cr . execute ( """ SELECT * FROM ir_cron
WHERE numbercall != 0
AND active AND nextcall < = now ( )
ORDER BY priority """ )
2011-07-14 14:32:09 +00:00
for job in cr . dictfetchall ( ) :
2011-09-28 22:44:29 +00:00
if not openerp . cron . get_thread_slots ( ) :
2011-07-14 14:32:09 +00:00
break
jobs [ job [ ' id ' ] ] = job
2011-09-28 22:44:29 +00:00
task_cr = db . cursor ( )
2011-07-14 14:32:09 +00:00
try :
2011-09-28 22:44:29 +00:00
# Try to grab an exclusive lock on the job row from within the task transaction
acquired_lock = False
task_cr . execute ( """ SELECT *
FROM ir_cron
WHERE id = % s
FOR UPDATE NOWAIT """ ,
( job [ ' id ' ] , ) , log_exceptions = False )
acquired_lock = True
2011-07-14 14:32:09 +00:00
except psycopg2 . OperationalError , e :
if e . pgcode == ' 55P03 ' :
2011-08-08 15:29:08 +00:00
# Class 55: Object not in prerequisite state; 55P03: lock_not_available
2011-09-28 22:44:29 +00:00
self . _logger . debug ( ' Another process/thread is already busy executing job ` %s `, skipping it. ' , job [ ' name ' ] )
2011-07-14 14:32:09 +00:00
continue
else :
2011-09-28 22:44:29 +00:00
# Unexpected OperationalError
2011-07-14 14:32:09 +00:00
raise
finally :
2011-09-28 22:44:29 +00:00
if not acquired_lock :
# we're exiting due to an exception while acquiring the lot
2011-07-14 14:32:09 +00:00
task_cr . close ( )
2011-09-28 22:44:29 +00:00
# Got the lock on the job row, now spawn a thread to execute it in the transaction with the lock
task_thread = threading . Thread ( target = self . _run_job , name = job [ ' name ' ] , args = ( task_cr , job , now ) )
2011-07-14 14:32:09 +00:00
# force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default)
task_thread . setDaemon ( False )
2011-09-28 22:44:29 +00:00
openerp . cron . take_thread_slot ( )
2011-07-14 14:32:09 +00:00
task_thread . start ( )
2011-09-28 22:44:29 +00:00
self . _logger . debug ( ' Cron execution thread for job ` %s ` spawned ' , job [ ' name ' ] )
2011-07-05 17:00:53 +00:00
2011-09-28 22:44:29 +00:00
# Find next earliest job ignoring currently processed jobs (by this and other cron threads)
find_next_time_query = """ SELECT min(nextcall) AS min_next_call
FROM ir_cron WHERE numbercall != 0 AND active """
if jobs :
cr . execute ( find_next_time_query + " AND id NOT IN %s " , ( tuple ( jobs . keys ( ) ) , ) )
2011-07-05 17:00:53 +00:00
else :
2011-09-28 22:44:29 +00:00
cr . execute ( find_next_time_query )
2010-09-06 15:29:27 +00:00
next_call = cr . dictfetchone ( ) [ ' min_next_call ' ]
2011-07-07 13:58:43 +00:00
if next_call :
2011-09-28 22:44:29 +00:00
next_call = time . mktime ( time . strptime ( next_call , DEFAULT_SERVER_DATETIME_FORMAT ) )
2011-07-07 13:58:43 +00:00
else :
2011-09-28 22:44:29 +00:00
# no matching cron job found in database, re-schedule arbitrarily in 1 day,
# this delay will likely be modified when running jobs complete their tasks
next_call = time . time ( ) + ( 24 * 3600 )
2011-07-05 17:00:53 +00:00
2011-09-28 22:44:29 +00:00
openerp . cron . schedule_wakeup ( next_call , db_name )
2009-10-08 19:50:28 +00:00
except Exception , ex :
2011-01-17 21:31:08 +00:00
self . _logger . warning ( ' Exception in cron: ' , exc_info = True )
2010-09-06 15:29:27 +00:00
2009-10-02 16:00:07 +00:00
finally :
cr . commit ( )
cr . close ( )
2011-02-16 14:22:42 +00:00
def update_running_cron ( self , cr ) :
2011-08-08 14:59:31 +00:00
""" Schedule as soon as possible a wake-up for this database. """
2011-02-16 14:22:42 +00:00
# Verify whether the server is already started and thus whether we need to commit
# immediately our changes and restart the cron agent in order to apply the change
# immediately. The commit() is needed because as soon as the cron is (re)started it
# will query the database with its own cursor, possibly before the end of the
# current transaction.
# This commit() is not an issue in most cases, but we must absolutely avoid it
# when the server is only starting or loading modules (hence the test on pool._init).
if not self . pool . _init :
cr . commit ( )
2011-09-28 22:44:29 +00:00
openerp . cron . schedule_wakeup ( WAKE_UP_NOW , self . pool . db . dbname )
2011-02-16 14:22:42 +00:00
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 )
2011-02-16 14:22:42 +00:00
self . update_running_cron ( cr )
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 write ( self , cr , user , ids , vals , context = None ) :
res = super ( ir_cron , self ) . write ( cr , user , ids , vals , context = context )
2011-02-16 14:22:42 +00:00
self . update_running_cron ( cr )
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 ) :
res = super ( ir_cron , self ) . unlink ( cr , uid , ids , context = context )
2011-02-16 14:22:42 +00:00
self . update_running_cron ( cr )
2009-09-16 09:20:36 +00:00
return res
2006-12-07 13:41:40 +00:00
ir_cron ( )
2008-07-23 15:01:27 +00:00
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: