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