source: moodle/trunk/fuentes/lib/classes/task/manager.php @ 1331

Last change on this file since 1331 was 1331, checked in by jrpelegrina, 3 years ago

Updated to moodle 3.0.3

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