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