[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/classes/task/ -> manager.php (source)

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Scheduled and adhoc task management.
  19   *
  20   * @package    core
  21   * @category   task
  22   * @copyright  2013 Damyon Wiese
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  namespace core\task;
  26  
  27  define('CORE_TASK_TASKS_FILENAME', 'db/tasks.php');
  28  /**
  29   * Collection of task related methods.
  30   *
  31   * Some locking rules for this class:
  32   * All changes to scheduled tasks must be protected with both - the global cron lock and the lock
  33   * for the specific scheduled task (in that order). Locks must be released in the reverse order.
  34   * @copyright  2013 Damyon Wiese
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class manager {
  38  
  39      /**
  40       * Given a component name, will load the list of tasks in the db/tasks.php file for that component.
  41       *
  42       * @param string $componentname - The name of the component to fetch the tasks for.
  43       * @return \core\task\scheduled_task[] - List of scheduled tasks for this component.
  44       */
  45      public static function load_default_scheduled_tasks_for_component($componentname) {
  46          $dir = \core_component::get_component_directory($componentname);
  47  
  48          if (!$dir) {
  49              return array();
  50          }
  51  
  52          $file = $dir . '/' . CORE_TASK_TASKS_FILENAME;
  53          if (!file_exists($file)) {
  54              return array();
  55          }
  56  
  57          $tasks = null;
  58          require_once($file);
  59  
  60          if (!isset($tasks)) {
  61              return array();
  62          }
  63  
  64          $scheduledtasks = array();
  65  
  66          foreach ($tasks as $task) {
  67              $record = (object) $task;
  68              $scheduledtask = self::scheduled_task_from_record($record);
  69              // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
  70              if ($scheduledtask) {
  71                  $scheduledtask->set_component($componentname);
  72                  $scheduledtasks[] = $scheduledtask;
  73              }
  74          }
  75  
  76          return $scheduledtasks;
  77      }
  78  
  79      /**
  80       * Update the database to contain a list of scheduled task for a component.
  81       * The list of scheduled tasks is taken from @load_scheduled_tasks_for_component.
  82       * Will throw exceptions for any errors.
  83       *
  84       * @param string $componentname - The frankenstyle component name.
  85       */
  86      public static function reset_scheduled_tasks_for_component($componentname) {
  87          global $DB;
  88          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
  89  
  90          if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
  91              throw new \moodle_exception('locktimeout');
  92          }
  93          $tasks = self::load_default_scheduled_tasks_for_component($componentname);
  94  
  95          $tasklocks = array();
  96          foreach ($tasks as $taskid => $task) {
  97              $classname = get_class($task);
  98              if (strpos($classname, '\\') !== 0) {
  99                  $classname = '\\' . $classname;
 100              }
 101  
 102              // If there is an existing task with a custom schedule, do not override it.
 103              $currenttask = self::get_scheduled_task($classname);
 104              if ($currenttask && $currenttask->is_customised()) {
 105                  $tasks[$taskid] = $currenttask;
 106              }
 107  
 108              if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
 109                  // Could not get all the locks required - release all locks and fail.
 110                  foreach ($tasklocks as $tasklock) {
 111                      $tasklock->release();
 112                  }
 113                  $cronlock->release();
 114                  throw new \moodle_exception('locktimeout');
 115              }
 116              $tasklocks[] = $lock;
 117          }
 118  
 119          // Got a lock on cron and all the tasks for this component, time to reset the config.
 120          $DB->delete_records('task_scheduled', array('component' => $componentname));
 121          foreach ($tasks as $task) {
 122              $record = self::record_from_scheduled_task($task);
 123              $DB->insert_record('task_scheduled', $record);
 124          }
 125  
 126          // Release the locks.
 127          foreach ($tasklocks as $tasklock) {
 128              $tasklock->release();
 129          }
 130  
 131          $cronlock->release();
 132      }
 133  
 134      /**
 135       * Queue an adhoc task to run in the background.
 136       *
 137       * @param \core\task\adhoc_task $task - The new adhoc task information to store.
 138       * @return boolean - True if the config was saved.
 139       */
 140      public static function queue_adhoc_task(adhoc_task $task) {
 141          global $DB;
 142  
 143          $record = self::record_from_adhoc_task($task);
 144          // Schedule it immediately.
 145          $record->nextruntime = time() - 1;
 146          $result = $DB->insert_record('task_adhoc', $record);
 147  
 148          return $result;
 149      }
 150  
 151      /**
 152       * Change the default configuration for a scheduled task.
 153       * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
 154       *
 155       * @param \core\task\scheduled_task $task - The new scheduled task information to store.
 156       * @return boolean - True if the config was saved.
 157       */
 158      public static function configure_scheduled_task(scheduled_task $task) {
 159          global $DB;
 160          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 161  
 162          if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
 163              throw new \moodle_exception('locktimeout');
 164          }
 165  
 166          $classname = get_class($task);
 167          if (strpos($classname, '\\') !== 0) {
 168              $classname = '\\' . $classname;
 169          }
 170          if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
 171              $cronlock->release();
 172              throw new \moodle_exception('locktimeout');
 173          }
 174  
 175          $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
 176  
 177          $record = self::record_from_scheduled_task($task);
 178          $record->id = $original->id;
 179          $record->nextruntime = $task->get_next_scheduled_time();
 180          $result = $DB->update_record('task_scheduled', $record);
 181  
 182          $lock->release();
 183          $cronlock->release();
 184          return $result;
 185      }
 186  
 187      /**
 188       * Utility method to create a DB record from a scheduled task.
 189       *
 190       * @param \core\task\scheduled_task $task
 191       * @return \stdClass
 192       */
 193      public static function record_from_scheduled_task($task) {
 194          $record = new \stdClass();
 195          $record->classname = get_class($task);
 196          if (strpos($record->classname, '\\') !== 0) {
 197              $record->classname = '\\' . $record->classname;
 198          }
 199          $record->component = $task->get_component();
 200          $record->blocking = $task->is_blocking();
 201          $record->customised = $task->is_customised();
 202          $record->lastruntime = $task->get_last_run_time();
 203          $record->nextruntime = $task->get_next_run_time();
 204          $record->faildelay = $task->get_fail_delay();
 205          $record->hour = $task->get_hour();
 206          $record->minute = $task->get_minute();
 207          $record->day = $task->get_day();
 208          $record->dayofweek = $task->get_day_of_week();
 209          $record->month = $task->get_month();
 210          $record->disabled = $task->get_disabled();
 211  
 212          return $record;
 213      }
 214  
 215      /**
 216       * Utility method to create a DB record from an adhoc task.
 217       *
 218       * @param \core\task\adhoc_task $task
 219       * @return \stdClass
 220       */
 221      public static function record_from_adhoc_task($task) {
 222          $record = new \stdClass();
 223          $record->classname = get_class($task);
 224          if (strpos($record->classname, '\\') !== 0) {
 225              $record->classname = '\\' . $record->classname;
 226          }
 227          $record->id = $task->get_id();
 228          $record->component = $task->get_component();
 229          $record->blocking = $task->is_blocking();
 230          $record->nextruntime = $task->get_next_run_time();
 231          $record->faildelay = $task->get_fail_delay();
 232          $record->customdata = $task->get_custom_data_as_string();
 233  
 234          return $record;
 235      }
 236  
 237      /**
 238       * Utility method to create an adhoc task from a DB record.
 239       *
 240       * @param \stdClass $record
 241       * @return \core\task\adhoc_task
 242       */
 243      public static function adhoc_task_from_record($record) {
 244          $classname = $record->classname;
 245          if (strpos($classname, '\\') !== 0) {
 246              $classname = '\\' . $classname;
 247          }
 248          if (!class_exists($classname)) {
 249              debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
 250              return false;
 251          }
 252          $task = new $classname;
 253          if (isset($record->nextruntime)) {
 254              $task->set_next_run_time($record->nextruntime);
 255          }
 256          if (isset($record->id)) {
 257              $task->set_id($record->id);
 258          }
 259          if (isset($record->component)) {
 260              $task->set_component($record->component);
 261          }
 262          $task->set_blocking(!empty($record->blocking));
 263          if (isset($record->faildelay)) {
 264              $task->set_fail_delay($record->faildelay);
 265          }
 266          if (isset($record->customdata)) {
 267              $task->set_custom_data_as_string($record->customdata);
 268          }
 269  
 270          return $task;
 271      }
 272  
 273      /**
 274       * Utility method to create a task from a DB record.
 275       *
 276       * @param \stdClass $record
 277       * @return \core\task\scheduled_task
 278       */
 279      public static function scheduled_task_from_record($record) {
 280          $classname = $record->classname;
 281          if (strpos($classname, '\\') !== 0) {
 282              $classname = '\\' . $classname;
 283          }
 284          if (!class_exists($classname)) {
 285              debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
 286              return false;
 287          }
 288          /** @var \core\task\scheduled_task $task */
 289          $task = new $classname;
 290          if (isset($record->lastruntime)) {
 291              $task->set_last_run_time($record->lastruntime);
 292          }
 293          if (isset($record->nextruntime)) {
 294              $task->set_next_run_time($record->nextruntime);
 295          }
 296          if (isset($record->customised)) {
 297              $task->set_customised($record->customised);
 298          }
 299          if (isset($record->component)) {
 300              $task->set_component($record->component);
 301          }
 302          $task->set_blocking(!empty($record->blocking));
 303          if (isset($record->minute)) {
 304              $task->set_minute($record->minute);
 305          }
 306          if (isset($record->hour)) {
 307              $task->set_hour($record->hour);
 308          }
 309          if (isset($record->day)) {
 310              $task->set_day($record->day);
 311          }
 312          if (isset($record->month)) {
 313              $task->set_month($record->month);
 314          }
 315          if (isset($record->dayofweek)) {
 316              $task->set_day_of_week($record->dayofweek);
 317          }
 318          if (isset($record->faildelay)) {
 319              $task->set_fail_delay($record->faildelay);
 320          }
 321          if (isset($record->disabled)) {
 322              $task->set_disabled($record->disabled);
 323          }
 324  
 325          return $task;
 326      }
 327  
 328      /**
 329       * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
 330       * Do not execute tasks loaded from this function - they have not been locked.
 331       * @param string $componentname - The name of the component to load the tasks for.
 332       * @return \core\task\scheduled_task[]
 333       */
 334      public static function load_scheduled_tasks_for_component($componentname) {
 335          global $DB;
 336  
 337          $tasks = array();
 338          // We are just reading - so no locks required.
 339          $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
 340          foreach ($records as $record) {
 341              $task = self::scheduled_task_from_record($record);
 342              // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 343              if ($task) {
 344                  $tasks[] = $task;
 345              }
 346          }
 347  
 348          return $tasks;
 349      }
 350  
 351      /**
 352       * This function load the scheduled task details for a given classname.
 353       *
 354       * @param string $classname
 355       * @return \core\task\scheduled_task or false
 356       */
 357      public static function get_scheduled_task($classname) {
 358          global $DB;
 359  
 360          if (strpos($classname, '\\') !== 0) {
 361              $classname = '\\' . $classname;
 362          }
 363          // We are just reading - so no locks required.
 364          $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
 365          if (!$record) {
 366              return false;
 367          }
 368          return self::scheduled_task_from_record($record);
 369      }
 370  
 371      /**
 372       * This function load the default scheduled task details for a given classname.
 373       *
 374       * @param string $classname
 375       * @return \core\task\scheduled_task or false
 376       */
 377      public static function get_default_scheduled_task($classname) {
 378          $task = self::get_scheduled_task($classname);
 379          $componenttasks = array();
 380  
 381          // Safety check in case no task was found for the given classname.
 382          if ($task) {
 383              $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
 384          }
 385  
 386          foreach ($componenttasks as $componenttask) {
 387              if (get_class($componenttask) == get_class($task)) {
 388                  return $componenttask;
 389              }
 390          }
 391  
 392          return false;
 393      }
 394  
 395      /**
 396       * This function will return a list of all the scheduled tasks that exist in the database.
 397       *
 398       * @return \core\task\scheduled_task[]
 399       */
 400      public static function get_all_scheduled_tasks() {
 401          global $DB;
 402  
 403          $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
 404          $tasks = array();
 405  
 406          foreach ($records as $record) {
 407              $task = self::scheduled_task_from_record($record);
 408              // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 409              if ($task) {
 410                  $tasks[] = $task;
 411              }
 412          }
 413  
 414          return $tasks;
 415      }
 416  
 417      /**
 418       * This function will dispatch the next adhoc task in the queue. The task will be handed out
 419       * with an open lock - possibly on the entire cron process. Make sure you call either
 420       * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
 421       *
 422       * @param int $timestart
 423       * @return \core\task\adhoc_task or null if not found
 424       */
 425      public static function get_next_adhoc_task($timestart) {
 426          global $DB;
 427          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 428  
 429          if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
 430              throw new \moodle_exception('locktimeout');
 431          }
 432  
 433          $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
 434          $params = array('timestart1' => $timestart);
 435          $records = $DB->get_records_select('task_adhoc', $where, $params);
 436  
 437          foreach ($records as $record) {
 438  
 439              if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) {
 440                  $classname = '\\' . $record->classname;
 441                  $task = self::adhoc_task_from_record($record);
 442                  // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 443                  if (!$task) {
 444                      $lock->release();
 445                      continue;
 446                  }
 447  
 448                  $task->set_lock($lock);
 449                  if (!$task->is_blocking()) {
 450                      $cronlock->release();
 451                  } else {
 452                      $task->set_cron_lock($cronlock);
 453                  }
 454                  return $task;
 455              }
 456          }
 457  
 458          // No tasks.
 459          $cronlock->release();
 460          return null;
 461      }
 462  
 463      /**
 464       * This function will dispatch the next scheduled task in the queue. The task will be handed out
 465       * with an open lock - possibly on the entire cron process. Make sure you call either
 466       * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
 467       *
 468       * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
 469       * @return \core\task\scheduled_task or null
 470       */
 471      public static function get_next_scheduled_task($timestart) {
 472          global $DB;
 473          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 474  
 475          if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
 476              throw new \moodle_exception('locktimeout');
 477          }
 478  
 479          $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
 480                    AND (nextruntime IS NULL OR nextruntime < :timestart2)
 481                    AND disabled = 0";
 482          $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
 483          $records = $DB->get_records_select('task_scheduled', $where, $params);
 484  
 485          $pluginmanager = \core_plugin_manager::instance();
 486  
 487          foreach ($records as $record) {
 488  
 489              if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
 490                  $classname = '\\' . $record->classname;
 491                  $task = self::scheduled_task_from_record($record);
 492                  // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 493                  if (!$task) {
 494                      $lock->release();
 495                      continue;
 496                  }
 497  
 498                  $task->set_lock($lock);
 499  
 500                  // See if the component is disabled.
 501                  $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
 502  
 503                  if ($plugininfo) {
 504                      if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
 505                          mtrace($task->get_name().' skipped - the component '.$task->get_component().' is disabled');
 506                          $lock->release();
 507                          continue;
 508                      }
 509                  }
 510  
 511                  if (!$task->is_blocking()) {
 512                      $cronlock->release();
 513                  } else {
 514                      $task->set_cron_lock($cronlock);
 515                  }
 516                  return $task;
 517              }
 518          }
 519  
 520          // No tasks.
 521          $cronlock->release();
 522          return null;
 523      }
 524  
 525      /**
 526       * This function indicates that an adhoc task was not completed successfully and should be retried.
 527       *
 528       * @param \core\task\adhoc_task $task
 529       */
 530      public static function adhoc_task_failed(adhoc_task $task) {
 531          global $DB;
 532          $delay = $task->get_fail_delay();
 533  
 534          // Reschedule task with exponential fall off for failing tasks.
 535          if (empty($delay)) {
 536              $delay = 60;
 537          } else {
 538              $delay *= 2;
 539          }
 540  
 541          // Max of 24 hour delay.
 542          if ($delay > 86400) {
 543              $delay = 86400;
 544          }
 545  
 546          $classname = get_class($task);
 547          if (strpos($classname, '\\') !== 0) {
 548              $classname = '\\' . $classname;
 549          }
 550  
 551          $task->set_next_run_time(time() + $delay);
 552          $task->set_fail_delay($delay);
 553          $record = self::record_from_adhoc_task($task);
 554          $DB->update_record('task_adhoc', $record);
 555  
 556          if ($task->is_blocking()) {
 557              $task->get_cron_lock()->release();
 558          }
 559          $task->get_lock()->release();
 560      }
 561  
 562      /**
 563       * This function indicates that an adhoc task was completed successfully.
 564       *
 565       * @param \core\task\adhoc_task $task
 566       */
 567      public static function adhoc_task_complete(adhoc_task $task) {
 568          global $DB;
 569  
 570          // Delete the adhoc task record - it is finished.
 571          $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
 572  
 573          // Reschedule and then release the locks.
 574          if ($task->is_blocking()) {
 575              $task->get_cron_lock()->release();
 576          }
 577          $task->get_lock()->release();
 578      }
 579  
 580      /**
 581       * This function indicates that a scheduled task was not completed successfully and should be retried.
 582       *
 583       * @param \core\task\scheduled_task $task
 584       */
 585      public static function scheduled_task_failed(scheduled_task $task) {
 586          global $DB;
 587  
 588          $delay = $task->get_fail_delay();
 589  
 590          // Reschedule task with exponential fall off for failing tasks.
 591          if (empty($delay)) {
 592              $delay = 60;
 593          } else {
 594              $delay *= 2;
 595          }
 596  
 597          // Max of 24 hour delay.
 598          if ($delay > 86400) {
 599              $delay = 86400;
 600          }
 601  
 602          $classname = get_class($task);
 603          if (strpos($classname, '\\') !== 0) {
 604              $classname = '\\' . $classname;
 605          }
 606  
 607          $record = $DB->get_record('task_scheduled', array('classname' => $classname));
 608          $record->nextruntime = time() + $delay;
 609          $record->faildelay = $delay;
 610          $DB->update_record('task_scheduled', $record);
 611  
 612          if ($task->is_blocking()) {
 613              $task->get_cron_lock()->release();
 614          }
 615          $task->get_lock()->release();
 616      }
 617  
 618      /**
 619       * This function indicates that a scheduled task was completed successfully and should be rescheduled.
 620       *
 621       * @param \core\task\scheduled_task $task
 622       */
 623      public static function scheduled_task_complete(scheduled_task $task) {
 624          global $DB;
 625  
 626          $classname = get_class($task);
 627          if (strpos($classname, '\\') !== 0) {
 628              $classname = '\\' . $classname;
 629          }
 630          $record = $DB->get_record('task_scheduled', array('classname' => $classname));
 631          if ($record) {
 632              $record->lastruntime = time();
 633              $record->faildelay = 0;
 634              $record->nextruntime = $task->get_next_scheduled_time();
 635  
 636              $DB->update_record('task_scheduled', $record);
 637          }
 638  
 639          // Reschedule and then release the locks.
 640          if ($task->is_blocking()) {
 641              $task->get_cron_lock()->release();
 642          }
 643          $task->get_lock()->release();
 644      }
 645  
 646      /**
 647       * This function is used to indicate that any long running cron processes should exit at the
 648       * next opportunity and restart. This is because something (e.g. DB changes) has changed and
 649       * the static caches may be stale.
 650       */
 651      public static function clear_static_caches() {
 652          global $DB;
 653          // Do not use get/set config here because the caches cannot be relied on.
 654          $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
 655          if ($record) {
 656              $record->value = time();
 657              $DB->update_record('config', $record);
 658          } else {
 659              $record = new \stdClass();
 660              $record->name = 'scheduledtaskreset';
 661              $record->value = time();
 662              $DB->insert_record('config', $record);
 663          }
 664      }
 665  
 666      /**
 667       * Return true if the static caches have been cleared since $starttime.
 668       * @param int $starttime The time this process started.
 669       * @return boolean True if static caches need resetting.
 670       */
 671      public static function static_caches_cleared_since($starttime) {
 672          global $DB;
 673          $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
 674          return $record && (intval($record->value) > $starttime);
 675      }
 676  }


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1