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