TYPO3 API  SVNRelease
class.tx_scheduler.php
Go to the documentation of this file.
00001 <?php
00002 /***************************************************************
00003 *  Copyright notice
00004 *
00005 *  (c) 2005 Christian Jul Jensen <julle@typo3.org>
00006 *
00007 *  All rights reserved
00008 *
00009 *  This script is part of the TYPO3 project. The TYPO3 project is
00010 *  free software; you can redistribute it and/or modify
00011 *  it under the terms of the GNU General Public License as published by
00012 *  the Free Software Foundation; either version 2 of the License, or
00013 *  (at your option) any later version.
00014 *
00015 *  The GNU General Public License can be found at
00016 *  http://www.gnu.org/copyleft/gpl.html.
00017 *
00018 *  This script is distributed in the hope that it will be useful,
00019 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
00020 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00021 *  GNU General Public License for more details.
00022 *
00023 *  This copyright notice MUST APPEAR in all copies of the script!
00024 ***************************************************************/
00025 /**
00026  * TYPO3 Scheduler. This class handles scheduling and execution of tasks.
00027  * Formerly known as "Gabriel TYPO3 arch angel"
00028  *
00029  * @author  François Suter <francois@typo3.org>
00030  * @author  Christian Jul Jensen <julle@typo3.org>
00031  *
00032  * @package     TYPO3
00033  * @subpackage  tx_scheduler
00034  *
00035  * $Id: class.tx_scheduler.php 9758 2010-12-05 11:25:36Z stephenking $
00036  */
00037 
00038 class tx_scheduler implements t3lib_Singleton {
00039     /**
00040      * @var array       $extConf: settings from the extension manager
00041      */
00042      var $extConf = array();
00043 
00044     /**
00045      * Constructor, makes sure all derived client classes are included
00046      *
00047      * @return  void
00048      */
00049     public function __construct() {
00050             // Get configuration from the extension manager
00051         $this->extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['scheduler']);
00052         if (empty($this->extConf['maxLifetime'])) {
00053             $this->extConf['maxLifetime'] = 1440;
00054         }
00055 
00056             // Clean up the serialized execution arrays
00057         $this->cleanExecutionArrays();
00058     }
00059 
00060     /**
00061      * Adds a task to the pool
00062      *
00063      * @param   tx_scheduler_Task   $task: the object representing the task to add
00064      * @param   string              $identifier: the identified of the task
00065      * @return  boolean             True if the task was successfully added, false otherwise
00066      */
00067     public function addTask(tx_scheduler_Task $task) {
00068         $taskUid = $task->getTaskUid();
00069         if (empty($taskUid)) {
00070             $fields = array(
00071                 'crdate' => $GLOBALS['EXEC_TIME'],
00072                 'classname' => get_class($task),
00073                 'disable' => $task->isDisabled(),
00074                 'serialized_task_object' => 'RESERVED'
00075             );
00076             $result = $GLOBALS['TYPO3_DB']->exec_INSERTquery('tx_scheduler_task', $fields);
00077             if ($result) {
00078                 $task->setTaskUid($GLOBALS['TYPO3_DB']->sql_insert_id());
00079                 $task->save();
00080                 $result = true;
00081             } else {
00082                 $result = false;
00083             }
00084         } else {
00085             $result = false;
00086         }
00087         return $result;
00088     }
00089 
00090     /**
00091      * Cleans the execution lists of the scheduled tasks, executions older than 24h are removed
00092      * TODO: find a way to actually kill the job
00093      *
00094      * @return  void
00095      */
00096     protected function cleanExecutionArrays() {
00097         $tstamp = $GLOBALS['EXEC_TIME'];
00098 
00099             // Select all tasks with executions
00100             // NOTE: this cleanup is done for disabled tasks too,
00101             // to avoid leaving old executions lying around
00102         $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
00103             'uid, classname, serialized_executions',
00104             'tx_scheduler_task',
00105             'serialized_executions != \'\''
00106         );
00107 
00108         $maxDuration = $this->extConf['maxLifetime'] * 60;
00109         while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
00110             if (($serialized_executions = unserialize($row['serialized_executions']))) {
00111                 $executions = array();
00112                 foreach ($serialized_executions AS $task) {
00113                     if (($tstamp - $task) < $maxDuration) {
00114                         $executions[] = $task;
00115                     } else {
00116                         $logMessage = 'Removing logged execution, assuming that the process is dead. Execution of \'' . $row['classname'] . '\' (UID: ' . $row['uid']. ') was started at '.date('Y-m-d H:i:s', $task);
00117                         $this->log($logMessage);
00118                     }
00119                 }
00120             }
00121 
00122             if (count($serialized_executions) != count($executions)) {
00123                 if (count($executions) == 0) {
00124                     $value = '';
00125                 } else {
00126                     $value = serialize($executions);
00127                 }
00128 
00129                 $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
00130                     'tx_scheduler_task',
00131                     'uid = ' . intval($row['uid']),
00132                     array('serialized_executions' => $value)
00133                 );
00134             }
00135         }
00136         $GLOBALS['TYPO3_DB']->sql_free_result($res);
00137     }
00138 
00139     /**
00140      * This method executes the given task and properly marks and records that execution
00141      * It is expected to return false if the task was barred from running or if it was not saved properly
00142      *
00143      * @param   tx_scheduler_Task   $task: the task to execute
00144      * @return  boolean             Whether the task was saved succesfully to the database or not
00145      */
00146     public function executeTask(tx_scheduler_Task $task) {
00147             // Trigger the saving of the task, as this will calculate its next execution time
00148             // This should be calculated all the time, even if the execution is skipped
00149             // (in case it is skipped, this pushes back execution to the next possible date)
00150         $task->save();
00151             // Set a scheduler object for the task again,
00152             // as it was removed during the save operation
00153         $task->setScheduler();
00154         $result = true;
00155 
00156             // Task is already running and multiple executions are not allowed
00157         if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
00158                 // Log multiple execution error
00159             $logMessage = 'Task is already running and multiple executions are not allowed, skipping! Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
00160             $this->log($logMessage);
00161 
00162             $result = false;
00163 
00164             // Task isn't running or multiple executions are allowed
00165         } else {
00166                 // Log scheduler invocation
00167             $logMessage = 'Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
00168             $this->log($logMessage);
00169 
00170                 // Register execution
00171             $executionID = $task->markExecution();
00172 
00173             $failure = null;
00174             try {
00175                     // Execute task
00176                 $successfullyExecuted = $task->execute();
00177 
00178                 if (!$successfullyExecuted) {
00179                     throw new tx_scheduler_FailedExecutionException(
00180                         'Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(),
00181                         1250596541
00182                     );
00183                 }
00184             } catch(Exception $e) {
00185                     // Store exception, so that it can be saved to database
00186                 $failure = $e;
00187             }
00188 
00189                 // Unregister excution
00190             $task->unmarkExecution($executionID, $failure);
00191 
00192                 // Log completion of execution
00193             $logMessage = 'Task executed. Class: ' . get_class($task). ', UID: ' . $task->getTaskUid();
00194             $this->log($logMessage);
00195 
00196                 // Now that the result of the task execution has been handled,
00197                 // throw the exception again, if any
00198             if ($failure instanceof Exception) {
00199                 throw $failure;
00200             }
00201         }
00202 
00203         return $result;
00204     }
00205 
00206     /**
00207      * This method stores information about the last run of the Scheduler into the system registry
00208      *
00209      * @param   string  $type: Type of run (manual or command-line (assumed to be cron))
00210      * @return  void
00211      */
00212     public function recordLastRun($type = 'cron') {
00213             // Validate input value
00214         if ($type != 'manual') {
00215             $type = 'cron';
00216         }
00217 
00218         /**
00219          * @var t3lib_Registry
00220          */
00221         $registry = t3lib_div::makeInstance('t3lib_Registry');
00222         $runInformation = array('start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type);
00223         $registry->set('tx_scheduler', 'lastRun', $runInformation);
00224     }
00225 
00226     /**
00227      * Removes a task completely from the system.
00228      * TODO: find a way to actually kill the existing jobs
00229      *
00230      * @param   tx_scheduler_Task   $task: the object representing the task to delete
00231      * @return  boolean             True if task was successfully deleted, false otherwise
00232      */
00233     public function removeTask(tx_scheduler_Task $task) {
00234         $taskUid = $task->getTaskUid();
00235         if (!empty($taskUid)) {
00236             return $GLOBALS['TYPO3_DB']->exec_DELETEquery('tx_scheduler_task', 'uid = ' . $taskUid);
00237         } else {
00238             return false;
00239         }
00240     }
00241 
00242     /**
00243      * Updates a task in the pool
00244      *
00245      * @param   tx_scheduler_Task   $task: Scheduler task object
00246      * @return  boolean             False if submitted task was not of proper class
00247      */
00248     public function saveTask(tx_scheduler_Task $task) {
00249         $taskUid = $task->getTaskUid();
00250         if (!empty($taskUid)) {
00251             try {
00252                 $executionTime = $task->getNextDueExecution();
00253                 $task->setExecutionTime($executionTime);
00254             }
00255             catch (Exception $e) {
00256                 $task->setDisabled(true);
00257                 $executionTime = 0;
00258             }
00259             $task->unsetScheduler();
00260             $fields = array(
00261                 'nextexecution' => $executionTime,
00262                 'classname' => get_class($task),
00263                 'disable' => $task->isDisabled(),
00264                 'serialized_task_object' => serialize($task)
00265             );
00266             return $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', "uid = '" . $taskUid . "'", $fields);
00267         } else {
00268             return false;
00269         }
00270     }
00271 
00272     /**
00273      * Fetches and unserializes a task object from the db. If an uid is given the object
00274      * with the uid is returned, else the object representing the next due task is returned.
00275      * If there are no due tasks the method throws an exception.
00276      *
00277      * @param   integer             $uid: primary key of a task
00278      * @return  tx_scheduler_Task   The fetched task object
00279      */
00280     public function fetchTask($uid = 0) {
00281         $whereClause = '';
00282             // Define where clause
00283             // If no uid is given, take any non-disabled task which has a next execution time in the past
00284         if (empty($uid)) {
00285             $whereClause = 'disable = 0 AND nextexecution != 0 AND nextexecution <= ' . $GLOBALS['EXEC_TIME'];
00286         } else {
00287             $whereClause = 'uid = ' . intval($uid);
00288         }
00289 
00290         $queryArray = array(
00291             'SELECT'    => 'uid, serialized_task_object',
00292             'FROM'      => 'tx_scheduler_task',
00293             'WHERE'     => $whereClause,
00294             'LIMIT'     => 1
00295         );
00296         $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($queryArray);
00297 
00298             // If there are no available tasks, thrown an exception
00299         if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
00300             throw new OutOfBoundsException('No task', 1247827244);
00301 
00302             // Otherwise unserialize the task and return it
00303         } else {
00304             $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
00305             $task = unserialize($row['serialized_task_object']);
00306 
00307             if ($this->isValidTaskObject($task)) {
00308                 // The task is valid, return it
00309 
00310                 $task->setScheduler();
00311 
00312             } else {
00313                 // Forcibly set the disable flag to 1 in the database,
00314                 // so that the task does not come up again and again for execution
00315 
00316                 $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', "uid = '" . $row['uid'] . "'", array('disable' => 1));
00317                     // Throw an exception to raise the problem
00318                 throw new UnexpectedValueException('Could not unserialize task', 1255083671);
00319             }
00320             $GLOBALS['TYPO3_DB']->sql_free_result($res);
00321         }
00322         return $task;
00323     }
00324 
00325      /**
00326       * This method is used to get the database record for a given task
00327       * It returns the database record and not the task object
00328       *
00329       * @param  integer $uid: primary key of the task to get
00330       * @return array   Database record for the task
00331       * @see    tx_scheduler::fetchTask()
00332       */
00333      public function fetchTaskRecord($uid) {
00334         $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'tx_scheduler_task', "uid = '" . intval($uid) . "'");
00335 
00336             // If the task is not found, throw an exception
00337         if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
00338             throw new OutOfBoundsException('No task', 1247827244);
00339 
00340             // Otherwise get the task's record
00341         } else {
00342             $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
00343             $GLOBALS['TYPO3_DB']->sql_free_result($res);
00344         }
00345         return $row;
00346      }
00347 
00348     /**
00349      * Fetches and unserializes task objects selected with some (SQL) condition
00350      * Objects are returned as an array
00351      *
00352      * @param   string      $where: part of a SQL where clause (without the "WHERE" keyword)
00353      * @param   boolean     $includeDisabledTasks: true if disabled tasks should be fetched too, false otherwise
00354      * @return  array       List of task objects
00355      */
00356     public function fetchTasksWithCondition($where, $includeDisabledTasks = false) {
00357         $tasks = array();
00358         if (!empty($where)) {
00359             $whereClause = $where;
00360         }
00361         if (!$includeDisabledTasks) {
00362             if (!empty($whereClause)) {
00363                 $whereClause .= ' AND ';
00364             }
00365             $whereClause .= 'disable = 0';
00366         }
00367         $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('serialized_task_object', 'tx_scheduler_task', $whereClause);
00368         if ($res) {
00369             while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
00370                 $task = unserialize($row['serialized_task_object']);
00371                     // Add the task to the list only if it is valid
00372                 if ($this->isValidTaskObject($task)) {
00373                     $task->setScheduler();
00374                     $tasks[] = $task;
00375                 }
00376             }
00377             $GLOBALS['TYPO3_DB']->sql_free_result($res);
00378         }
00379         return $tasks;
00380     }
00381 
00382     /**
00383      * This method encapsulates a very simple test for the purpose of clarity.
00384      * Registered tasks are stored in the database along with a serialized task object.
00385      * When a registered task is fetched, its object is unserialized.
00386      * At that point, if the class corresponding to the object is not available anymore
00387      * (e.g. because the extension providing it has been uninstalled),
00388      * the unserialization will produce an incomplete object.
00389      * This test checks whether the unserialized object is of the right (parent) class or not.
00390      *
00391      * @param   object      The object to test
00392      * @return  boolean     True if object is a task, false otherwise
00393      */
00394     public function isValidTaskObject($task) {
00395         return $task instanceof tx_scheduler_Task;
00396     }
00397 
00398     /**
00399      * This is a utility method that writes some message to the BE Log
00400      * It could be expanded to write to some other log
00401      *
00402      * @param   string      The message to write to the log
00403      * @param   integer     Status (0 = message, 1 = error)
00404      * @param   mixed       Key for the message
00405      * @return  void
00406      */
00407     public function log($message, $status = 0, $code = 'scheduler') {
00408             // Log only if enabled
00409         if (!empty($this->extConf['enableBELog'])) {
00410             $GLOBALS['BE_USER']->writelog(
00411                 4,
00412                 0,
00413                 $status,
00414                 $code,
00415                 '[scheduler]: ' . $message,
00416                 array()
00417             );
00418         }
00419     }
00420 }
00421 
00422 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['ext/scheduler/class.tx_scheduler.php'])) {
00423     include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['ext/scheduler/class.tx_scheduler.php']);
00424 }
00425 
00426 
00427 ?>