[ Index ]

PHP Cross Reference of Unnamed Project

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


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1