Merge branch 'master_MDL-70520' of https://github.com/golenkovm/moodle
[moodle.git] / lib / classes / task / manager.php
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/>.
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;
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 {
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);
50         if (!$dir) {
51             return array();
52         }
54         $file = $dir . '/' . CORE_TASK_TASKS_FILENAME;
55         if (!file_exists($file)) {
56             return array();
57         }
59         $tasks = null;
60         include($file);
62         if (!isset($tasks)) {
63             return array();
64         }
66         $scheduledtasks = array();
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         }
78         return $scheduledtasks;
79     }
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();
93         foreach ($tasks as $taskid => $task) {
94             $classname = self::get_canonical_class_name($task);
96             $validtasks[] = $classname;
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                 }
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());
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         }
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     }
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     }
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;
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) . ' = ?';
151         if ($record->userid) {
152             $params[] = $record->userid;
153             $sql .= " AND userid = ? ";
154         }
155         return $DB->get_record_select('task_adhoc', $sql, $params);
156     }
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;
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     }
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;
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         }
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         }
204         // Check if the same task is already scheduled.
205         if ($checkforexisting && self::task_is_scheduled($task)) {
206             return false;
207         }
209         // Queue the task.
210         $result = $DB->insert_record('task_adhoc', $record);
212         return $result;
213     }
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;
225         $classname = self::get_canonical_class_name($task);
227         $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
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);
235         return $result;
236     }
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();
263         return $record;
264     }
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();
287         return $record;
288     }
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         }
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         }
333         return $task;
334     }
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;
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         }
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));
404         return $task;
405     }
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;
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         }
427         return $tasks;
428     }
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;
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     }
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;
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));
461         return array_map(function($record) {
462             return self::adhoc_task_from_record($record);
463         }, $records);
464     }
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();
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         }
484         foreach ($componenttasks as $componenttask) {
485             if (get_class($componenttask) == get_class($task)) {
486                 return $componenttask;
487             }
488         }
490         return false;
491     }
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;
501         $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
502         $tasks = array();
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         }
512         return $tasks;
513     }
515     /**
516      * Ensure quality of service for the ad hoc task queue.
517      *
518      * This reshuffles the adhoc tasks queue to balance by type to ensure a
519      * level of quality of service per type, while still maintaining the
520      * relative order of tasks queued by timestamp.
521      *
522      * @param array $records array of task records
523      * @param array $records array of same task records shuffled
524      */
525     public static function ensure_adhoc_task_qos(array $records): array {
527         $count = count($records);
528         if ($count == 0) {
529             return $records;
530         }
532         $queues = []; // This holds a queue for each type of adhoc task.
533         $limits = []; // The relative limits of each type of task.
534         $limittotal = 0;
536         // Split the single queue up into queues per type.
537         foreach ($records as $record) {
538             $type = $record->classname;
539             if (!array_key_exists($type, $queues)) {
540                 $queues[$type] = [];
541             }
542             if (!array_key_exists($type, $limits)) {
543                 $limits[$type] = 1;
544                 $limittotal += 1;
545             }
546             $queues[$type][] = $record;
547         }
549         $qos = []; // Our new queue with ensured quality of service.
550         $seed = $count % $limittotal; // Which task queue to shuffle from first?
552         $move = 1; // How many tasks to shuffle at a time.
553         do {
554             $shuffled = 0;
556             // Now cycle through task type queues and interleaving the tasks
557             // back into a single queue.
558             foreach ($limits as $type => $limit) {
560                 // Just interleaving the queue is not enough, because after
561                 // any task is processed the whole queue is rebuilt again. So
562                 // we need to deterministically start on different types of
563                 // tasks so that *on average* we rotate through each type of task.
564                 //
565                 // We achieve this by using a $seed to start moving tasks off a
566                 // different queue each time. The seed is based on the task count
567                 // modulo the number of types of tasks on the queue. As we count
568                 // down this naturally cycles through each type of record.
569                 if ($seed < 1) {
570                     $shuffled = 1;
571                     $seed += 1;
572                     continue;
573                 }
574                 $tasks = array_splice($queues[$type], 0, $move);
575                 $qos = array_merge($qos, $tasks);
577                 // Stop if we didn't move any tasks onto the main queue.
578                 $shuffled += count($tasks);
579             }
580             // Generally the only tasks that matter are those that are near the start so
581             // after we have shuffled the first few 1 by 1, start shuffling larger groups.
582             if (count($qos) >= (4 * count($limits))) {
583                 $move *= 2;
584             }
585         } while ($shuffled > 0);
587         return $qos;
588     }
590     /**
591      * This function will dispatch the next adhoc task in the queue. The task will be handed out
592      * with an open lock - possibly on the entire cron process. Make sure you call either
593      * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
594      *
595      * @param int $timestart
596      * @param bool $checklimits Should we check limits?
597      * @return \core\task\adhoc_task or null if not found
598      * @throws \moodle_exception
599      */
600     public static function get_next_adhoc_task($timestart, $checklimits = true) {
601         global $DB;
603         $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
604         $params = array('timestart1' => $timestart);
605         $records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC', '*', 0, 2000);
606         $records = self::ensure_adhoc_task_qos($records);
608         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
610         $skipclasses = array();
612         foreach ($records as $record) {
614             if (in_array($record->classname, $skipclasses)) {
615                 // Skip the task if it can't be started due to per-task concurrency limit.
616                 continue;
617             }
619             if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) {
621                 // Safety check, see if the task has been already processed by another cron run.
622                 $record = $DB->get_record('task_adhoc', array('id' => $record->id));
623                 if (!$record) {
624                     $lock->release();
625                     continue;
626                 }
628                 $task = self::adhoc_task_from_record($record);
629                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
630                 if (!$task) {
631                     $lock->release();
632                     continue;
633                 }
635                 $tasklimit = $task->get_concurrency_limit();
636                 if ($checklimits && $tasklimit > 0) {
637                     if ($concurrencylock = self::get_concurrent_task_lock($task)) {
638                         $task->set_concurrency_lock($concurrencylock);
639                     } else {
640                         // Unable to obtain a concurrency lock.
641                         mtrace("Skipping $record->classname adhoc task class as the per-task limit of $tasklimit is reached.");
642                         $skipclasses[] = $record->classname;
643                         $lock->release();
644                         continue;
645                     }
646                 }
648                 // The global cron lock is under the most contention so request it
649                 // as late as possible and release it as soon as possible.
650                 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
651                     $lock->release();
652                     throw new \moodle_exception('locktimeout');
653                 }
655                 $task->set_lock($lock);
656                 if (!$task->is_blocking()) {
657                     $cronlock->release();
658                 } else {
659                     $task->set_cron_lock($cronlock);
660                 }
661                 return $task;
662             }
663         }
665         return null;
666     }
668     /**
669      * This function will dispatch the next scheduled task in the queue. The task will be handed out
670      * with an open lock - possibly on the entire cron process. Make sure you call either
671      * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
672      *
673      * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
674      * @return \core\task\scheduled_task or null
675      * @throws \moodle_exception
676      */
677     public static function get_next_scheduled_task($timestart) {
678         global $DB;
679         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
681         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
682                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
683                   ORDER BY lastruntime, id ASC";
684         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
685         $records = $DB->get_records_select('task_scheduled', $where, $params);
687         $pluginmanager = \core_plugin_manager::instance();
689         foreach ($records as $record) {
691             $task = self::scheduled_task_from_record($record);
692             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
693             // Also check to see if task is disabled or enabled after applying overrides.
694             if (!$task || $task->get_disabled()) {
695                 continue;
696             }
698             if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
699                 $classname = '\\' . $record->classname;
701                 $task->set_lock($lock);
703                 // See if the component is disabled.
704                 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
706                 if ($plugininfo) {
707                     if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
708                         $lock->release();
709                         continue;
710                     }
711                 }
713                 if (!self::scheduled_task_has_override($record->classname)) {
714                     // Make sure the task data is unchanged unless an override is being used.
715                     if (!$DB->record_exists('task_scheduled', (array)$record)) {
716                         $lock->release();
717                         continue;
718                     }
719                 }
721                 // The global cron lock is under the most contention so request it
722                 // as late as possible and release it as soon as possible.
723                 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
724                     $lock->release();
725                     throw new \moodle_exception('locktimeout');
726                 }
728                 if (!$task->is_blocking()) {
729                     $cronlock->release();
730                 } else {
731                     $task->set_cron_lock($cronlock);
732                 }
733                 return $task;
734             }
735         }
737         return null;
738     }
740     /**
741      * This function indicates that an adhoc task was not completed successfully and should be retried.
742      *
743      * @param \core\task\adhoc_task $task
744      */
745     public static function adhoc_task_failed(adhoc_task $task) {
746         global $DB;
747         // Finalise the log output.
748         logmanager::finalise_log(true);
750         $delay = $task->get_fail_delay();
752         // Reschedule task with exponential fall off for failing tasks.
753         if (empty($delay)) {
754             $delay = 60;
755         } else {
756             $delay *= 2;
757         }
759         // Max of 24 hour delay.
760         if ($delay > 86400) {
761             $delay = 86400;
762         }
764         // Reschedule and then release the locks.
765         $task->set_timestarted();
766         $task->set_hostname();
767         $task->set_pid();
768         $task->set_next_run_time(time() + $delay);
769         $task->set_fail_delay($delay);
770         $record = self::record_from_adhoc_task($task);
771         $DB->update_record('task_adhoc', $record);
773         $task->release_concurrency_lock();
774         if ($task->is_blocking()) {
775             $task->get_cron_lock()->release();
776         }
777         $task->get_lock()->release();
778     }
780     /**
781      * Records that a adhoc task is starting to run.
782      *
783      * @param adhoc_task $task Task that is starting
784      * @param int $time Start time (leave blank for now)
785      * @throws \dml_exception
786      * @throws \coding_exception
787      */
788     public static function adhoc_task_starting(adhoc_task $task, int $time = 0) {
789         global $DB;
790         $pid = (int)getmypid();
791         $hostname = (string)gethostname();
793         if (empty($time)) {
794             $time = time();
795         }
797         $task->set_timestarted($time);
798         $task->set_hostname($hostname);
799         $task->set_pid($pid);
801         $record = self::record_from_adhoc_task($task);
802         $DB->update_record('task_adhoc', $record);
803     }
805     /**
806      * This function indicates that an adhoc task was completed successfully.
807      *
808      * @param \core\task\adhoc_task $task
809      */
810     public static function adhoc_task_complete(adhoc_task $task) {
811         global $DB;
813         // Finalise the log output.
814         logmanager::finalise_log();
815         $task->set_timestarted();
816         $task->set_hostname();
817         $task->set_pid();
819         // Delete the adhoc task record - it is finished.
820         $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
822         // Release the locks.
823         $task->release_concurrency_lock();
824         if ($task->is_blocking()) {
825             $task->get_cron_lock()->release();
826         }
827         $task->get_lock()->release();
828     }
830     /**
831      * This function indicates that a scheduled task was not completed successfully and should be retried.
832      *
833      * @param \core\task\scheduled_task $task
834      */
835     public static function scheduled_task_failed(scheduled_task $task) {
836         global $DB;
837         // Finalise the log output.
838         logmanager::finalise_log(true);
840         $delay = $task->get_fail_delay();
842         // Reschedule task with exponential fall off for failing tasks.
843         if (empty($delay)) {
844             $delay = 60;
845         } else {
846             $delay *= 2;
847         }
849         // Max of 24 hour delay.
850         if ($delay > 86400) {
851             $delay = 86400;
852         }
854         $task->set_timestarted();
855         $task->set_hostname();
856         $task->set_pid();
858         $classname = self::get_canonical_class_name($task);
860         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
861         $record->nextruntime = time() + $delay;
862         $record->faildelay = $delay;
863         $record->timestarted = null;
864         $record->hostname = null;
865         $record->pid = null;
866         $DB->update_record('task_scheduled', $record);
868         if ($task->is_blocking()) {
869             $task->get_cron_lock()->release();
870         }
871         $task->get_lock()->release();
872     }
874     /**
875      * Clears the fail delay for the given task and updates its next run time based on the schedule.
876      *
877      * @param scheduled_task $task Task to reset
878      * @throws \dml_exception If there is a database error
879      */
880     public static function clear_fail_delay(scheduled_task $task) {
881         global $DB;
883         $record = new \stdClass();
884         $record->id = $DB->get_field('task_scheduled', 'id',
885                 ['classname' => self::get_canonical_class_name($task)]);
886         $record->nextruntime = $task->get_next_scheduled_time();
887         $record->faildelay = 0;
888         $DB->update_record('task_scheduled', $record);
889     }
891     /**
892      * Records that a scheduled task is starting to run.
893      *
894      * @param scheduled_task $task Task that is starting
895      * @param int $time Start time (0 = current)
896      * @throws \dml_exception If the task doesn't exist
897      */
898     public static function scheduled_task_starting(scheduled_task $task, int $time = 0) {
899         global $DB;
900         $pid = (int)getmypid();
901         $hostname = (string)gethostname();
903         if (!$time) {
904             $time = time();
905         }
907         $task->set_timestarted($time);
908         $task->set_hostname($hostname);
909         $task->set_pid($pid);
911         $classname = self::get_canonical_class_name($task);
912         $record = $DB->get_record('task_scheduled', ['classname' => $classname], '*', MUST_EXIST);
913         $record->timestarted = $time;
914         $record->hostname = $hostname;
915         $record->pid = $pid;
916         $DB->update_record('task_scheduled', $record);
917     }
919     /**
920      * This function indicates that a scheduled task was completed successfully and should be rescheduled.
921      *
922      * @param \core\task\scheduled_task $task
923      */
924     public static function scheduled_task_complete(scheduled_task $task) {
925         global $DB;
927         // Finalise the log output.
928         logmanager::finalise_log();
929         $task->set_timestarted();
930         $task->set_hostname();
931         $task->set_pid();
933         $classname = self::get_canonical_class_name($task);
934         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
935         if ($record) {
936             $record->lastruntime = time();
937             $record->faildelay = 0;
938             $record->nextruntime = $task->get_next_scheduled_time();
939             $record->timestarted = null;
940             $record->hostname = null;
941             $record->pid = null;
943             $DB->update_record('task_scheduled', $record);
944         }
946         // Reschedule and then release the locks.
947         if ($task->is_blocking()) {
948             $task->get_cron_lock()->release();
949         }
950         $task->get_lock()->release();
951     }
953     /**
954      * Gets a list of currently-running tasks.
955      *
956      * @param  string $sort Sorting method
957      * @return array Array of scheduled and adhoc tasks
958      * @throws \dml_exception
959      */
960     public static function get_running_tasks($sort = ''): array {
961         global $DB;
962         if (empty($sort)) {
963             $sort = 'timestarted ASC, classname ASC';
964         }
965         $params = ['now1' => time(), 'now2' => time()];
967         $sql = "SELECT subquery.*
968                   FROM (SELECT concat('s', ts.id) as uniqueid,
969                                ts.id,
970                                'scheduled' as type,
971                                ts.classname,
972                                (:now1 - ts.timestarted) as time,
973                                ts.timestarted,
974                                ts.hostname,
975                                ts.pid
976                           FROM {task_scheduled} ts
977                          WHERE ts.timestarted IS NOT NULL
978                          UNION ALL
979                         SELECT concat('a', ta.id) as uniqueid,
980                                ta.id,
981                                'adhoc' as type,
982                                ta.classname,
983                                (:now2 - ta.timestarted) as time,
984                                ta.timestarted,
985                                ta.hostname,
986                                ta.pid
987                           FROM {task_adhoc} ta
988                          WHERE ta.timestarted IS NOT NULL) subquery
989               ORDER BY " . $sort;
991         return $DB->get_records_sql($sql, $params);
992     }
994     /**
995      * This function is used to indicate that any long running cron processes should exit at the
996      * next opportunity and restart. This is because something (e.g. DB changes) has changed and
997      * the static caches may be stale.
998      */
999     public static function clear_static_caches() {
1000         global $DB;
1001         // Do not use get/set config here because the caches cannot be relied on.
1002         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
1003         if ($record) {
1004             $record->value = time();
1005             $DB->update_record('config', $record);
1006         } else {
1007             $record = new \stdClass();
1008             $record->name = 'scheduledtaskreset';
1009             $record->value = time();
1010             $DB->insert_record('config', $record);
1011         }
1012     }
1014     /**
1015      * Return true if the static caches have been cleared since $starttime.
1016      * @param int $starttime The time this process started.
1017      * @return boolean True if static caches need resetting.
1018      */
1019     public static function static_caches_cleared_since($starttime) {
1020         global $DB;
1021         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
1022         return $record && (intval($record->value) > $starttime);
1023     }
1025     /**
1026      * Gets class name for use in database table. Always begins with a \.
1027      *
1028      * @param string|task_base $taskorstring Task object or a string
1029      */
1030     protected static function get_canonical_class_name($taskorstring) {
1031         if (is_string($taskorstring)) {
1032             $classname = $taskorstring;
1033         } else {
1034             $classname = get_class($taskorstring);
1035         }
1036         if (strpos($classname, '\\') !== 0) {
1037             $classname = '\\' . $classname;
1038         }
1039         return $classname;
1040     }
1042     /**
1043      * Gets the concurrent lock required to run an adhoc task.
1044      *
1045      * @param   adhoc_task $task The task to obtain the lock for
1046      * @return  \core\lock\lock The lock if one was obtained successfully
1047      * @throws  \coding_exception
1048      */
1049     protected static function get_concurrent_task_lock(adhoc_task $task): ?\core\lock\lock {
1050         $adhoclock = null;
1051         $cronlockfactory = \core\lock\lock_config::get_lock_factory(get_class($task));
1053         for ($run = 0; $run < $task->get_concurrency_limit(); $run++) {
1054             if ($adhoclock = $cronlockfactory->get_lock("concurrent_run_{$run}", 0)) {
1055                 return $adhoclock;
1056             }
1057         }
1059         return null;
1060     }
1062     /**
1063      * Find the path of PHP CLI binary.
1064      *
1065      * @return string|false The PHP CLI executable PATH
1066      */
1067     protected static function find_php_cli_path() {
1068         global $CFG;
1070         if (!empty($CFG->pathtophp) && is_executable(trim($CFG->pathtophp))) {
1071             return $CFG->pathtophp;
1072         }
1074         return false;
1075     }
1077     /**
1078      * Returns if Moodle have access to PHP CLI binary or not.
1079      *
1080      * @return bool
1081      */
1082     public static function is_runnable():bool {
1083         return self::find_php_cli_path() !== false;
1084     }
1086     /**
1087      * Executes a cron from web invocation using PHP CLI.
1088      *
1089      * @param \core\task\task_base $task Task that be executed via CLI.
1090      * @return bool
1091      * @throws \moodle_exception
1092      */
1093     public static function run_from_cli(\core\task\task_base $task):bool {
1094         global $CFG;
1096         if (!self::is_runnable()) {
1097             $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
1098             throw new \moodle_exception('cannotfindthepathtothecli', 'core_task', $redirecturl->out());
1099         } else {
1100             // Shell-escaped path to the PHP binary.
1101             $phpbinary = escapeshellarg(self::find_php_cli_path());
1103             // Shell-escaped path CLI script.
1104             $pathcomponents = [$CFG->dirroot, $CFG->admin, 'cli', 'scheduled_task.php'];
1105             $scriptpath     = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents));
1107             // Shell-escaped task name.
1108             $classname = get_class($task);
1109             $taskarg   = escapeshellarg("--execute={$classname}") . " " . escapeshellarg("--force");
1111             // Build the CLI command.
1112             $command = "{$phpbinary} {$scriptpath} {$taskarg}";
1114             // Execute it.
1115             passthru($command);
1116         }
1118         return true;
1119     }
1121     /**
1122      * For a given scheduled task record, this method will check to see if any overrides have
1123      * been applied in config and return a copy of the record with any overridden values.
1124      *
1125      * The format of the config value is:
1126      *      $CFG->scheduled_tasks = array(
1127      *          '$classname' => array(
1128      *              'schedule' => '* * * * *',
1129      *              'disabled' => 1,
1130      *          ),
1131      *      );
1132      *
1133      * Where $classname is the value of the task's classname, i.e. '\core\task\grade_cron_task'.
1134      *
1135      * @param \stdClass $record scheduled task record
1136      * @return \stdClass scheduled task with any configured overrides
1137      */
1138     protected static function get_record_with_config_overrides(\stdClass $record): \stdClass {
1139         global $CFG;
1141         $scheduledtaskkey = self::scheduled_task_get_override_key($record->classname);
1142         $overriddenrecord = $record;
1144         if ($scheduledtaskkey) {
1145             $overriddenrecord->customised = true;
1146             $taskconfig = $CFG->scheduled_tasks[$scheduledtaskkey];
1148             if (isset($taskconfig['disabled'])) {
1149                 $overriddenrecord->disabled = $taskconfig['disabled'];
1150             }
1151             if (isset($taskconfig['schedule'])) {
1152                 list (
1153                     $overriddenrecord->minute,
1154                     $overriddenrecord->hour,
1155                     $overriddenrecord->day,
1156                     $overriddenrecord->dayofweek,
1157                     $overriddenrecord->month) = explode(' ', $taskconfig['schedule']);
1158             }
1159         }
1161         return $overriddenrecord;
1162     }
1164     /**
1165      * This checks whether or not there is a value set in config
1166      * for a scheduled task.
1167      *
1168      * @param string $classname Scheduled task's classname
1169      * @return bool true if there is an entry in config
1170      */
1171     public static function scheduled_task_has_override(string $classname): bool {
1172         return self::scheduled_task_get_override_key($classname) !== null;
1173     }
1175     /**
1176      * Get the key within the scheduled tasks config object that
1177      * for a classname.
1178      *
1179      * @param string $classname the scheduled task classname to find
1180      * @return string the key if found, otherwise null
1181      */
1182     public static function scheduled_task_get_override_key(string $classname): ?string {
1183         global $CFG;
1185         if (isset($CFG->scheduled_tasks)) {
1186             // Firstly, attempt to get a match against the full classname.
1187             if (isset($CFG->scheduled_tasks[$classname])) {
1188                 return $classname;
1189             }
1191             // Check to see if there is a wildcard matching the classname.
1192             foreach (array_keys($CFG->scheduled_tasks) as $key) {
1193                 if (strpos($key, '*') === false) {
1194                     continue;
1195                 }
1197                 $pattern = '/' . str_replace('\\', '\\\\', str_replace('*', '.*', $key)) . '/';
1199                 if (preg_match($pattern, $classname)) {
1200                     return $key;
1201                 }
1202             }
1203         }
1205         return null;
1206     }