Differences Between: [Versions 310 and 311] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Scheduled and adhoc task management. 19 * 20 * @package core 21 * @category task 22 * @copyright 2013 Damyon Wiese 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 namespace core\task; 26 27 define('CORE_TASK_TASKS_FILENAME', 'db/tasks.php'); 28 /** 29 * Collection of task related methods. 30 * 31 * Some locking rules for this class: 32 * All changes to scheduled tasks must be protected with both - the global cron lock and the lock 33 * for the specific scheduled task (in that order). Locks must be released in the reverse order. 34 * @copyright 2013 Damyon Wiese 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class manager { 38 39 /** 40 * Given a component name, will load the list of tasks in the db/tasks.php file for that component. 41 * 42 * @param string $componentname - The name of the component to fetch the tasks for. 43 * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int. 44 * If false, they are left as 'R' 45 * @return \core\task\scheduled_task[] - List of scheduled tasks for this component. 46 */ 47 public static function load_default_scheduled_tasks_for_component($componentname, $expandr = true) { 48 $dir = \core_component::get_component_directory($componentname); 49 50 if (!$dir) { 51 return array(); 52 } 53 54 $file = $dir . '/' . CORE_TASK_TASKS_FILENAME; 55 if (!file_exists($file)) { 56 return array(); 57 } 58 59 $tasks = null; 60 include($file); 61 62 if (!isset($tasks)) { 63 return array(); 64 } 65 66 $scheduledtasks = array(); 67 68 foreach ($tasks as $task) { 69 $record = (object) $task; 70 $scheduledtask = self::scheduled_task_from_record($record, $expandr, false); 71 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 72 if ($scheduledtask) { 73 $scheduledtask->set_component($componentname); 74 $scheduledtasks[] = $scheduledtask; 75 } 76 } 77 78 return $scheduledtasks; 79 } 80 81 /** 82 * Update the database to contain a list of scheduled task for a component. 83 * The list of scheduled tasks is taken from @load_scheduled_tasks_for_component. 84 * Will throw exceptions for any errors. 85 * 86 * @param string $componentname - The frankenstyle component name. 87 */ 88 public static function reset_scheduled_tasks_for_component($componentname) { 89 global $DB; 90 $tasks = self::load_default_scheduled_tasks_for_component($componentname); 91 $validtasks = array(); 92 93 foreach ($tasks as $taskid => $task) { 94 $classname = self::get_canonical_class_name($task); 95 96 $validtasks[] = $classname; 97 98 if ($currenttask = self::get_scheduled_task($classname)) { 99 if ($currenttask->is_customised()) { 100 // If there is an existing task with a custom schedule, do not override it. 101 continue; 102 } 103 104 // Update the record from the default task data. 105 self::configure_scheduled_task($task); 106 } else { 107 // Ensure that the first run follows the schedule. 108 $task->set_next_run_time($task->get_next_scheduled_time()); 109 110 // Insert the new task in the database. 111 $record = self::record_from_scheduled_task($task); 112 $DB->insert_record('task_scheduled', $record); 113 } 114 } 115 116 // Delete any task that is not defined in the component any more. 117 $sql = "component = :component"; 118 $params = array('component' => $componentname); 119 if (!empty($validtasks)) { 120 list($insql, $inparams) = $DB->get_in_or_equal($validtasks, SQL_PARAMS_NAMED, 'param', false); 121 $sql .= ' AND classname ' . $insql; 122 $params = array_merge($params, $inparams); 123 } 124 $DB->delete_records_select('task_scheduled', $sql, $params); 125 } 126 127 /** 128 * Checks if the task with the same classname, component and customdata is already scheduled 129 * 130 * @param adhoc_task $task 131 * @return bool 132 */ 133 protected static function task_is_scheduled($task) { 134 return false !== self::get_queued_adhoc_task_record($task); 135 } 136 137 /** 138 * Checks if the task with the same classname, component and customdata is already scheduled 139 * 140 * @param adhoc_task $task 141 * @return bool 142 */ 143 protected static function get_queued_adhoc_task_record($task) { 144 global $DB; 145 146 $record = self::record_from_adhoc_task($task); 147 $params = [$record->classname, $record->component, $record->customdata]; 148 $sql = 'classname = ? AND component = ? AND ' . 149 $DB->sql_compare_text('customdata', \core_text::strlen($record->customdata) + 1) . ' = ?'; 150 151 if ($record->userid) { 152 $params[] = $record->userid; 153 $sql .= " AND userid = ? "; 154 } 155 return $DB->get_record_select('task_adhoc', $sql, $params); 156 } 157 158 /** 159 * Schedule a new task, or reschedule an existing adhoc task which has matching data. 160 * 161 * Only a task matching the same user, classname, component, and customdata will be rescheduled. 162 * If these values do not match exactly then a new task is scheduled. 163 * 164 * @param \core\task\adhoc_task $task - The new adhoc task information to store. 165 * @since Moodle 3.7 166 */ 167 public static function reschedule_or_queue_adhoc_task(adhoc_task $task) : void { 168 global $DB; 169 170 if ($existingrecord = self::get_queued_adhoc_task_record($task)) { 171 // Only update the next run time if it is explicitly set on the task. 172 $nextruntime = $task->get_next_run_time(); 173 if ($nextruntime && ($existingrecord->nextruntime != $nextruntime)) { 174 $DB->set_field('task_adhoc', 'nextruntime', $nextruntime, ['id' => $existingrecord->id]); 175 } 176 } else { 177 // There is nothing queued yet. Just queue as normal. 178 self::queue_adhoc_task($task); 179 } 180 } 181 182 /** 183 * Queue an adhoc task to run in the background. 184 * 185 * @param \core\task\adhoc_task $task - The new adhoc task information to store. 186 * @param bool $checkforexisting - If set to true and the task with the same user, classname, component and customdata 187 * is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks. 188 * @return boolean - True if the config was saved. 189 */ 190 public static function queue_adhoc_task(adhoc_task $task, $checkforexisting = false) { 191 global $DB; 192 193 if ($userid = $task->get_userid()) { 194 // User found. Check that they are suitable. 195 \core_user::require_active_user(\core_user::get_user($userid, '*', MUST_EXIST), true, true); 196 } 197 198 $record = self::record_from_adhoc_task($task); 199 // Schedule it immediately if nextruntime not explicitly set. 200 if (!$task->get_next_run_time()) { 201 $record->nextruntime = time() - 1; 202 } 203 204 // Check if the same task is already scheduled. 205 if ($checkforexisting && self::task_is_scheduled($task)) { 206 return false; 207 } 208 209 // Queue the task. 210 $result = $DB->insert_record('task_adhoc', $record); 211 212 return $result; 213 } 214 215 /** 216 * Change the default configuration for a scheduled task. 217 * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}. 218 * 219 * @param \core\task\scheduled_task $task - The new scheduled task information to store. 220 * @return boolean - True if the config was saved. 221 */ 222 public static function configure_scheduled_task(scheduled_task $task) { 223 global $DB; 224 225 $classname = self::get_canonical_class_name($task); 226 227 $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST); 228 229 $record = self::record_from_scheduled_task($task); 230 $record->id = $original->id; 231 $record->nextruntime = $task->get_next_scheduled_time(); 232 unset($record->lastruntime); 233 $result = $DB->update_record('task_scheduled', $record); 234 235 return $result; 236 } 237 238 /** 239 * Utility method to create a DB record from a scheduled task. 240 * 241 * @param \core\task\scheduled_task $task 242 * @return \stdClass 243 */ 244 public static function record_from_scheduled_task($task) { 245 $record = new \stdClass(); 246 $record->classname = self::get_canonical_class_name($task); 247 $record->component = $task->get_component(); 248 $record->blocking = $task->is_blocking(); 249 $record->customised = $task->is_customised(); 250 $record->lastruntime = $task->get_last_run_time(); 251 $record->nextruntime = $task->get_next_run_time(); 252 $record->faildelay = $task->get_fail_delay(); 253 $record->hour = $task->get_hour(); 254 $record->minute = $task->get_minute(); 255 $record->day = $task->get_day(); 256 $record->dayofweek = $task->get_day_of_week(); 257 $record->month = $task->get_month(); 258 $record->disabled = $task->get_disabled(); 259 $record->timestarted = $task->get_timestarted(); 260 $record->hostname = $task->get_hostname(); 261 $record->pid = $task->get_pid(); 262 263 return $record; 264 } 265 266 /** 267 * Utility method to create a DB record from an adhoc task. 268 * 269 * @param \core\task\adhoc_task $task 270 * @return \stdClass 271 */ 272 public static function record_from_adhoc_task($task) { 273 $record = new \stdClass(); 274 $record->classname = self::get_canonical_class_name($task); 275 $record->id = $task->get_id(); 276 $record->component = $task->get_component(); 277 $record->blocking = $task->is_blocking(); 278 $record->nextruntime = $task->get_next_run_time(); 279 $record->faildelay = $task->get_fail_delay(); 280 $record->customdata = $task->get_custom_data_as_string(); 281 $record->userid = $task->get_userid(); 282 $record->timecreated = time(); 283 $record->timestarted = $task->get_timestarted(); 284 $record->hostname = $task->get_hostname(); 285 $record->pid = $task->get_pid(); 286 287 return $record; 288 } 289 290 /** 291 * Utility method to create an adhoc task from a DB record. 292 * 293 * @param \stdClass $record 294 * @return \core\task\adhoc_task 295 */ 296 public static function adhoc_task_from_record($record) { 297 $classname = self::get_canonical_class_name($record->classname); 298 if (!class_exists($classname)) { 299 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER); 300 return false; 301 } 302 $task = new $classname; 303 if (isset($record->nextruntime)) { 304 $task->set_next_run_time($record->nextruntime); 305 } 306 if (isset($record->id)) { 307 $task->set_id($record->id); 308 } 309 if (isset($record->component)) { 310 $task->set_component($record->component); 311 } 312 $task->set_blocking(!empty($record->blocking)); 313 if (isset($record->faildelay)) { 314 $task->set_fail_delay($record->faildelay); 315 } 316 if (isset($record->customdata)) { 317 $task->set_custom_data_as_string($record->customdata); 318 } 319 320 if (isset($record->userid)) { 321 $task->set_userid($record->userid); 322 } 323 if (isset($record->timestarted)) { 324 $task->set_timestarted($record->timestarted); 325 } 326 if (isset($record->hostname)) { 327 $task->set_hostname($record->hostname); 328 } 329 if (isset($record->pid)) { 330 $task->set_pid($record->pid); 331 } 332 333 return $task; 334 } 335 336 /** 337 * Utility method to create a task from a DB record. 338 * 339 * @param \stdClass $record 340 * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int. 341 * If false, they are left as 'R' 342 * @param bool $override - if true loads overridden settings from config. 343 * @return \core\task\scheduled_task|false 344 */ 345 public static function scheduled_task_from_record($record, $expandr = true, $override = true) { 346 $classname = self::get_canonical_class_name($record->classname); 347 if (!class_exists($classname)) { 348 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER); 349 return false; 350 } 351 /** @var \core\task\scheduled_task $task */ 352 $task = new $classname; 353 354 if ($override) { 355 // Update values with those defined in the config, if any are set. 356 $record = self::get_record_with_config_overrides($record); 357 } 358 359 if (isset($record->lastruntime)) { 360 $task->set_last_run_time($record->lastruntime); 361 } 362 if (isset($record->nextruntime)) { 363 $task->set_next_run_time($record->nextruntime); 364 } 365 if (isset($record->customised)) { 366 $task->set_customised($record->customised); 367 } 368 if (isset($record->component)) { 369 $task->set_component($record->component); 370 } 371 $task->set_blocking(!empty($record->blocking)); 372 if (isset($record->minute)) { 373 $task->set_minute($record->minute, $expandr); 374 } 375 if (isset($record->hour)) { 376 $task->set_hour($record->hour, $expandr); 377 } 378 if (isset($record->day)) { 379 $task->set_day($record->day); 380 } 381 if (isset($record->month)) { 382 $task->set_month($record->month); 383 } 384 if (isset($record->dayofweek)) { 385 $task->set_day_of_week($record->dayofweek, $expandr); 386 } 387 if (isset($record->faildelay)) { 388 $task->set_fail_delay($record->faildelay); 389 } 390 if (isset($record->disabled)) { 391 $task->set_disabled($record->disabled); 392 } 393 if (isset($record->timestarted)) { 394 $task->set_timestarted($record->timestarted); 395 } 396 if (isset($record->hostname)) { 397 $task->set_hostname($record->hostname); 398 } 399 if (isset($record->pid)) { 400 $task->set_pid($record->pid); 401 } 402 $task->set_overridden(self::scheduled_task_has_override($classname)); 403 404 return $task; 405 } 406 407 /** 408 * Given a component name, will load the list of tasks from the scheduled_tasks table for that component. 409 * Do not execute tasks loaded from this function - they have not been locked. 410 * @param string $componentname - The name of the component to load the tasks for. 411 * @return \core\task\scheduled_task[] 412 */ 413 public static function load_scheduled_tasks_for_component($componentname) { 414 global $DB; 415 416 $tasks = array(); 417 // We are just reading - so no locks required. 418 $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING); 419 foreach ($records as $record) { 420 $task = self::scheduled_task_from_record($record); 421 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 422 if ($task) { 423 $tasks[] = $task; 424 } 425 } 426 427 return $tasks; 428 } 429 430 /** 431 * This function load the scheduled task details for a given classname. 432 * 433 * @param string $classname 434 * @return \core\task\scheduled_task or false 435 */ 436 public static function get_scheduled_task($classname) { 437 global $DB; 438 439 $classname = self::get_canonical_class_name($classname); 440 // We are just reading - so no locks required. 441 $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING); 442 if (!$record) { 443 return false; 444 } 445 return self::scheduled_task_from_record($record); 446 } 447 448 /** 449 * This function load the adhoc tasks for a given classname. 450 * 451 * @param string $classname 452 * @return \core\task\adhoc_task[] 453 */ 454 public static function get_adhoc_tasks($classname) { 455 global $DB; 456 457 $classname = self::get_canonical_class_name($classname); 458 // We are just reading - so no locks required. 459 $records = $DB->get_records('task_adhoc', array('classname' => $classname)); 460 461 return array_map(function($record) { 462 return self::adhoc_task_from_record($record); 463 }, $records); 464 } 465 466 /** 467 * This function load the default scheduled task details for a given classname. 468 * 469 * @param string $classname 470 * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int. 471 * If false, they are left as 'R' 472 * @return \core\task\scheduled_task|false 473 */ 474 public static function get_default_scheduled_task($classname, $expandr = true) { 475 $task = self::get_scheduled_task($classname); 476 $componenttasks = array(); 477 478 // Safety check in case no task was found for the given classname. 479 if ($task) { 480 $componenttasks = self::load_default_scheduled_tasks_for_component( 481 $task->get_component(), $expandr); 482 } 483 484 foreach ($componenttasks as $componenttask) { 485 if (get_class($componenttask) == get_class($task)) { 486 return $componenttask; 487 } 488 } 489 490 return false; 491 } 492 493 /** 494 * This function will return a list of all the scheduled tasks that exist in the database. 495 * 496 * @return \core\task\scheduled_task[] 497 */ 498 public static function get_all_scheduled_tasks() { 499 global $DB; 500 501 $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING); 502 $tasks = array(); 503 504 foreach ($records as $record) { 505 $task = self::scheduled_task_from_record($record); 506 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 507 if ($task) { 508 $tasks[] = $task; 509 } 510 } 511 512 return $tasks; 513 } 514 515 /** 516 * This function will return a list of all adhoc tasks that have a faildelay 517 * 518 * @param int $delay filter how long the task has been delayed 519 * @return \core\task\adhoc_task[] 520 */ 521 public static function get_failed_adhoc_tasks(int $delay = 0): array { 522 global $DB; 523 524 $tasks = []; 525 $records = $DB->get_records_sql('SELECT * from {task_adhoc} WHERE faildelay > ?', [$delay]); 526 527 foreach ($records as $record) { 528 $task = self::adhoc_task_from_record($record); 529 if ($task) { 530 $tasks[] = $task; 531 } 532 } 533 return $tasks; 534 } 535 536 /** 537 * Ensure quality of service for the ad hoc task queue. 538 * 539 * This reshuffles the adhoc tasks queue to balance by type to ensure a 540 * level of quality of service per type, while still maintaining the 541 * relative order of tasks queued by timestamp. 542 * 543 * @param array $records array of task records 544 * @param array $records array of same task records shuffled 545 */ 546 public static function ensure_adhoc_task_qos(array $records): array { 547 548 $count = count($records); 549 if ($count == 0) { 550 return $records; 551 } 552 553 $queues = []; // This holds a queue for each type of adhoc task. 554 $limits = []; // The relative limits of each type of task. 555 $limittotal = 0; 556 557 // Split the single queue up into queues per type. 558 foreach ($records as $record) { 559 $type = $record->classname; 560 if (!array_key_exists($type, $queues)) { 561 $queues[$type] = []; 562 } 563 if (!array_key_exists($type, $limits)) { 564 $limits[$type] = 1; 565 $limittotal += 1; 566 } 567 $queues[$type][] = $record; 568 } 569 570 $qos = []; // Our new queue with ensured quality of service. 571 $seed = $count % $limittotal; // Which task queue to shuffle from first? 572 573 $move = 1; // How many tasks to shuffle at a time. 574 do { 575 $shuffled = 0; 576 577 // Now cycle through task type queues and interleaving the tasks 578 // back into a single queue. 579 foreach ($limits as $type => $limit) { 580 581 // Just interleaving the queue is not enough, because after 582 // any task is processed the whole queue is rebuilt again. So 583 // we need to deterministically start on different types of 584 // tasks so that *on average* we rotate through each type of task. 585 // 586 // We achieve this by using a $seed to start moving tasks off a 587 // different queue each time. The seed is based on the task count 588 // modulo the number of types of tasks on the queue. As we count 589 // down this naturally cycles through each type of record. 590 if ($seed < 1) { 591 $shuffled = 1; 592 $seed += 1; 593 continue; 594 } 595 $tasks = array_splice($queues[$type], 0, $move); 596 $qos = array_merge($qos, $tasks); 597 598 // Stop if we didn't move any tasks onto the main queue. 599 $shuffled += count($tasks); 600 } 601 // Generally the only tasks that matter are those that are near the start so 602 // after we have shuffled the first few 1 by 1, start shuffling larger groups. 603 if (count($qos) >= (4 * count($limits))) { 604 $move *= 2; 605 } 606 } while ($shuffled > 0); 607 608 return $qos; 609 } 610 611 /** 612 * This function will dispatch the next adhoc task in the queue. The task will be handed out 613 * with an open lock - possibly on the entire cron process. Make sure you call either 614 * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task. 615 * 616 * @param int $timestart 617 * @param bool $checklimits Should we check limits? 618 * @return \core\task\adhoc_task or null if not found 619 * @throws \moodle_exception 620 */ 621 public static function get_next_adhoc_task($timestart, $checklimits = true) { 622 global $DB; 623 624 $where = '(nextruntime IS NULL OR nextruntime < :timestart1)'; 625 $params = array('timestart1' => $timestart); 626 $records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC', '*', 0, 2000); 627 $records = self::ensure_adhoc_task_qos($records); 628 629 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); 630 631 $skipclasses = array(); 632 633 foreach ($records as $record) { 634 635 if (in_array($record->classname, $skipclasses)) { 636 // Skip the task if it can't be started due to per-task concurrency limit. 637 continue; 638 } 639 640 if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) { 641 642 // Safety check, see if the task has been already processed by another cron run. 643 $record = $DB->get_record('task_adhoc', array('id' => $record->id)); 644 if (!$record) { 645 $lock->release(); 646 continue; 647 } 648 649 $task = self::adhoc_task_from_record($record); 650 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 651 if (!$task) { 652 $lock->release(); 653 continue; 654 } 655 656 $tasklimit = $task->get_concurrency_limit(); 657 if ($checklimits && $tasklimit > 0) { 658 if ($concurrencylock = self::get_concurrent_task_lock($task)) { 659 $task->set_concurrency_lock($concurrencylock); 660 } else { 661 // Unable to obtain a concurrency lock. 662 mtrace("Skipping $record->classname adhoc task class as the per-task limit of $tasklimit is reached."); 663 $skipclasses[] = $record->classname; 664 $lock->release(); 665 continue; 666 } 667 } 668 669 // The global cron lock is under the most contention so request it 670 // as late as possible and release it as soon as possible. 671 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) { 672 $lock->release(); 673 throw new \moodle_exception('locktimeout'); 674 } 675 676 $task->set_lock($lock); 677 if (!$task->is_blocking()) { 678 $cronlock->release(); 679 } else { 680 $task->set_cron_lock($cronlock); 681 } 682 return $task; 683 } 684 } 685 686 return null; 687 } 688 689 /** 690 * This function will dispatch the next scheduled task in the queue. The task will be handed out 691 * with an open lock - possibly on the entire cron process. Make sure you call either 692 * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task. 693 * 694 * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this. 695 * @return \core\task\scheduled_task or null 696 * @throws \moodle_exception 697 */ 698 public static function get_next_scheduled_task($timestart) { 699 global $DB; 700 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); 701 702 $where = "(lastruntime IS NULL OR lastruntime < :timestart1) 703 AND (nextruntime IS NULL OR nextruntime < :timestart2) 704 ORDER BY lastruntime, id ASC"; 705 $params = array('timestart1' => $timestart, 'timestart2' => $timestart); 706 $records = $DB->get_records_select('task_scheduled', $where, $params); 707 708 $pluginmanager = \core_plugin_manager::instance(); 709 710 foreach ($records as $record) { 711 712 $task = self::scheduled_task_from_record($record); 713 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). 714 // Also check to see if task is disabled or enabled after applying overrides. 715 if (!$task || $task->get_disabled()) { 716 continue; 717 } 718 719 if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) { 720 $classname = '\\' . $record->classname; 721 722 $task->set_lock($lock); 723 724 // See if the component is disabled. 725 $plugininfo = $pluginmanager->get_plugin_info($task->get_component()); 726 727 if ($plugininfo) { 728 if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) { 729 $lock->release(); 730 continue; 731 } 732 } 733 734 if (!self::scheduled_task_has_override($record->classname)) { 735 // Make sure the task data is unchanged unless an override is being used. 736 if (!$DB->record_exists('task_scheduled', (array)$record)) { 737 $lock->release(); 738 continue; 739 } 740 } 741 742 // The global cron lock is under the most contention so request it 743 // as late as possible and release it as soon as possible. 744 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) { 745 $lock->release(); 746 throw new \moodle_exception('locktimeout'); 747 } 748 749 if (!$task->is_blocking()) { 750 $cronlock->release(); 751 } else { 752 $task->set_cron_lock($cronlock); 753 } 754 return $task; 755 } 756 } 757 758 return null; 759 } 760 761 /** 762 * This function indicates that an adhoc task was not completed successfully and should be retried. 763 * 764 * @param \core\task\adhoc_task $task 765 */ 766 public static function adhoc_task_failed(adhoc_task $task) { 767 global $DB; 768 // Finalise the log output. 769 logmanager::finalise_log(true); 770 771 $delay = $task->get_fail_delay(); 772 773 // Reschedule task with exponential fall off for failing tasks. 774 if (empty($delay)) { 775 $delay = 60; 776 } else { 777 $delay *= 2; 778 } 779 780 // Max of 24 hour delay. 781 if ($delay > 86400) { 782 $delay = 86400; 783 } 784 785 // Reschedule and then release the locks. 786 $task->set_timestarted(); 787 $task->set_hostname(); 788 $task->set_pid(); 789 $task->set_next_run_time(time() + $delay); 790 $task->set_fail_delay($delay); 791 $record = self::record_from_adhoc_task($task); 792 $DB->update_record('task_adhoc', $record); 793 794 $task->release_concurrency_lock(); 795 if ($task->is_blocking()) { 796 $task->get_cron_lock()->release(); 797 } 798 $task->get_lock()->release(); 799 } 800 801 /** 802 * Records that a adhoc task is starting to run. 803 * 804 * @param adhoc_task $task Task that is starting 805 * @param int $time Start time (leave blank for now) 806 * @throws \dml_exception 807 * @throws \coding_exception 808 */ 809 public static function adhoc_task_starting(adhoc_task $task, int $time = 0) { 810 global $DB; 811 $pid = (int)getmypid(); 812 $hostname = (string)gethostname(); 813 814 if (empty($time)) { 815 $time = time(); 816 } 817 818 $task->set_timestarted($time); 819 $task->set_hostname($hostname); 820 $task->set_pid($pid); 821 822 $record = self::record_from_adhoc_task($task); 823 $DB->update_record('task_adhoc', $record); 824 } 825 826 /** 827 * This function indicates that an adhoc task was completed successfully. 828 * 829 * @param \core\task\adhoc_task $task 830 */ 831 public static function adhoc_task_complete(adhoc_task $task) { 832 global $DB; 833 834 // Finalise the log output. 835 logmanager::finalise_log(); 836 $task->set_timestarted(); 837 $task->set_hostname(); 838 $task->set_pid(); 839 840 // Delete the adhoc task record - it is finished. 841 $DB->delete_records('task_adhoc', array('id' => $task->get_id())); 842 843 // Release the locks. 844 $task->release_concurrency_lock(); 845 if ($task->is_blocking()) { 846 $task->get_cron_lock()->release(); 847 } 848 $task->get_lock()->release(); 849 } 850 851 /** 852 * This function indicates that a scheduled task was not completed successfully and should be retried. 853 * 854 * @param \core\task\scheduled_task $task 855 */ 856 public static function scheduled_task_failed(scheduled_task $task) { 857 global $DB; 858 // Finalise the log output. 859 logmanager::finalise_log(true); 860 861 $delay = $task->get_fail_delay(); 862 863 // Reschedule task with exponential fall off for failing tasks. 864 if (empty($delay)) { 865 $delay = 60; 866 } else { 867 $delay *= 2; 868 } 869 870 // Max of 24 hour delay. 871 if ($delay > 86400) { 872 $delay = 86400; 873 } 874 875 $task->set_timestarted(); 876 $task->set_hostname(); 877 $task->set_pid(); 878 879 $classname = self::get_canonical_class_name($task); 880 881 $record = $DB->get_record('task_scheduled', array('classname' => $classname)); 882 $record->nextruntime = time() + $delay; 883 $record->faildelay = $delay; 884 $record->timestarted = null; 885 $record->hostname = null; 886 $record->pid = null; 887 $DB->update_record('task_scheduled', $record); 888 889 if ($task->is_blocking()) { 890 $task->get_cron_lock()->release(); 891 } 892 $task->get_lock()->release(); 893 } 894 895 /** 896 * Clears the fail delay for the given task and updates its next run time based on the schedule. 897 * 898 * @param scheduled_task $task Task to reset 899 * @throws \dml_exception If there is a database error 900 */ 901 public static function clear_fail_delay(scheduled_task $task) { 902 global $DB; 903 904 $record = new \stdClass(); 905 $record->id = $DB->get_field('task_scheduled', 'id', 906 ['classname' => self::get_canonical_class_name($task)]); 907 $record->nextruntime = $task->get_next_scheduled_time(); 908 $record->faildelay = 0; 909 $DB->update_record('task_scheduled', $record); 910 } 911 912 /** 913 * Records that a scheduled task is starting to run. 914 * 915 * @param scheduled_task $task Task that is starting 916 * @param int $time Start time (0 = current) 917 * @throws \dml_exception If the task doesn't exist 918 */ 919 public static function scheduled_task_starting(scheduled_task $task, int $time = 0) { 920 global $DB; 921 $pid = (int)getmypid(); 922 $hostname = (string)gethostname(); 923 924 if (!$time) { 925 $time = time(); 926 } 927 928 $task->set_timestarted($time); 929 $task->set_hostname($hostname); 930 $task->set_pid($pid); 931 932 $classname = self::get_canonical_class_name($task); 933 $record = $DB->get_record('task_scheduled', ['classname' => $classname], '*', MUST_EXIST); 934 $record->timestarted = $time; 935 $record->hostname = $hostname; 936 $record->pid = $pid; 937 $DB->update_record('task_scheduled', $record); 938 } 939 940 /** 941 * This function indicates that a scheduled task was completed successfully and should be rescheduled. 942 * 943 * @param \core\task\scheduled_task $task 944 */ 945 public static function scheduled_task_complete(scheduled_task $task) { 946 global $DB; 947 948 // Finalise the log output. 949 logmanager::finalise_log(); 950 $task->set_timestarted(); 951 $task->set_hostname(); 952 $task->set_pid(); 953 954 $classname = self::get_canonical_class_name($task); 955 $record = $DB->get_record('task_scheduled', array('classname' => $classname)); 956 if ($record) { 957 $record->lastruntime = time(); 958 $record->faildelay = 0; 959 $record->nextruntime = $task->get_next_scheduled_time(); 960 $record->timestarted = null; 961 $record->hostname = null; 962 $record->pid = null; 963 964 $DB->update_record('task_scheduled', $record); 965 } 966 967 // Reschedule and then release the locks. 968 if ($task->is_blocking()) { 969 $task->get_cron_lock()->release(); 970 } 971 $task->get_lock()->release(); 972 } 973 974 /** 975 * Gets a list of currently-running tasks. 976 * 977 * @param string $sort Sorting method 978 * @return array Array of scheduled and adhoc tasks 979 * @throws \dml_exception 980 */ 981 public static function get_running_tasks($sort = ''): array { 982 global $DB; 983 if (empty($sort)) { 984 $sort = 'timestarted ASC, classname ASC'; 985 } 986 $params = ['now1' => time(), 'now2' => time()]; 987 988 $sql = "SELECT subquery.* 989 FROM (SELECT " . $DB->sql_concat("'s'", 'ts.id') . " as uniqueid, 990 ts.id, 991 'scheduled' as type, 992 ts.classname, 993 (:now1 - ts.timestarted) as time, 994 ts.timestarted, 995 ts.hostname, 996 ts.pid 997 FROM {task_scheduled} ts 998 WHERE ts.timestarted IS NOT NULL 999 UNION ALL 1000 SELECT " . $DB->sql_concat("'a'", 'ta.id') . " as uniqueid, 1001 ta.id, 1002 'adhoc' as type, 1003 ta.classname, 1004 (:now2 - ta.timestarted) as time, 1005 ta.timestarted, 1006 ta.hostname, 1007 ta.pid 1008 FROM {task_adhoc} ta 1009 WHERE ta.timestarted IS NOT NULL) subquery 1010 ORDER BY " . $sort; 1011 1012 return $DB->get_records_sql($sql, $params); 1013 } 1014 1015 /** 1016 * This function is used to indicate that any long running cron processes should exit at the 1017 * next opportunity and restart. This is because something (e.g. DB changes) has changed and 1018 * the static caches may be stale. 1019 */ 1020 public static function clear_static_caches() { 1021 global $DB; 1022 // Do not use get/set config here because the caches cannot be relied on. 1023 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset')); 1024 if ($record) { 1025 $record->value = time(); 1026 $DB->update_record('config', $record); 1027 } else { 1028 $record = new \stdClass(); 1029 $record->name = 'scheduledtaskreset'; 1030 $record->value = time(); 1031 $DB->insert_record('config', $record); 1032 } 1033 } 1034 1035 /** 1036 * Return true if the static caches have been cleared since $starttime. 1037 * @param int $starttime The time this process started. 1038 * @return boolean True if static caches need resetting. 1039 */ 1040 public static function static_caches_cleared_since($starttime) { 1041 global $DB; 1042 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset')); 1043 return $record && (intval($record->value) > $starttime); 1044 } 1045 1046 /** 1047 * Gets class name for use in database table. Always begins with a \. 1048 * 1049 * @param string|task_base $taskorstring Task object or a string 1050 */ 1051 protected static function get_canonical_class_name($taskorstring) { 1052 if (is_string($taskorstring)) { 1053 $classname = $taskorstring; 1054 } else { 1055 $classname = get_class($taskorstring); 1056 } 1057 if (strpos($classname, '\\') !== 0) { 1058 $classname = '\\' . $classname; 1059 } 1060 return $classname; 1061 } 1062 1063 /** 1064 * Gets the concurrent lock required to run an adhoc task. 1065 * 1066 * @param adhoc_task $task The task to obtain the lock for 1067 * @return \core\lock\lock The lock if one was obtained successfully 1068 * @throws \coding_exception 1069 */ 1070 protected static function get_concurrent_task_lock(adhoc_task $task): ?\core\lock\lock { 1071 $adhoclock = null; 1072 $cronlockfactory = \core\lock\lock_config::get_lock_factory(get_class($task)); 1073 1074 for ($run = 0; $run < $task->get_concurrency_limit(); $run++) { 1075 if ($adhoclock = $cronlockfactory->get_lock("concurrent_run_{$run}", 0)) { 1076 return $adhoclock; 1077 } 1078 } 1079 1080 return null; 1081 } 1082 1083 /** 1084 * Find the path of PHP CLI binary. 1085 * 1086 * @return string|false The PHP CLI executable PATH 1087 */ 1088 protected static function find_php_cli_path() { 1089 global $CFG; 1090 1091 if (!empty($CFG->pathtophp) && is_executable(trim($CFG->pathtophp))) { 1092 return $CFG->pathtophp; 1093 } 1094 1095 return false; 1096 } 1097 1098 /** 1099 * Returns if Moodle have access to PHP CLI binary or not. 1100 * 1101 * @return bool 1102 */ 1103 public static function is_runnable():bool { 1104 return self::find_php_cli_path() !== false; 1105 } 1106 1107 /** 1108 * Executes a cron from web invocation using PHP CLI. 1109 * 1110 * @param \core\task\task_base $task Task that be executed via CLI. 1111 * @return bool 1112 * @throws \moodle_exception 1113 */ 1114 public static function run_from_cli(\core\task\task_base $task):bool { 1115 global $CFG; 1116 1117 if (!self::is_runnable()) { 1118 $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']); 1119 throw new \moodle_exception('cannotfindthepathtothecli', 'tool_task', $redirecturl->out()); 1120 } else { 1121 // Shell-escaped path to the PHP binary. 1122 $phpbinary = escapeshellarg(self::find_php_cli_path()); 1123 1124 // Shell-escaped path CLI script. 1125 $pathcomponents = [$CFG->dirroot, $CFG->admin, 'cli', 'scheduled_task.php']; 1126 $scriptpath = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents)); 1127 1128 // Shell-escaped task name. 1129 $classname = get_class($task); 1130 $taskarg = escapeshellarg("--execute={$classname}") . " " . escapeshellarg("--force"); 1131 1132 // Build the CLI command. 1133 $command = "{$phpbinary} {$scriptpath} {$taskarg}"; 1134 1135 // Execute it. 1136 self::passthru_via_mtrace($command); 1137 } 1138 1139 return true; 1140 } 1141 1142 /** 1143 * This behaves similar to passthru but filters every line via 1144 * the mtrace function so it can be post processed. 1145 * 1146 * @param string $command to run 1147 * @return void 1148 */ 1149 public static function passthru_via_mtrace(string $command) { 1150 $descriptorspec = [ 1151 0 => ['pipe', 'r'], // STDIN. 1152 1 => ['pipe', 'w'], // STDOUT. 1153 2 => ['pipe', 'w'], // STDERR. 1154 ]; 1155 flush(); 1156 $process = proc_open($command, $descriptorspec, $pipes, realpath('./'), []); 1157 if (is_resource($process)) { 1158 while ($s = fgets($pipes[1])) { 1159 mtrace($s, ''); 1160 flush(); 1161 } 1162 } 1163 1164 fclose($pipes[0]); 1165 fclose($pipes[1]); 1166 fclose($pipes[2]); 1167 proc_close($process); 1168 } 1169 1170 /** 1171 * For a given scheduled task record, this method will check to see if any overrides have 1172 * been applied in config and return a copy of the record with any overridden values. 1173 * 1174 * The format of the config value is: 1175 * $CFG->scheduled_tasks = array( 1176 * '$classname' => array( 1177 * 'schedule' => '* * * * *', 1178 * 'disabled' => 1, 1179 * ), 1180 * ); 1181 * 1182 * Where $classname is the value of the task's classname, i.e. '\core\task\grade_cron_task'. 1183 * 1184 * @param \stdClass $record scheduled task record 1185 * @return \stdClass scheduled task with any configured overrides 1186 */ 1187 protected static function get_record_with_config_overrides(\stdClass $record): \stdClass { 1188 global $CFG; 1189 1190 $scheduledtaskkey = self::scheduled_task_get_override_key($record->classname); 1191 $overriddenrecord = $record; 1192 1193 if ($scheduledtaskkey) { 1194 $overriddenrecord->customised = true; 1195 $taskconfig = $CFG->scheduled_tasks[$scheduledtaskkey]; 1196 1197 if (isset($taskconfig['disabled'])) { 1198 $overriddenrecord->disabled = $taskconfig['disabled']; 1199 } 1200 if (isset($taskconfig['schedule'])) { 1201 list ( 1202 $overriddenrecord->minute, 1203 $overriddenrecord->hour, 1204 $overriddenrecord->day, 1205 $overriddenrecord->month, 1206 $overriddenrecord->dayofweek 1207 ) = explode(' ', $taskconfig['schedule']); 1208 } 1209 } 1210 1211 return $overriddenrecord; 1212 } 1213 1214 /** 1215 * This checks whether or not there is a value set in config 1216 * for a scheduled task. 1217 * 1218 * @param string $classname Scheduled task's classname 1219 * @return bool true if there is an entry in config 1220 */ 1221 public static function scheduled_task_has_override(string $classname): bool { 1222 return self::scheduled_task_get_override_key($classname) !== null; 1223 } 1224 1225 /** 1226 * Get the key within the scheduled tasks config object that 1227 * for a classname. 1228 * 1229 * @param string $classname the scheduled task classname to find 1230 * @return string the key if found, otherwise null 1231 */ 1232 public static function scheduled_task_get_override_key(string $classname): ?string { 1233 global $CFG; 1234 1235 if (isset($CFG->scheduled_tasks)) { 1236 // Firstly, attempt to get a match against the full classname. 1237 if (isset($CFG->scheduled_tasks[$classname])) { 1238 return $classname; 1239 } 1240 1241 // Check to see if there is a wildcard matching the classname. 1242 foreach (array_keys($CFG->scheduled_tasks) as $key) { 1243 if (strpos($key, '*') === false) { 1244 continue; 1245 } 1246 1247 $pattern = '/' . str_replace('\\', '\\\\', str_replace('*', '.*', $key)) . '/'; 1248 1249 if (preg_match($pattern, $classname)) { 1250 return $key; 1251 } 1252 } 1253 } 1254 1255 return null; 1256 } 1257 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body