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