MDL-49399 task: Add admin log viewer
[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         global $DB;
133         $record = self::record_from_adhoc_task($task);
134         $params = [$record->classname, $record->component, $record->customdata];
135         $sql = 'classname = ? AND component = ? AND ' .
136             $DB->sql_compare_text('customdata', \core_text::strlen($record->customdata) + 1) . ' = ?';
138         if ($record->userid) {
139             $params[] = $record->userid;
140             $sql .= " AND userid = ? ";
141         }
142         return $DB->record_exists_select('task_adhoc', $sql, $params);
143     }
145     /**
146      * Queue an adhoc task to run in the background.
147      *
148      * @param \core\task\adhoc_task $task - The new adhoc task information to store.
149      * @param bool $checkforexisting - If set to true and the task with the same classname, component and customdata
150      *     is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks.
151      * @return boolean - True if the config was saved.
152      */
153     public static function queue_adhoc_task(adhoc_task $task, $checkforexisting = false) {
154         global $DB;
156         if ($userid = $task->get_userid()) {
157             // User found. Check that they are suitable.
158             \core_user::require_active_user(\core_user::get_user($userid, '*', MUST_EXIST), true, true);
159         }
161         $record = self::record_from_adhoc_task($task);
162         // Schedule it immediately if nextruntime not explicitly set.
163         if (!$task->get_next_run_time()) {
164             $record->nextruntime = time() - 1;
165         }
167         // Check if the same task is already scheduled.
168         if ($checkforexisting && self::task_is_scheduled($task)) {
169             return false;
170         }
172         // Queue the task.
173         $result = $DB->insert_record('task_adhoc', $record);
175         return $result;
176     }
178     /**
179      * Change the default configuration for a scheduled task.
180      * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
181      *
182      * @param \core\task\scheduled_task $task - The new scheduled task information to store.
183      * @return boolean - True if the config was saved.
184      */
185     public static function configure_scheduled_task(scheduled_task $task) {
186         global $DB;
188         $classname = self::get_canonical_class_name($task);
190         $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
192         $record = self::record_from_scheduled_task($task);
193         $record->id = $original->id;
194         $record->nextruntime = $task->get_next_scheduled_time();
195         $result = $DB->update_record('task_scheduled', $record);
197         return $result;
198     }
200     /**
201      * Utility method to create a DB record from a scheduled task.
202      *
203      * @param \core\task\scheduled_task $task
204      * @return \stdClass
205      */
206     public static function record_from_scheduled_task($task) {
207         $record = new \stdClass();
208         $record->classname = self::get_canonical_class_name($task);
209         $record->component = $task->get_component();
210         $record->blocking = $task->is_blocking();
211         $record->customised = $task->is_customised();
212         $record->lastruntime = $task->get_last_run_time();
213         $record->nextruntime = $task->get_next_run_time();
214         $record->faildelay = $task->get_fail_delay();
215         $record->hour = $task->get_hour();
216         $record->minute = $task->get_minute();
217         $record->day = $task->get_day();
218         $record->dayofweek = $task->get_day_of_week();
219         $record->month = $task->get_month();
220         $record->disabled = $task->get_disabled();
222         return $record;
223     }
225     /**
226      * Utility method to create a DB record from an adhoc task.
227      *
228      * @param \core\task\adhoc_task $task
229      * @return \stdClass
230      */
231     public static function record_from_adhoc_task($task) {
232         $record = new \stdClass();
233         $record->classname = self::get_canonical_class_name($task);
234         $record->id = $task->get_id();
235         $record->component = $task->get_component();
236         $record->blocking = $task->is_blocking();
237         $record->nextruntime = $task->get_next_run_time();
238         $record->faildelay = $task->get_fail_delay();
239         $record->customdata = $task->get_custom_data_as_string();
240         $record->userid = $task->get_userid();
242         return $record;
243     }
245     /**
246      * Utility method to create an adhoc task from a DB record.
247      *
248      * @param \stdClass $record
249      * @return \core\task\adhoc_task
250      */
251     public static function adhoc_task_from_record($record) {
252         $classname = self::get_canonical_class_name($record->classname);
253         if (!class_exists($classname)) {
254             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
255             return false;
256         }
257         $task = new $classname;
258         if (isset($record->nextruntime)) {
259             $task->set_next_run_time($record->nextruntime);
260         }
261         if (isset($record->id)) {
262             $task->set_id($record->id);
263         }
264         if (isset($record->component)) {
265             $task->set_component($record->component);
266         }
267         $task->set_blocking(!empty($record->blocking));
268         if (isset($record->faildelay)) {
269             $task->set_fail_delay($record->faildelay);
270         }
271         if (isset($record->customdata)) {
272             $task->set_custom_data_as_string($record->customdata);
273         }
275         if (isset($record->userid)) {
276             $task->set_userid($record->userid);
277         }
279         return $task;
280     }
282     /**
283      * Utility method to create a task from a DB record.
284      *
285      * @param \stdClass $record
286      * @return \core\task\scheduled_task
287      */
288     public static function scheduled_task_from_record($record) {
289         $classname = self::get_canonical_class_name($record->classname);
290         if (!class_exists($classname)) {
291             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
292             return false;
293         }
294         /** @var \core\task\scheduled_task $task */
295         $task = new $classname;
296         if (isset($record->lastruntime)) {
297             $task->set_last_run_time($record->lastruntime);
298         }
299         if (isset($record->nextruntime)) {
300             $task->set_next_run_time($record->nextruntime);
301         }
302         if (isset($record->customised)) {
303             $task->set_customised($record->customised);
304         }
305         if (isset($record->component)) {
306             $task->set_component($record->component);
307         }
308         $task->set_blocking(!empty($record->blocking));
309         if (isset($record->minute)) {
310             $task->set_minute($record->minute);
311         }
312         if (isset($record->hour)) {
313             $task->set_hour($record->hour);
314         }
315         if (isset($record->day)) {
316             $task->set_day($record->day);
317         }
318         if (isset($record->month)) {
319             $task->set_month($record->month);
320         }
321         if (isset($record->dayofweek)) {
322             $task->set_day_of_week($record->dayofweek);
323         }
324         if (isset($record->faildelay)) {
325             $task->set_fail_delay($record->faildelay);
326         }
327         if (isset($record->disabled)) {
328             $task->set_disabled($record->disabled);
329         }
331         return $task;
332     }
334     /**
335      * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
336      * Do not execute tasks loaded from this function - they have not been locked.
337      * @param string $componentname - The name of the component to load the tasks for.
338      * @return \core\task\scheduled_task[]
339      */
340     public static function load_scheduled_tasks_for_component($componentname) {
341         global $DB;
343         $tasks = array();
344         // We are just reading - so no locks required.
345         $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
346         foreach ($records as $record) {
347             $task = self::scheduled_task_from_record($record);
348             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
349             if ($task) {
350                 $tasks[] = $task;
351             }
352         }
354         return $tasks;
355     }
357     /**
358      * This function load the scheduled task details for a given classname.
359      *
360      * @param string $classname
361      * @return \core\task\scheduled_task or false
362      */
363     public static function get_scheduled_task($classname) {
364         global $DB;
366         $classname = self::get_canonical_class_name($classname);
367         // We are just reading - so no locks required.
368         $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
369         if (!$record) {
370             return false;
371         }
372         return self::scheduled_task_from_record($record);
373     }
375     /**
376      * This function load the adhoc tasks for a given classname.
377      *
378      * @param string $classname
379      * @return \core\task\adhoc_task[]
380      */
381     public static function get_adhoc_tasks($classname) {
382         global $DB;
384         $classname = self::get_canonical_class_name($classname);
385         // We are just reading - so no locks required.
386         $records = $DB->get_records('task_adhoc', array('classname' => $classname));
388         return array_map(function($record) {
389             return self::adhoc_task_from_record($record);
390         }, $records);
391     }
393     /**
394      * This function load the default scheduled task details for a given classname.
395      *
396      * @param string $classname
397      * @return \core\task\scheduled_task or false
398      */
399     public static function get_default_scheduled_task($classname) {
400         $task = self::get_scheduled_task($classname);
401         $componenttasks = array();
403         // Safety check in case no task was found for the given classname.
404         if ($task) {
405             $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
406         }
408         foreach ($componenttasks as $componenttask) {
409             if (get_class($componenttask) == get_class($task)) {
410                 return $componenttask;
411             }
412         }
414         return false;
415     }
417     /**
418      * This function will return a list of all the scheduled tasks that exist in the database.
419      *
420      * @return \core\task\scheduled_task[]
421      */
422     public static function get_all_scheduled_tasks() {
423         global $DB;
425         $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
426         $tasks = array();
428         foreach ($records as $record) {
429             $task = self::scheduled_task_from_record($record);
430             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
431             if ($task) {
432                 $tasks[] = $task;
433             }
434         }
436         return $tasks;
437     }
439     /**
440      * This function will dispatch the next adhoc task in the queue. The task will be handed out
441      * with an open lock - possibly on the entire cron process. Make sure you call either
442      * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
443      *
444      * @param int $timestart
445      * @return \core\task\adhoc_task or null if not found
446      */
447     public static function get_next_adhoc_task($timestart) {
448         global $DB;
449         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
451         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
452             throw new \moodle_exception('locktimeout');
453         }
455         $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
456         $params = array('timestart1' => $timestart);
457         $records = $DB->get_records_select('task_adhoc', $where, $params);
459         foreach ($records as $record) {
461             if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) {
462                 $classname = '\\' . $record->classname;
464                 // Safety check, see if the task has been already processed by another cron run.
465                 $record = $DB->get_record('task_adhoc', array('id' => $record->id));
466                 if (!$record) {
467                     $lock->release();
468                     continue;
469                 }
471                 $task = self::adhoc_task_from_record($record);
472                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
473                 if (!$task) {
474                     $lock->release();
475                     continue;
476                 }
478                 $task->set_lock($lock);
479                 if (!$task->is_blocking()) {
480                     $cronlock->release();
481                 } else {
482                     $task->set_cron_lock($cronlock);
483                 }
484                 return $task;
485             }
486         }
488         // No tasks.
489         $cronlock->release();
490         return null;
491     }
493     /**
494      * This function will dispatch the next scheduled task in the queue. The task will be handed out
495      * with an open lock - possibly on the entire cron process. Make sure you call either
496      * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
497      *
498      * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
499      * @return \core\task\scheduled_task or null
500      */
501     public static function get_next_scheduled_task($timestart) {
502         global $DB;
503         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
505         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
506             throw new \moodle_exception('locktimeout');
507         }
509         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
510                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
511                   AND disabled = 0
512                   ORDER BY lastruntime, id ASC";
513         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
514         $records = $DB->get_records_select('task_scheduled', $where, $params);
516         $pluginmanager = \core_plugin_manager::instance();
518         foreach ($records as $record) {
520             if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
521                 $classname = '\\' . $record->classname;
522                 $task = self::scheduled_task_from_record($record);
523                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
524                 if (!$task) {
525                     $lock->release();
526                     continue;
527                 }
529                 $task->set_lock($lock);
531                 // See if the component is disabled.
532                 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
534                 if ($plugininfo) {
535                     if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
536                         $lock->release();
537                         continue;
538                     }
539                 }
541                 // Make sure the task data is unchanged.
542                 if (!$DB->record_exists('task_scheduled', (array) $record)) {
543                     $lock->release();
544                     continue;
545                 }
547                 if (!$task->is_blocking()) {
548                     $cronlock->release();
549                 } else {
550                     $task->set_cron_lock($cronlock);
551                 }
552                 return $task;
553             }
554         }
556         // No tasks.
557         $cronlock->release();
558         return null;
559     }
561     /**
562      * This function indicates that an adhoc task was not completed successfully and should be retried.
563      *
564      * @param \core\task\adhoc_task $task
565      */
566     public static function adhoc_task_failed(adhoc_task $task) {
567         global $DB;
568         $delay = $task->get_fail_delay();
570         // Reschedule task with exponential fall off for failing tasks.
571         if (empty($delay)) {
572             $delay = 60;
573         } else {
574             $delay *= 2;
575         }
577         // Max of 24 hour delay.
578         if ($delay > 86400) {
579             $delay = 86400;
580         }
582         $classname = self::get_canonical_class_name($task);
584         $task->set_next_run_time(time() + $delay);
585         $task->set_fail_delay($delay);
586         $record = self::record_from_adhoc_task($task);
587         $DB->update_record('task_adhoc', $record);
589         if ($task->is_blocking()) {
590             $task->get_cron_lock()->release();
591         }
592         $task->get_lock()->release();
594         // Finalise the log output.
595         logmanager::finalise_log(true);
596     }
598     /**
599      * This function indicates that an adhoc task was completed successfully.
600      *
601      * @param \core\task\adhoc_task $task
602      */
603     public static function adhoc_task_complete(adhoc_task $task) {
604         global $DB;
606         // Finalise the log output.
607         logmanager::finalise_log();
609         // Delete the adhoc task record - it is finished.
610         $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
612         // Reschedule and then release the locks.
613         if ($task->is_blocking()) {
614             $task->get_cron_lock()->release();
615         }
616         $task->get_lock()->release();
617     }
619     /**
620      * This function indicates that a scheduled task was not completed successfully and should be retried.
621      *
622      * @param \core\task\scheduled_task $task
623      */
624     public static function scheduled_task_failed(scheduled_task $task) {
625         global $DB;
627         $delay = $task->get_fail_delay();
629         // Reschedule task with exponential fall off for failing tasks.
630         if (empty($delay)) {
631             $delay = 60;
632         } else {
633             $delay *= 2;
634         }
636         // Max of 24 hour delay.
637         if ($delay > 86400) {
638             $delay = 86400;
639         }
641         $classname = self::get_canonical_class_name($task);
643         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
644         $record->nextruntime = time() + $delay;
645         $record->faildelay = $delay;
646         $DB->update_record('task_scheduled', $record);
648         if ($task->is_blocking()) {
649             $task->get_cron_lock()->release();
650         }
651         $task->get_lock()->release();
653         // Finalise the log output.
654         logmanager::finalise_log(true);
655     }
657     /**
658      * Clears the fail delay for the given task and updates its next run time based on the schedule.
659      *
660      * @param scheduled_task $task Task to reset
661      * @throws \dml_exception If there is a database error
662      */
663     public static function clear_fail_delay(scheduled_task $task) {
664         global $DB;
666         $record = new \stdClass();
667         $record->id = $DB->get_field('task_scheduled', 'id',
668                 ['classname' => self::get_canonical_class_name($task)]);
669         $record->nextruntime = $task->get_next_scheduled_time();
670         $record->faildelay = 0;
671         $DB->update_record('task_scheduled', $record);
672     }
674     /**
675      * This function indicates that a scheduled task was completed successfully and should be rescheduled.
676      *
677      * @param \core\task\scheduled_task $task
678      */
679     public static function scheduled_task_complete(scheduled_task $task) {
680         global $DB;
682         // Finalise the log output.
683         logmanager::finalise_log();
685         $classname = self::get_canonical_class_name($task);
686         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
687         if ($record) {
688             $record->lastruntime = time();
689             $record->faildelay = 0;
690             $record->nextruntime = $task->get_next_scheduled_time();
692             $DB->update_record('task_scheduled', $record);
693         }
695         // Reschedule and then release the locks.
696         if ($task->is_blocking()) {
697             $task->get_cron_lock()->release();
698         }
699         $task->get_lock()->release();
700     }
702     /**
703      * This function is used to indicate that any long running cron processes should exit at the
704      * next opportunity and restart. This is because something (e.g. DB changes) has changed and
705      * the static caches may be stale.
706      */
707     public static function clear_static_caches() {
708         global $DB;
709         // Do not use get/set config here because the caches cannot be relied on.
710         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
711         if ($record) {
712             $record->value = time();
713             $DB->update_record('config', $record);
714         } else {
715             $record = new \stdClass();
716             $record->name = 'scheduledtaskreset';
717             $record->value = time();
718             $DB->insert_record('config', $record);
719         }
720     }
722     /**
723      * Return true if the static caches have been cleared since $starttime.
724      * @param int $starttime The time this process started.
725      * @return boolean True if static caches need resetting.
726      */
727     public static function static_caches_cleared_since($starttime) {
728         global $DB;
729         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
730         return $record && (intval($record->value) > $starttime);
731     }
733     /**
734      * Gets class name for use in database table. Always begins with a \.
735      *
736      * @param string|task_base $taskorstring Task object or a string
737      */
738     protected static function get_canonical_class_name($taskorstring) {
739         if (is_string($taskorstring)) {
740             $classname = $taskorstring;
741         } else {
742             $classname = get_class($taskorstring);
743         }
744         if (strpos($classname, '\\') !== 0) {
745             $classname = '\\' . $classname;
746         }
747         return $classname;
748     }