Merge branch 'MDL-51885-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         include($file);
60         if (!isset($tasks)) {
61             return array();
62         }
64         $scheduledtasks = array();
66         foreach ($tasks as $task) {
67             $record = (object) $task;
68             $scheduledtask = self::scheduled_task_from_record($record);
69             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
70             if ($scheduledtask) {
71                 $scheduledtask->set_component($componentname);
72                 $scheduledtasks[] = $scheduledtask;
73             }
74         }
76         return $scheduledtasks;
77     }
79     /**
80      * Update the database to contain a list of scheduled task for a component.
81      * The list of scheduled tasks is taken from @load_scheduled_tasks_for_component.
82      * Will throw exceptions for any errors.
83      *
84      * @param string $componentname - The frankenstyle component name.
85      */
86     public static function reset_scheduled_tasks_for_component($componentname) {
87         global $DB;
88         $tasks = self::load_default_scheduled_tasks_for_component($componentname);
89         $validtasks = array();
91         foreach ($tasks as $taskid => $task) {
92             $classname = get_class($task);
93             if (strpos($classname, '\\') !== 0) {
94                 $classname = '\\' . $classname;
95             }
97             $validtasks[] = $classname;
99             if ($currenttask = self::get_scheduled_task($classname)) {
100                 if ($currenttask->is_customised()) {
101                     // If there is an existing task with a custom schedule, do not override it.
102                     continue;
103                 }
105                 // Update the record from the default task data.
106                 self::configure_scheduled_task($task);
107             } else {
108                 // Ensure that the first run follows the schedule.
109                 $task->set_next_run_time($task->get_next_scheduled_time());
111                 // Insert the new task in the database.
112                 $record = self::record_from_scheduled_task($task);
113                 $DB->insert_record('task_scheduled', $record);
114             }
115         }
117         // Delete any task that is not defined in the component any more.
118         $sql = "component = :component";
119         $params = array('component' => $componentname);
120         if (!empty($validtasks)) {
121             list($insql, $inparams) = $DB->get_in_or_equal($validtasks, SQL_PARAMS_NAMED, 'param', false);
122             $sql .= ' AND classname ' . $insql;
123             $params = array_merge($params, $inparams);
124         }
125         $DB->delete_records_select('task_scheduled', $sql, $params);
126     }
128     /**
129      * Checks if the task with the same classname, component and customdata is already scheduled
130      *
131      * @param adhoc_task $task
132      * @return bool
133      */
134     protected static function task_is_scheduled($task) {
135         global $DB;
136         $record = self::record_from_adhoc_task($task);
137         $params = [$record->classname, $record->component, $record->customdata];
138         $sql = 'classname = ? AND component = ? AND ' .
139             $DB->sql_compare_text('customdata', \core_text::strlen($record->customdata) + 1) . ' = ?';
141         if ($record->userid) {
142             $params[] = $record->userid;
143             $sql .= " AND userid = ? ";
144         }
145         return $DB->record_exists_select('task_adhoc', $sql, $params);
146     }
148     /**
149      * Queue an adhoc task to run in the background.
150      *
151      * @param \core\task\adhoc_task $task - The new adhoc task information to store.
152      * @param bool $checkforexisting - If set to true and the task with the same classname, component and customdata
153      *     is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks.
154      * @return boolean - True if the config was saved.
155      */
156     public static function queue_adhoc_task(adhoc_task $task, $checkforexisting = false) {
157         global $DB;
159         if ($userid = $task->get_userid()) {
160             // User found. Check that they are suitable.
161             \core_user::require_active_user(\core_user::get_user($userid, '*', MUST_EXIST), true, true);
162         }
164         $record = self::record_from_adhoc_task($task);
165         // Schedule it immediately if nextruntime not explicitly set.
166         if (!$task->get_next_run_time()) {
167             $record->nextruntime = time() - 1;
168         }
170         // Check if the same task is already scheduled.
171         if ($checkforexisting && self::task_is_scheduled($task)) {
172             return false;
173         }
175         // Queue the task.
176         $result = $DB->insert_record('task_adhoc', $record);
178         return $result;
179     }
181     /**
182      * Change the default configuration for a scheduled task.
183      * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
184      *
185      * @param \core\task\scheduled_task $task - The new scheduled task information to store.
186      * @return boolean - True if the config was saved.
187      */
188     public static function configure_scheduled_task(scheduled_task $task) {
189         global $DB;
191         $classname = get_class($task);
192         if (strpos($classname, '\\') !== 0) {
193             $classname = '\\' . $classname;
194         }
196         $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
198         $record = self::record_from_scheduled_task($task);
199         $record->id = $original->id;
200         $record->nextruntime = $task->get_next_scheduled_time();
201         $result = $DB->update_record('task_scheduled', $record);
203         return $result;
204     }
206     /**
207      * Utility method to create a DB record from a scheduled task.
208      *
209      * @param \core\task\scheduled_task $task
210      * @return \stdClass
211      */
212     public static function record_from_scheduled_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->component = $task->get_component();
219         $record->blocking = $task->is_blocking();
220         $record->customised = $task->is_customised();
221         $record->lastruntime = $task->get_last_run_time();
222         $record->nextruntime = $task->get_next_run_time();
223         $record->faildelay = $task->get_fail_delay();
224         $record->hour = $task->get_hour();
225         $record->minute = $task->get_minute();
226         $record->day = $task->get_day();
227         $record->dayofweek = $task->get_day_of_week();
228         $record->month = $task->get_month();
229         $record->disabled = $task->get_disabled();
231         return $record;
232     }
234     /**
235      * Utility method to create a DB record from an adhoc task.
236      *
237      * @param \core\task\adhoc_task $task
238      * @return \stdClass
239      */
240     public static function record_from_adhoc_task($task) {
241         $record = new \stdClass();
242         $record->classname = get_class($task);
243         if (strpos($record->classname, '\\') !== 0) {
244             $record->classname = '\\' . $record->classname;
245         }
246         $record->id = $task->get_id();
247         $record->component = $task->get_component();
248         $record->blocking = $task->is_blocking();
249         $record->nextruntime = $task->get_next_run_time();
250         $record->faildelay = $task->get_fail_delay();
251         $record->customdata = $task->get_custom_data_as_string();
252         $record->userid = $task->get_userid();
254         return $record;
255     }
257     /**
258      * Utility method to create an adhoc task from a DB record.
259      *
260      * @param \stdClass $record
261      * @return \core\task\adhoc_task
262      */
263     public static function adhoc_task_from_record($record) {
264         $classname = $record->classname;
265         if (strpos($classname, '\\') !== 0) {
266             $classname = '\\' . $classname;
267         }
268         if (!class_exists($classname)) {
269             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
270             return false;
271         }
272         $task = new $classname;
273         if (isset($record->nextruntime)) {
274             $task->set_next_run_time($record->nextruntime);
275         }
276         if (isset($record->id)) {
277             $task->set_id($record->id);
278         }
279         if (isset($record->component)) {
280             $task->set_component($record->component);
281         }
282         $task->set_blocking(!empty($record->blocking));
283         if (isset($record->faildelay)) {
284             $task->set_fail_delay($record->faildelay);
285         }
286         if (isset($record->customdata)) {
287             $task->set_custom_data_as_string($record->customdata);
288         }
290         if (isset($record->userid)) {
291             $task->set_userid($record->userid);
292         }
294         return $task;
295     }
297     /**
298      * Utility method to create a task from a DB record.
299      *
300      * @param \stdClass $record
301      * @return \core\task\scheduled_task
302      */
303     public static function scheduled_task_from_record($record) {
304         $classname = $record->classname;
305         if (strpos($classname, '\\') !== 0) {
306             $classname = '\\' . $classname;
307         }
308         if (!class_exists($classname)) {
309             debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
310             return false;
311         }
312         /** @var \core\task\scheduled_task $task */
313         $task = new $classname;
314         if (isset($record->lastruntime)) {
315             $task->set_last_run_time($record->lastruntime);
316         }
317         if (isset($record->nextruntime)) {
318             $task->set_next_run_time($record->nextruntime);
319         }
320         if (isset($record->customised)) {
321             $task->set_customised($record->customised);
322         }
323         if (isset($record->component)) {
324             $task->set_component($record->component);
325         }
326         $task->set_blocking(!empty($record->blocking));
327         if (isset($record->minute)) {
328             $task->set_minute($record->minute);
329         }
330         if (isset($record->hour)) {
331             $task->set_hour($record->hour);
332         }
333         if (isset($record->day)) {
334             $task->set_day($record->day);
335         }
336         if (isset($record->month)) {
337             $task->set_month($record->month);
338         }
339         if (isset($record->dayofweek)) {
340             $task->set_day_of_week($record->dayofweek);
341         }
342         if (isset($record->faildelay)) {
343             $task->set_fail_delay($record->faildelay);
344         }
345         if (isset($record->disabled)) {
346             $task->set_disabled($record->disabled);
347         }
349         return $task;
350     }
352     /**
353      * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
354      * Do not execute tasks loaded from this function - they have not been locked.
355      * @param string $componentname - The name of the component to load the tasks for.
356      * @return \core\task\scheduled_task[]
357      */
358     public static function load_scheduled_tasks_for_component($componentname) {
359         global $DB;
361         $tasks = array();
362         // We are just reading - so no locks required.
363         $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
364         foreach ($records as $record) {
365             $task = self::scheduled_task_from_record($record);
366             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
367             if ($task) {
368                 $tasks[] = $task;
369             }
370         }
372         return $tasks;
373     }
375     /**
376      * This function load the scheduled task details for a given classname.
377      *
378      * @param string $classname
379      * @return \core\task\scheduled_task or false
380      */
381     public static function get_scheduled_task($classname) {
382         global $DB;
384         if (strpos($classname, '\\') !== 0) {
385             $classname = '\\' . $classname;
386         }
387         // We are just reading - so no locks required.
388         $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
389         if (!$record) {
390             return false;
391         }
392         return self::scheduled_task_from_record($record);
393     }
395     /**
396      * This function load the adhoc tasks for a given classname.
397      *
398      * @param string $classname
399      * @return \core\task\adhoc_task[]
400      */
401     public static function get_adhoc_tasks($classname) {
402         global $DB;
404         if (strpos($classname, '\\') !== 0) {
405             $classname = '\\' . $classname;
406         }
407         // We are just reading - so no locks required.
408         $records = $DB->get_records('task_adhoc', array('classname' => $classname));
410         return array_map(function($record) {
411             return self::adhoc_task_from_record($record);
412         }, $records);
413     }
415     /**
416      * This function load the default scheduled task details for a given classname.
417      *
418      * @param string $classname
419      * @return \core\task\scheduled_task or false
420      */
421     public static function get_default_scheduled_task($classname) {
422         $task = self::get_scheduled_task($classname);
423         $componenttasks = array();
425         // Safety check in case no task was found for the given classname.
426         if ($task) {
427             $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
428         }
430         foreach ($componenttasks as $componenttask) {
431             if (get_class($componenttask) == get_class($task)) {
432                 return $componenttask;
433             }
434         }
436         return false;
437     }
439     /**
440      * This function will return a list of all the scheduled tasks that exist in the database.
441      *
442      * @return \core\task\scheduled_task[]
443      */
444     public static function get_all_scheduled_tasks() {
445         global $DB;
447         $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
448         $tasks = array();
450         foreach ($records as $record) {
451             $task = self::scheduled_task_from_record($record);
452             // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
453             if ($task) {
454                 $tasks[] = $task;
455             }
456         }
458         return $tasks;
459     }
461     /**
462      * This function will dispatch the next adhoc task in the queue. The task will be handed out
463      * with an open lock - possibly on the entire cron process. Make sure you call either
464      * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
465      *
466      * @param int $timestart
467      * @return \core\task\adhoc_task or null if not found
468      */
469     public static function get_next_adhoc_task($timestart) {
470         global $DB;
471         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
473         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
474             throw new \moodle_exception('locktimeout');
475         }
477         $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
478         $params = array('timestart1' => $timestart);
479         $records = $DB->get_records_select('task_adhoc', $where, $params);
481         foreach ($records as $record) {
483             if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) {
484                 $classname = '\\' . $record->classname;
486                 // Safety check, see if the task has been already processed by another cron run.
487                 $record = $DB->get_record('task_adhoc', array('id' => $record->id));
488                 if (!$record) {
489                     $lock->release();
490                     continue;
491                 }
493                 $task = self::adhoc_task_from_record($record);
494                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
495                 if (!$task) {
496                     $lock->release();
497                     continue;
498                 }
500                 $task->set_lock($lock);
501                 if (!$task->is_blocking()) {
502                     $cronlock->release();
503                 } else {
504                     $task->set_cron_lock($cronlock);
505                 }
506                 return $task;
507             }
508         }
510         // No tasks.
511         $cronlock->release();
512         return null;
513     }
515     /**
516      * This function will dispatch the next scheduled task in the queue. The task will be handed out
517      * with an open lock - possibly on the entire cron process. Make sure you call either
518      * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
519      *
520      * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
521      * @return \core\task\scheduled_task or null
522      */
523     public static function get_next_scheduled_task($timestart) {
524         global $DB;
525         $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
527         if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
528             throw new \moodle_exception('locktimeout');
529         }
531         $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
532                   AND (nextruntime IS NULL OR nextruntime < :timestart2)
533                   AND disabled = 0
534                   ORDER BY lastruntime, id ASC";
535         $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
536         $records = $DB->get_records_select('task_scheduled', $where, $params);
538         $pluginmanager = \core_plugin_manager::instance();
540         foreach ($records as $record) {
542             if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
543                 $classname = '\\' . $record->classname;
544                 $task = self::scheduled_task_from_record($record);
545                 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
546                 if (!$task) {
547                     $lock->release();
548                     continue;
549                 }
551                 $task->set_lock($lock);
553                 // See if the component is disabled.
554                 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
556                 if ($plugininfo) {
557                     if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
558                         $lock->release();
559                         continue;
560                     }
561                 }
563                 // Make sure the task data is unchanged.
564                 if (!$DB->record_exists('task_scheduled', (array) $record)) {
565                     $lock->release();
566                     continue;
567                 }
569                 if (!$task->is_blocking()) {
570                     $cronlock->release();
571                 } else {
572                     $task->set_cron_lock($cronlock);
573                 }
574                 return $task;
575             }
576         }
578         // No tasks.
579         $cronlock->release();
580         return null;
581     }
583     /**
584      * This function indicates that an adhoc task was not completed successfully and should be retried.
585      *
586      * @param \core\task\adhoc_task $task
587      */
588     public static function adhoc_task_failed(adhoc_task $task) {
589         global $DB;
590         $delay = $task->get_fail_delay();
592         // Reschedule task with exponential fall off for failing tasks.
593         if (empty($delay)) {
594             $delay = 60;
595         } else {
596             $delay *= 2;
597         }
599         // Max of 24 hour delay.
600         if ($delay > 86400) {
601             $delay = 86400;
602         }
604         $classname = get_class($task);
605         if (strpos($classname, '\\') !== 0) {
606             $classname = '\\' . $classname;
607         }
609         $task->set_next_run_time(time() + $delay);
610         $task->set_fail_delay($delay);
611         $record = self::record_from_adhoc_task($task);
612         $DB->update_record('task_adhoc', $record);
614         if ($task->is_blocking()) {
615             $task->get_cron_lock()->release();
616         }
617         $task->get_lock()->release();
618     }
620     /**
621      * This function indicates that an adhoc task was completed successfully.
622      *
623      * @param \core\task\adhoc_task $task
624      */
625     public static function adhoc_task_complete(adhoc_task $task) {
626         global $DB;
628         // Delete the adhoc task record - it is finished.
629         $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
631         // Reschedule and then release the locks.
632         if ($task->is_blocking()) {
633             $task->get_cron_lock()->release();
634         }
635         $task->get_lock()->release();
636     }
638     /**
639      * This function indicates that a scheduled task was not completed successfully and should be retried.
640      *
641      * @param \core\task\scheduled_task $task
642      */
643     public static function scheduled_task_failed(scheduled_task $task) {
644         global $DB;
646         $delay = $task->get_fail_delay();
648         // Reschedule task with exponential fall off for failing tasks.
649         if (empty($delay)) {
650             $delay = 60;
651         } else {
652             $delay *= 2;
653         }
655         // Max of 24 hour delay.
656         if ($delay > 86400) {
657             $delay = 86400;
658         }
660         $classname = get_class($task);
661         if (strpos($classname, '\\') !== 0) {
662             $classname = '\\' . $classname;
663         }
665         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
666         $record->nextruntime = time() + $delay;
667         $record->faildelay = $delay;
668         $DB->update_record('task_scheduled', $record);
670         if ($task->is_blocking()) {
671             $task->get_cron_lock()->release();
672         }
673         $task->get_lock()->release();
674     }
676     /**
677      * This function indicates that a scheduled task was completed successfully and should be rescheduled.
678      *
679      * @param \core\task\scheduled_task $task
680      */
681     public static function scheduled_task_complete(scheduled_task $task) {
682         global $DB;
684         $classname = get_class($task);
685         if (strpos($classname, '\\') !== 0) {
686             $classname = '\\' . $classname;
687         }
688         $record = $DB->get_record('task_scheduled', array('classname' => $classname));
689         if ($record) {
690             $record->lastruntime = time();
691             $record->faildelay = 0;
692             $record->nextruntime = $task->get_next_scheduled_time();
694             $DB->update_record('task_scheduled', $record);
695         }
697         // Reschedule and then release the locks.
698         if ($task->is_blocking()) {
699             $task->get_cron_lock()->release();
700         }
701         $task->get_lock()->release();
702     }
704     /**
705      * This function is used to indicate that any long running cron processes should exit at the
706      * next opportunity and restart. This is because something (e.g. DB changes) has changed and
707      * the static caches may be stale.
708      */
709     public static function clear_static_caches() {
710         global $DB;
711         // Do not use get/set config here because the caches cannot be relied on.
712         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
713         if ($record) {
714             $record->value = time();
715             $DB->update_record('config', $record);
716         } else {
717             $record = new \stdClass();
718             $record->name = 'scheduledtaskreset';
719             $record->value = time();
720             $DB->insert_record('config', $record);
721         }
722     }
724     /**
725      * Return true if the static caches have been cleared since $starttime.
726      * @param int $starttime The time this process started.
727      * @return boolean True if static caches need resetting.
728      */
729     public static function static_caches_cleared_since($starttime) {
730         global $DB;
731         $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
732         return $record && (intval($record->value) > $starttime);
733     }