Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
  44       *      If false, they are left as 'R'
  45       * @return \core\task\scheduled_task[] - List of scheduled tasks for this component.
  46       */
  47      public static function load_default_scheduled_tasks_for_component($componentname, $expandr = true) {
  48          $dir = \core_component::get_component_directory($componentname);
  49  
  50          if (!$dir) {
  51              return array();
  52          }
  53  
  54          $file = $dir . '/' . CORE_TASK_TASKS_FILENAME;
  55          if (!file_exists($file)) {
  56              return array();
  57          }
  58  
  59          $tasks = null;
  60          include($file);
  61  
  62          if (!isset($tasks)) {
  63              return array();
  64          }
  65  
  66          $scheduledtasks = array();
  67  
  68          foreach ($tasks as $task) {
  69              $record = (object) $task;
  70              $scheduledtask = self::scheduled_task_from_record($record, $expandr);
  71              // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
  72              if ($scheduledtask) {
  73                  $scheduledtask->set_component($componentname);
  74                  $scheduledtasks[] = $scheduledtask;
  75              }
  76          }
  77  
  78          return $scheduledtasks;
  79      }
  80  
  81      /**
  82       * Update the database to contain a list of scheduled task for a component.
  83       * The list of scheduled tasks is taken from @load_scheduled_tasks_for_component.
  84       * Will throw exceptions for any errors.
  85       *
  86       * @param string $componentname - The frankenstyle component name.
  87       */
  88      public static function reset_scheduled_tasks_for_component($componentname) {
  89          global $DB;
  90          $tasks = self::load_default_scheduled_tasks_for_component($componentname);
  91          $validtasks = array();
  92  
  93          foreach ($tasks as $taskid => $task) {
  94              $classname = self::get_canonical_class_name($task);
  95  
  96              $validtasks[] = $classname;
  97  
  98              if ($currenttask = self::get_scheduled_task($classname)) {
  99                  if ($currenttask->is_customised()) {
 100                      // If there is an existing task with a custom schedule, do not override it.
 101                      continue;
 102                  }
 103  
 104                  // Update the record from the default task data.
 105                  self::configure_scheduled_task($task);
 106              } else {
 107                  // Ensure that the first run follows the schedule.
 108                  $task->set_next_run_time($task->get_next_scheduled_time());
 109  
 110                  // Insert the new task in the database.
 111                  $record = self::record_from_scheduled_task($task);
 112                  $DB->insert_record('task_scheduled', $record);
 113              }
 114          }
 115  
 116          // Delete any task that is not defined in the component any more.
 117          $sql = "component = :component";
 118          $params = array('component' => $componentname);
 119          if (!empty($validtasks)) {
 120              list($insql, $inparams) = $DB->get_in_or_equal($validtasks, SQL_PARAMS_NAMED, 'param', false);
 121              $sql .= ' AND classname ' . $insql;
 122              $params = array_merge($params, $inparams);
 123          }
 124          $DB->delete_records_select('task_scheduled', $sql, $params);
 125      }
 126  
 127      /**
 128       * Checks if the task with the same classname, component and customdata is already scheduled
 129       *
 130       * @param adhoc_task $task
 131       * @return bool
 132       */
 133      protected static function task_is_scheduled($task) {
 134          return false !== self::get_queued_adhoc_task_record($task);
 135      }
 136  
 137      /**
 138       * Checks if the task with the same classname, component and customdata is already scheduled
 139       *
 140       * @param adhoc_task $task
 141       * @return bool
 142       */
 143      protected static function get_queued_adhoc_task_record($task) {
 144          global $DB;
 145  
 146          $record = self::record_from_adhoc_task($task);
 147          $params = [$record->classname, $record->component, $record->customdata];
 148          $sql = 'classname = ? AND component = ? AND ' .
 149              $DB->sql_compare_text('customdata', \core_text::strlen($record->customdata) + 1) . ' = ?';
 150  
 151          if ($record->userid) {
 152              $params[] = $record->userid;
 153              $sql .= " AND userid = ? ";
 154          }
 155          return $DB->get_record_select('task_adhoc', $sql, $params);
 156      }
 157  
 158      /**
 159       * Schedule a new task, or reschedule an existing adhoc task which has matching data.
 160       *
 161       * Only a task matching the same user, classname, component, and customdata will be rescheduled.
 162       * If these values do not match exactly then a new task is scheduled.
 163       *
 164       * @param \core\task\adhoc_task $task - The new adhoc task information to store.
 165       * @since Moodle 3.7
 166       */
 167      public static function reschedule_or_queue_adhoc_task(adhoc_task $task) : void {
 168          global $DB;
 169  
 170          if ($existingrecord = self::get_queued_adhoc_task_record($task)) {
 171              // Only update the next run time if it is explicitly set on the task.
 172              $nextruntime = $task->get_next_run_time();
 173              if ($nextruntime && ($existingrecord->nextruntime != $nextruntime)) {
 174                  $DB->set_field('task_adhoc', 'nextruntime', $nextruntime, ['id' => $existingrecord->id]);
 175              }
 176          } else {
 177              // There is nothing queued yet. Just queue as normal.
 178              self::queue_adhoc_task($task);
 179          }
 180      }
 181  
 182      /**
 183       * Queue an adhoc task to run in the background.
 184       *
 185       * @param \core\task\adhoc_task $task - The new adhoc task information to store.
 186       * @param bool $checkforexisting - If set to true and the task with the same user, classname, component and customdata
 187       *     is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks.
 188       * @return boolean - True if the config was saved.
 189       */
 190      public static function queue_adhoc_task(adhoc_task $task, $checkforexisting = false) {
 191          global $DB;
 192  
 193          if ($userid = $task->get_userid()) {
 194              // User found. Check that they are suitable.
 195              \core_user::require_active_user(\core_user::get_user($userid, '*', MUST_EXIST), true, true);
 196          }
 197  
 198          $record = self::record_from_adhoc_task($task);
 199          // Schedule it immediately if nextruntime not explicitly set.
 200          if (!$task->get_next_run_time()) {
 201              $record->nextruntime = time() - 1;
 202          }
 203  
 204          // Check if the same task is already scheduled.
 205          if ($checkforexisting && self::task_is_scheduled($task)) {
 206              return false;
 207          }
 208  
 209          // Queue the task.
 210          $result = $DB->insert_record('task_adhoc', $record);
 211  
 212          return $result;
 213      }
 214  
 215      /**
 216       * Change the default configuration for a scheduled task.
 217       * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
 218       *
 219       * @param \core\task\scheduled_task $task - The new scheduled task information to store.
 220       * @return boolean - True if the config was saved.
 221       */
 222      public static function configure_scheduled_task(scheduled_task $task) {
 223          global $DB;
 224  
 225          $classname = self::get_canonical_class_name($task);
 226  
 227          $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
 228  
 229          $record = self::record_from_scheduled_task($task);
 230          $record->id = $original->id;
 231          $record->nextruntime = $task->get_next_scheduled_time();
 232          unset($record->lastruntime);
 233          $result = $DB->update_record('task_scheduled', $record);
 234  
 235          return $result;
 236      }
 237  
 238      /**
 239       * Utility method to create a DB record from a scheduled task.
 240       *
 241       * @param \core\task\scheduled_task $task
 242       * @return \stdClass
 243       */
 244      public static function record_from_scheduled_task($task) {
 245          $record = new \stdClass();
 246          $record->classname = self::get_canonical_class_name($task);
 247          $record->component = $task->get_component();
 248          $record->blocking = $task->is_blocking();
 249          $record->customised = $task->is_customised();
 250          $record->lastruntime = $task->get_last_run_time();
 251          $record->nextruntime = $task->get_next_run_time();
 252          $record->faildelay = $task->get_fail_delay();
 253          $record->hour = $task->get_hour();
 254          $record->minute = $task->get_minute();
 255          $record->day = $task->get_day();
 256          $record->dayofweek = $task->get_day_of_week();
 257          $record->month = $task->get_month();
 258          $record->disabled = $task->get_disabled();
 259  
 260          return $record;
 261      }
 262  
 263      /**
 264       * Utility method to create a DB record from an adhoc task.
 265       *
 266       * @param \core\task\adhoc_task $task
 267       * @return \stdClass
 268       */
 269      public static function record_from_adhoc_task($task) {
 270          $record = new \stdClass();
 271          $record->classname = self::get_canonical_class_name($task);
 272          $record->id = $task->get_id();
 273          $record->component = $task->get_component();
 274          $record->blocking = $task->is_blocking();
 275          $record->nextruntime = $task->get_next_run_time();
 276          $record->faildelay = $task->get_fail_delay();
 277          $record->customdata = $task->get_custom_data_as_string();
 278          $record->userid = $task->get_userid();
 279  
 280          return $record;
 281      }
 282  
 283      /**
 284       * Utility method to create an adhoc task from a DB record.
 285       *
 286       * @param \stdClass $record
 287       * @return \core\task\adhoc_task
 288       */
 289      public static function adhoc_task_from_record($record) {
 290          $classname = self::get_canonical_class_name($record->classname);
 291          if (!class_exists($classname)) {
 292              debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
 293              return false;
 294          }
 295          $task = new $classname;
 296          if (isset($record->nextruntime)) {
 297              $task->set_next_run_time($record->nextruntime);
 298          }
 299          if (isset($record->id)) {
 300              $task->set_id($record->id);
 301          }
 302          if (isset($record->component)) {
 303              $task->set_component($record->component);
 304          }
 305          $task->set_blocking(!empty($record->blocking));
 306          if (isset($record->faildelay)) {
 307              $task->set_fail_delay($record->faildelay);
 308          }
 309          if (isset($record->customdata)) {
 310              $task->set_custom_data_as_string($record->customdata);
 311          }
 312  
 313          if (isset($record->userid)) {
 314              $task->set_userid($record->userid);
 315          }
 316  
 317          return $task;
 318      }
 319  
 320      /**
 321       * Utility method to create a task from a DB record.
 322       *
 323       * @param \stdClass $record
 324       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 325       *      If false, they are left as 'R'
 326       * @return \core\task\scheduled_task|false
 327       */
 328      public static function scheduled_task_from_record($record, $expandr = true) {
 329          $classname = self::get_canonical_class_name($record->classname);
 330          if (!class_exists($classname)) {
 331              debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
 332              return false;
 333          }
 334          /** @var \core\task\scheduled_task $task */
 335          $task = new $classname;
 336          if (isset($record->lastruntime)) {
 337              $task->set_last_run_time($record->lastruntime);
 338          }
 339          if (isset($record->nextruntime)) {
 340              $task->set_next_run_time($record->nextruntime);
 341          }
 342          if (isset($record->customised)) {
 343              $task->set_customised($record->customised);
 344          }
 345          if (isset($record->component)) {
 346              $task->set_component($record->component);
 347          }
 348          $task->set_blocking(!empty($record->blocking));
 349          if (isset($record->minute)) {
 350              $task->set_minute($record->minute, $expandr);
 351          }
 352          if (isset($record->hour)) {
 353              $task->set_hour($record->hour, $expandr);
 354          }
 355          if (isset($record->day)) {
 356              $task->set_day($record->day);
 357          }
 358          if (isset($record->month)) {
 359              $task->set_month($record->month);
 360          }
 361          if (isset($record->dayofweek)) {
 362              $task->set_day_of_week($record->dayofweek, $expandr);
 363          }
 364          if (isset($record->faildelay)) {
 365              $task->set_fail_delay($record->faildelay);
 366          }
 367          if (isset($record->disabled)) {
 368              $task->set_disabled($record->disabled);
 369          }
 370  
 371          return $task;
 372      }
 373  
 374      /**
 375       * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
 376       * Do not execute tasks loaded from this function - they have not been locked.
 377       * @param string $componentname - The name of the component to load the tasks for.
 378       * @return \core\task\scheduled_task[]
 379       */
 380      public static function load_scheduled_tasks_for_component($componentname) {
 381          global $DB;
 382  
 383          $tasks = array();
 384          // We are just reading - so no locks required.
 385          $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
 386          foreach ($records as $record) {
 387              $task = self::scheduled_task_from_record($record);
 388              // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 389              if ($task) {
 390                  $tasks[] = $task;
 391              }
 392          }
 393  
 394          return $tasks;
 395      }
 396  
 397      /**
 398       * This function load the scheduled task details for a given classname.
 399       *
 400       * @param string $classname
 401       * @return \core\task\scheduled_task or false
 402       */
 403      public static function get_scheduled_task($classname) {
 404          global $DB;
 405  
 406          $classname = self::get_canonical_class_name($classname);
 407          // We are just reading - so no locks required.
 408          $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
 409          if (!$record) {
 410              return false;
 411          }
 412          return self::scheduled_task_from_record($record);
 413      }
 414  
 415      /**
 416       * This function load the adhoc tasks for a given classname.
 417       *
 418       * @param string $classname
 419       * @return \core\task\adhoc_task[]
 420       */
 421      public static function get_adhoc_tasks($classname) {
 422          global $DB;
 423  
 424          $classname = self::get_canonical_class_name($classname);
 425          // We are just reading - so no locks required.
 426          $records = $DB->get_records('task_adhoc', array('classname' => $classname));
 427  
 428          return array_map(function($record) {
 429              return self::adhoc_task_from_record($record);
 430          }, $records);
 431      }
 432  
 433      /**
 434       * This function load the default scheduled task details for a given classname.
 435       *
 436       * @param string $classname
 437       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 438       *      If false, they are left as 'R'
 439       * @return \core\task\scheduled_task|false
 440       */
 441      public static function get_default_scheduled_task($classname, $expandr = true) {
 442          $task = self::get_scheduled_task($classname);
 443          $componenttasks = array();
 444  
 445          // Safety check in case no task was found for the given classname.
 446          if ($task) {
 447              $componenttasks = self::load_default_scheduled_tasks_for_component(
 448                      $task->get_component(), $expandr);
 449          }
 450  
 451          foreach ($componenttasks as $componenttask) {
 452              if (get_class($componenttask) == get_class($task)) {
 453                  return $componenttask;
 454              }
 455          }
 456  
 457          return false;
 458      }
 459  
 460      /**
 461       * This function will return a list of all the scheduled tasks that exist in the database.
 462       *
 463       * @return \core\task\scheduled_task[]
 464       */
 465      public static function get_all_scheduled_tasks() {
 466          global $DB;
 467  
 468          $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
 469          $tasks = array();
 470  
 471          foreach ($records as $record) {
 472              $task = self::scheduled_task_from_record($record);
 473              // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 474              if ($task) {
 475                  $tasks[] = $task;
 476              }
 477          }
 478  
 479          return $tasks;
 480      }
 481  
 482      /**
 483       * Ensure quality of service for the ad hoc task queue.
 484       *
 485       * This reshuffles the adhoc tasks queue to balance by type to ensure a
 486       * level of quality of service per type, while still maintaining the
 487       * relative order of tasks queued by timestamp.
 488       *
 489       * @param array $records array of task records
 490       * @param array $records array of same task records shuffled
 491       */
 492      public static function ensure_adhoc_task_qos(array $records): array {
 493  
 494          $count = count($records);
 495          if ($count == 0) {
 496              return $records;
 497          }
 498  
 499          $queues = []; // This holds a queue for each type of adhoc task.
 500          $limits = []; // The relative limits of each type of task.
 501          $limittotal = 0;
 502  
 503          // Split the single queue up into queues per type.
 504          foreach ($records as $record) {
 505              $type = $record->classname;
 506              if (!array_key_exists($type, $queues)) {
 507                  $queues[$type] = [];
 508              }
 509              if (!array_key_exists($type, $limits)) {
 510                  $limits[$type] = 1;
 511                  $limittotal += 1;
 512              }
 513              $queues[$type][] = $record;
 514          }
 515  
 516          $qos = []; // Our new queue with ensured quality of service.
 517          $seed = $count % $limittotal; // Which task queue to shuffle from first?
 518  
 519          $move = 1; // How many tasks to shuffle at a time.
 520          do {
 521              $shuffled = 0;
 522  
 523              // Now cycle through task type queues and interleaving the tasks
 524              // back into a single queue.
 525              foreach ($limits as $type => $limit) {
 526  
 527                  // Just interleaving the queue is not enough, because after
 528                  // any task is processed the whole queue is rebuilt again. So
 529                  // we need to deterministically start on different types of
 530                  // tasks so that *on average* we rotate through each type of task.
 531                  //
 532                  // We achieve this by using a $seed to start moving tasks off a
 533                  // different queue each time. The seed is based on the task count
 534                  // modulo the number of types of tasks on the queue. As we count
 535                  // down this naturally cycles through each type of record.
 536                  if ($seed < 1) {
 537                      $shuffled = 1;
 538                      $seed += 1;
 539                      continue;
 540                  }
 541                  $tasks = array_splice($queues[$type], 0, $move);
 542                  $qos = array_merge($qos, $tasks);
 543  
 544                  // Stop if we didn't move any tasks onto the main queue.
 545                  $shuffled += count($tasks);
 546              }
 547              // Generally the only tasks that matter are those that are near the start so
 548              // after we have shuffled the first few 1 by 1, start shuffling larger groups.
 549              if (count($qos) >= (4 * count($limits))) {
 550                  $move *= 2;
 551              }
 552          } while ($shuffled > 0);
 553  
 554          return $qos;
 555      }
 556  
 557      /**
 558       * This function will dispatch the next adhoc task in the queue. The task will be handed out
 559       * with an open lock - possibly on the entire cron process. Make sure you call either
 560       * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
 561       *
 562       * @param int $timestart
 563       * @param bool $checklimits Should we check limits?
 564       * @return \core\task\adhoc_task or null if not found
 565       * @throws \moodle_exception
 566       */
 567      public static function get_next_adhoc_task($timestart, $checklimits = true) {
 568          global $DB;
 569  
 570          $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
 571          $params = array('timestart1' => $timestart);
 572          $records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC', '*', 0, 2000);
 573          $records = self::ensure_adhoc_task_qos($records);
 574  
 575          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 576  
 577          $skipclasses = array();
 578  
 579          foreach ($records as $record) {
 580  
 581              if (in_array($record->classname, $skipclasses)) {
 582                  // Skip the task if it can't be started due to per-task concurrency limit.
 583                  continue;
 584              }
 585  
 586              if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) {
 587  
 588                  // Safety check, see if the task has been already processed by another cron run.
 589                  $record = $DB->get_record('task_adhoc', array('id' => $record->id));
 590                  if (!$record) {
 591                      $lock->release();
 592                      continue;
 593                  }
 594  
 595                  $task = self::adhoc_task_from_record($record);
 596                  // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 597                  if (!$task) {
 598                      $lock->release();
 599                      continue;
 600                  }
 601  
 602                  $tasklimit = $task->get_concurrency_limit();
 603                  if ($checklimits && $tasklimit > 0) {
 604                      if ($concurrencylock = self::get_concurrent_task_lock($task)) {
 605                          $task->set_concurrency_lock($concurrencylock);
 606                      } else {
 607                          // Unable to obtain a concurrency lock.
 608                          mtrace("Skipping $record->classname adhoc task class as the per-task limit of $tasklimit is reached.");
 609                          $skipclasses[] = $record->classname;
 610                          $lock->release();
 611                          continue;
 612                      }
 613                  }
 614  
 615                  // The global cron lock is under the most contention so request it
 616                  // as late as possible and release it as soon as possible.
 617                  if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
 618                      $lock->release();
 619                      throw new \moodle_exception('locktimeout');
 620                  }
 621  
 622                  $task->set_lock($lock);
 623                  if (!$task->is_blocking()) {
 624                      $cronlock->release();
 625                  } else {
 626                      $task->set_cron_lock($cronlock);
 627                  }
 628                  return $task;
 629              }
 630          }
 631  
 632          return null;
 633      }
 634  
 635      /**
 636       * This function will dispatch the next scheduled task in the queue. The task will be handed out
 637       * with an open lock - possibly on the entire cron process. Make sure you call either
 638       * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
 639       *
 640       * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
 641       * @return \core\task\scheduled_task or null
 642       * @throws \moodle_exception
 643       */
 644      public static function get_next_scheduled_task($timestart) {
 645          global $DB;
 646          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 647  
 648          $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
 649                    AND (nextruntime IS NULL OR nextruntime < :timestart2)
 650                    AND disabled = 0
 651                    ORDER BY lastruntime, id ASC";
 652          $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
 653          $records = $DB->get_records_select('task_scheduled', $where, $params);
 654  
 655          $pluginmanager = \core_plugin_manager::instance();
 656  
 657          foreach ($records as $record) {
 658  
 659              if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
 660                  $classname = '\\' . $record->classname;
 661                  $task = self::scheduled_task_from_record($record);
 662                  // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 663                  if (!$task) {
 664                      $lock->release();
 665                      continue;
 666                  }
 667  
 668                  $task->set_lock($lock);
 669  
 670                  // See if the component is disabled.
 671                  $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
 672  
 673                  if ($plugininfo) {
 674                      if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
 675                          $lock->release();
 676                          continue;
 677                      }
 678                  }
 679  
 680                  // Make sure the task data is unchanged.
 681                  if (!$DB->record_exists('task_scheduled', (array) $record)) {
 682                      $lock->release();
 683                      continue;
 684                  }
 685  
 686                  // The global cron lock is under the most contention so request it
 687                  // as late as possible and release it as soon as possible.
 688                  if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
 689                      $lock->release();
 690                      throw new \moodle_exception('locktimeout');
 691                  }
 692  
 693                  if (!$task->is_blocking()) {
 694                      $cronlock->release();
 695                  } else {
 696                      $task->set_cron_lock($cronlock);
 697                  }
 698                  return $task;
 699              }
 700          }
 701  
 702          return null;
 703      }
 704  
 705      /**
 706       * This function indicates that an adhoc task was not completed successfully and should be retried.
 707       *
 708       * @param \core\task\adhoc_task $task
 709       */
 710      public static function adhoc_task_failed(adhoc_task $task) {
 711          global $DB;
 712          $delay = $task->get_fail_delay();
 713  
 714          // Reschedule task with exponential fall off for failing tasks.
 715          if (empty($delay)) {
 716              $delay = 60;
 717          } else {
 718              $delay *= 2;
 719          }
 720  
 721          // Max of 24 hour delay.
 722          if ($delay > 86400) {
 723              $delay = 86400;
 724          }
 725  
 726          // Reschedule and then release the locks.
 727          $task->set_next_run_time(time() + $delay);
 728          $task->set_fail_delay($delay);
 729          $record = self::record_from_adhoc_task($task);
 730          $DB->update_record('task_adhoc', $record);
 731  
 732          $task->release_concurrency_lock();
 733          if ($task->is_blocking()) {
 734              $task->get_cron_lock()->release();
 735          }
 736          $task->get_lock()->release();
 737  
 738          // Finalise the log output.
 739          logmanager::finalise_log(true);
 740      }
 741  
 742      /**
 743       * This function indicates that an adhoc task was completed successfully.
 744       *
 745       * @param \core\task\adhoc_task $task
 746       */
 747      public static function adhoc_task_complete(adhoc_task $task) {
 748          global $DB;
 749  
 750          // Finalise the log output.
 751          logmanager::finalise_log();
 752  
 753          // Delete the adhoc task record - it is finished.
 754          $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
 755  
 756          // Release the locks.
 757          $task->release_concurrency_lock();
 758          if ($task->is_blocking()) {
 759              $task->get_cron_lock()->release();
 760          }
 761          $task->get_lock()->release();
 762      }
 763  
 764      /**
 765       * This function indicates that a scheduled task was not completed successfully and should be retried.
 766       *
 767       * @param \core\task\scheduled_task $task
 768       */
 769      public static function scheduled_task_failed(scheduled_task $task) {
 770          global $DB;
 771  
 772          $delay = $task->get_fail_delay();
 773  
 774          // Reschedule task with exponential fall off for failing tasks.
 775          if (empty($delay)) {
 776              $delay = 60;
 777          } else {
 778              $delay *= 2;
 779          }
 780  
 781          // Max of 24 hour delay.
 782          if ($delay > 86400) {
 783              $delay = 86400;
 784          }
 785  
 786          $classname = self::get_canonical_class_name($task);
 787  
 788          $record = $DB->get_record('task_scheduled', array('classname' => $classname));
 789          $record->nextruntime = time() + $delay;
 790          $record->faildelay = $delay;
 791          $DB->update_record('task_scheduled', $record);
 792  
 793          if ($task->is_blocking()) {
 794              $task->get_cron_lock()->release();
 795          }
 796          $task->get_lock()->release();
 797  
 798          // Finalise the log output.
 799          logmanager::finalise_log(true);
 800      }
 801  
 802      /**
 803       * Clears the fail delay for the given task and updates its next run time based on the schedule.
 804       *
 805       * @param scheduled_task $task Task to reset
 806       * @throws \dml_exception If there is a database error
 807       */
 808      public static function clear_fail_delay(scheduled_task $task) {
 809          global $DB;
 810  
 811          $record = new \stdClass();
 812          $record->id = $DB->get_field('task_scheduled', 'id',
 813                  ['classname' => self::get_canonical_class_name($task)]);
 814          $record->nextruntime = $task->get_next_scheduled_time();
 815          $record->faildelay = 0;
 816          $DB->update_record('task_scheduled', $record);
 817      }
 818  
 819      /**
 820       * This function indicates that a scheduled task was completed successfully and should be rescheduled.
 821       *
 822       * @param \core\task\scheduled_task $task
 823       */
 824      public static function scheduled_task_complete(scheduled_task $task) {
 825          global $DB;
 826  
 827          // Finalise the log output.
 828          logmanager::finalise_log();
 829  
 830          $classname = self::get_canonical_class_name($task);
 831          $record = $DB->get_record('task_scheduled', array('classname' => $classname));
 832          if ($record) {
 833              $record->lastruntime = time();
 834              $record->faildelay = 0;
 835              $record->nextruntime = $task->get_next_scheduled_time();
 836  
 837              $DB->update_record('task_scheduled', $record);
 838          }
 839  
 840          // Reschedule and then release the locks.
 841          if ($task->is_blocking()) {
 842              $task->get_cron_lock()->release();
 843          }
 844          $task->get_lock()->release();
 845      }
 846  
 847      /**
 848       * This function is used to indicate that any long running cron processes should exit at the
 849       * next opportunity and restart. This is because something (e.g. DB changes) has changed and
 850       * the static caches may be stale.
 851       */
 852      public static function clear_static_caches() {
 853          global $DB;
 854          // Do not use get/set config here because the caches cannot be relied on.
 855          $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
 856          if ($record) {
 857              $record->value = time();
 858              $DB->update_record('config', $record);
 859          } else {
 860              $record = new \stdClass();
 861              $record->name = 'scheduledtaskreset';
 862              $record->value = time();
 863              $DB->insert_record('config', $record);
 864          }
 865      }
 866  
 867      /**
 868       * Return true if the static caches have been cleared since $starttime.
 869       * @param int $starttime The time this process started.
 870       * @return boolean True if static caches need resetting.
 871       */
 872      public static function static_caches_cleared_since($starttime) {
 873          global $DB;
 874          $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
 875          return $record && (intval($record->value) > $starttime);
 876      }
 877  
 878      /**
 879       * Gets class name for use in database table. Always begins with a \.
 880       *
 881       * @param string|task_base $taskorstring Task object or a string
 882       */
 883      protected static function get_canonical_class_name($taskorstring) {
 884          if (is_string($taskorstring)) {
 885              $classname = $taskorstring;
 886          } else {
 887              $classname = get_class($taskorstring);
 888          }
 889          if (strpos($classname, '\\') !== 0) {
 890              $classname = '\\' . $classname;
 891          }
 892          return $classname;
 893      }
 894  
 895      /**
 896       * Gets the concurrent lock required to run an adhoc task.
 897       *
 898       * @param   adhoc_task $task The task to obtain the lock for
 899       * @return  \core\lock\lock The lock if one was obtained successfully
 900       * @throws  \coding_exception
 901       */
 902      protected static function get_concurrent_task_lock(adhoc_task $task): ?\core\lock\lock {
 903          $adhoclock = null;
 904          $cronlockfactory = \core\lock\lock_config::get_lock_factory(get_class($task));
 905  
 906          for ($run = 0; $run < $task->get_concurrency_limit(); $run++) {
 907              if ($adhoclock = $cronlockfactory->get_lock("concurrent_run_{$run}", 0)) {
 908                  return $adhoclock;
 909              }
 910          }
 911  
 912          return null;
 913      }
 914  
 915      /**
 916       * Find the path of PHP CLI binary.
 917       *
 918       * @return string|false The PHP CLI executable PATH
 919       */
 920      protected static function find_php_cli_path() {
 921          global $CFG;
 922  
 923          if (!empty($CFG->pathtophp) && is_executable(trim($CFG->pathtophp))) {
 924              return $CFG->pathtophp;
 925          }
 926  
 927          return false;
 928      }
 929  
 930      /**
 931       * Returns if Moodle have access to PHP CLI binary or not.
 932       *
 933       * @return bool
 934       */
 935      public static function is_runnable():bool {
 936          return self::find_php_cli_path() !== false;
 937      }
 938  
 939      /**
 940       * Executes a cron from web invocation using PHP CLI.
 941       *
 942       * @param \core\task\task_base $task Task that be executed via CLI.
 943       * @return bool
 944       * @throws \moodle_exception
 945       */
 946      public static function run_from_cli(\core\task\task_base $task):bool {
 947          global $CFG;
 948  
 949          if (!self::is_runnable()) {
 950              $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
 951              throw new \moodle_exception('cannotfindthepathtothecli', 'core_task', $redirecturl->out());
 952          } else {
 953              // Shell-escaped path to the PHP binary.
 954              $phpbinary = escapeshellarg(self::find_php_cli_path());
 955  
 956              // Shell-escaped path CLI script.
 957              $pathcomponents = [$CFG->dirroot, $CFG->admin, 'cli', 'scheduled_task.php'];
 958              $scriptpath     = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents));
 959  
 960              // Shell-escaped task name.
 961              $classname = get_class($task);
 962              $taskarg   = escapeshellarg("--execute={$classname}");
 963  
 964              // Build the CLI command.
 965              $command = "{$phpbinary} {$scriptpath} {$taskarg}";
 966  
 967              // Execute it.
 968              self::passthru_via_mtrace($command);
 969          }
 970  
 971          return true;
 972      }
 973  
 974      /**
 975       * This behaves similar to passthru but filters every line via
 976       * the mtrace function so it can be post processed.
 977       *
 978       * @param string $command to run
 979       * @return void
 980       */
 981      public static function passthru_via_mtrace(string $command) {
 982          $descriptorspec = [
 983              0 => ['pipe', 'r'], // STDIN.
 984              1 => ['pipe', 'w'], // STDOUT.
 985              2 => ['pipe', 'w'], // STDERR.
 986          ];
 987          flush();
 988          $process = proc_open($command, $descriptorspec, $pipes, realpath('./'), []);
 989          if (is_resource($process)) {
 990              while ($s = fgets($pipes[1])) {
 991                  mtrace($s, '');
 992                  flush();
 993              }
 994          }
 995  
 996          fclose($pipes[0]);
 997          fclose($pipes[1]);
 998          fclose($pipes[2]);
 999          proc_close($process);
1000      }
1001  
1002  }