Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 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 use core\lock\lock; 28 use core\lock\lock_factory; 29 30 define('CORE_TASK_TASKS_FILENAME', 'db/tasks.php'); 31 /** 32 * Collection of task related methods. 33 * 34 * Some locking rules for this class: 35 * All changes to scheduled tasks must be protected with both - the global cron lock and the lock 36 * for the specific scheduled task (in that order). Locks must be released in the reverse order. 37 * @copyright 2013 Damyon Wiese 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class manager { 41 42 /** 43 * @var int Used to tell the adhoc task queue to fairly distribute tasks. 44 */ 45 const ADHOC_TASK_QUEUE_MODE_DISTRIBUTING = 0; 46 47 /** 48 * @var int Used to tell the adhoc task queue to try and fill unused capacity. 49 */ 50 const ADHOC_TASK_QUEUE_MODE_FILLING = 1; 51 52 /** 53 * @var array A cached queue of adhoc tasks 54 */ 55 public static $miniqueue; 56 57 /** 58 * @var int The last recorded number of unique adhoc tasks. 59 */ 60 public static $numtasks; 61 62 /** 63 * @var string Used to determine if the adhoc task queue is distributing or filling capacity. 64 */ 65 public static $mode; 66 67 /** 68 * Given a component name, will load the list of tasks in the db/tasks.php file for that component. 69 * 70 * @param string $componentname - The name of the component to fetch the tasks for. 71 * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int. 72 * If false, they are left as 'R' 73 * @return \core\task\scheduled_task[] - List of scheduled tasks for this component. 74 */ 75 public static function load_default_scheduled_tasks_for_component($componentname, $expandr = true) { 76 $dir = \core_component::get_component_directory($componentname); 77 78 if (!$dir) { 79 return array(); 80 } 81 82 $file = $dir . '/' . CORE_TASK_TASKS_FILENAME; 83 if (!file_exists($file)) { 84 return array(); 85 } 86 87 $tasks = null; 88 include($file); 89 90 if (!isset($tasks)) { 91 return array(); 92 } 93 94 $scheduledtasks = array(); 95 96 foreach ($tasks as $task) { 97 $record = (object) $task; 98 $scheduledtask = self::scheduled_task_from_record($record, $expandr, false); 99 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 100 if ($scheduledtask) { 101 $scheduledtask->set_component($componentname); 102 $scheduledtasks[] = $scheduledtask; 103 } 104 } 105 106 return $scheduledtasks; 107 } 108 109 /** 110 * Update the database to contain a list of scheduled task for a component. 111 * The list of scheduled tasks is taken from @load_scheduled_tasks_for_component. 112 * Will throw exceptions for any errors. 113 * 114 * @param string $componentname - The frankenstyle component name. 115 */ 116 public static function reset_scheduled_tasks_for_component($componentname) { 117 global $DB; 118 $tasks = self::load_default_scheduled_tasks_for_component($componentname); 119 $validtasks = array(); 120 121 foreach ($tasks as $taskid => $task) { 122 $classname = self::get_canonical_class_name($task); 123 124 $validtasks[] = $classname; 125 126 if ($currenttask = self::get_scheduled_task($classname)) { 127 if ($currenttask->is_customised()) { 128 // If there is an existing task with a custom schedule, do not override it. 129 continue; 130 } 131 132 // Update the record from the default task data. 133 self::configure_scheduled_task($task); 134 } else { 135 // Ensure that the first run follows the schedule. 136 $task->set_next_run_time($task->get_next_scheduled_time()); 137 138 // Insert the new task in the database. 139 $record = self::record_from_scheduled_task($task); 140 $DB->insert_record('task_scheduled', $record); 141 } 142 } 143 144 // Delete any task that is not defined in the component any more. 145 $sql = "component = :component"; 146 $params = array('component' => $componentname); 147 if (!empty($validtasks)) { 148 list($insql, $inparams) = $DB->get_in_or_equal($validtasks, SQL_PARAMS_NAMED, 'param', false); 149 $sql .= ' AND classname ' . $insql; 150 $params = array_merge($params, $inparams); 151 } 152 $DB->delete_records_select('task_scheduled', $sql, $params); 153 } 154 155 /** 156 * Checks if the task with the same classname, component and customdata is already scheduled 157 * 158 * @param adhoc_task $task 159 * @return bool 160 */ 161 protected static function task_is_scheduled($task) { 162 return false !== self::get_queued_adhoc_task_record($task); 163 } 164 165 /** 166 * Checks if the task with the same classname, component and customdata is already scheduled 167 * 168 * @param adhoc_task $task 169 * @return \stdClass|false 170 */ 171 protected static function get_queued_adhoc_task_record($task) { 172 global $DB; 173 174 $record = self::record_from_adhoc_task($task); 175 $params = [$record->classname, $record->component, $record->customdata]; 176 $sql = 'classname = ? AND component = ? AND ' . 177 $DB->sql_compare_text('customdata', \core_text::strlen($record->customdata) + 1) . ' = ?'; 178 179 if ($record->userid) { 180 $params[] = $record->userid; 181 $sql .= " AND userid = ? "; 182 } 183 return $DB->get_record_select('task_adhoc', $sql, $params); 184 } 185 186 /** 187 * Schedule a new task, or reschedule an existing adhoc task which has matching data. 188 * 189 * Only a task matching the same user, classname, component, and customdata will be rescheduled. 190 * If these values do not match exactly then a new task is scheduled. 191 * 192 * @param \core\task\adhoc_task $task - The new adhoc task information to store. 193 * @since Moodle 3.7 194 */ 195 public static function reschedule_or_queue_adhoc_task(adhoc_task $task) : void { 196 global $DB; 197 198 if ($existingrecord = self::get_queued_adhoc_task_record($task)) { 199 // Only update the next run time if it is explicitly set on the task. 200 $nextruntime = $task->get_next_run_time(); 201 if ($nextruntime && ($existingrecord->nextruntime != $nextruntime)) { 202 $DB->set_field('task_adhoc', 'nextruntime', $nextruntime, ['id' => $existingrecord->id]); 203 } 204 } else { 205 // There is nothing queued yet. Just queue as normal. 206 self::queue_adhoc_task($task); 207 } 208 } 209 210 /** 211 * Queue an adhoc task to run in the background. 212 * 213 * @param \core\task\adhoc_task $task - The new adhoc task information to store. 214 * @param bool $checkforexisting - If set to true and the task with the same user, classname, component and customdata 215 * is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks. 216 * @return boolean - True if the config was saved. 217 */ 218 public static function queue_adhoc_task(adhoc_task $task, $checkforexisting = false) { 219 global $DB; 220 221 if ($userid = $task->get_userid()) { 222 // User found. Check that they are suitable. 223 \core_user::require_active_user(\core_user::get_user($userid, '*', MUST_EXIST), true, true); 224 } 225 226 $record = self::record_from_adhoc_task($task); 227 // Schedule it immediately if nextruntime not explicitly set. 228 if (!$task->get_next_run_time()) { 229 $record->nextruntime = time() - 1; 230 } 231 232 // Check if the same task is already scheduled. 233 if ($checkforexisting && self::task_is_scheduled($task)) { 234 return false; 235 } 236 237 // Queue the task. 238 $result = $DB->insert_record('task_adhoc', $record); 239 240 return $result; 241 } 242 243 /** 244 * Change the default configuration for a scheduled task. 245 * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}. 246 * 247 * @param \core\task\scheduled_task $task - The new scheduled task information to store. 248 * @return boolean - True if the config was saved. 249 */ 250 public static function configure_scheduled_task(scheduled_task $task) { 251 global $DB; 252 253 $classname = self::get_canonical_class_name($task); 254 255 $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST); 256 257 $record = self::record_from_scheduled_task($task); 258 $record->id = $original->id; 259 $record->nextruntime = $task->get_next_scheduled_time(); 260 unset($record->lastruntime); 261 $result = $DB->update_record('task_scheduled', $record); 262 263 return $result; 264 } 265 266 /** 267 * Utility method to create a DB record from a scheduled task. 268 * 269 * @param \core\task\scheduled_task $task 270 * @return \stdClass 271 */ 272 public static function record_from_scheduled_task($task) { 273 $record = new \stdClass(); 274 $record->classname = self::get_canonical_class_name($task); 275 $record->component = $task->get_component(); 276 $record->blocking = $task->is_blocking(); 277 $record->customised = $task->is_customised(); 278 $record->lastruntime = $task->get_last_run_time(); 279 $record->nextruntime = $task->get_next_run_time(); 280 $record->faildelay = $task->get_fail_delay(); 281 $record->hour = $task->get_hour(); 282 $record->minute = $task->get_minute(); 283 $record->day = $task->get_day(); 284 $record->dayofweek = $task->get_day_of_week(); 285 $record->month = $task->get_month(); 286 $record->disabled = $task->get_disabled(); 287 $record->timestarted = $task->get_timestarted(); 288 $record->hostname = $task->get_hostname(); 289 $record->pid = $task->get_pid(); 290 291 return $record; 292 } 293 294 /** 295 * Utility method to create a DB record from an adhoc task. 296 * 297 * @param \core\task\adhoc_task $task 298 * @return \stdClass 299 */ 300 public static function record_from_adhoc_task($task) { 301 $record = new \stdClass(); 302 $record->classname = self::get_canonical_class_name($task); 303 $record->id = $task->get_id(); 304 $record->component = $task->get_component(); 305 $record->blocking = $task->is_blocking(); 306 $record->nextruntime = $task->get_next_run_time(); 307 $record->faildelay = $task->get_fail_delay(); 308 $record->customdata = $task->get_custom_data_as_string(); 309 $record->userid = $task->get_userid(); 310 $record->timecreated = time(); 311 $record->timestarted = $task->get_timestarted(); 312 $record->hostname = $task->get_hostname(); 313 $record->pid = $task->get_pid(); 314 315 return $record; 316 } 317 318 /** 319 * Utility method to create an adhoc task from a DB record. 320 * 321 * @param \stdClass $record 322 * @return \core\task\adhoc_task 323 * @throws \moodle_exception 324 */ 325 public static function adhoc_task_from_record($record) { 326 $classname = self::get_canonical_class_name($record->classname); 327 if (!class_exists($classname)) { 328 throw new \moodle_exception('invalidtaskclassname', '', '', $record->classname); 329 } 330 $task = new $classname; 331 if (isset($record->nextruntime)) { 332 $task->set_next_run_time($record->nextruntime); 333 } 334 if (isset($record->id)) { 335 $task->set_id($record->id); 336 } 337 if (isset($record->component)) { 338 $task->set_component($record->component); 339 } 340 $task->set_blocking(!empty($record->blocking)); 341 if (isset($record->faildelay)) { 342 $task->set_fail_delay($record->faildelay); 343 } 344 if (isset($record->customdata)) { 345 $task->set_custom_data_as_string($record->customdata); 346 } 347 348 if (isset($record->userid)) { 349 $task->set_userid($record->userid); 350 } 351 if (isset($record->timestarted)) { 352 $task->set_timestarted($record->timestarted); 353 } 354 if (isset($record->hostname)) { 355 $task->set_hostname($record->hostname); 356 } 357 if (isset($record->pid)) { 358 $task->set_pid($record->pid); 359 } 360 361 return $task; 362 } 363 364 /** 365 * Utility method to create a task from a DB record. 366 * 367 * @param \stdClass $record 368 * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int. 369 * If false, they are left as 'R' 370 * @param bool $override - if true loads overridden settings from config. 371 * @return \core\task\scheduled_task|false 372 */ 373 public static function scheduled_task_from_record($record, $expandr = true, $override = true) { 374 $classname = self::get_canonical_class_name($record->classname); 375 if (!class_exists($classname)) { 376 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER); 377 return false; 378 } 379 /** @var \core\task\scheduled_task $task */ 380 $task = new $classname; 381 382 if ($override) { 383 // Update values with those defined in the config, if any are set. 384 $record = self::get_record_with_config_overrides($record); 385 } 386 387 if (isset($record->lastruntime)) { 388 $task->set_last_run_time($record->lastruntime); 389 } 390 if (isset($record->nextruntime)) { 391 $task->set_next_run_time($record->nextruntime); 392 } 393 if (isset($record->customised)) { 394 $task->set_customised($record->customised); 395 } 396 if (isset($record->component)) { 397 $task->set_component($record->component); 398 } 399 $task->set_blocking(!empty($record->blocking)); 400 if (isset($record->minute)) { 401 $task->set_minute($record->minute, $expandr); 402 } 403 if (isset($record->hour)) { 404 $task->set_hour($record->hour, $expandr); 405 } 406 if (isset($record->day)) { 407 $task->set_day($record->day); 408 } 409 if (isset($record->month)) { 410 $task->set_month($record->month); 411 } 412 if (isset($record->dayofweek)) { 413 $task->set_day_of_week($record->dayofweek, $expandr); 414 } 415 if (isset($record->faildelay)) { 416 $task->set_fail_delay($record->faildelay); 417 } 418 if (isset($record->disabled)) { 419 $task->set_disabled($record->disabled); 420 } 421 if (isset($record->timestarted)) { 422 $task->set_timestarted($record->timestarted); 423 } 424 if (isset($record->hostname)) { 425 $task->set_hostname($record->hostname); 426 } 427 if (isset($record->pid)) { 428 $task->set_pid($record->pid); 429 } 430 $task->set_overridden(self::scheduled_task_has_override($classname)); 431 432 return $task; 433 } 434 435 /** 436 * Given a component name, will load the list of tasks from the scheduled_tasks table for that component. 437 * Do not execute tasks loaded from this function - they have not been locked. 438 * @param string $componentname - The name of the component to load the tasks for. 439 * @return \core\task\scheduled_task[] 440 */ 441 public static function load_scheduled_tasks_for_component($componentname) { 442 global $DB; 443 444 $tasks = array(); 445 // We are just reading - so no locks required. 446 $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING); 447 foreach ($records as $record) { 448 $task = self::scheduled_task_from_record($record); 449 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 450 if ($task) { 451 $tasks[] = $task; 452 } 453 } 454 455 return $tasks; 456 } 457 458 /** 459 * This function load the scheduled task details for a given classname. 460 * 461 * @param string $classname 462 * @return \core\task\scheduled_task or false 463 */ 464 public static function get_scheduled_task($classname) { 465 global $DB; 466 467 $classname = self::get_canonical_class_name($classname); 468 // We are just reading - so no locks required. 469 $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING); 470 if (!$record) { 471 return false; 472 } 473 return self::scheduled_task_from_record($record); 474 } 475 476 /** 477 * This function load the adhoc tasks for a given classname. 478 * 479 * @param string $classname 480 * @param bool $failedonly 481 * @param bool $skiprunning do not return tasks that are in the running state 482 * @return array 483 */ 484 public static function get_adhoc_tasks(string $classname, bool $failedonly = false, bool $skiprunning = false): array { 485 global $DB; 486 487 $conds[] = 'classname = ?'; 488 $params[] = self::get_canonical_class_name($classname); 489 490 if ($failedonly) { 491 $conds[] = 'faildelay > 0'; 492 } 493 if ($skiprunning) { 494 $conds[] = 'timestarted IS NULL'; 495 } 496 497 // We are just reading - so no locks required. 498 $sql = 'SELECT * FROM {task_adhoc}'; 499 if ($conds) { 500 $sql .= ' WHERE '.implode(' AND ', $conds); 501 } 502 $rs = $DB->get_records_sql($sql, $params); 503 return array_map(function($record) { 504 return self::adhoc_task_from_record($record); 505 }, $rs); 506 } 507 508 /** 509 * This function returns adhoc tasks summary per component classname 510 * 511 * @return array 512 */ 513 public static function get_adhoc_tasks_summary(): array { 514 global $DB; 515 516 $now = time(); 517 $records = $DB->get_records('task_adhoc'); 518 $summary = []; 519 foreach ($records as $r) { 520 if (!isset($summary[$r->component])) { 521 $summary[$r->component] = []; 522 } 523 524 if (isset($summary[$r->component][$r->classname])) { 525 $classsummary = $summary[$r->component][$r->classname]; 526 } else { 527 $classsummary = [ 528 'nextruntime' => null, 529 'count' => 0, 530 'failed' => 0, 531 'running' => 0, 532 'due' => 0, 533 ]; 534 } 535 536 $classsummary['count']++; 537 $nextruntime = (int)$r->nextruntime; 538 if (!$classsummary['nextruntime'] || $nextruntime < $classsummary['nextruntime']) { 539 $classsummary['nextruntime'] = $nextruntime; 540 } 541 542 if ((int)$r->timestarted > 0) { 543 $classsummary['running']++; 544 } else { 545 if ((int)$r->faildelay > 0) { 546 $classsummary['failed']++; 547 } 548 549 if ($nextruntime <= $now) { 550 $classsummary['due']++; 551 } 552 } 553 554 $summary[$r->component][$r->classname] = $classsummary; 555 } 556 return $summary; 557 } 558 559 /** 560 * This function load the default scheduled task details for a given classname. 561 * 562 * @param string $classname 563 * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int. 564 * If false, they are left as 'R' 565 * @return \core\task\scheduled_task|false 566 */ 567 public static function get_default_scheduled_task($classname, $expandr = true) { 568 $task = self::get_scheduled_task($classname); 569 $componenttasks = array(); 570 571 // Safety check in case no task was found for the given classname. 572 if ($task) { 573 $componenttasks = self::load_default_scheduled_tasks_for_component( 574 $task->get_component(), $expandr); 575 } 576 577 foreach ($componenttasks as $componenttask) { 578 if (get_class($componenttask) == get_class($task)) { 579 return $componenttask; 580 } 581 } 582 583 return false; 584 } 585 586 /** 587 * This function will return a list of all the scheduled tasks that exist in the database. 588 * 589 * @return \core\task\scheduled_task[] 590 */ 591 public static function get_all_scheduled_tasks() { 592 global $DB; 593 594 $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING); 595 $tasks = array(); 596 597 foreach ($records as $record) { 598 $task = self::scheduled_task_from_record($record); 599 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 600 if ($task) { 601 $tasks[] = $task; 602 } 603 } 604 605 return $tasks; 606 } 607 608 /** 609 * This function will return a list of all adhoc tasks that have a faildelay 610 * 611 * @param int $delay filter how long the task has been delayed 612 * @return \core\task\adhoc_task[] 613 */ 614 public static function get_failed_adhoc_tasks(int $delay = 0): array { 615 global $DB; 616 617 $tasks = []; 618 $records = $DB->get_records_sql('SELECT * from {task_adhoc} WHERE faildelay > ?', [$delay]); 619 620 foreach ($records as $record) { 621 try { 622 $tasks[] = self::adhoc_task_from_record($record); 623 } catch (\moodle_exception $e) { 624 debugging("Failed to load task: $record->classname", DEBUG_DEVELOPER, $e->getTrace()); 625 } 626 } 627 return $tasks; 628 } 629 630 /** 631 * Ensure quality of service for the ad hoc task queue. 632 * 633 * This reshuffles the adhoc tasks queue to balance by type to ensure a 634 * level of quality of service per type, while still maintaining the 635 * relative order of tasks queued by timestamp. 636 * 637 * @param array $records array of task records 638 * @param array $records array of same task records shuffled 639 * @deprecated since Moodle 4.1 MDL-67648 - please do not use this method anymore. 640 * @todo MDL-74843 This method will be deleted in Moodle 4.5 641 * @see \core\task\manager::get_next_adhoc_task 642 */ 643 public static function ensure_adhoc_task_qos(array $records): array { 644 debugging('The method \core\task\manager::ensure_adhoc_task_qos is deprecated. 645 Please use \core\task\manager::get_next_adhoc_task instead.', DEBUG_DEVELOPER); 646 647 $count = count($records); 648 if ($count == 0) { 649 return $records; 650 } 651 652 $queues = []; // This holds a queue for each type of adhoc task. 653 $limits = []; // The relative limits of each type of task. 654 $limittotal = 0; 655 656 // Split the single queue up into queues per type. 657 foreach ($records as $record) { 658 $type = $record->classname; 659 if (!array_key_exists($type, $queues)) { 660 $queues[$type] = []; 661 } 662 if (!array_key_exists($type, $limits)) { 663 $limits[$type] = 1; 664 $limittotal += 1; 665 } 666 $queues[$type][] = $record; 667 } 668 669 $qos = []; // Our new queue with ensured quality of service. 670 $seed = $count % $limittotal; // Which task queue to shuffle from first? 671 672 $move = 1; // How many tasks to shuffle at a time. 673 do { 674 $shuffled = 0; 675 676 // Now cycle through task type queues and interleaving the tasks 677 // back into a single queue. 678 foreach ($limits as $type => $limit) { 679 680 // Just interleaving the queue is not enough, because after 681 // any task is processed the whole queue is rebuilt again. So 682 // we need to deterministically start on different types of 683 // tasks so that *on average* we rotate through each type of task. 684 // 685 // We achieve this by using a $seed to start moving tasks off a 686 // different queue each time. The seed is based on the task count 687 // modulo the number of types of tasks on the queue. As we count 688 // down this naturally cycles through each type of record. 689 if ($seed < 1) { 690 $shuffled = 1; 691 $seed += 1; 692 continue; 693 } 694 $tasks = array_splice($queues[$type], 0, $move); 695 $qos = array_merge($qos, $tasks); 696 697 // Stop if we didn't move any tasks onto the main queue. 698 $shuffled += count($tasks); 699 } 700 // Generally the only tasks that matter are those that are near the start so 701 // after we have shuffled the first few 1 by 1, start shuffling larger groups. 702 if (count($qos) >= (4 * count($limits))) { 703 $move *= 2; 704 } 705 } while ($shuffled > 0); 706 707 return $qos; 708 } 709 710 /** 711 * This function will dispatch the next adhoc task in the queue. The task will be handed out 712 * with an open lock - possibly on the entire cron process. Make sure you call either 713 * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task. 714 * 715 * @param int $timestart 716 * @param bool $checklimits Should we check limits? 717 * @param string|null $classname Return only task of this class 718 * @return \core\task\adhoc_task|null 719 * @throws \moodle_exception 720 */ 721 public static function get_next_adhoc_task(int $timestart, ?bool $checklimits = true, ?string $classname = null): ?adhoc_task { 722 global $DB; 723 724 $concurrencylimit = get_config('core', 'task_adhoc_concurrency_limit'); 725 $cachedqueuesize = 1200; 726 727 $uniquetasksinqueue = array_map( 728 ['\core\task\manager', 'adhoc_task_from_record'], 729 $DB->get_records_sql( 730 'SELECT classname FROM {task_adhoc} WHERE nextruntime < :timestart GROUP BY classname', 731 ['timestart' => $timestart] 732 ) 733 ); 734 735 if (!isset(self::$numtasks) || self::$numtasks !== count($uniquetasksinqueue)) { 736 self::$numtasks = count($uniquetasksinqueue); 737 self::$miniqueue = []; 738 } 739 740 $concurrencylimits = []; 741 if ($checklimits) { 742 $concurrencylimits = array_map( 743 function ($task) { 744 return $task->get_concurrency_limit(); 745 }, 746 $uniquetasksinqueue 747 ); 748 } 749 750 /* 751 * The maximum number of cron runners that an individual task is allowed to use. 752 * For example if the concurrency limit is 20 and there are 5 unique types of tasks 753 * in the queue, each task should not be allowed to consume more than 3 (i.e., ⌊20/6⌋). 754 * The + 1 is needed to prevent the queue from becoming full of only one type of class. 755 * i.e., if it wasn't there and there were 20 tasks of the same type in the queue, every 756 * runner would become consumed with the same (potentially long-running task) and no more 757 * tasks can run. This way, some resources are always available if some new types 758 * of tasks enter the queue. 759 * 760 * We use the short-ternary to force the value to 1 in the case when the number of tasks 761 * exceeds the runners (e.g., there are 8 tasks and 4 runners, ⌊4/(8+1)⌋ = 0). 762 */ 763 $slots = floor($concurrencylimit / (count($uniquetasksinqueue) + 1)) ?: 1; 764 if (empty(self::$miniqueue)) { 765 self::$mode = self::ADHOC_TASK_QUEUE_MODE_DISTRIBUTING; 766 self::$miniqueue = self::get_candidate_adhoc_tasks( 767 $timestart, 768 $cachedqueuesize, 769 $slots, 770 $concurrencylimits 771 ); 772 } 773 774 // The query to cache tasks is expensive on big data sets, so we use this cheap 775 // query to get the ordering (which is the interesting part about the main query) 776 // We can use this information to filter the cache and also order it. 777 $runningtasks = $DB->get_records_sql( 778 'SELECT classname, COALESCE(COUNT(*), 0) running, MIN(timestarted) earliest 779 FROM {task_adhoc} 780 WHERE timestarted IS NOT NULL 781 AND nextruntime < :timestart 782 GROUP BY classname 783 ORDER BY running ASC, earliest DESC', 784 ['timestart' => $timestart] 785 ); 786 787 /* 788 * Each runner has a cache, so the same task can be in multiple runners' caches. 789 * We need to check that each task we have cached hasn't gone over its fair number 790 * of slots. This filtering is only applied during distributing mode as when we are 791 * filling capacity we intend for fast tasks to go over their slot limit. 792 */ 793 if (self::$mode === self::ADHOC_TASK_QUEUE_MODE_DISTRIBUTING) { 794 self::$miniqueue = array_filter( 795 self::$miniqueue, 796 function (\stdClass $task) use ($runningtasks, $slots) { 797 return !array_key_exists($task->classname, $runningtasks) || $runningtasks[$task->classname]->running < $slots; 798 } 799 ); 800 } 801 802 /* 803 * If this happens that means each task has consumed its fair share of capacity, but there's still 804 * runners left over (and we are one of them). Fetch tasks without checking slot limits. 805 */ 806 if (empty(self::$miniqueue) && array_sum(array_column($runningtasks, 'running')) < $concurrencylimit) { 807 self::$mode = self::ADHOC_TASK_QUEUE_MODE_FILLING; 808 self::$miniqueue = self::get_candidate_adhoc_tasks( 809 $timestart, 810 $cachedqueuesize, 811 false, 812 $concurrencylimits 813 ); 814 } 815 816 // Used below to order the cache. 817 $ordering = array_flip(array_keys($runningtasks)); 818 819 // Order the queue so it's consistent with the ordering from the DB. 820 usort( 821 self::$miniqueue, 822 function ($a, $b) use ($ordering) { 823 return ($ordering[$a->classname] ?? -1) - ($ordering[$b->classname] ?? -1); 824 } 825 ); 826 827 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); 828 829 $skipclasses = array(); 830 831 foreach (self::$miniqueue as $taskid => $record) { 832 833 if (!empty($classname) && $record->classname != self::get_canonical_class_name($classname)) { 834 // Skip the task if The class is specified, and doesn't match. 835 continue; 836 } 837 838 if (in_array($record->classname, $skipclasses)) { 839 // Skip the task if it can't be started due to per-task concurrency limit. 840 continue; 841 } 842 843 if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) { 844 845 // Safety check, see if the task has been already processed by another cron run. 846 $record = $DB->get_record('task_adhoc', array('id' => $record->id)); 847 if (!$record) { 848 $lock->release(); 849 unset(self::$miniqueue[$taskid]); 850 continue; 851 } 852 853 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 854 try { 855 $task = self::adhoc_task_from_record($record); 856 } catch (\moodle_exception $e) { 857 debugging("Failed to load task: $record->classname", DEBUG_DEVELOPER); 858 $lock->release(); 859 unset(self::$miniqueue[$taskid]); 860 continue; 861 } 862 863 $tasklimit = $task->get_concurrency_limit(); 864 if ($checklimits && $tasklimit > 0) { 865 if ($concurrencylock = self::get_concurrent_task_lock($task)) { 866 $task->set_concurrency_lock($concurrencylock); 867 } else { 868 // Unable to obtain a concurrency lock. 869 mtrace("Skipping $record->classname adhoc task class as the per-task limit of $tasklimit is reached."); 870 $skipclasses[] = $record->classname; 871 unset(self::$miniqueue[$taskid]); 872 $lock->release(); 873 continue; 874 } 875 } 876 877 self::set_locks($task, $lock, $cronlockfactory); 878 unset(self::$miniqueue[$taskid]); 879 880 return $task; 881 } else { 882 unset(self::$miniqueue[$taskid]); 883 } 884 } 885 886 return null; 887 } 888 889 /** 890 * Return a list of candidate adhoc tasks to run. 891 * 892 * @param int $timestart Only return tasks where nextruntime is less than this value 893 * @param int $limit Limit the list to this many results 894 * @param int|null $runmax Only return tasks that have less than this value currently running 895 * @param array $pertasklimits An array of classname => limit specifying how many instance of a task may be returned 896 * @return array Array of candidate tasks 897 */ 898 public static function get_candidate_adhoc_tasks( 899 int $timestart, 900 int $limit, 901 ?int $runmax, 902 array $pertasklimits = [] 903 ): array { 904 global $DB; 905 906 $pertaskclauses = array_map( 907 function (string $class, int $limit, int $index): array { 908 $limitcheck = $limit > 0 ? " AND COALESCE(run.running, 0) < :running_$index" : ""; 909 $limitparam = $limit > 0 ? ["running_$index" => $limit] : []; 910 911 return [ 912 "sql" => "(q.classname = :classname_$index" . $limitcheck . ")", 913 "params" => ["classname_$index" => $class] + $limitparam 914 ]; 915 }, 916 array_keys($pertasklimits), 917 $pertasklimits, 918 $pertasklimits ? range(1, count($pertasklimits)) : [] 919 ); 920 921 $pertasksql = implode(" OR ", array_column($pertaskclauses, 'sql')); 922 $pertaskparams = $pertaskclauses ? array_merge(...array_column($pertaskclauses, 'params')) : []; 923 924 $params = ['timestart' => $timestart] + 925 ($runmax ? ['runmax' => $runmax] : []) + 926 $pertaskparams; 927 928 return $DB->get_records_sql( 929 "SELECT q.id, q.classname, q.timestarted, COALESCE(run.running, 0) running, run.earliest 930 FROM {task_adhoc} q 931 LEFT JOIN ( 932 SELECT classname, COUNT(*) running, MIN(timestarted) earliest 933 FROM {task_adhoc} run 934 WHERE timestarted IS NOT NULL 935 GROUP BY classname 936 ) run ON run.classname = q.classname 937 WHERE nextruntime < :timestart 938 AND q.timestarted IS NULL " . 939 (!empty($pertasksql) ? "AND (" . $pertasksql . ") " : "") . 940 ($runmax ? "AND (COALESCE(run.running, 0)) < :runmax " : "") . 941 "ORDER BY COALESCE(run.running, 0) ASC, run.earliest DESC, q.nextruntime ASC, q.id ASC", 942 $params, 943 0, 944 $limit 945 ); 946 } 947 948 /** 949 * This function will get an adhoc task by id. The task will be handed out 950 * with an open lock - possibly on the entire cron process. Make sure you call either 951 * {@see ::adhoc_task_failed} or {@see ::adhoc_task_complete} to release the lock and reschedule the task. 952 * 953 * @param int $taskid 954 * @return \core\task\adhoc_task|null 955 * @throws \moodle_exception 956 */ 957 public static function get_adhoc_task(int $taskid): ?adhoc_task { 958 global $DB; 959 960 $record = $DB->get_record('task_adhoc', ['id' => $taskid]); 961 if (!$record) { 962 throw new \moodle_exception('invalidtaskid'); 963 } 964 965 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); 966 967 if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) { 968 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 969 try { 970 $task = self::adhoc_task_from_record($record); 971 } catch (\moodle_exception $e) { 972 $lock->release(); 973 throw $e; 974 } 975 976 self::set_locks($task, $lock, $cronlockfactory); 977 return $task; 978 } 979 980 return null; 981 } 982 983 /** 984 * This function will set locks on the task. 985 * 986 * @param adhoc_task $task 987 * @param lock $lock task lock 988 * @param lock_factory $cronlockfactory 989 * @throws \moodle_exception 990 */ 991 private static function set_locks(adhoc_task $task, lock $lock, lock_factory $cronlockfactory): void { 992 // The global cron lock is under the most contention so request it 993 // as late as possible and release it as soon as possible. 994 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) { 995 $lock->release(); 996 throw new \moodle_exception('locktimeout'); 997 } 998 999 $task->set_lock($lock); 1000 if (!$task->is_blocking()) { 1001 $cronlock->release(); 1002 } else { 1003 $task->set_cron_lock($cronlock); 1004 } 1005 } 1006 1007 /** 1008 * This function will dispatch the next scheduled task in the queue. The task will be handed out 1009 * with an open lock - possibly on the entire cron process. Make sure you call either 1010 * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task. 1011 * 1012 * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this. 1013 * @return \core\task\scheduled_task or null 1014 * @throws \moodle_exception 1015 */ 1016 public static function get_next_scheduled_task($timestart) { 1017 global $DB; 1018 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); 1019 1020 $where = "(lastruntime IS NULL OR lastruntime < :timestart1) 1021 AND (nextruntime IS NULL OR nextruntime < :timestart2) 1022 ORDER BY lastruntime, id ASC"; 1023 $params = array('timestart1' => $timestart, 'timestart2' => $timestart); 1024 $records = $DB->get_records_select('task_scheduled', $where, $params); 1025 1026 $pluginmanager = \core_plugin_manager::instance(); 1027 1028 foreach ($records as $record) { 1029 1030 $task = self::scheduled_task_from_record($record); 1031 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 1032 // Also check to see if task is disabled or enabled after applying overrides. 1033 if (!$task || $task->get_disabled()) { 1034 continue; 1035 } 1036 1037 if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) { 1038 $classname = '\\' . $record->classname; 1039 1040 $task->set_lock($lock); 1041 1042 // See if the component is disabled. 1043 $plugininfo = $pluginmanager->get_plugin_info($task->get_component()); 1044 1045 if ($plugininfo) { 1046 if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) { 1047 $lock->release(); 1048 continue; 1049 } 1050 } 1051 1052 if (!self::scheduled_task_has_override($record->classname)) { 1053 // Make sure the task data is unchanged unless an override is being used. 1054 if (!$DB->record_exists('task_scheduled', (array)$record)) { 1055 $lock->release(); 1056 continue; 1057 } 1058 } 1059 1060 // The global cron lock is under the most contention so request it 1061 // as late as possible and release it as soon as possible. 1062 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) { 1063 $lock->release(); 1064 throw new \moodle_exception('locktimeout'); 1065 } 1066 1067 if (!$task->is_blocking()) { 1068 $cronlock->release(); 1069 } else { 1070 $task->set_cron_lock($cronlock); 1071 } 1072 return $task; 1073 } 1074 } 1075 1076 return null; 1077 } 1078 1079 /** 1080 * This function indicates that an adhoc task was not completed successfully and should be retried. 1081 * 1082 * @param \core\task\adhoc_task $task 1083 */ 1084 public static function adhoc_task_failed(adhoc_task $task) { 1085 global $DB; 1086 // Finalise the log output. 1087 logmanager::finalise_log(true); 1088 1089 $delay = $task->get_fail_delay(); 1090 1091 // Reschedule task with exponential fall off for failing tasks. 1092 if (empty($delay)) { 1093 $delay = 60; 1094 } else { 1095 $delay *= 2; 1096 } 1097 1098 // Max of 24 hour delay. 1099 if ($delay > 86400) { 1100 $delay = 86400; 1101 } 1102 1103 // Reschedule and then release the locks. 1104 $task->set_timestarted(); 1105 $task->set_hostname(); 1106 $task->set_pid(); 1107 $task->set_next_run_time(time() + $delay); 1108 $task->set_fail_delay($delay); 1109 $record = self::record_from_adhoc_task($task); 1110 $DB->update_record('task_adhoc', $record); 1111 1112 $task->release_concurrency_lock(); 1113 if ($task->is_blocking()) { 1114 $task->get_cron_lock()->release(); 1115 } 1116 $task->get_lock()->release(); 1117 } 1118 1119 /** 1120 * Records that a adhoc task is starting to run. 1121 * 1122 * @param adhoc_task $task Task that is starting 1123 * @param int $time Start time (leave blank for now) 1124 * @throws \dml_exception 1125 * @throws \coding_exception 1126 */ 1127 public static function adhoc_task_starting(adhoc_task $task, int $time = 0) { 1128 global $DB; 1129 $pid = (int)getmypid(); 1130 $hostname = (string)gethostname(); 1131 1132 if (empty($time)) { 1133 $time = time(); 1134 } 1135 1136 $task->set_timestarted($time); 1137 $task->set_hostname($hostname); 1138 $task->set_pid($pid); 1139 1140 $record = self::record_from_adhoc_task($task); 1141 $DB->update_record('task_adhoc', $record); 1142 } 1143 1144 /** 1145 * This function indicates that an adhoc task was completed successfully. 1146 * 1147 * @param \core\task\adhoc_task $task 1148 */ 1149 public static function adhoc_task_complete(adhoc_task $task) { 1150 global $DB; 1151 1152 // Finalise the log output. 1153 logmanager::finalise_log(); 1154 $task->set_timestarted(); 1155 $task->set_hostname(); 1156 $task->set_pid(); 1157 1158 // Delete the adhoc task record - it is finished. 1159 $DB->delete_records('task_adhoc', array('id' => $task->get_id())); 1160 1161 // Release the locks. 1162 $task->release_concurrency_lock(); 1163 if ($task->is_blocking()) { 1164 $task->get_cron_lock()->release(); 1165 } 1166 $task->get_lock()->release(); 1167 } 1168 1169 /** 1170 * This function indicates that a scheduled task was not completed successfully and should be retried. 1171 * 1172 * @param \core\task\scheduled_task $task 1173 */ 1174 public static function scheduled_task_failed(scheduled_task $task) { 1175 global $DB; 1176 // Finalise the log output. 1177 logmanager::finalise_log(true); 1178 1179 $delay = $task->get_fail_delay(); 1180 1181 // Reschedule task with exponential fall off for failing tasks. 1182 if (empty($delay)) { 1183 $delay = 60; 1184 } else { 1185 $delay *= 2; 1186 } 1187 1188 // Max of 24 hour delay. 1189 if ($delay > 86400) { 1190 $delay = 86400; 1191 } 1192 1193 $task->set_timestarted(); 1194 $task->set_hostname(); 1195 $task->set_pid(); 1196 1197 $classname = self::get_canonical_class_name($task); 1198 1199 $record = $DB->get_record('task_scheduled', array('classname' => $classname)); 1200 $record->nextruntime = time() + $delay; 1201 $record->faildelay = $delay; 1202 $record->timestarted = null; 1203 $record->hostname = null; 1204 $record->pid = null; 1205 $DB->update_record('task_scheduled', $record); 1206 1207 if ($task->is_blocking()) { 1208 $task->get_cron_lock()->release(); 1209 } 1210 $task->get_lock()->release(); 1211 } 1212 1213 /** 1214 * Clears the fail delay for the given task and updates its next run time based on the schedule. 1215 * 1216 * @param scheduled_task $task Task to reset 1217 * @throws \dml_exception If there is a database error 1218 */ 1219 public static function clear_fail_delay(scheduled_task $task) { 1220 global $DB; 1221 1222 $record = new \stdClass(); 1223 $record->id = $DB->get_field('task_scheduled', 'id', 1224 ['classname' => self::get_canonical_class_name($task)]); 1225 $record->nextruntime = $task->get_next_scheduled_time(); 1226 $record->faildelay = 0; 1227 $DB->update_record('task_scheduled', $record); 1228 } 1229 1230 /** 1231 * Records that a scheduled task is starting to run. 1232 * 1233 * @param scheduled_task $task Task that is starting 1234 * @param int $time Start time (0 = current) 1235 * @throws \dml_exception If the task doesn't exist 1236 */ 1237 public static function scheduled_task_starting(scheduled_task $task, int $time = 0) { 1238 global $DB; 1239 $pid = (int)getmypid(); 1240 $hostname = (string)gethostname(); 1241 1242 if (!$time) { 1243 $time = time(); 1244 } 1245 1246 $task->set_timestarted($time); 1247 $task->set_hostname($hostname); 1248 $task->set_pid($pid); 1249 1250 $classname = self::get_canonical_class_name($task); 1251 $record = $DB->get_record('task_scheduled', ['classname' => $classname], '*', MUST_EXIST); 1252 $record->timestarted = $time; 1253 $record->hostname = $hostname; 1254 $record->pid = $pid; 1255 $DB->update_record('task_scheduled', $record); 1256 } 1257 1258 /** 1259 * This function indicates that a scheduled task was completed successfully and should be rescheduled. 1260 * 1261 * @param \core\task\scheduled_task $task 1262 */ 1263 public static function scheduled_task_complete(scheduled_task $task) { 1264 global $DB; 1265 1266 // Finalise the log output. 1267 logmanager::finalise_log(); 1268 $task->set_timestarted(); 1269 $task->set_hostname(); 1270 $task->set_pid(); 1271 1272 $classname = self::get_canonical_class_name($task); 1273 $record = $DB->get_record('task_scheduled', array('classname' => $classname)); 1274 if ($record) { 1275 $record->lastruntime = time(); 1276 $record->faildelay = 0; 1277 $record->nextruntime = $task->get_next_scheduled_time(); 1278 $record->timestarted = null; 1279 $record->hostname = null; 1280 $record->pid = null; 1281 1282 $DB->update_record('task_scheduled', $record); 1283 } 1284 1285 // Reschedule and then release the locks. 1286 if ($task->is_blocking()) { 1287 $task->get_cron_lock()->release(); 1288 } 1289 $task->get_lock()->release(); 1290 } 1291 1292 /** 1293 * Gets a list of currently-running tasks. 1294 * 1295 * @param string $sort Sorting method 1296 * @return array Array of scheduled and adhoc tasks 1297 * @throws \dml_exception 1298 */ 1299 public static function get_running_tasks($sort = ''): array { 1300 global $DB; 1301 if (empty($sort)) { 1302 $sort = 'timestarted ASC, classname ASC'; 1303 } 1304 $params = ['now1' => time(), 'now2' => time()]; 1305 1306 $sql = "SELECT subquery.* 1307 FROM (SELECT " . $DB->sql_concat("'s'", 'ts.id') . " as uniqueid, 1308 ts.id, 1309 'scheduled' as type, 1310 ts.classname, 1311 (:now1 - ts.timestarted) as time, 1312 ts.timestarted, 1313 ts.hostname, 1314 ts.pid 1315 FROM {task_scheduled} ts 1316 WHERE ts.timestarted IS NOT NULL 1317 UNION ALL 1318 SELECT " . $DB->sql_concat("'a'", 'ta.id') . " as uniqueid, 1319 ta.id, 1320 'adhoc' as type, 1321 ta.classname, 1322 (:now2 - ta.timestarted) as time, 1323 ta.timestarted, 1324 ta.hostname, 1325 ta.pid 1326 FROM {task_adhoc} ta 1327 WHERE ta.timestarted IS NOT NULL) subquery 1328 ORDER BY " . $sort; 1329 1330 return $DB->get_records_sql($sql, $params); 1331 } 1332 1333 /** 1334 * Cleanup stale task metadata. 1335 */ 1336 public static function cleanup_metadata() { 1337 global $DB; 1338 1339 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); 1340 $runningtasks = self::get_running_tasks(); 1341 1342 foreach ($runningtasks as $runningtask) { 1343 if ($runningtask->timestarted > time() - HOURSECS) { 1344 continue; 1345 } 1346 1347 if ($runningtask->type == 'adhoc') { 1348 $lock = $cronlockfactory->get_lock('adhoc_' . $runningtask->id, 0); 1349 } 1350 1351 if ($runningtask->type == 'scheduled') { 1352 $lock = $cronlockfactory->get_lock($runningtask->classname, 0); 1353 } 1354 1355 // If we got this lock it means one of three things: 1356 // 1357 // 1. The task was stopped abnormally and the metadata was not cleaned up 1358 // 2. This is the process running the cleanup task 1359 // 3. We took so long getting to it in this loop that it did finish, and we now have the lock 1360 // 1361 // In the case of 1. we need to make the task as failed, in the case of 2. and 3. we do nothing. 1362 if (!empty($lock)) { 1363 if ($runningtask->classname == "\\" . \core\task\task_lock_cleanup_task::class) { 1364 $lock->release(); 1365 continue; 1366 } 1367 1368 // We need to get the record again to verify whether or not we are dealing with case 3. 1369 $taskrecord = $DB->get_record('task_' . $runningtask->type, ['id' => $runningtask->id]); 1370 1371 if ($runningtask->type == 'scheduled') { 1372 // Empty timestarted indicates that this task finished (case 3) and was properly cleaned up. 1373 if (empty($taskrecord->timestarted)) { 1374 $lock->release(); 1375 continue; 1376 } 1377 1378 $task = self::scheduled_task_from_record($taskrecord); 1379 $task->set_lock($lock); 1380 self::scheduled_task_failed($task); 1381 } else if ($runningtask->type == 'adhoc') { 1382 // Ad hoc tasks are removed from the DB if they finish successfully. 1383 // If we can't re-get this task, that means it finished and was properly 1384 // cleaned up. 1385 if (!$taskrecord) { 1386 $lock->release(); 1387 continue; 1388 } 1389 1390 $task = self::adhoc_task_from_record($taskrecord); 1391 $task->set_lock($lock); 1392 self::adhoc_task_failed($task); 1393 } 1394 } 1395 } 1396 } 1397 1398 /** 1399 * This function is used to indicate that any long running cron processes should exit at the 1400 * next opportunity and restart. This is because something (e.g. DB changes) has changed and 1401 * the static caches may be stale. 1402 */ 1403 public static function clear_static_caches() { 1404 global $DB; 1405 // Do not use get/set config here because the caches cannot be relied on. 1406 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset')); 1407 if ($record) { 1408 $record->value = time(); 1409 $DB->update_record('config', $record); 1410 } else { 1411 $record = new \stdClass(); 1412 $record->name = 'scheduledtaskreset'; 1413 $record->value = time(); 1414 $DB->insert_record('config', $record); 1415 } 1416 } 1417 1418 /** 1419 * Return true if the static caches have been cleared since $starttime. 1420 * @param int $starttime The time this process started. 1421 * @return boolean True if static caches need resetting. 1422 */ 1423 public static function static_caches_cleared_since($starttime) { 1424 global $DB; 1425 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset')); 1426 return $record && (intval($record->value) > $starttime); 1427 } 1428 1429 /** 1430 * Gets class name for use in database table. Always begins with a \. 1431 * 1432 * @param string|task_base $taskorstring Task object or a string 1433 */ 1434 public static function get_canonical_class_name($taskorstring) { 1435 if (is_string($taskorstring)) { 1436 $classname = $taskorstring; 1437 } else { 1438 $classname = get_class($taskorstring); 1439 } 1440 if (strpos($classname, '\\') !== 0) { 1441 $classname = '\\' . $classname; 1442 } 1443 return $classname; 1444 } 1445 1446 /** 1447 * Gets the concurrent lock required to run an adhoc task. 1448 * 1449 * @param adhoc_task $task The task to obtain the lock for 1450 * @return \core\lock\lock The lock if one was obtained successfully 1451 * @throws \coding_exception 1452 */ 1453 protected static function get_concurrent_task_lock(adhoc_task $task): ?\core\lock\lock { 1454 $adhoclock = null; 1455 $cronlockfactory = \core\lock\lock_config::get_lock_factory(get_class($task)); 1456 1457 for ($run = 0; $run < $task->get_concurrency_limit(); $run++) { 1458 if ($adhoclock = $cronlockfactory->get_lock("concurrent_run_{$run}", 0)) { 1459 return $adhoclock; 1460 } 1461 } 1462 1463 return null; 1464 } 1465 1466 /** 1467 * Find the path of PHP CLI binary. 1468 * 1469 * @return string|false The PHP CLI executable PATH 1470 */ 1471 protected static function find_php_cli_path() { 1472 global $CFG; 1473 1474 if (!empty($CFG->pathtophp) && is_executable(trim($CFG->pathtophp))) { 1475 return $CFG->pathtophp; 1476 } 1477 1478 return false; 1479 } 1480 1481 /** 1482 * Returns if Moodle have access to PHP CLI binary or not. 1483 * 1484 * @return bool 1485 */ 1486 public static function is_runnable():bool { 1487 return self::find_php_cli_path() !== false; 1488 } 1489 1490 /** 1491 * Executes a cron from web invocation using PHP CLI. 1492 * 1493 * @param scheduled_task $task Task that be executed via CLI. 1494 * @return bool 1495 * @throws \moodle_exception 1496 */ 1497 public static function run_from_cli(scheduled_task $task): bool { 1498 global $CFG; 1499 1500 if (!self::is_runnable()) { 1501 $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']); 1502 throw new \moodle_exception('cannotfindthepathtothecli', 'tool_task', $redirecturl->out()); 1503 } else { 1504 // Shell-escaped path to the PHP binary. 1505 $phpbinary = escapeshellarg(self::find_php_cli_path()); 1506 1507 // Shell-escaped path CLI script. 1508 $pathcomponents = [$CFG->dirroot, $CFG->admin, 'cli', 'scheduled_task.php']; 1509 $scriptpath = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents)); 1510 1511 // Shell-escaped task name. 1512 $classname = get_class($task); 1513 $taskarg = escapeshellarg("--execute={$classname}") . " " . escapeshellarg("--force"); 1514 1515 // Build the CLI command. 1516 $command = "{$phpbinary} {$scriptpath} {$taskarg}"; 1517 1518 // Execute it. 1519 self::passthru_via_mtrace($command); 1520 } 1521 1522 return true; 1523 } 1524 1525 /** 1526 * This behaves similar to passthru but filters every line via 1527 * the mtrace function so it can be post processed. 1528 * 1529 * @param string $command to run 1530 * @return void 1531 */ 1532 public static function passthru_via_mtrace(string $command) { 1533 $descriptorspec = [ 1534 0 => ['pipe', 'r'], // STDIN. 1535 1 => ['pipe', 'w'], // STDOUT. 1536 2 => ['pipe', 'w'], // STDERR. 1537 ]; 1538 flush(); 1539 $process = proc_open($command, $descriptorspec, $pipes, realpath('./'), []); 1540 if (is_resource($process)) { 1541 while ($s = fgets($pipes[1])) { 1542 mtrace($s, ''); 1543 flush(); 1544 } 1545 } 1546 1547 fclose($pipes[0]); 1548 fclose($pipes[1]); 1549 fclose($pipes[2]); 1550 proc_close($process); 1551 } 1552 1553 /** 1554 * Executes an ad hoc task from web invocation using PHP CLI. 1555 * 1556 * @param int $taskid Task to execute via CLI. 1557 * @throws \moodle_exception 1558 */ 1559 public static function run_adhoc_from_cli(int $taskid) { 1560 // Shell-escaped task name. 1561 $taskarg = escapeshellarg("--id={$taskid}"); 1562 1563 self::run_adhoc_from_cli_base($taskarg); 1564 } 1565 1566 /** 1567 * Executes ad hoc tasks from web invocation using PHP CLI. 1568 * 1569 * @param bool|null $failedonly 1570 * @param string|null $classname Task class to execute via CLI. 1571 * @throws \moodle_exception 1572 */ 1573 public static function run_all_adhoc_from_cli(?bool $failedonly = false, ?string $classname = null) { 1574 $taskargs = []; 1575 if ($failedonly) { 1576 $taskargs[] = '--failed'; 1577 } 1578 if ($classname) { 1579 // Shell-escaped task select. 1580 $taskargs[] = escapeshellarg("--classname={$classname}"); 1581 } 1582 1583 self::run_adhoc_from_cli_base($taskargs ? implode(' ', $taskargs) : '--execute'); 1584 } 1585 1586 /** 1587 * Executes an ad hoc task from web invocation using PHP CLI. 1588 * 1589 * @param string $taskarg Task to execute via CLI. 1590 * @throws \moodle_exception 1591 */ 1592 private static function run_adhoc_from_cli_base(string $taskarg): void { 1593 global $CFG; 1594 1595 if (!self::is_runnable()) { 1596 $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']); 1597 throw new \moodle_exception('cannotfindthepathtothecli', 'tool_task', $redirecturl->out()); 1598 } 1599 1600 // Shell-escaped path to the PHP binary. 1601 $phpbinary = escapeshellarg(self::find_php_cli_path()); 1602 1603 // Shell-escaped path CLI script. 1604 $pathcomponents = [$CFG->dirroot, $CFG->admin, 'cli', 'adhoc_task.php']; 1605 $scriptpath = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents)); 1606 1607 // Build the CLI command. 1608 $command = "{$phpbinary} {$scriptpath} {$taskarg} --force"; 1609 1610 // We cannot run it in phpunit. 1611 if (PHPUNIT_TEST) { 1612 echo $command; 1613 return; 1614 } 1615 1616 // Execute it. 1617 self::passthru_via_mtrace($command); 1618 } 1619 1620 /** 1621 * For a given scheduled task record, this method will check to see if any overrides have 1622 * been applied in config and return a copy of the record with any overridden values. 1623 * 1624 * The format of the config value is: 1625 * $CFG->scheduled_tasks = array( 1626 * '$classname' => array( 1627 * 'schedule' => '* * * * *', 1628 * 'disabled' => 1, 1629 * ), 1630 * ); 1631 * 1632 * Where $classname is the value of the task's classname, i.e. '\core\task\grade_cron_task'. 1633 * 1634 * @param \stdClass $record scheduled task record 1635 * @return \stdClass scheduled task with any configured overrides 1636 */ 1637 protected static function get_record_with_config_overrides(\stdClass $record): \stdClass { 1638 global $CFG; 1639 1640 $scheduledtaskkey = self::scheduled_task_get_override_key($record->classname); 1641 $overriddenrecord = $record; 1642 1643 if ($scheduledtaskkey) { 1644 $overriddenrecord->customised = true; 1645 $taskconfig = $CFG->scheduled_tasks[$scheduledtaskkey]; 1646 1647 if (isset($taskconfig['disabled'])) { 1648 $overriddenrecord->disabled = $taskconfig['disabled']; 1649 } 1650 if (isset($taskconfig['schedule'])) { 1651 list ( 1652 $overriddenrecord->minute, 1653 $overriddenrecord->hour, 1654 $overriddenrecord->day, 1655 $overriddenrecord->month, 1656 $overriddenrecord->dayofweek 1657 ) = explode(' ', $taskconfig['schedule']); 1658 } 1659 } 1660 1661 return $overriddenrecord; 1662 } 1663 1664 /** 1665 * This checks whether or not there is a value set in config 1666 * for a scheduled task. 1667 * 1668 * @param string $classname Scheduled task's classname 1669 * @return bool true if there is an entry in config 1670 */ 1671 public static function scheduled_task_has_override(string $classname): bool { 1672 return self::scheduled_task_get_override_key($classname) !== null; 1673 } 1674 1675 /** 1676 * Get the key within the scheduled tasks config object that 1677 * for a classname. 1678 * 1679 * @param string $classname the scheduled task classname to find 1680 * @return string the key if found, otherwise null 1681 */ 1682 public static function scheduled_task_get_override_key(string $classname): ?string { 1683 global $CFG; 1684 1685 if (isset($CFG->scheduled_tasks)) { 1686 // Firstly, attempt to get a match against the full classname. 1687 if (isset($CFG->scheduled_tasks[$classname])) { 1688 return $classname; 1689 } 1690 1691 // Check to see if there is a wildcard matching the classname. 1692 foreach (array_keys($CFG->scheduled_tasks) as $key) { 1693 if (strpos($key, '*') === false) { 1694 continue; 1695 } 1696 1697 $pattern = '/' . str_replace('\\', '\\\\', str_replace('*', '.*', $key)) . '/'; 1698 1699 if (preg_match($pattern, $classname)) { 1700 return $key; 1701 } 1702 } 1703 } 1704 1705 return null; 1706 } 1707 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body