Merge branch 'MDL-33981-Master' of https://github.com/tuanngocnguyen/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);
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         $result = $DB->update_record('task_scheduled', $record);
234         return $result;
235     }
237     /**
238      * Utility method to create a DB record from a scheduled task.
239      *
240      * @param \core\task\scheduled_task $task
241      * @return \stdClass
242      */
243     public static function record_from_scheduled_task($task) {
244         $record = new \stdClass();
245         $record->classname = self::get_canonical_class_name($task);
246         $record->component = $task->get_component();
247         $record->blocking = $task->is_blocking();
248         $record->customised = $task->is_customised();
249         $record->lastruntime = $task->get_last_run_time();
250         $record->nextruntime = $task->get_next_run_time();
251         $record->faildelay = $task->get_fail_delay();
252         $record->hour = $task->get_hour();
253         $record->minute = $task->get_minute();
254         $record->day = $task->get_day();
255         $record->dayofweek = $task->get_day_of_week();
256         $record->month = $task->get_month();
257         $record->disabled = $task->get_disabled();
258         $record->timestarted = $task->get_timestarted();
259         $record->hostname = $task->get_hostname();
260         $record->pid = $task->get_pid();
262         return $record;
263     }
265     /**
266      * Utility method to create a DB record from an adhoc task.
267      *
268      * @param \core\task\adhoc_task $task
269      * @return \stdClass
270      */
271     public static function record_from_adhoc_task($task) {
272         $record = new \stdClass();
273         $record->classname = self::get_canonical_class_name($task);
274         $record->id = $task->get_id();
275         $record->component = $task->get_component();
276         $record->blocking = $task->is_blocking();
277         $record->nextruntime = $task->get_next_run_time();
278         $record->faildelay = $task->get_fail_delay();
279         $record->customdata = $task->get_custom_data_as_string();
280         $record->userid = $task->get_userid();
281         $record->timecreated = time();
282         $record->timestarted = $task->get_timestarted();
283         $record->hostname = $task->get_hostname();
284         $record->pid = $task->get_pid();
286         return $record;
287     }
289     /**
290      * Utility method to create an adhoc task from a DB record.
291      *
292      * @param \stdClass $record
293      * @return \core\task\adhoc_task
294      */
295     public static function adhoc_task_from_record($record) {
296         $classname = self::get_canonical_class_name($record->classname);
297         if (!class_exists($classname)) {
298             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
299             return false;
300         }
301         $task = new $classname;
302         if (isset($record->nextruntime)) {
303             $task->set_next_run_time($record->nextruntime);
304         }
305         if (isset($record->id)) {
306             $task->set_id($record->id);
307         }
308         if (isset($record->component)) {
309             $task->set_component($record->component);
310         }
311         $task->set_blocking(!empty($record->blocking));
312         if (isset($record->faildelay)) {
313             $task->set_fail_delay($record->faildelay);
314         }
315         if (isset($record->customdata)) {
316             $task->set_custom_data_as_string($record->customdata);
317         }
319         if (isset($record->userid)) {
320             $task->set_userid($record->userid);
321         }
322         if (isset($record->timestarted)) {
323             $task->set_timestarted($record->timestarted);
324         }
325         if (isset($record->hostname)) {
326             $task->set_hostname($record->hostname);
327         }
328         if (isset($record->pid)) {
329             $task->set_pid($record->pid);
330         }
332         return $task;
333     }
335     /**
336      * Utility method to create a task from a DB record.
337      *
338      * @param \stdClass $record
339      * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
340      *      If false, they are left as 'R'
341      * @return \core\task\scheduled_task|false
342      */
343     public static function scheduled_task_from_record($record, $expandr = true) {
344         $classname = self::get_canonical_class_name($record->classname);
345         if (!class_exists($classname)) {
346             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
347             return false;
348         }
349         /** @var \core\task\scheduled_task $task */
350         $task = new $classname;
351         if (isset($record->lastruntime)) {
352             $task->set_last_run_time($record->lastruntime);
353         }
354         if (isset($record->nextruntime)) {
355             $task->set_next_run_time($record->nextruntime);
356         }
357         if (isset($record->customised)) {
358             $task->set_customised($record->customised);
359         }
360         if (isset($record->component)) {
361             $task->set_component($record->component);
362         }
363         $task->set_blocking(!empty($record->blocking));
364         if (isset($record->minute)) {
365             $task->set_minute($record->minute, $expandr);
366         }
367         if (isset($record->hour)) {
368             $task->set_hour($record->hour, $expandr);
369         }
370         if (isset($record->day)) {
371             $task->set_day($record->day);
372         }
373         if (isset($record->month)) {
374             $task->set_month($record->month);
375         }
376         if (isset($record->dayofweek)) {
377             $task->set_day_of_week($record->dayofweek, $expandr);
378         }
379         if (isset($record->faildelay)) {
380             $task->set_fail_delay($record->faildelay);
381         }
382         if (isset($record->disabled)) {
383             $task->set_disabled($record->disabled);
384         }
385         if (isset($record->timestarted)) {
386             $task->set_timestarted($record->timestarted);
387         }
388         if (isset($record->hostname)) {
389             $task->set_hostname($record->hostname);
390         }
391         if (isset($record->pid)) {
392             $task->set_pid($record->pid);
393         }
395         return $task;
396     }
398     /**
399      * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
400      * Do not execute tasks loaded from this function - they have not been locked.
401      * @param string $componentname - The name of the component to load the tasks for.
402      * @return \core\task\scheduled_task[]
403      */
404     public static function load_scheduled_tasks_for_component($componentname) {
405         global $DB;
407         $tasks = array();
408         // We are just reading - so no locks required.
409         $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
410         foreach ($records as $record) {
411             $task = self::scheduled_task_from_record($record);
412             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
413             if ($task) {
414                 $tasks[] = $task;
415             }
416         }
418         return $tasks;
419     }
421     /**
422      * This function load the scheduled task details for a given classname.
423      *
424      * @param string $classname
425      * @return \core\task\scheduled_task or false
426      */
427     public static function get_scheduled_task($classname) {
428         global $DB;
430         $classname = self::get_canonical_class_name($classname);
431         // We are just reading - so no locks required.
432         $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
433         if (!$record) {
434             return false;
435         }
436         return self::scheduled_task_from_record($record);
437     }
439     /**
440      * This function load the adhoc tasks for a given classname.
441      *
442      * @param string $classname
443      * @return \core\task\adhoc_task[]
444      */
445     public static function get_adhoc_tasks($classname) {
446         global $DB;
448         $classname = self::get_canonical_class_name($classname);
449         // We are just reading - so no locks required.
450         $records = $DB->get_records('task_adhoc', array('classname' => $classname));
452         return array_map(function($record) {
453             return self::adhoc_task_from_record($record);
454         }, $records);
455     }
457     /**
458      * This function load the default scheduled task details for a given classname.
459      *
460      * @param string $classname
461      * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
462      *      If false, they are left as 'R'
463      * @return \core\task\scheduled_task|false
464      */
465     public static function get_default_scheduled_task($classname, $expandr = true) {
466         $task = self::get_scheduled_task($classname);
467         $componenttasks = array();
469         // Safety check in case no task was found for the given classname.
470         if ($task) {
471             $componenttasks = self::load_default_scheduled_tasks_for_component(
472                     $task->get_component(), $expandr);
473         }
475         foreach ($componenttasks as $componenttask) {
476             if (get_class($componenttask) == get_class($task)) {
477                 return $componenttask;
478             }
479         }
481         return false;
482     }
484     /**
485      * This function will return a list of all the scheduled tasks that exist in the database.
486      *
487      * @return \core\task\scheduled_task[]
488      */
489     public static function get_all_scheduled_tasks() {
490         global $DB;
492         $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
493         $tasks = array();
495         foreach ($records as $record) {
496             $task = self::scheduled_task_from_record($record);
497             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
498             if ($task) {
499                 $tasks[] = $task;
500             }
501         }
503         return $tasks;
504     }
506     /**
507      * Ensure quality of service for the ad hoc task queue.
508      *
509      * This reshuffles the adhoc tasks queue to balance by type to ensure a
510      * level of quality of service per type, while still maintaining the
511      * relative order of tasks queued by timestamp.
512      *
513      * @param array $records array of task records
514      * @param array $records array of same task records shuffled
515      */
516     public static function ensure_adhoc_task_qos(array $records): array {
518         $count = count($records);
519         if ($count == 0) {
520             return $records;
521         }
523         $queues = []; // This holds a queue for each type of adhoc task.
524         $limits = []; // The relative limits of each type of task.
525         $limittotal = 0;
527         // Split the single queue up into queues per type.
528         foreach ($records as $record) {
529             $type = $record->classname;
530             if (!array_key_exists($type, $queues)) {
531                 $queues[$type] = [];
532             }
533             if (!array_key_exists($type, $limits)) {
534                 $limits[$type] = 1;
535                 $limittotal += 1;
536             }
537             $queues[$type][] = $record;
538         }
540         $qos = []; // Our new queue with ensured quality of service.
541         $seed = $count % $limittotal; // Which task queue to shuffle from first?
543         $move = 1; // How many tasks to shuffle at a time.
544         do {
545             $shuffled = 0;
547             // Now cycle through task type queues and interleaving the tasks
548             // back into a single queue.
549             foreach ($limits as $type => $limit) {
551                 // Just interleaving the queue is not enough, because after
552                 // any task is processed the whole queue is rebuilt again. So
553                 // we need to deterministically start on different types of
554                 // tasks so that *on average* we rotate through each type of task.
555                 //
556                 // We achieve this by using a $seed to start moving tasks off a
557                 // different queue each time. The seed is based on the task count
558                 // modulo the number of types of tasks on the queue. As we count
559                 // down this naturally cycles through each type of record.
560                 if ($seed < 1) {
561                     $shuffled = 1;
562                     $seed += 1;
563                     continue;
564                 }
565                 $tasks = array_splice($queues[$type], 0, $move);
566                 $qos = array_merge($qos, $tasks);
568                 // Stop if we didn't move any tasks onto the main queue.
569                 $shuffled += count($tasks);
570             }
571             // Generally the only tasks that matter are those that are near the start so
572             // after we have shuffled the first few 1 by 1, start shuffling larger groups.
573             if (count($qos) >= (4 * count($limits))) {
574                 $move *= 2;
575             }
576         } while ($shuffled > 0);
578         return $qos;
579     }
581     /**
582      * This function will dispatch the next adhoc task in the queue. The task will be handed out
583      * with an open lock - possibly on the entire cron process. Make sure you call either
584      * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
585      *
586      * @param int $timestart
587      * @param bool $checklimits Should we check limits?
588      * @return \core\task\adhoc_task or null if not found
589      * @throws \moodle_exception
590      */
591     public static function get_next_adhoc_task($timestart, $checklimits = true) {
592         global $DB;
594         $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
595         $params = array('timestart1' => $timestart);
596         $records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC', '*', 0, 2000);
597         $records = self::ensure_adhoc_task_qos($records);
599         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
601         $skipclasses = array();
603         foreach ($records as $record) {
605             if (in_array($record->classname, $skipclasses)) {
606                 // Skip the task if it can't be started due to per-task concurrency limit.
607                 continue;
608             }
610             if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) {
612                 // Safety check, see if the task has been already processed by another cron run.
613                 $record = $DB->get_record('task_adhoc', array('id' => $record->id));
614                 if (!$record) {
615                     $lock->release();
616                     continue;
617                 }
619                 $task = self::adhoc_task_from_record($record);
620                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
621                 if (!$task) {
622                     $lock->release();
623                     continue;
624                 }
626                 $tasklimit = $task->get_concurrency_limit();
627                 if ($checklimits && $tasklimit > 0) {
628                     if ($concurrencylock = self::get_concurrent_task_lock($task)) {
629                         $task->set_concurrency_lock($concurrencylock);
630                     } else {
631                         // Unable to obtain a concurrency lock.
632                         mtrace("Skipping $record->classname adhoc task class as the per-task limit of $tasklimit is reached.");
633                         $skipclasses[] = $record->classname;
634                         $lock->release();
635                         continue;
636                     }
637                 }
639                 // The global cron lock is under the most contention so request it
640                 // as late as possible and release it as soon as possible.
641                 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
642                     $lock->release();
643                     throw new \moodle_exception('locktimeout');
644                 }
646                 $task->set_lock($lock);
647                 if (!$task->is_blocking()) {
648                     $cronlock->release();
649                 } else {
650                     $task->set_cron_lock($cronlock);
651                 }
652                 return $task;
653             }
654         }
656         return null;
657     }
659     /**
660      * This function will dispatch the next scheduled task in the queue. The task will be handed out
661      * with an open lock - possibly on the entire cron process. Make sure you call either
662      * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
663      *
664      * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
665      * @return \core\task\scheduled_task or null
666      * @throws \moodle_exception
667      */
668     public static function get_next_scheduled_task($timestart) {
669         global $DB;
670         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
672         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
673                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
674                   AND disabled = 0
675                   ORDER BY lastruntime, id ASC";
676         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
677         $records = $DB->get_records_select('task_scheduled', $where, $params);
679         $pluginmanager = \core_plugin_manager::instance();
681         foreach ($records as $record) {
683             if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
684                 $classname = '\\' . $record->classname;
685                 $task = self::scheduled_task_from_record($record);
686                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
687                 if (!$task) {
688                     $lock->release();
689                     continue;
690                 }
692                 $task->set_lock($lock);
694                 // See if the component is disabled.
695                 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
697                 if ($plugininfo) {
698                     if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
699                         $lock->release();
700                         continue;
701                     }
702                 }
704                 // Make sure the task data is unchanged.
705                 if (!$DB->record_exists('task_scheduled', (array) $record)) {
706                     $lock->release();
707                     continue;
708                 }
710                 // The global cron lock is under the most contention so request it
711                 // as late as possible and release it as soon as possible.
712                 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
713                     $lock->release();
714                     throw new \moodle_exception('locktimeout');
715                 }
717                 if (!$task->is_blocking()) {
718                     $cronlock->release();
719                 } else {
720                     $task->set_cron_lock($cronlock);
721                 }
722                 return $task;
723             }
724         }
726         return null;
727     }
729     /**
730      * This function indicates that an adhoc task was not completed successfully and should be retried.
731      *
732      * @param \core\task\adhoc_task $task
733      */
734     public static function adhoc_task_failed(adhoc_task $task) {
735         global $DB;
736         // Finalise the log output.
737         logmanager::finalise_log(true);
739         $delay = $task->get_fail_delay();
741         // Reschedule task with exponential fall off for failing tasks.
742         if (empty($delay)) {
743             $delay = 60;
744         } else {
745             $delay *= 2;
746         }
748         // Max of 24 hour delay.
749         if ($delay > 86400) {
750             $delay = 86400;
751         }
753         // Reschedule and then release the locks.
754         $task->set_timestarted();
755         $task->set_hostname();
756         $task->set_pid();
757         $task->set_next_run_time(time() + $delay);
758         $task->set_fail_delay($delay);
759         $record = self::record_from_adhoc_task($task);
760         $DB->update_record('task_adhoc', $record);
762         $task->release_concurrency_lock();
763         if ($task->is_blocking()) {
764             $task->get_cron_lock()->release();
765         }
766         $task->get_lock()->release();
767     }
769     /**
770      * Records that a adhoc task is starting to run.
771      *
772      * @param adhoc_task $task Task that is starting
773      * @param int $time Start time (leave blank for now)
774      * @throws \dml_exception
775      * @throws \coding_exception
776      */
777     public static function adhoc_task_starting(adhoc_task $task, int $time = 0) {
778         global $DB;
779         $pid = (int)getmypid();
780         $hostname = (string)gethostname();
782         if (empty($time)) {
783             $time = time();
784         }
786         $task->set_timestarted($time);
787         $task->set_hostname($hostname);
788         $task->set_pid($pid);
790         $record = self::record_from_adhoc_task($task);
791         $DB->update_record('task_adhoc', $record);
792     }
794     /**
795      * This function indicates that an adhoc task was completed successfully.
796      *
797      * @param \core\task\adhoc_task $task
798      */
799     public static function adhoc_task_complete(adhoc_task $task) {
800         global $DB;
802         // Finalise the log output.
803         logmanager::finalise_log();
804         $task->set_timestarted();
805         $task->set_hostname();
806         $task->set_pid();
808         // Delete the adhoc task record - it is finished.
809         $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
811         // Release the locks.
812         $task->release_concurrency_lock();
813         if ($task->is_blocking()) {
814             $task->get_cron_lock()->release();
815         }
816         $task->get_lock()->release();
817     }
819     /**
820      * This function indicates that a scheduled task was not completed successfully and should be retried.
821      *
822      * @param \core\task\scheduled_task $task
823      */
824     public static function scheduled_task_failed(scheduled_task $task) {
825         global $DB;
826         // Finalise the log output.
827         logmanager::finalise_log(true);
829         $delay = $task->get_fail_delay();
831         // Reschedule task with exponential fall off for failing tasks.
832         if (empty($delay)) {
833             $delay = 60;
834         } else {
835             $delay *= 2;
836         }
838         // Max of 24 hour delay.
839         if ($delay > 86400) {
840             $delay = 86400;
841         }
843         $task->set_timestarted();
844         $task->set_hostname();
845         $task->set_pid();
847         $classname = self::get_canonical_class_name($task);
849         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
850         $record->nextruntime = time() + $delay;
851         $record->faildelay = $delay;
852         $record->timestarted = null;
853         $record->hostname = null;
854         $record->pid = null;
855         $DB->update_record('task_scheduled', $record);
857         if ($task->is_blocking()) {
858             $task->get_cron_lock()->release();
859         }
860         $task->get_lock()->release();
861     }
863     /**
864      * Clears the fail delay for the given task and updates its next run time based on the schedule.
865      *
866      * @param scheduled_task $task Task to reset
867      * @throws \dml_exception If there is a database error
868      */
869     public static function clear_fail_delay(scheduled_task $task) {
870         global $DB;
872         $record = new \stdClass();
873         $record->id = $DB->get_field('task_scheduled', 'id',
874                 ['classname' => self::get_canonical_class_name($task)]);
875         $record->nextruntime = $task->get_next_scheduled_time();
876         $record->faildelay = 0;
877         $DB->update_record('task_scheduled', $record);
878     }
880     /**
881      * Records that a scheduled task is starting to run.
882      *
883      * @param scheduled_task $task Task that is starting
884      * @param int $time Start time (0 = current)
885      * @throws \dml_exception If the task doesn't exist
886      */
887     public static function scheduled_task_starting(scheduled_task $task, int $time = 0) {
888         global $DB;
889         $pid = (int)getmypid();
890         $hostname = (string)gethostname();
892         if (!$time) {
893             $time = time();
894         }
896         $task->set_timestarted($time);
897         $task->set_hostname($hostname);
898         $task->set_pid($pid);
900         $classname = self::get_canonical_class_name($task);
901         $record = $DB->get_record('task_scheduled', ['classname' => $classname], '*', MUST_EXIST);
902         $record->timestarted = $time;
903         $record->hostname = $hostname;
904         $record->pid = $pid;
905         $DB->update_record('task_scheduled', $record);
906     }
908     /**
909      * This function indicates that a scheduled task was completed successfully and should be rescheduled.
910      *
911      * @param \core\task\scheduled_task $task
912      */
913     public static function scheduled_task_complete(scheduled_task $task) {
914         global $DB;
916         // Finalise the log output.
917         logmanager::finalise_log();
918         $task->set_timestarted();
919         $task->set_hostname();
920         $task->set_pid();
922         $classname = self::get_canonical_class_name($task);
923         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
924         if ($record) {
925             $record->lastruntime = time();
926             $record->faildelay = 0;
927             $record->nextruntime = $task->get_next_scheduled_time();
928             $record->timestarted = null;
929             $record->hostname = null;
930             $record->pid = null;
932             $DB->update_record('task_scheduled', $record);
933         }
935         // Reschedule and then release the locks.
936         if ($task->is_blocking()) {
937             $task->get_cron_lock()->release();
938         }
939         $task->get_lock()->release();
940     }
942     /**
943      * Gets a list of currently-running tasks.
944      *
945      * @param  string $sort Sorting method
946      * @return array Array of scheduled and adhoc tasks
947      * @throws \dml_exception
948      */
949     public static function get_running_tasks($sort = ''): array {
950         global $DB;
951         if (empty($sort)) {
952             $sort = 'timestarted ASC, classname ASC';
953         }
954         $params = ['now1' => time(), 'now2' => time()];
956         $sql = "SELECT subquery.*
957                   FROM (SELECT concat('s', ts.id) as uniqueid,
958                                ts.id,
959                                'scheduled' as type,
960                                ts.classname,
961                                (:now1 - ts.timestarted) as time,
962                                ts.timestarted,
963                                ts.hostname,
964                                ts.pid
965                           FROM {task_scheduled} ts
966                          WHERE ts.timestarted IS NOT NULL
967                          UNION ALL
968                         SELECT concat('a', ta.id) as uniqueid,
969                                ta.id,
970                                'adhoc' as type,
971                                ta.classname,
972                                (:now2 - ta.timestarted) as time,
973                                ta.timestarted,
974                                ta.hostname,
975                                ta.pid
976                           FROM {task_adhoc} ta
977                          WHERE ta.timestarted IS NOT NULL) subquery
978               ORDER BY " . $sort;
980         return $DB->get_records_sql($sql, $params);
981     }
983     /**
984      * This function is used to indicate that any long running cron processes should exit at the
985      * next opportunity and restart. This is because something (e.g. DB changes) has changed and
986      * the static caches may be stale.
987      */
988     public static function clear_static_caches() {
989         global $DB;
990         // Do not use get/set config here because the caches cannot be relied on.
991         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
992         if ($record) {
993             $record->value = time();
994             $DB->update_record('config', $record);
995         } else {
996             $record = new \stdClass();
997             $record->name = 'scheduledtaskreset';
998             $record->value = time();
999             $DB->insert_record('config', $record);
1000         }
1001     }
1003     /**
1004      * Return true if the static caches have been cleared since $starttime.
1005      * @param int $starttime The time this process started.
1006      * @return boolean True if static caches need resetting.
1007      */
1008     public static function static_caches_cleared_since($starttime) {
1009         global $DB;
1010         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
1011         return $record && (intval($record->value) > $starttime);
1012     }
1014     /**
1015      * Gets class name for use in database table. Always begins with a \.
1016      *
1017      * @param string|task_base $taskorstring Task object or a string
1018      */
1019     protected static function get_canonical_class_name($taskorstring) {
1020         if (is_string($taskorstring)) {
1021             $classname = $taskorstring;
1022         } else {
1023             $classname = get_class($taskorstring);
1024         }
1025         if (strpos($classname, '\\') !== 0) {
1026             $classname = '\\' . $classname;
1027         }
1028         return $classname;
1029     }
1031     /**
1032      * Gets the concurrent lock required to run an adhoc task.
1033      *
1034      * @param   adhoc_task $task The task to obtain the lock for
1035      * @return  \core\lock\lock The lock if one was obtained successfully
1036      * @throws  \coding_exception
1037      */
1038     protected static function get_concurrent_task_lock(adhoc_task $task): ?\core\lock\lock {
1039         $adhoclock = null;
1040         $cronlockfactory = \core\lock\lock_config::get_lock_factory(get_class($task));
1042         for ($run = 0; $run < $task->get_concurrency_limit(); $run++) {
1043             if ($adhoclock = $cronlockfactory->get_lock("concurrent_run_{$run}", 0)) {
1044                 return $adhoclock;
1045             }
1046         }
1048         return null;
1049     }
1051     /**
1052      * Find the path of PHP CLI binary.
1053      *
1054      * @return string|false The PHP CLI executable PATH
1055      */
1056     protected static function find_php_cli_path() {
1057         global $CFG;
1059         if (!empty($CFG->pathtophp) && is_executable(trim($CFG->pathtophp))) {
1060             return $CFG->pathtophp;
1061         }
1063         return false;
1064     }
1066     /**
1067      * Returns if Moodle have access to PHP CLI binary or not.
1068      *
1069      * @return bool
1070      */
1071     public static function is_runnable():bool {
1072         return self::find_php_cli_path() !== false;
1073     }
1075     /**
1076      * Executes a cron from web invocation using PHP CLI.
1077      *
1078      * @param \core\task\task_base $task Task that be executed via CLI.
1079      * @return bool
1080      * @throws \moodle_exception
1081      */
1082     public static function run_from_cli(\core\task\task_base $task):bool {
1083         global $CFG;
1085         if (!self::is_runnable()) {
1086             $redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
1087             throw new \moodle_exception('cannotfindthepathtothecli', 'core_task', $redirecturl->out());
1088         } else {
1089             // Shell-escaped path to the PHP binary.
1090             $phpbinary = escapeshellarg(self::find_php_cli_path());
1092             // Shell-escaped path CLI script.
1093             $pathcomponents = [$CFG->dirroot, $CFG->admin, 'cli', 'scheduled_task.php'];
1094             $scriptpath     = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents));
1096             // Shell-escaped task name.
1097             $classname = get_class($task);
1098             $taskarg   = escapeshellarg("--execute={$classname}") . " " . escapeshellarg("--force");
1100             // Build the CLI command.
1101             $command = "{$phpbinary} {$scriptpath} {$taskarg}";
1103             // Execute it.
1104             passthru($command);
1105         }
1107         return true;
1108     }