Merge branch 'MDL-46506-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             $scheduledtask->set_component($componentname);
70             $scheduledtasks[] = $scheduledtask;
71         }
73         return $scheduledtasks;
74     }
76     /**
77      * Update the database to contain a list of scheduled task for a component.
78      * The list of scheduled tasks is taken from @load_scheduled_tasks_for_component.
79      * Will throw exceptions for any errors.
80      *
81      * @param string $componentname - The frankenstyle component name.
82      */
83     public static function reset_scheduled_tasks_for_component($componentname) {
84         global $DB;
85         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
87         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
88             throw new \moodle_exception('locktimeout');
89         }
90         $tasks = self::load_default_scheduled_tasks_for_component($componentname);
92         $tasklocks = array();
93         foreach ($tasks as $task) {
94             $classname = get_class($task);
95             if (strpos($classname, '\\') !== 0) {
96                 $classname = '\\' . $classname;
97             }
99             if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
100                 // Could not get all the locks required - release all locks and fail.
101                 foreach ($tasklocks as $tasklock) {
102                     $tasklock->release();
103                 }
104                 $cronlock->release();
105                 throw new \moodle_exception('locktimeout');
106             }
107             $tasklocks[] = $lock;
108         }
110         // Got a lock on cron and all the tasks for this component, time to reset the config.
111         $DB->delete_records('task_scheduled', array('component' => $componentname));
112         foreach ($tasks as $task) {
113             $record = self::record_from_scheduled_task($task);
114             $DB->insert_record('task_scheduled', $record);
115         }
117         // Release the locks.
118         foreach ($tasklocks as $tasklock) {
119             $tasklock->release();
120         }
122         $cronlock->release();
123     }
125     /**
126      * Queue an adhoc task to run in the background.
127      *
128      * @param \core\task\adhoc_task $task - The new adhoc task information to store.
129      * @return boolean - True if the config was saved.
130      */
131     public static function queue_adhoc_task(adhoc_task $task) {
132         global $DB;
134         $record = self::record_from_adhoc_task($task);
135         // Schedule it immediately.
136         $record->nextruntime = time() - 1;
137         $result = $DB->insert_record('task_adhoc', $record);
139         return $result;
140     }
142     /**
143      * Change the default configuration for a scheduled task.
144      * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
145      *
146      * @param \core\task\scheduled_task $task - The new scheduled task information to store.
147      * @return boolean - True if the config was saved.
148      */
149     public static function configure_scheduled_task(scheduled_task $task) {
150         global $DB;
151         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
153         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
154             throw new \moodle_exception('locktimeout');
155         }
157         $classname = get_class($task);
158         if (strpos($classname, '\\') !== 0) {
159             $classname = '\\' . $classname;
160         }
161         if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
162             $cronlock->release();
163             throw new \moodle_exception('locktimeout');
164         }
166         $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
168         $record = self::record_from_scheduled_task($task);
169         $record->id = $original->id;
170         $record->nextruntime = $task->get_next_scheduled_time();
171         $result = $DB->update_record('task_scheduled', $record);
173         $lock->release();
174         $cronlock->release();
175         return $result;
176     }
178     /**
179      * Utility method to create a DB record from a scheduled task.
180      *
181      * @param \core\task\scheduled_task $task
182      * @return \stdClass
183      */
184     public static function record_from_scheduled_task($task) {
185         $record = new \stdClass();
186         $record->classname = get_class($task);
187         if (strpos($record->classname, '\\') !== 0) {
188             $record->classname = '\\' . $record->classname;
189         }
190         $record->component = $task->get_component();
191         $record->blocking = $task->is_blocking();
192         $record->customised = $task->is_customised();
193         $record->lastruntime = $task->get_last_run_time();
194         $record->nextruntime = $task->get_next_run_time();
195         $record->faildelay = $task->get_fail_delay();
196         $record->hour = $task->get_hour();
197         $record->minute = $task->get_minute();
198         $record->day = $task->get_day();
199         $record->dayofweek = $task->get_day_of_week();
200         $record->month = $task->get_month();
201         $record->disabled = $task->get_disabled();
203         return $record;
204     }
206     /**
207      * Utility method to create a DB record from an adhoc task.
208      *
209      * @param \core\task\adhoc_task $task
210      * @return \stdClass
211      */
212     public static function record_from_adhoc_task($task) {
213         $record = new \stdClass();
214         $record->classname = get_class($task);
215         if (strpos($record->classname, '\\') !== 0) {
216             $record->classname = '\\' . $record->classname;
217         }
218         $record->id = $task->get_id();
219         $record->component = $task->get_component();
220         $record->blocking = $task->is_blocking();
221         $record->nextruntime = $task->get_next_run_time();
222         $record->faildelay = $task->get_fail_delay();
223         $record->customdata = $task->get_custom_data_as_string();
225         return $record;
226     }
228     /**
229      * Utility method to create an adhoc task from a DB record.
230      *
231      * @param \stdClass $record
232      * @return \core\task\adhoc_task
233      */
234     public static function adhoc_task_from_record($record) {
235         $classname = $record->classname;
236         if (strpos($classname, '\\') !== 0) {
237             $classname = '\\' . $classname;
238         }
239         if (!class_exists($classname)) {
240             return false;
241         }
242         $task = new $classname;
243         if (isset($record->nextruntime)) {
244             $task->set_next_run_time($record->nextruntime);
245         }
246         if (isset($record->id)) {
247             $task->set_id($record->id);
248         }
249         if (isset($record->component)) {
250             $task->set_component($record->component);
251         }
252         $task->set_blocking(!empty($record->blocking));
253         if (isset($record->faildelay)) {
254             $task->set_fail_delay($record->faildelay);
255         }
256         if (isset($record->customdata)) {
257             $task->set_custom_data_as_string($record->customdata);
258         }
260         return $task;
261     }
263     /**
264      * Utility method to create a task from a DB record.
265      *
266      * @param \stdClass $record
267      * @return \core\task\scheduled_task
268      */
269     public static function scheduled_task_from_record($record) {
270         $classname = $record->classname;
271         if (strpos($classname, '\\') !== 0) {
272             $classname = '\\' . $classname;
273         }
274         if (!class_exists($classname)) {
275             return false;
276         }
277         /** @var \core\task\scheduled_task $task */
278         $task = new $classname;
279         if (isset($record->lastruntime)) {
280             $task->set_last_run_time($record->lastruntime);
281         }
282         if (isset($record->nextruntime)) {
283             $task->set_next_run_time($record->nextruntime);
284         }
285         if (isset($record->customised)) {
286             $task->set_customised($record->customised);
287         }
288         if (isset($record->component)) {
289             $task->set_component($record->component);
290         }
291         $task->set_blocking(!empty($record->blocking));
292         if (isset($record->minute)) {
293             $task->set_minute($record->minute);
294         }
295         if (isset($record->hour)) {
296             $task->set_hour($record->hour);
297         }
298         if (isset($record->day)) {
299             $task->set_day($record->day);
300         }
301         if (isset($record->month)) {
302             $task->set_month($record->month);
303         }
304         if (isset($record->dayofweek)) {
305             $task->set_day_of_week($record->dayofweek);
306         }
307         if (isset($record->faildelay)) {
308             $task->set_fail_delay($record->faildelay);
309         }
310         if (isset($record->disabled)) {
311             $task->set_disabled($record->disabled);
312         }
314         return $task;
315     }
317     /**
318      * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
319      * Do not execute tasks loaded from this function - they have not been locked.
320      * @param string $componentname - The name of the component to load the tasks for.
321      * @return \core\task\scheduled_task[]
322      */
323     public static function load_scheduled_tasks_for_component($componentname) {
324         global $DB;
326         $tasks = array();
327         // We are just reading - so no locks required.
328         $records = $DB->get_records('task_scheduled', array('componentname' => $componentname), 'classname', '*', IGNORE_MISSING);
329         foreach ($records as $record) {
330             $task = self::scheduled_task_from_record($record);
331             $tasks[] = $task;
332         }
334         return $tasks;
335     }
337     /**
338      * This function load the scheduled task details for a given classname.
339      *
340      * @param string $classname
341      * @return \core\task\scheduled_task or false
342      */
343     public static function get_scheduled_task($classname) {
344         global $DB;
346         if (strpos($classname, '\\') !== 0) {
347             $classname = '\\' . $classname;
348         }
349         // We are just reading - so no locks required.
350         $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
351         if (!$record) {
352             return false;
353         }
354         return self::scheduled_task_from_record($record);
355     }
357     /**
358      * This function load the default 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_default_scheduled_task($classname) {
364         $task = self::get_scheduled_task($classname);
366         $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
368         foreach ($componenttasks as $componenttask) {
369             if (get_class($componenttask) == get_class($task)) {
370                 return $componenttask;
371             }
372         }
374         return false;
375     }
377     /**
378      * This function will return a list of all the scheduled tasks that exist in the database.
379      *
380      * @return \core\task\scheduled_task[]
381      */
382     public static function get_all_scheduled_tasks() {
383         global $DB;
385         $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
386         $tasks = array();
388         foreach ($records as $record) {
389             $task = self::scheduled_task_from_record($record);
390             $tasks[] = $task;
391         }
393         return $tasks;
394     }
396     /**
397      * This function will dispatch the next adhoc task in the queue. The task will be handed out
398      * with an open lock - possibly on the entire cron process. Make sure you call either
399      * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
400      *
401      * @param int $timestart
402      * @return \core\task\adhoc_task or null if not found
403      */
404     public static function get_next_adhoc_task($timestart) {
405         global $DB;
406         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
408         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
409             throw new \moodle_exception('locktimeout');
410         }
412         $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
413         $params = array('timestart1' => $timestart);
414         $records = $DB->get_records_select('task_adhoc', $where, $params);
416         foreach ($records as $record) {
418             if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) {
419                 $classname = '\\' . $record->classname;
420                 $task = self::adhoc_task_from_record($record);
422                 $task->set_lock($lock);
423                 if (!$task->is_blocking()) {
424                     $cronlock->release();
425                 } else {
426                     $task->set_cron_lock($cronlock);
427                 }
428                 return $task;
429             }
430         }
432         // No tasks.
433         $cronlock->release();
434         return null;
435     }
437     /**
438      * This function will dispatch the next scheduled task in the queue. The task will be handed out
439      * with an open lock - possibly on the entire cron process. Make sure you call either
440      * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
441      *
442      * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
443      * @return \core\task\scheduled_task or null
444      */
445     public static function get_next_scheduled_task($timestart) {
446         global $DB;
447         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
449         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
450             throw new \moodle_exception('locktimeout');
451         }
453         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
454                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
455                   AND disabled = 0";
456         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
457         $records = $DB->get_records_select('task_scheduled', $where, $params);
459         $pluginmanager = \core_plugin_manager::instance();
461         foreach ($records as $record) {
463             if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
464                 $classname = '\\' . $record->classname;
465                 $task = self::scheduled_task_from_record($record);
467                 $task->set_lock($lock);
469                 // See if the component is disabled.
470                 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
472                 if ($plugininfo) {
473                     if (!$task->get_run_if_component_disabled() && !$plugininfo->is_enabled()) {
474                         $lock->release();
475                         continue;
476                     }
477                 }
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 indicates that an adhoc task was not completed successfully and should be retried.
495      *
496      * @param \core\task\adhoc_task $task
497      */
498     public static function adhoc_task_failed(adhoc_task $task) {
499         global $DB;
500         $delay = $task->get_fail_delay();
502         // Reschedule task with exponential fall off for failing tasks.
503         if (empty($delay)) {
504             $delay = 60;
505         } else {
506             $delay *= 2;
507         }
509         // Max of 24 hour delay.
510         if ($delay > 86400) {
511             $delay = 86400;
512         }
514         $classname = get_class($task);
515         if (strpos($classname, '\\') !== 0) {
516             $classname = '\\' . $classname;
517         }
519         $task->set_next_run_time(time() + $delay);
520         $task->set_fail_delay($delay);
521         $record = self::record_from_adhoc_task($task);
522         $DB->update_record('task_adhoc', $record);
524         if ($task->is_blocking()) {
525             $task->get_cron_lock()->release();
526         }
527         $task->get_lock()->release();
528     }
530     /**
531      * This function indicates that an adhoc task was completed successfully.
532      *
533      * @param \core\task\adhoc_task $task
534      */
535     public static function adhoc_task_complete(adhoc_task $task) {
536         global $DB;
538         // Delete the adhoc task record - it is finished.
539         $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
541         // Reschedule and then release the locks.
542         if ($task->is_blocking()) {
543             $task->get_cron_lock()->release();
544         }
545         $task->get_lock()->release();
546     }
548     /**
549      * This function indicates that a scheduled task was not completed successfully and should be retried.
550      *
551      * @param \core\task\scheduled_task $task
552      */
553     public static function scheduled_task_failed(scheduled_task $task) {
554         global $DB;
556         $delay = $task->get_fail_delay();
558         // Reschedule task with exponential fall off for failing tasks.
559         if (empty($delay)) {
560             $delay = 60;
561         } else {
562             $delay *= 2;
563         }
565         // Max of 24 hour delay.
566         if ($delay > 86400) {
567             $delay = 86400;
568         }
570         $classname = get_class($task);
571         if (strpos($classname, '\\') !== 0) {
572             $classname = '\\' . $classname;
573         }
575         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
576         $record->nextruntime = time() + $delay;
577         $record->faildelay = $delay;
578         $DB->update_record('task_scheduled', $record);
580         if ($task->is_blocking()) {
581             $task->get_cron_lock()->release();
582         }
583         $task->get_lock()->release();
584     }
586     /**
587      * This function indicates that a scheduled task was completed successfully and should be rescheduled.
588      *
589      * @param \core\task\scheduled_task $task
590      */
591     public static function scheduled_task_complete(scheduled_task $task) {
592         global $DB;
594         $classname = get_class($task);
595         if (strpos($classname, '\\') !== 0) {
596             $classname = '\\' . $classname;
597         }
598         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
599         if ($record) {
600             $record->lastruntime = time();
601             $record->faildelay = 0;
602             $record->nextruntime = $task->get_next_scheduled_time();
604             $DB->update_record('task_scheduled', $record);
605         }
607         // Reschedule and then release the locks.
608         if ($task->is_blocking()) {
609             $task->get_cron_lock()->release();
610         }
611         $task->get_lock()->release();
612     }
614     /**
615      * This function is used to indicate that any long running cron processes should exit at the
616      * next opportunity and restart. This is because something (e.g. DB changes) has changed and
617      * the static caches may be stale.
618      */
619     public static function clear_static_caches() {
620         global $DB;
621         // Do not use get/set config here because the caches cannot be relied on.
622         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
623         if ($record) {
624             $record->value = time();
625             $DB->update_record('config', $record);
626         } else {
627             $record = new \stdClass();
628             $record->name = 'scheduledtaskreset';
629             $record->value = time();
630             $DB->insert_record('config', $record);
631         }
632     }
634     /**
635      * Return true if the static caches have been cleared since $starttime.
636      * @param int $starttime The time this process started.
637      * @return boolean True if static caches need resetting.
638      */
639     public static function static_caches_cleared_since($starttime) {
640         global $DB;
641         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
642         return $record && (intval($record->value) > $starttime);
643     }