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