Merge branch 'MDL-47808-master' of git://github.com/andrewnicols/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         require_once($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         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
90         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
91             throw new \moodle_exception('locktimeout');
92         }
93         $tasks = self::load_default_scheduled_tasks_for_component($componentname);
95         $tasklocks = array();
96         foreach ($tasks as $task) {
97             $classname = get_class($task);
98             if (strpos($classname, '\\') !== 0) {
99                 $classname = '\\' . $classname;
100             }
102             if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
103                 // Could not get all the locks required - release all locks and fail.
104                 foreach ($tasklocks as $tasklock) {
105                     $tasklock->release();
106                 }
107                 $cronlock->release();
108                 throw new \moodle_exception('locktimeout');
109             }
110             $tasklocks[] = $lock;
111         }
113         // Got a lock on cron and all the tasks for this component, time to reset the config.
114         $DB->delete_records('task_scheduled', array('component' => $componentname));
115         foreach ($tasks as $task) {
116             $record = self::record_from_scheduled_task($task);
117             $DB->insert_record('task_scheduled', $record);
118         }
120         // Release the locks.
121         foreach ($tasklocks as $tasklock) {
122             $tasklock->release();
123         }
125         $cronlock->release();
126     }
128     /**
129      * Queue an adhoc task to run in the background.
130      *
131      * @param \core\task\adhoc_task $task - The new adhoc task information to store.
132      * @return boolean - True if the config was saved.
133      */
134     public static function queue_adhoc_task(adhoc_task $task) {
135         global $DB;
137         $record = self::record_from_adhoc_task($task);
138         // Schedule it immediately.
139         $record->nextruntime = time() - 1;
140         $result = $DB->insert_record('task_adhoc', $record);
142         return $result;
143     }
145     /**
146      * Change the default configuration for a scheduled task.
147      * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
148      *
149      * @param \core\task\scheduled_task $task - The new scheduled task information to store.
150      * @return boolean - True if the config was saved.
151      */
152     public static function configure_scheduled_task(scheduled_task $task) {
153         global $DB;
154         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
156         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
157             throw new \moodle_exception('locktimeout');
158         }
160         $classname = get_class($task);
161         if (strpos($classname, '\\') !== 0) {
162             $classname = '\\' . $classname;
163         }
164         if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
165             $cronlock->release();
166             throw new \moodle_exception('locktimeout');
167         }
169         $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
171         $record = self::record_from_scheduled_task($task);
172         $record->id = $original->id;
173         $record->nextruntime = $task->get_next_scheduled_time();
174         $result = $DB->update_record('task_scheduled', $record);
176         $lock->release();
177         $cronlock->release();
178         return $result;
179     }
181     /**
182      * Utility method to create a DB record from a scheduled task.
183      *
184      * @param \core\task\scheduled_task $task
185      * @return \stdClass
186      */
187     public static function record_from_scheduled_task($task) {
188         $record = new \stdClass();
189         $record->classname = get_class($task);
190         if (strpos($record->classname, '\\') !== 0) {
191             $record->classname = '\\' . $record->classname;
192         }
193         $record->component = $task->get_component();
194         $record->blocking = $task->is_blocking();
195         $record->customised = $task->is_customised();
196         $record->lastruntime = $task->get_last_run_time();
197         $record->nextruntime = $task->get_next_run_time();
198         $record->faildelay = $task->get_fail_delay();
199         $record->hour = $task->get_hour();
200         $record->minute = $task->get_minute();
201         $record->day = $task->get_day();
202         $record->dayofweek = $task->get_day_of_week();
203         $record->month = $task->get_month();
204         $record->disabled = $task->get_disabled();
206         return $record;
207     }
209     /**
210      * Utility method to create a DB record from an adhoc task.
211      *
212      * @param \core\task\adhoc_task $task
213      * @return \stdClass
214      */
215     public static function record_from_adhoc_task($task) {
216         $record = new \stdClass();
217         $record->classname = get_class($task);
218         if (strpos($record->classname, '\\') !== 0) {
219             $record->classname = '\\' . $record->classname;
220         }
221         $record->id = $task->get_id();
222         $record->component = $task->get_component();
223         $record->blocking = $task->is_blocking();
224         $record->nextruntime = $task->get_next_run_time();
225         $record->faildelay = $task->get_fail_delay();
226         $record->customdata = $task->get_custom_data_as_string();
228         return $record;
229     }
231     /**
232      * Utility method to create an adhoc task from a DB record.
233      *
234      * @param \stdClass $record
235      * @return \core\task\adhoc_task
236      */
237     public static function adhoc_task_from_record($record) {
238         $classname = $record->classname;
239         if (strpos($classname, '\\') !== 0) {
240             $classname = '\\' . $classname;
241         }
242         if (!class_exists($classname)) {
243             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
244             return false;
245         }
246         $task = new $classname;
247         if (isset($record->nextruntime)) {
248             $task->set_next_run_time($record->nextruntime);
249         }
250         if (isset($record->id)) {
251             $task->set_id($record->id);
252         }
253         if (isset($record->component)) {
254             $task->set_component($record->component);
255         }
256         $task->set_blocking(!empty($record->blocking));
257         if (isset($record->faildelay)) {
258             $task->set_fail_delay($record->faildelay);
259         }
260         if (isset($record->customdata)) {
261             $task->set_custom_data_as_string($record->customdata);
262         }
264         return $task;
265     }
267     /**
268      * Utility method to create a task from a DB record.
269      *
270      * @param \stdClass $record
271      * @return \core\task\scheduled_task
272      */
273     public static function scheduled_task_from_record($record) {
274         $classname = $record->classname;
275         if (strpos($classname, '\\') !== 0) {
276             $classname = '\\' . $classname;
277         }
278         if (!class_exists($classname)) {
279             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
280             return false;
281         }
282         /** @var \core\task\scheduled_task $task */
283         $task = new $classname;
284         if (isset($record->lastruntime)) {
285             $task->set_last_run_time($record->lastruntime);
286         }
287         if (isset($record->nextruntime)) {
288             $task->set_next_run_time($record->nextruntime);
289         }
290         if (isset($record->customised)) {
291             $task->set_customised($record->customised);
292         }
293         if (isset($record->component)) {
294             $task->set_component($record->component);
295         }
296         $task->set_blocking(!empty($record->blocking));
297         if (isset($record->minute)) {
298             $task->set_minute($record->minute);
299         }
300         if (isset($record->hour)) {
301             $task->set_hour($record->hour);
302         }
303         if (isset($record->day)) {
304             $task->set_day($record->day);
305         }
306         if (isset($record->month)) {
307             $task->set_month($record->month);
308         }
309         if (isset($record->dayofweek)) {
310             $task->set_day_of_week($record->dayofweek);
311         }
312         if (isset($record->faildelay)) {
313             $task->set_fail_delay($record->faildelay);
314         }
315         if (isset($record->disabled)) {
316             $task->set_disabled($record->disabled);
317         }
319         return $task;
320     }
322     /**
323      * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
324      * Do not execute tasks loaded from this function - they have not been locked.
325      * @param string $componentname - The name of the component to load the tasks for.
326      * @return \core\task\scheduled_task[]
327      */
328     public static function load_scheduled_tasks_for_component($componentname) {
329         global $DB;
331         $tasks = array();
332         // We are just reading - so no locks required.
333         $records = $DB->get_records('task_scheduled', array('componentname' => $componentname), 'classname', '*', IGNORE_MISSING);
334         foreach ($records as $record) {
335             $task = self::scheduled_task_from_record($record);
336             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
337             if ($task) {
338                 $tasks[] = $task;
339             }
340         }
342         return $tasks;
343     }
345     /**
346      * This function load the scheduled task details for a given classname.
347      *
348      * @param string $classname
349      * @return \core\task\scheduled_task or false
350      */
351     public static function get_scheduled_task($classname) {
352         global $DB;
354         if (strpos($classname, '\\') !== 0) {
355             $classname = '\\' . $classname;
356         }
357         // We are just reading - so no locks required.
358         $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
359         if (!$record) {
360             return false;
361         }
362         return self::scheduled_task_from_record($record);
363     }
365     /**
366      * This function load the default scheduled task details for a given classname.
367      *
368      * @param string $classname
369      * @return \core\task\scheduled_task or false
370      */
371     public static function get_default_scheduled_task($classname) {
372         $task = self::get_scheduled_task($classname);
373         $componenttasks = array();
375         // Safety check in case no task was found for the given classname.
376         if ($task) {
377             $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
378         }
380         foreach ($componenttasks as $componenttask) {
381             if (get_class($componenttask) == get_class($task)) {
382                 return $componenttask;
383             }
384         }
386         return false;
387     }
389     /**
390      * This function will return a list of all the scheduled tasks that exist in the database.
391      *
392      * @return \core\task\scheduled_task[]
393      */
394     public static function get_all_scheduled_tasks() {
395         global $DB;
397         $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
398         $tasks = array();
400         foreach ($records as $record) {
401             $task = self::scheduled_task_from_record($record);
402             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
403             if ($task) {
404                 $tasks[] = $task;
405             }
406         }
408         return $tasks;
409     }
411     /**
412      * This function will dispatch the next adhoc task in the queue. The task will be handed out
413      * with an open lock - possibly on the entire cron process. Make sure you call either
414      * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
415      *
416      * @param int $timestart
417      * @return \core\task\adhoc_task or null if not found
418      */
419     public static function get_next_adhoc_task($timestart) {
420         global $DB;
421         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
423         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
424             throw new \moodle_exception('locktimeout');
425         }
427         $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
428         $params = array('timestart1' => $timestart);
429         $records = $DB->get_records_select('task_adhoc', $where, $params);
431         foreach ($records as $record) {
433             if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) {
434                 $classname = '\\' . $record->classname;
435                 $task = self::adhoc_task_from_record($record);
436                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
437                 if (!$task) {
438                     $lock->release();
439                     continue;
440                 }
442                 $task->set_lock($lock);
443                 if (!$task->is_blocking()) {
444                     $cronlock->release();
445                 } else {
446                     $task->set_cron_lock($cronlock);
447                 }
448                 return $task;
449             }
450         }
452         // No tasks.
453         $cronlock->release();
454         return null;
455     }
457     /**
458      * This function will dispatch the next scheduled task in the queue. The task will be handed out
459      * with an open lock - possibly on the entire cron process. Make sure you call either
460      * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
461      *
462      * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
463      * @return \core\task\scheduled_task or null
464      */
465     public static function get_next_scheduled_task($timestart) {
466         global $DB;
467         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
469         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
470             throw new \moodle_exception('locktimeout');
471         }
473         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
474                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
475                   AND disabled = 0";
476         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
477         $records = $DB->get_records_select('task_scheduled', $where, $params);
479         $pluginmanager = \core_plugin_manager::instance();
481         foreach ($records as $record) {
483             if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
484                 $classname = '\\' . $record->classname;
485                 $task = self::scheduled_task_from_record($record);
486                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
487                 if (!$task) {
488                     $lock->release();
489                     continue;
490                 }
492                 $task->set_lock($lock);
494                 // See if the component is disabled.
495                 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
497                 if ($plugininfo) {
498                     if (!$task->get_run_if_component_disabled() && !$plugininfo->is_enabled()) {
499                         $lock->release();
500                         continue;
501                     }
502                 }
504                 if (!$task->is_blocking()) {
505                     $cronlock->release();
506                 } else {
507                     $task->set_cron_lock($cronlock);
508                 }
509                 return $task;
510             }
511         }
513         // No tasks.
514         $cronlock->release();
515         return null;
516     }
518     /**
519      * This function indicates that an adhoc task was not completed successfully and should be retried.
520      *
521      * @param \core\task\adhoc_task $task
522      */
523     public static function adhoc_task_failed(adhoc_task $task) {
524         global $DB;
525         $delay = $task->get_fail_delay();
527         // Reschedule task with exponential fall off for failing tasks.
528         if (empty($delay)) {
529             $delay = 60;
530         } else {
531             $delay *= 2;
532         }
534         // Max of 24 hour delay.
535         if ($delay > 86400) {
536             $delay = 86400;
537         }
539         $classname = get_class($task);
540         if (strpos($classname, '\\') !== 0) {
541             $classname = '\\' . $classname;
542         }
544         $task->set_next_run_time(time() + $delay);
545         $task->set_fail_delay($delay);
546         $record = self::record_from_adhoc_task($task);
547         $DB->update_record('task_adhoc', $record);
549         if ($task->is_blocking()) {
550             $task->get_cron_lock()->release();
551         }
552         $task->get_lock()->release();
553     }
555     /**
556      * This function indicates that an adhoc task was completed successfully.
557      *
558      * @param \core\task\adhoc_task $task
559      */
560     public static function adhoc_task_complete(adhoc_task $task) {
561         global $DB;
563         // Delete the adhoc task record - it is finished.
564         $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
566         // Reschedule and then release the locks.
567         if ($task->is_blocking()) {
568             $task->get_cron_lock()->release();
569         }
570         $task->get_lock()->release();
571     }
573     /**
574      * This function indicates that a scheduled task was not completed successfully and should be retried.
575      *
576      * @param \core\task\scheduled_task $task
577      */
578     public static function scheduled_task_failed(scheduled_task $task) {
579         global $DB;
581         $delay = $task->get_fail_delay();
583         // Reschedule task with exponential fall off for failing tasks.
584         if (empty($delay)) {
585             $delay = 60;
586         } else {
587             $delay *= 2;
588         }
590         // Max of 24 hour delay.
591         if ($delay > 86400) {
592             $delay = 86400;
593         }
595         $classname = get_class($task);
596         if (strpos($classname, '\\') !== 0) {
597             $classname = '\\' . $classname;
598         }
600         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
601         $record->nextruntime = time() + $delay;
602         $record->faildelay = $delay;
603         $DB->update_record('task_scheduled', $record);
605         if ($task->is_blocking()) {
606             $task->get_cron_lock()->release();
607         }
608         $task->get_lock()->release();
609     }
611     /**
612      * This function indicates that a scheduled task was completed successfully and should be rescheduled.
613      *
614      * @param \core\task\scheduled_task $task
615      */
616     public static function scheduled_task_complete(scheduled_task $task) {
617         global $DB;
619         $classname = get_class($task);
620         if (strpos($classname, '\\') !== 0) {
621             $classname = '\\' . $classname;
622         }
623         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
624         if ($record) {
625             $record->lastruntime = time();
626             $record->faildelay = 0;
627             $record->nextruntime = $task->get_next_scheduled_time();
629             $DB->update_record('task_scheduled', $record);
630         }
632         // Reschedule and then release the locks.
633         if ($task->is_blocking()) {
634             $task->get_cron_lock()->release();
635         }
636         $task->get_lock()->release();
637     }
639     /**
640      * This function is used to indicate that any long running cron processes should exit at the
641      * next opportunity and restart. This is because something (e.g. DB changes) has changed and
642      * the static caches may be stale.
643      */
644     public static function clear_static_caches() {
645         global $DB;
646         // Do not use get/set config here because the caches cannot be relied on.
647         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
648         if ($record) {
649             $record->value = time();
650             $DB->update_record('config', $record);
651         } else {
652             $record = new \stdClass();
653             $record->name = 'scheduledtaskreset';
654             $record->value = time();
655             $DB->insert_record('config', $record);
656         }
657     }
659     /**
660      * Return true if the static caches have been cleared since $starttime.
661      * @param int $starttime The time this process started.
662      * @return boolean True if static caches need resetting.
663      */
664     public static function static_caches_cleared_since($starttime) {
665         global $DB;
666         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
667         return $record && (intval($record->value) > $starttime);
668     }