Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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          $record->timestarted = $task->get_timestarted();
 260          $record->hostname = $task->get_hostname();
 261          $record->pid = $task->get_pid();
 262  
 263          return $record;
 264      }
 265  
 266      /**
 267       * Utility method to create a DB record from an adhoc task.
 268       *
 269       * @param \core\task\adhoc_task $task
 270       * @return \stdClass
 271       */
 272      public static function record_from_adhoc_task($task) {
 273          $record = new \stdClass();
 274          $record->classname = self::get_canonical_class_name($task);
 275          $record->id = $task->get_id();
 276          $record->component = $task->get_component();
 277          $record->blocking = $task->is_blocking();
 278          $record->nextruntime = $task->get_next_run_time();
 279          $record->faildelay = $task->get_fail_delay();
 280          $record->customdata = $task->get_custom_data_as_string();
 281          $record->userid = $task->get_userid();
 282          $record->timecreated = time();
 283          $record->timestarted = $task->get_timestarted();
 284          $record->hostname = $task->get_hostname();
 285          $record->pid = $task->get_pid();
 286  
 287          return $record;
 288      }
 289  
 290      /**
 291       * Utility method to create an adhoc task from a DB record.
 292       *
 293       * @param \stdClass $record
 294       * @return \core\task\adhoc_task
 295       */
 296      public static function adhoc_task_from_record($record) {
 297          $classname = self::get_canonical_class_name($record->classname);
 298          if (!class_exists($classname)) {
 299              debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
 300              return false;
 301          }
 302          $task = new $classname;
 303          if (isset($record->nextruntime)) {
 304              $task->set_next_run_time($record->nextruntime);
 305          }
 306          if (isset($record->id)) {
 307              $task->set_id($record->id);
 308          }
 309          if (isset($record->component)) {
 310              $task->set_component($record->component);
 311          }
 312          $task->set_blocking(!empty($record->blocking));
 313          if (isset($record->faildelay)) {
 314              $task->set_fail_delay($record->faildelay);
 315          }
 316          if (isset($record->customdata)) {
 317              $task->set_custom_data_as_string($record->customdata);
 318          }
 319  
 320          if (isset($record->userid)) {
 321              $task->set_userid($record->userid);
 322          }
 323          if (isset($record->timestarted)) {
 324              $task->set_timestarted($record->timestarted);
 325          }
 326          if (isset($record->hostname)) {
 327              $task->set_hostname($record->hostname);
 328          }
 329          if (isset($record->pid)) {
 330              $task->set_pid($record->pid);
 331          }
 332  
 333          return $task;
 334      }
 335  
 336      /**
 337       * Utility method to create a task from a DB record.
 338       *
 339       * @param \stdClass $record
 340       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 341       *      If false, they are left as 'R'
 342       * @return \core\task\scheduled_task|false
 343       */
 344      public static function scheduled_task_from_record($record, $expandr = true) {
 345          $classname = self::get_canonical_class_name($record->classname);
 346          if (!class_exists($classname)) {
 347              debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
 348              return false;
 349          }
 350          /** @var \core\task\scheduled_task $task */
 351          $task = new $classname;
 352          if (isset($record->lastruntime)) {
 353              $task->set_last_run_time($record->lastruntime);
 354          }
 355          if (isset($record->nextruntime)) {
 356              $task->set_next_run_time($record->nextruntime);
 357          }
 358          if (isset($record->customised)) {
 359              $task->set_customised($record->customised);
 360          }
 361          if (isset($record->component)) {
 362              $task->set_component($record->component);
 363          }
 364          $task->set_blocking(!empty($record->blocking));
 365          if (isset($record->minute)) {
 366              $task->set_minute($record->minute, $expandr);
 367          }
 368          if (isset($record->hour)) {
 369              $task->set_hour($record->hour, $expandr);
 370          }
 371          if (isset($record->day)) {
 372              $task->set_day($record->day);
 373          }
 374          if (isset($record->month)) {
 375              $task->set_month($record->month);
 376          }
 377          if (isset($record->dayofweek)) {
 378              $task->set_day_of_week($record->dayofweek, $expandr);
 379          }
 380          if (isset($record->faildelay)) {
 381              $task->set_fail_delay($record->faildelay);
 382          }
 383          if (isset($record->disabled)) {
 384              $task->set_disabled($record->disabled);
 385          }
 386          if (isset($record->timestarted)) {
 387              $task->set_timestarted($record->timestarted);
 388          }
 389          if (isset($record->hostname)) {
 390              $task->set_hostname($record->hostname);
 391          }
 392          if (isset($record->pid)) {
 393              $task->set_pid($record->pid);
 394          }
 395  
 396          return $task;
 397      }
 398  
 399      /**
 400       * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
 401       * Do not execute tasks loaded from this function - they have not been locked.
 402       * @param string $componentname - The name of the component to load the tasks for.
 403       * @return \core\task\scheduled_task[]
 404       */
 405      public static function load_scheduled_tasks_for_component($componentname) {
 406          global $DB;
 407  
 408          $tasks = array();
 409          // We are just reading - so no locks required.
 410          $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
 411          foreach ($records as $record) {
 412              $task = self::scheduled_task_from_record($record);
 413              // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 414              if ($task) {
 415                  $tasks[] = $task;
 416              }
 417          }
 418  
 419          return $tasks;
 420      }
 421  
 422      /**
 423       * This function load the scheduled task details for a given classname.
 424       *
 425       * @param string $classname
 426       * @return \core\task\scheduled_task or false
 427       */
 428      public static function get_scheduled_task($classname) {
 429          global $DB;
 430  
 431          $classname = self::get_canonical_class_name($classname);
 432          // We are just reading - so no locks required.
 433          $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
 434          if (!$record) {
 435              return false;
 436          }
 437          return self::scheduled_task_from_record($record);
 438      }
 439  
 440      /**
 441       * This function load the adhoc tasks for a given classname.
 442       *
 443       * @param string $classname
 444       * @return \core\task\adhoc_task[]
 445       */
 446      public static function get_adhoc_tasks($classname) {
 447          global $DB;
 448  
 449          $classname = self::get_canonical_class_name($classname);
 450          // We are just reading - so no locks required.
 451          $records = $DB->get_records('task_adhoc', array('classname' => $classname));
 452  
 453          return array_map(function($record) {
 454              return self::adhoc_task_from_record($record);
 455          }, $records);
 456      }
 457  
 458      /**
 459       * This function load the default scheduled task details for a given classname.
 460       *
 461       * @param string $classname
 462       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 463       *      If false, they are left as 'R'
 464       * @return \core\task\scheduled_task|false
 465       */
 466      public static function get_default_scheduled_task($classname, $expandr = true) {
 467          $task = self::get_scheduled_task($classname);
 468          $componenttasks = array();
 469  
 470          // Safety check in case no task was found for the given classname.
 471          if ($task) {
 472              $componenttasks = self::load_default_scheduled_tasks_for_component(
 473                      $task->get_component(), $expandr);
 474          }
 475  
 476          foreach ($componenttasks as $componenttask) {
 477              if (get_class($componenttask) == get_class($task)) {
 478                  return $componenttask;
 479              }
 480          }
 481  
 482          return false;
 483      }
 484  
 485      /**
 486       * This function will return a list of all the scheduled tasks that exist in the database.
 487       *
 488       * @return \core\task\scheduled_task[]
 489       */
 490      public static function get_all_scheduled_tasks() {
 491          global $DB;
 492  
 493          $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
 494          $tasks = array();
 495  
 496          foreach ($records as $record) {
 497              $task = self::scheduled_task_from_record($record);
 498              // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 499              if ($task) {
 500                  $tasks[] = $task;
 501              }
 502          }
 503  
 504          return $tasks;
 505      }
 506  
 507      /**
 508       * Ensure quality of service for the ad hoc task queue.
 509       *
 510       * This reshuffles the adhoc tasks queue to balance by type to ensure a
 511       * level of quality of service per type, while still maintaining the
 512       * relative order of tasks queued by timestamp.
 513       *
 514       * @param array $records array of task records
 515       * @param array $records array of same task records shuffled
 516       */
 517      public static function ensure_adhoc_task_qos(array $records): array {
 518  
 519          $count = count($records);
 520          if ($count == 0) {
 521              return $records;
 522          }
 523  
 524          $queues = []; // This holds a queue for each type of adhoc task.
 525          $limits = []; // The relative limits of each type of task.
 526          $limittotal = 0;
 527  
 528          // Split the single queue up into queues per type.
 529          foreach ($records as $record) {
 530              $type = $record->classname;
 531              if (!array_key_exists($type, $queues)) {
 532                  $queues[$type] = [];
 533              }
 534              if (!array_key_exists($type, $limits)) {
 535                  $limits[$type] = 1;
 536                  $limittotal += 1;
 537              }
 538              $queues[$type][] = $record;
 539          }
 540  
 541          $qos = []; // Our new queue with ensured quality of service.
 542          $seed = $count % $limittotal; // Which task queue to shuffle from first?
 543  
 544          $move = 1; // How many tasks to shuffle at a time.
 545          do {
 546              $shuffled = 0;
 547  
 548              // Now cycle through task type queues and interleaving the tasks
 549              // back into a single queue.
 550              foreach ($limits as $type => $limit) {
 551  
 552                  // Just interleaving the queue is not enough, because after
 553                  // any task is processed the whole queue is rebuilt again. So
 554                  // we need to deterministically start on different types of
 555                  // tasks so that *on average* we rotate through each type of task.
 556                  //
 557                  // We achieve this by using a $seed to start moving tasks off a
 558                  // different queue each time. The seed is based on the task count
 559                  // modulo the number of types of tasks on the queue. As we count
 560                  // down this naturally cycles through each type of record.
 561                  if ($seed < 1) {
 562                      $shuffled = 1;
 563                      $seed += 1;
 564                      continue;
 565                  }
 566                  $tasks = array_splice($queues[$type], 0, $move);
 567                  $qos = array_merge($qos, $tasks);
 568  
 569                  // Stop if we didn't move any tasks onto the main queue.
 570                  $shuffled += count($tasks);
 571              }
 572              // Generally the only tasks that matter are those that are near the start so
 573              // after we have shuffled the first few 1 by 1, start shuffling larger groups.
 574              if (count($qos) >= (4 * count($limits))) {
 575                  $move *= 2;
 576              }
 577          } while ($shuffled > 0);
 578  
 579          return $qos;
 580      }
 581  
 582      /**
 583       * This function will dispatch the next adhoc task in the queue. The task will be handed out
 584       * with an open lock - possibly on the entire cron process. Make sure you call either
 585       * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
 586       *
 587       * @param int $timestart
 588       * @param bool $checklimits Should we check limits?
 589       * @return \core\task\adhoc_task or null if not found
 590       * @throws \moodle_exception
 591       */
 592      public static function get_next_adhoc_task($timestart, $checklimits = true) {
 593          global $DB;
 594  
 595          $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
 596          $params = array('timestart1' => $timestart);
 597          $records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC', '*', 0, 2000);
 598          $records = self::ensure_adhoc_task_qos($records);
 599  
 600          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 601  
 602          $skipclasses = array();
 603  
 604          foreach ($records as $record) {
 605  
 606              if (in_array($record->classname, $skipclasses)) {
 607                  // Skip the task if it can't be started due to per-task concurrency limit.
 608                  continue;
 609              }
 610  
 611              if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) {
 612  
 613                  // Safety check, see if the task has been already processed by another cron run.
 614                  $record = $DB->get_record('task_adhoc', array('id' => $record->id));
 615                  if (!$record) {
 616                      $lock->release();
 617                      continue;
 618                  }
 619  
 620                  $task = self::adhoc_task_from_record($record);
 621                  // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 622                  if (!$task) {
 623                      $lock->release();
 624                      continue;
 625                  }
 626  
 627                  $tasklimit = $task->get_concurrency_limit();
 628                  if ($checklimits && $tasklimit > 0) {
 629                      if ($concurrencylock = self::get_concurrent_task_lock($task)) {
 630                          $task->set_concurrency_lock($concurrencylock);
 631                      } else {
 632                          // Unable to obtain a concurrency lock.
 633                          mtrace("Skipping $record->classname adhoc task class as the per-task limit of $tasklimit is reached.");
 634                          $skipclasses[] = $record->classname;
 635                          $lock->release();
 636                          continue;
 637                      }
 638                  }
 639  
 640                  // The global cron lock is under the most contention so request it
 641                  // as late as possible and release it as soon as possible.
 642                  if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
 643                      $lock->release();
 644                      throw new \moodle_exception('locktimeout');
 645                  }
 646  
 647                  $task->set_lock($lock);
 648                  if (!$task->is_blocking()) {
 649                      $cronlock->release();
 650                  } else {
 651                      $task->set_cron_lock($cronlock);
 652                  }
 653                  return $task;
 654              }
 655          }
 656  
 657          return null;
 658      }
 659  
 660      /**
 661       * This function will dispatch the next scheduled task in the queue. The task will be handed out
 662       * with an open lock - possibly on the entire cron process. Make sure you call either
 663       * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
 664       *
 665       * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
 666       * @return \core\task\scheduled_task or null
 667       * @throws \moodle_exception
 668       */
 669      public static function get_next_scheduled_task($timestart) {
 670          global $DB;
 671          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 672  
 673          $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
 674                    AND (nextruntime IS NULL OR nextruntime < :timestart2)
 675                    AND disabled = 0
 676                    ORDER BY lastruntime, id ASC";
 677          $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
 678          $records = $DB->get_records_select('task_scheduled', $where, $params);
 679  
 680          $pluginmanager = \core_plugin_manager::instance();
 681  
 682          foreach ($records as $record) {
 683  
 684              if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
 685                  $classname = '\\' . $record->classname;
 686                  $task = self::scheduled_task_from_record($record);
 687                  // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
 688                  if (!$task) {
 689                      $lock->release();
 690                      continue;
 691                  }
 692  
 693                  $task->set_lock($lock);
 694  
 695                  // See if the component is disabled.
 696                  $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
 697  
 698                  if ($plugininfo) {
 699                      if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
 700                          $lock->release();
 701                          continue;
 702                      }
 703                  }
 704  
 705                  // Make sure the task data is unchanged.
 706                  if (!$DB->record_exists('task_scheduled', (array) $record)) {
 707                      $lock->release();
 708                      continue;
 709                  }
 710  
 711                  // The global cron lock is under the most contention so request it
 712                  // as late as possible and release it as soon as possible.
 713                  if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
 714                      $lock->release();
 715                      throw new \moodle_exception('locktimeout');
 716                  }
 717  
 718                  if (!$task->is_blocking()) {
 719                      $cronlock->release();
 720                  } else {
 721                      $task->set_cron_lock($cronlock);
 722                  }
 723                  return $task;
 724              }
 725          }
 726  
 727          return null;
 728      }
 729  
 730      /**
 731       * This function indicates that an adhoc task was not completed successfully and should be retried.
 732       *
 733       * @param \core\task\adhoc_task $task
 734       */
 735      public static function adhoc_task_failed(adhoc_task $task) {
 736          global $DB;
 737          // Finalise the log output.
 738          logmanager::finalise_log(true);
 739  
 740          $delay = $task->get_fail_delay();
 741  
 742          // Reschedule task with exponential fall off for failing tasks.
 743          if (empty($delay)) {
 744              $delay = 60;
 745          } else {
 746              $delay *= 2;
 747          }
 748  
 749          // Max of 24 hour delay.
 750          if ($delay > 86400) {
 751              $delay = 86400;
 752          }
 753  
 754          // Reschedule and then release the locks.
 755          $task->set_timestarted();
 756          $task->set_hostname();
 757          $task->set_pid();
 758          $task->set_next_run_time(time() + $delay);
 759          $task->set_fail_delay($delay);
 760          $record = self::record_from_adhoc_task($task);
 761          $DB->update_record('task_adhoc', $record);
 762  
 763          $task->release_concurrency_lock();
 764          if ($task->is_blocking()) {
 765              $task->get_cron_lock()->release();
 766          }
 767          $task->get_lock()->release();
 768      }
 769  
 770      /**
 771       * Records that a adhoc task is starting to run.
 772       *
 773       * @param adhoc_task $task Task that is starting
 774       * @param int $time Start time (leave blank for now)
 775       * @throws \dml_exception
 776       * @throws \coding_exception
 777       */
 778      public static function adhoc_task_starting(adhoc_task $task, int $time = 0) {
 779          global $DB;
 780          $pid = (int)getmypid();
 781          $hostname = (string)gethostname();
 782  
 783          if (empty($time)) {
 784              $time = time();
 785          }
 786  
 787          $task->set_timestarted($time);
 788          $task->set_hostname($hostname);
 789          $task->set_pid($pid);
 790  
 791          $record = self::record_from_adhoc_task($task);
 792          $DB->update_record('task_adhoc', $record);
 793      }
 794  
 795      /**
 796       * This function indicates that an adhoc task was completed successfully.
 797       *
 798       * @param \core\task\adhoc_task $task
 799       */
 800      public static function adhoc_task_complete(adhoc_task $task) {
 801          global $DB;
 802  
 803          // Finalise the log output.
 804          logmanager::finalise_log();
 805          $task->set_timestarted();
 806          $task->set_hostname();
 807          $task->set_pid();
 808  
 809          // Delete the adhoc task record - it is finished.
 810          $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
 811  
 812          // Release the locks.
 813          $task->release_concurrency_lock();
 814          if ($task->is_blocking()) {
 815              $task->get_cron_lock()->release();
 816          }
 817          $task->get_lock()->release();
 818      }
 819  
 820      /**
 821       * This function indicates that a scheduled task was not completed successfully and should be retried.
 822       *
 823       * @param \core\task\scheduled_task $task
 824       */
 825      public static function scheduled_task_failed(scheduled_task $task) {
 826          global $DB;
 827          // Finalise the log output.
 828          logmanager::finalise_log(true);
 829  
 830          $delay = $task->get_fail_delay();
 831  
 832          // Reschedule task with exponential fall off for failing tasks.
 833          if (empty($delay)) {
 834              $delay = 60;
 835          } else {
 836              $delay *= 2;
 837          }
 838  
 839          // Max of 24 hour delay.
 840          if ($delay > 86400) {
 841              $delay = 86400;
 842          }
 843  
 844          $task->set_timestarted();
 845          $task->set_hostname();
 846          $task->set_pid();
 847  
 848          $classname = self::get_canonical_class_name($task);
 849  
 850          $record = $DB->get_record('task_scheduled', array('classname' => $classname));
 851          $record->nextruntime = time() + $delay;
 852          $record->faildelay = $delay;
 853          $record->timestarted = null;
 854          $record->hostname = null;
 855          $record->pid = null;
 856          $DB->update_record('task_scheduled', $record);
 857  
 858          if ($task->is_blocking()) {
 859              $task->get_cron_lock()->release();
 860          }
 861          $task->get_lock()->release();
 862      }
 863  
 864      /**
 865       * Clears the fail delay for the given task and updates its next run time based on the schedule.
 866       *
 867       * @param scheduled_task $task Task to reset
 868       * @throws \dml_exception If there is a database error
 869       */
 870      public static function clear_fail_delay(scheduled_task $task) {
 871          global $DB;
 872  
 873          $record = new \stdClass();
 874          $record->id = $DB->get_field('task_scheduled', 'id',
 875                  ['classname' => self::get_canonical_class_name($task)]);
 876          $record->nextruntime = $task->get_next_scheduled_time();
 877          $record->faildelay = 0;
 878          $DB->update_record('task_scheduled', $record);
 879      }
 880  
 881      /**
 882       * Records that a scheduled task is starting to run.
 883       *
 884       * @param scheduled_task $task Task that is starting
 885       * @param int $time Start time (0 = current)
 886       * @throws \dml_exception If the task doesn't exist
 887       */
 888      public static function scheduled_task_starting(scheduled_task $task, int $time = 0) {
 889          global $DB;
 890          $pid = (int)getmypid();
 891          $hostname = (string)gethostname();
 892  
 893          if (!$time) {
 894              $time = time();
 895          }
 896  
 897          $task->set_timestarted($time);
 898          $task->set_hostname($hostname);
 899          $task->set_pid($pid);
 900  
 901          $classname = self::get_canonical_class_name($task);
 902          $record = $DB->get_record('task_scheduled', ['classname' => $classname], '*', MUST_EXIST);
 903          $record->timestarted = $time;
 904          $record->hostname = $hostname;
 905          $record->pid = $pid;
 906          $DB->update_record('task_scheduled', $record);
 907      }
 908  
 909      /**
 910       * This function indicates that a scheduled task was completed successfully and should be rescheduled.
 911       *
 912       * @param \core\task\scheduled_task $task
 913       */
 914      public static function scheduled_task_complete(scheduled_task $task) {
 915          global $DB;
 916  
 917          // Finalise the log output.
 918          logmanager::finalise_log();
 919          $task->set_timestarted();
 920          $task->set_hostname();
 921          $task->set_pid();
 922  
 923          $classname = self::get_canonical_class_name($task);
 924          $record = $DB->get_record('task_scheduled', array('classname' => $classname));
 925          if ($record) {
 926              $record->lastruntime = time();
 927              $record->faildelay = 0;
 928              $record->nextruntime = $task->get_next_scheduled_time();
 929              $record->timestarted = null;
 930              $record->hostname = null;
 931              $record->pid = null;
 932  
 933              $DB->update_record('task_scheduled', $record);
 934          }
 935  
 936          // Reschedule and then release the locks.
 937          if ($task->is_blocking()) {
 938              $task->get_cron_lock()->release();
 939          }
 940          $task->get_lock()->release();
 941      }
 942  
 943      /**
 944       * Gets a list of currently-running tasks.
 945       *
 946       * @param  string $sort Sorting method
 947       * @return array Array of scheduled and adhoc tasks
 948       * @throws \dml_exception
 949       */
 950      public static function get_running_tasks($sort = ''): array {
 951          global $DB;
 952          if (empty($sort)) {
 953              $sort = 'timestarted ASC, classname ASC';
 954          }
 955          $params = ['now1' => time(), 'now2' => time()];
 956  
 957          $sql = "SELECT subquery.*
 958                    FROM (SELECT concat('s', ts.id) as uniqueid,
 959                                 ts.id,
 960                                 'scheduled' as type,
 961                                 ts.classname,
 962                                 (:now1 - ts.timestarted) as time,
 963                                 ts.timestarted,
 964                                 ts.hostname,
 965                                 ts.pid
 966                            FROM {task_scheduled} ts
 967                           WHERE ts.timestarted IS NOT NULL
 968                           UNION ALL
 969                          SELECT concat('a', ta.id) as uniqueid,
 970                                 ta.id,
 971                                 'adhoc' as type,
 972                                 ta.classname,
 973                                 (:now2 - ta.timestarted) as time,
 974                                 ta.timestarted,
 975                                 ta.hostname,
 976                                 ta.pid
 977                            FROM {task_adhoc} ta
 978                           WHERE ta.timestarted IS NOT NULL) subquery
 979                ORDER BY " . $sort;
 980  
 981          return $DB->get_records_sql($sql, $params);
 982      }
 983  
 984      /**
 985       * This function is used to indicate that any long running cron processes should exit at the
 986       * next opportunity and restart. This is because something (e.g. DB changes) has changed and
 987       * the static caches may be stale.
 988       */
 989      public static function clear_static_caches() {
 990          global $DB;
 991          // Do not use get/set config here because the caches cannot be relied on.
 992          $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
 993          if ($record) {
 994              $record->value = time();
 995              $DB->update_record('config', $record);
 996          } else {
 997              $record = new \stdClass();
 998              $record->name = 'scheduledtaskreset';
 999              $record->value = time();
1000              $DB->insert_record('config', $record);
1001          }
1002      }
1003  
1004      /**
1005       * Return true if the static caches have been cleared since $starttime.
1006       * @param int $starttime The time this process started.
1007       * @return boolean True if static caches need resetting.
1008       */
1009      public static function static_caches_cleared_since($starttime) {
1010          global $DB;
1011          $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
1012          return $record && (intval($record->value) > $starttime);
1013      }
1014  
1015      /**
1016       * Gets class name for use in database table. Always begins with a \.
1017       *
1018       * @param string|task_base $taskorstring Task object or a string
1019       */
1020      protected static function get_canonical_class_name($taskorstring) {
1021          if (is_string($taskorstring)) {
1022              $classname = $taskorstring;
1023          } else {
1024              $classname = get_class($taskorstring);
1025          }
1026          if (strpos($classname, '\\') !== 0) {
1027              $classname = '\\' . $classname;
1028          }
1029          return $classname;
1030      }
1031  
1032      /**
1033       * Gets the concurrent lock required to run an adhoc task.
1034       *
1035       * @param   adhoc_task $task The task to obtain the lock for
1036       * @return  \core\lock\lock The lock if one was obtained successfully
1037       * @throws  \coding_exception
1038       */
1039      protected static function get_concurrent_task_lock(adhoc_task $task): ?\core\lock\lock {
1040          $adhoclock = null;
1041          $cronlockfactory = \core\lock\lock_config::get_lock_factory(get_class($task));
1042  
1043          for ($run = 0; $run < $task->get_concurrency_limit(); $run++) {
1044              if ($adhoclock = $cronlockfactory->get_lock("concurrent_run_{$run}", 0)) {
1045                  return $adhoclock;
1046              }
1047          }
1048  
1049          return null;
1050      }
1051  
1052      /**
1053       * Find the path of PHP CLI binary.
1054       *
1055       * @return string|false The PHP CLI executable PATH
1056       */
1057      protected static function find_php_cli_path() {
1058          global $CFG;
1059  
1060          if (!empty($CFG->pathtophp) && is_executable(trim($CFG->pathtophp))) {
1061              return $CFG->pathtophp;
1062          }
1063  
1064          return false;
1065      }
1066  
1067      /**
1068       * Returns if Moodle have access to PHP CLI binary or not.
1069       *
1070       * @return bool
1071       */
1072      public static function is_runnable():bool {
1073          return self::find_php_cli_path() !== false;
1074      }
1075  
1076      /**
1077       * Executes a cron from web invocation using PHP CLI.
1078       *
1079       * @param \core\task\task_base $task Task that be executed via CLI.
1080       * @return bool
1081       * @throws \moodle_exception
1082       */
1083      public static function run_from_cli(\core\task\task_base $task):bool {
1084          global $CFG;
1085  
1086          if (!self::is_runnable()) {
1087              $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
1088              throw new \moodle_exception('cannotfindthepathtothecli', 'core_task', $redirecturl->out());
1089          } else {
1090              // Shell-escaped path to the PHP binary.
1091              $phpbinary = escapeshellarg(self::find_php_cli_path());
1092  
1093              // Shell-escaped path CLI script.
1094              $pathcomponents = [$CFG->dirroot, $CFG->admin, 'cli', 'scheduled_task.php'];
1095              $scriptpath     = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents));
1096  
1097              // Shell-escaped task name.
1098              $classname = get_class($task);
1099              $taskarg   = escapeshellarg("--execute={$classname}") . " " . escapeshellarg("--force");
1100  
1101              // Build the CLI command.
1102              $command = "{$phpbinary} {$scriptpath} {$taskarg}";
1103  
1104              // Execute it.
1105              passthru($command);
1106          }
1107  
1108          return true;
1109      }
1110  }