Merge branch 'master_MDL-67485' 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      * @return \core\task\scheduled_task[] - List of scheduled tasks for this component.
44      */
45     public static function load_default_scheduled_tasks_for_component($componentname) {
46         $dir = \core_component::get_component_directory($componentname);
48         if (!$dir) {
49             return array();
50         }
52         $file = $dir . '/' . CORE_TASK_TASKS_FILENAME;
53         if (!file_exists($file)) {
54             return array();
55         }
57         $tasks = null;
58         include($file);
60         if (!isset($tasks)) {
61             return array();
62         }
64         $scheduledtasks = array();
66         foreach ($tasks as $task) {
67             $record = (object) $task;
68             $scheduledtask = self::scheduled_task_from_record($record);
69             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
70             if ($scheduledtask) {
71                 $scheduledtask->set_component($componentname);
72                 $scheduledtasks[] = $scheduledtask;
73             }
74         }
76         return $scheduledtasks;
77     }
79     /**
80      * Update the database to contain a list of scheduled task for a component.
81      * The list of scheduled tasks is taken from @load_scheduled_tasks_for_component.
82      * Will throw exceptions for any errors.
83      *
84      * @param string $componentname - The frankenstyle component name.
85      */
86     public static function reset_scheduled_tasks_for_component($componentname) {
87         global $DB;
88         $tasks = self::load_default_scheduled_tasks_for_component($componentname);
89         $validtasks = array();
91         foreach ($tasks as $taskid => $task) {
92             $classname = self::get_canonical_class_name($task);
94             $validtasks[] = $classname;
96             if ($currenttask = self::get_scheduled_task($classname)) {
97                 if ($currenttask->is_customised()) {
98                     // If there is an existing task with a custom schedule, do not override it.
99                     continue;
100                 }
102                 // Update the record from the default task data.
103                 self::configure_scheduled_task($task);
104             } else {
105                 // Ensure that the first run follows the schedule.
106                 $task->set_next_run_time($task->get_next_scheduled_time());
108                 // Insert the new task in the database.
109                 $record = self::record_from_scheduled_task($task);
110                 $DB->insert_record('task_scheduled', $record);
111             }
112         }
114         // Delete any task that is not defined in the component any more.
115         $sql = "component = :component";
116         $params = array('component' => $componentname);
117         if (!empty($validtasks)) {
118             list($insql, $inparams) = $DB->get_in_or_equal($validtasks, SQL_PARAMS_NAMED, 'param', false);
119             $sql .= ' AND classname ' . $insql;
120             $params = array_merge($params, $inparams);
121         }
122         $DB->delete_records_select('task_scheduled', $sql, $params);
123     }
125     /**
126      * Checks if the task with the same classname, component and customdata is already scheduled
127      *
128      * @param adhoc_task $task
129      * @return bool
130      */
131     protected static function task_is_scheduled($task) {
132         return false !== self::get_queued_adhoc_task_record($task);
133     }
135     /**
136      * Checks if the task with the same classname, component and customdata is already scheduled
137      *
138      * @param adhoc_task $task
139      * @return bool
140      */
141     protected static function get_queued_adhoc_task_record($task) {
142         global $DB;
144         $record = self::record_from_adhoc_task($task);
145         $params = [$record->classname, $record->component, $record->customdata];
146         $sql = 'classname = ? AND component = ? AND ' .
147             $DB->sql_compare_text('customdata', \core_text::strlen($record->customdata) + 1) . ' = ?';
149         if ($record->userid) {
150             $params[] = $record->userid;
151             $sql .= " AND userid = ? ";
152         }
153         return $DB->get_record_select('task_adhoc', $sql, $params);
154     }
156     /**
157      * Schedule a new task, or reschedule an existing adhoc task which has matching data.
158      *
159      * Only a task matching the same user, classname, component, and customdata will be rescheduled.
160      * If these values do not match exactly then a new task is scheduled.
161      *
162      * @param \core\task\adhoc_task $task - The new adhoc task information to store.
163      * @since Moodle 3.7
164      */
165     public static function reschedule_or_queue_adhoc_task(adhoc_task $task) : void {
166         global $DB;
168         if ($existingrecord = self::get_queued_adhoc_task_record($task)) {
169             // Only update the next run time if it is explicitly set on the task.
170             $nextruntime = $task->get_next_run_time();
171             if ($nextruntime && ($existingrecord->nextruntime != $nextruntime)) {
172                 $DB->set_field('task_adhoc', 'nextruntime', $nextruntime, ['id' => $existingrecord->id]);
173             }
174         } else {
175             // There is nothing queued yet. Just queue as normal.
176             self::queue_adhoc_task($task);
177         }
178     }
180     /**
181      * Queue an adhoc task to run in the background.
182      *
183      * @param \core\task\adhoc_task $task - The new adhoc task information to store.
184      * @param bool $checkforexisting - If set to true and the task with the same user, classname, component and customdata
185      *     is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks.
186      * @return boolean - True if the config was saved.
187      */
188     public static function queue_adhoc_task(adhoc_task $task, $checkforexisting = false) {
189         global $DB;
191         if ($userid = $task->get_userid()) {
192             // User found. Check that they are suitable.
193             \core_user::require_active_user(\core_user::get_user($userid, '*', MUST_EXIST), true, true);
194         }
196         $record = self::record_from_adhoc_task($task);
197         // Schedule it immediately if nextruntime not explicitly set.
198         if (!$task->get_next_run_time()) {
199             $record->nextruntime = time() - 1;
200         }
202         // Check if the same task is already scheduled.
203         if ($checkforexisting && self::task_is_scheduled($task)) {
204             return false;
205         }
207         // Queue the task.
208         $result = $DB->insert_record('task_adhoc', $record);
210         return $result;
211     }
213     /**
214      * Change the default configuration for a scheduled task.
215      * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
216      *
217      * @param \core\task\scheduled_task $task - The new scheduled task information to store.
218      * @return boolean - True if the config was saved.
219      */
220     public static function configure_scheduled_task(scheduled_task $task) {
221         global $DB;
223         $classname = self::get_canonical_class_name($task);
225         $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
227         $record = self::record_from_scheduled_task($task);
228         $record->id = $original->id;
229         $record->nextruntime = $task->get_next_scheduled_time();
230         $result = $DB->update_record('task_scheduled', $record);
232         return $result;
233     }
235     /**
236      * Utility method to create a DB record from a scheduled task.
237      *
238      * @param \core\task\scheduled_task $task
239      * @return \stdClass
240      */
241     public static function record_from_scheduled_task($task) {
242         $record = new \stdClass();
243         $record->classname = self::get_canonical_class_name($task);
244         $record->component = $task->get_component();
245         $record->blocking = $task->is_blocking();
246         $record->customised = $task->is_customised();
247         $record->lastruntime = $task->get_last_run_time();
248         $record->nextruntime = $task->get_next_run_time();
249         $record->faildelay = $task->get_fail_delay();
250         $record->hour = $task->get_hour();
251         $record->minute = $task->get_minute();
252         $record->day = $task->get_day();
253         $record->dayofweek = $task->get_day_of_week();
254         $record->month = $task->get_month();
255         $record->disabled = $task->get_disabled();
257         return $record;
258     }
260     /**
261      * Utility method to create a DB record from an adhoc task.
262      *
263      * @param \core\task\adhoc_task $task
264      * @return \stdClass
265      */
266     public static function record_from_adhoc_task($task) {
267         $record = new \stdClass();
268         $record->classname = self::get_canonical_class_name($task);
269         $record->id = $task->get_id();
270         $record->component = $task->get_component();
271         $record->blocking = $task->is_blocking();
272         $record->nextruntime = $task->get_next_run_time();
273         $record->faildelay = $task->get_fail_delay();
274         $record->customdata = $task->get_custom_data_as_string();
275         $record->userid = $task->get_userid();
277         return $record;
278     }
280     /**
281      * Utility method to create an adhoc task from a DB record.
282      *
283      * @param \stdClass $record
284      * @return \core\task\adhoc_task
285      */
286     public static function adhoc_task_from_record($record) {
287         $classname = self::get_canonical_class_name($record->classname);
288         if (!class_exists($classname)) {
289             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
290             return false;
291         }
292         $task = new $classname;
293         if (isset($record->nextruntime)) {
294             $task->set_next_run_time($record->nextruntime);
295         }
296         if (isset($record->id)) {
297             $task->set_id($record->id);
298         }
299         if (isset($record->component)) {
300             $task->set_component($record->component);
301         }
302         $task->set_blocking(!empty($record->blocking));
303         if (isset($record->faildelay)) {
304             $task->set_fail_delay($record->faildelay);
305         }
306         if (isset($record->customdata)) {
307             $task->set_custom_data_as_string($record->customdata);
308         }
310         if (isset($record->userid)) {
311             $task->set_userid($record->userid);
312         }
314         return $task;
315     }
317     /**
318      * Utility method to create a task from a DB record.
319      *
320      * @param \stdClass $record
321      * @return \core\task\scheduled_task
322      */
323     public static function scheduled_task_from_record($record) {
324         $classname = self::get_canonical_class_name($record->classname);
325         if (!class_exists($classname)) {
326             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
327             return false;
328         }
329         /** @var \core\task\scheduled_task $task */
330         $task = new $classname;
331         if (isset($record->lastruntime)) {
332             $task->set_last_run_time($record->lastruntime);
333         }
334         if (isset($record->nextruntime)) {
335             $task->set_next_run_time($record->nextruntime);
336         }
337         if (isset($record->customised)) {
338             $task->set_customised($record->customised);
339         }
340         if (isset($record->component)) {
341             $task->set_component($record->component);
342         }
343         $task->set_blocking(!empty($record->blocking));
344         if (isset($record->minute)) {
345             $task->set_minute($record->minute);
346         }
347         if (isset($record->hour)) {
348             $task->set_hour($record->hour);
349         }
350         if (isset($record->day)) {
351             $task->set_day($record->day);
352         }
353         if (isset($record->month)) {
354             $task->set_month($record->month);
355         }
356         if (isset($record->dayofweek)) {
357             $task->set_day_of_week($record->dayofweek);
358         }
359         if (isset($record->faildelay)) {
360             $task->set_fail_delay($record->faildelay);
361         }
362         if (isset($record->disabled)) {
363             $task->set_disabled($record->disabled);
364         }
366         return $task;
367     }
369     /**
370      * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
371      * Do not execute tasks loaded from this function - they have not been locked.
372      * @param string $componentname - The name of the component to load the tasks for.
373      * @return \core\task\scheduled_task[]
374      */
375     public static function load_scheduled_tasks_for_component($componentname) {
376         global $DB;
378         $tasks = array();
379         // We are just reading - so no locks required.
380         $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
381         foreach ($records as $record) {
382             $task = self::scheduled_task_from_record($record);
383             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
384             if ($task) {
385                 $tasks[] = $task;
386             }
387         }
389         return $tasks;
390     }
392     /**
393      * This function load the scheduled task details for a given classname.
394      *
395      * @param string $classname
396      * @return \core\task\scheduled_task or false
397      */
398     public static function get_scheduled_task($classname) {
399         global $DB;
401         $classname = self::get_canonical_class_name($classname);
402         // We are just reading - so no locks required.
403         $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
404         if (!$record) {
405             return false;
406         }
407         return self::scheduled_task_from_record($record);
408     }
410     /**
411      * This function load the adhoc tasks for a given classname.
412      *
413      * @param string $classname
414      * @return \core\task\adhoc_task[]
415      */
416     public static function get_adhoc_tasks($classname) {
417         global $DB;
419         $classname = self::get_canonical_class_name($classname);
420         // We are just reading - so no locks required.
421         $records = $DB->get_records('task_adhoc', array('classname' => $classname));
423         return array_map(function($record) {
424             return self::adhoc_task_from_record($record);
425         }, $records);
426     }
428     /**
429      * This function load the default scheduled task details for a given classname.
430      *
431      * @param string $classname
432      * @return \core\task\scheduled_task or false
433      */
434     public static function get_default_scheduled_task($classname) {
435         $task = self::get_scheduled_task($classname);
436         $componenttasks = array();
438         // Safety check in case no task was found for the given classname.
439         if ($task) {
440             $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
441         }
443         foreach ($componenttasks as $componenttask) {
444             if (get_class($componenttask) == get_class($task)) {
445                 return $componenttask;
446             }
447         }
449         return false;
450     }
452     /**
453      * This function will return a list of all the scheduled tasks that exist in the database.
454      *
455      * @return \core\task\scheduled_task[]
456      */
457     public static function get_all_scheduled_tasks() {
458         global $DB;
460         $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
461         $tasks = array();
463         foreach ($records as $record) {
464             $task = self::scheduled_task_from_record($record);
465             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
466             if ($task) {
467                 $tasks[] = $task;
468             }
469         }
471         return $tasks;
472     }
474     /**
475      * Ensure quality of service for the ad hoc task queue.
476      *
477      * This reshuffles the adhoc tasks queue to balance by type to ensure a
478      * level of quality of service per type, while still maintaining the
479      * relative order of tasks queued by timestamp.
480      *
481      * @param array $records array of task records
482      * @param array $records array of same task records shuffled
483      */
484     public static function ensure_adhoc_task_qos(array $records): array {
486         $count = count($records);
487         if ($count == 0) {
488             return $records;
489         }
491         $queues = []; // This holds a queue for each type of adhoc task.
492         $limits = []; // The relative limits of each type of task.
493         $limittotal = 0;
495         // Split the single queue up into queues per type.
496         foreach ($records as $record) {
497             $type = $record->classname;
498             if (!array_key_exists($type, $queues)) {
499                 $queues[$type] = [];
500             }
501             if (!array_key_exists($type, $limits)) {
502                 $limits[$type] = 1;
503                 $limittotal += 1;
504             }
505             $queues[$type][] = $record;
506         }
508         $qos = []; // Our new queue with ensured quality of service.
509         $seed = $count % $limittotal; // Which task queue to shuffle from first?
511         $move = 1; // How many tasks to shuffle at a time.
512         do {
513             $shuffled = 0;
515             // Now cycle through task type queues and interleaving the tasks
516             // back into a single queue.
517             foreach ($limits as $type => $limit) {
519                 // Just interleaving the queue is not enough, because after
520                 // any task is processed the whole queue is rebuilt again. So
521                 // we need to deterministically start on different types of
522                 // tasks so that *on average* we rotate through each type of task.
523                 //
524                 // We achieve this by using a $seed to start moving tasks off a
525                 // different queue each time. The seed is based on the task count
526                 // modulo the number of types of tasks on the queue. As we count
527                 // down this naturally cycles through each type of record.
528                 if ($seed < 1) {
529                     $shuffled = 1;
530                     $seed += 1;
531                     continue;
532                 }
533                 $tasks = array_splice($queues[$type], 0, $move);
534                 $qos = array_merge($qos, $tasks);
536                 // Stop if we didn't move any tasks onto the main queue.
537                 $shuffled += count($tasks);
538             }
539             // Generally the only tasks that matter are those that are near the start so
540             // after we have shuffled the first few 1 by 1, start shuffling larger groups.
541             if (count($qos) >= (4 * count($limits))) {
542                 $move *= 2;
543             }
544         } while ($shuffled > 0);
546         return $qos;
547     }
549     /**
550      * This function will dispatch the next adhoc task in the queue. The task will be handed out
551      * with an open lock - possibly on the entire cron process. Make sure you call either
552      * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
553      *
554      * @param int $timestart
555      * @param bool $checklimits Should we check limits?
556      * @return \core\task\adhoc_task or null if not found
557      * @throws \moodle_exception
558      */
559     public static function get_next_adhoc_task($timestart, $checklimits = true) {
560         global $DB;
562         $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
563         $params = array('timestart1' => $timestart);
564         $records = $DB->get_records_select('task_adhoc', $where, $params);
566         $records = self::ensure_adhoc_task_qos($records);
568         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
569         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
570             throw new \moodle_exception('locktimeout');
571         }
573         $skipclasses = array();
575         foreach ($records as $record) {
577             if (in_array($record->classname, $skipclasses)) {
578                 // Skip the task if it can't be started due to per-task concurrency limit.
579                 continue;
580             }
582             if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) {
584                 // Safety check, see if the task has been already processed by another cron run.
585                 $record = $DB->get_record('task_adhoc', array('id' => $record->id));
586                 if (!$record) {
587                     $lock->release();
588                     continue;
589                 }
591                 $task = self::adhoc_task_from_record($record);
592                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
593                 if (!$task) {
594                     $lock->release();
595                     continue;
596                 }
598                 $tasklimit = $task->get_concurrency_limit();
599                 if ($checklimits && $tasklimit > 0) {
600                     if ($concurrencylock = self::get_concurrent_task_lock($task)) {
601                         $task->set_concurrency_lock($concurrencylock);
602                     } else {
603                         // Unable to obtain a concurrency lock.
604                         mtrace("Skipping $record->classname adhoc task class as the per-task limit of $tasklimit is reached.");
605                         $skipclasses[] = $record->classname;
606                         $lock->release();
607                         continue;
608                     }
609                 }
611                 $task->set_lock($lock);
612                 if (!$task->is_blocking()) {
613                     $cronlock->release();
614                 } else {
615                     $task->set_cron_lock($cronlock);
616                 }
617                 return $task;
618             }
619         }
621         // No tasks.
622         $cronlock->release();
623         return null;
624     }
626     /**
627      * This function will dispatch the next scheduled task in the queue. The task will be handed out
628      * with an open lock - possibly on the entire cron process. Make sure you call either
629      * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
630      *
631      * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
632      * @return \core\task\scheduled_task or null
633      * @throws \moodle_exception
634      */
635     public static function get_next_scheduled_task($timestart) {
636         global $DB;
637         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
639         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
640             throw new \moodle_exception('locktimeout');
641         }
643         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
644                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
645                   AND disabled = 0
646                   ORDER BY lastruntime, id ASC";
647         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
648         $records = $DB->get_records_select('task_scheduled', $where, $params);
650         $pluginmanager = \core_plugin_manager::instance();
652         foreach ($records as $record) {
654             if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
655                 $classname = '\\' . $record->classname;
656                 $task = self::scheduled_task_from_record($record);
657                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
658                 if (!$task) {
659                     $lock->release();
660                     continue;
661                 }
663                 $task->set_lock($lock);
665                 // See if the component is disabled.
666                 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
668                 if ($plugininfo) {
669                     if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
670                         $lock->release();
671                         continue;
672                     }
673                 }
675                 // Make sure the task data is unchanged.
676                 if (!$DB->record_exists('task_scheduled', (array) $record)) {
677                     $lock->release();
678                     continue;
679                 }
681                 if (!$task->is_blocking()) {
682                     $cronlock->release();
683                 } else {
684                     $task->set_cron_lock($cronlock);
685                 }
686                 return $task;
687             }
688         }
690         // No tasks.
691         $cronlock->release();
692         return null;
693     }
695     /**
696      * This function indicates that an adhoc task was not completed successfully and should be retried.
697      *
698      * @param \core\task\adhoc_task $task
699      */
700     public static function adhoc_task_failed(adhoc_task $task) {
701         global $DB;
702         $delay = $task->get_fail_delay();
704         // Reschedule task with exponential fall off for failing tasks.
705         if (empty($delay)) {
706             $delay = 60;
707         } else {
708             $delay *= 2;
709         }
711         // Max of 24 hour delay.
712         if ($delay > 86400) {
713             $delay = 86400;
714         }
716         // Reschedule and then release the locks.
717         $task->set_next_run_time(time() + $delay);
718         $task->set_fail_delay($delay);
719         $record = self::record_from_adhoc_task($task);
720         $DB->update_record('task_adhoc', $record);
722         $task->release_concurrency_lock();
723         if ($task->is_blocking()) {
724             $task->get_cron_lock()->release();
725         }
726         $task->get_lock()->release();
728         // Finalise the log output.
729         logmanager::finalise_log(true);
730     }
732     /**
733      * This function indicates that an adhoc task was completed successfully.
734      *
735      * @param \core\task\adhoc_task $task
736      */
737     public static function adhoc_task_complete(adhoc_task $task) {
738         global $DB;
740         // Finalise the log output.
741         logmanager::finalise_log();
743         // Delete the adhoc task record - it is finished.
744         $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
746         // Release the locks.
747         $task->release_concurrency_lock();
748         if ($task->is_blocking()) {
749             $task->get_cron_lock()->release();
750         }
751         $task->get_lock()->release();
752     }
754     /**
755      * This function indicates that a scheduled task was not completed successfully and should be retried.
756      *
757      * @param \core\task\scheduled_task $task
758      */
759     public static function scheduled_task_failed(scheduled_task $task) {
760         global $DB;
762         $delay = $task->get_fail_delay();
764         // Reschedule task with exponential fall off for failing tasks.
765         if (empty($delay)) {
766             $delay = 60;
767         } else {
768             $delay *= 2;
769         }
771         // Max of 24 hour delay.
772         if ($delay > 86400) {
773             $delay = 86400;
774         }
776         $classname = self::get_canonical_class_name($task);
778         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
779         $record->nextruntime = time() + $delay;
780         $record->faildelay = $delay;
781         $DB->update_record('task_scheduled', $record);
783         if ($task->is_blocking()) {
784             $task->get_cron_lock()->release();
785         }
786         $task->get_lock()->release();
788         // Finalise the log output.
789         logmanager::finalise_log(true);
790     }
792     /**
793      * Clears the fail delay for the given task and updates its next run time based on the schedule.
794      *
795      * @param scheduled_task $task Task to reset
796      * @throws \dml_exception If there is a database error
797      */
798     public static function clear_fail_delay(scheduled_task $task) {
799         global $DB;
801         $record = new \stdClass();
802         $record->id = $DB->get_field('task_scheduled', 'id',
803                 ['classname' => self::get_canonical_class_name($task)]);
804         $record->nextruntime = $task->get_next_scheduled_time();
805         $record->faildelay = 0;
806         $DB->update_record('task_scheduled', $record);
807     }
809     /**
810      * This function indicates that a scheduled task was completed successfully and should be rescheduled.
811      *
812      * @param \core\task\scheduled_task $task
813      */
814     public static function scheduled_task_complete(scheduled_task $task) {
815         global $DB;
817         // Finalise the log output.
818         logmanager::finalise_log();
820         $classname = self::get_canonical_class_name($task);
821         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
822         if ($record) {
823             $record->lastruntime = time();
824             $record->faildelay = 0;
825             $record->nextruntime = $task->get_next_scheduled_time();
827             $DB->update_record('task_scheduled', $record);
828         }
830         // Reschedule and then release the locks.
831         if ($task->is_blocking()) {
832             $task->get_cron_lock()->release();
833         }
834         $task->get_lock()->release();
835     }
837     /**
838      * This function is used to indicate that any long running cron processes should exit at the
839      * next opportunity and restart. This is because something (e.g. DB changes) has changed and
840      * the static caches may be stale.
841      */
842     public static function clear_static_caches() {
843         global $DB;
844         // Do not use get/set config here because the caches cannot be relied on.
845         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
846         if ($record) {
847             $record->value = time();
848             $DB->update_record('config', $record);
849         } else {
850             $record = new \stdClass();
851             $record->name = 'scheduledtaskreset';
852             $record->value = time();
853             $DB->insert_record('config', $record);
854         }
855     }
857     /**
858      * Return true if the static caches have been cleared since $starttime.
859      * @param int $starttime The time this process started.
860      * @return boolean True if static caches need resetting.
861      */
862     public static function static_caches_cleared_since($starttime) {
863         global $DB;
864         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
865         return $record && (intval($record->value) > $starttime);
866     }
868     /**
869      * Gets class name for use in database table. Always begins with a \.
870      *
871      * @param string|task_base $taskorstring Task object or a string
872      */
873     protected static function get_canonical_class_name($taskorstring) {
874         if (is_string($taskorstring)) {
875             $classname = $taskorstring;
876         } else {
877             $classname = get_class($taskorstring);
878         }
879         if (strpos($classname, '\\') !== 0) {
880             $classname = '\\' . $classname;
881         }
882         return $classname;
883     }
885     /**
886      * Gets the concurrent lock required to run an adhoc task.
887      *
888      * @param   adhoc_task $task The task to obtain the lock for
889      * @return  \core\lock\lock The lock if one was obtained successfully
890      * @throws  \coding_exception
891      */
892     protected static function get_concurrent_task_lock(adhoc_task $task): ?\core\lock\lock {
893         $adhoclock = null;
894         $cronlockfactory = \core\lock\lock_config::get_lock_factory(get_class($task));
896         for ($run = 0; $run < $task->get_concurrency_limit(); $run++) {
897             if ($adhoclock = $cronlockfactory->get_lock("concurrent_run_{$run}", 0)) {
898                 return $adhoclock;
899             }
900         }
902         return null;
903     }