2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * Scheduled and adhoc task management.
22 * @copyright 2013 Damyon Wiese
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 define('CORE_TASK_TASKS_FILENAME', 'db/tasks.php');
29 * Collection of task related methods.
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
40 * Given a component name, will load the list of tasks in the db/tasks.php file for that component.
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.
45 public static function load_default_scheduled_tasks_for_component($componentname) {
46 $dir = \core_component::get_component_directory($componentname);
52 $file = $dir . '/' . CORE_TASK_TASKS_FILENAME;
53 if (!file_exists($file)) {
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).
71 $scheduledtask->set_component($componentname);
72 $scheduledtasks[] = $scheduledtask;
76 return $scheduledtasks;
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.
84 * @param string $componentname - The frankenstyle component name.
86 public static function reset_scheduled_tasks_for_component($componentname) {
88 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
90 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
91 throw new \moodle_exception('locktimeout');
93 $tasks = self::load_default_scheduled_tasks_for_component($componentname);
96 foreach ($tasks as $taskid => $task) {
97 $classname = get_class($task);
98 if (strpos($classname, '\\') !== 0) {
99 $classname = '\\' . $classname;
102 // If there is an existing task with a custom schedule, do not override it.
103 $currenttask = self::get_scheduled_task($classname);
104 if ($currenttask && $currenttask->is_customised()) {
105 $tasks[$taskid] = $currenttask;
108 if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
109 // Could not get all the locks required - release all locks and fail.
110 foreach ($tasklocks as $tasklock) {
111 $tasklock->release();
113 $cronlock->release();
114 throw new \moodle_exception('locktimeout');
116 $tasklocks[] = $lock;
119 // Got a lock on cron and all the tasks for this component, time to reset the config.
120 $DB->delete_records('task_scheduled', array('component' => $componentname));
121 foreach ($tasks as $task) {
122 $record = self::record_from_scheduled_task($task);
123 $DB->insert_record('task_scheduled', $record);
126 // Release the locks.
127 foreach ($tasklocks as $tasklock) {
128 $tasklock->release();
131 $cronlock->release();
135 * Queue an adhoc task to run in the background.
137 * @param \core\task\adhoc_task $task - The new adhoc task information to store.
138 * @return boolean - True if the config was saved.
140 public static function queue_adhoc_task(adhoc_task $task) {
143 $record = self::record_from_adhoc_task($task);
144 // Schedule it immediately.
145 $record->nextruntime = time() - 1;
146 $result = $DB->insert_record('task_adhoc', $record);
152 * Change the default configuration for a scheduled task.
153 * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
155 * @param \core\task\scheduled_task $task - The new scheduled task information to store.
156 * @return boolean - True if the config was saved.
158 public static function configure_scheduled_task(scheduled_task $task) {
160 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
162 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
163 throw new \moodle_exception('locktimeout');
166 $classname = get_class($task);
167 if (strpos($classname, '\\') !== 0) {
168 $classname = '\\' . $classname;
170 if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
171 $cronlock->release();
172 throw new \moodle_exception('locktimeout');
175 $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
177 $record = self::record_from_scheduled_task($task);
178 $record->id = $original->id;
179 $record->nextruntime = $task->get_next_scheduled_time();
180 $result = $DB->update_record('task_scheduled', $record);
183 $cronlock->release();
188 * Utility method to create a DB record from a scheduled task.
190 * @param \core\task\scheduled_task $task
193 public static function record_from_scheduled_task($task) {
194 $record = new \stdClass();
195 $record->classname = get_class($task);
196 if (strpos($record->classname, '\\') !== 0) {
197 $record->classname = '\\' . $record->classname;
199 $record->component = $task->get_component();
200 $record->blocking = $task->is_blocking();
201 $record->customised = $task->is_customised();
202 $record->lastruntime = $task->get_last_run_time();
203 $record->nextruntime = $task->get_next_run_time();
204 $record->faildelay = $task->get_fail_delay();
205 $record->hour = $task->get_hour();
206 $record->minute = $task->get_minute();
207 $record->day = $task->get_day();
208 $record->dayofweek = $task->get_day_of_week();
209 $record->month = $task->get_month();
210 $record->disabled = $task->get_disabled();
216 * Utility method to create a DB record from an adhoc task.
218 * @param \core\task\adhoc_task $task
221 public static function record_from_adhoc_task($task) {
222 $record = new \stdClass();
223 $record->classname = get_class($task);
224 if (strpos($record->classname, '\\') !== 0) {
225 $record->classname = '\\' . $record->classname;
227 $record->id = $task->get_id();
228 $record->component = $task->get_component();
229 $record->blocking = $task->is_blocking();
230 $record->nextruntime = $task->get_next_run_time();
231 $record->faildelay = $task->get_fail_delay();
232 $record->customdata = $task->get_custom_data_as_string();
238 * Utility method to create an adhoc task from a DB record.
240 * @param \stdClass $record
241 * @return \core\task\adhoc_task
243 public static function adhoc_task_from_record($record) {
244 $classname = $record->classname;
245 if (strpos($classname, '\\') !== 0) {
246 $classname = '\\' . $classname;
248 if (!class_exists($classname)) {
249 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
252 $task = new $classname;
253 if (isset($record->nextruntime)) {
254 $task->set_next_run_time($record->nextruntime);
256 if (isset($record->id)) {
257 $task->set_id($record->id);
259 if (isset($record->component)) {
260 $task->set_component($record->component);
262 $task->set_blocking(!empty($record->blocking));
263 if (isset($record->faildelay)) {
264 $task->set_fail_delay($record->faildelay);
266 if (isset($record->customdata)) {
267 $task->set_custom_data_as_string($record->customdata);
274 * Utility method to create a task from a DB record.
276 * @param \stdClass $record
277 * @return \core\task\scheduled_task
279 public static function scheduled_task_from_record($record) {
280 $classname = $record->classname;
281 if (strpos($classname, '\\') !== 0) {
282 $classname = '\\' . $classname;
284 if (!class_exists($classname)) {
285 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
288 /** @var \core\task\scheduled_task $task */
289 $task = new $classname;
290 if (isset($record->lastruntime)) {
291 $task->set_last_run_time($record->lastruntime);
293 if (isset($record->nextruntime)) {
294 $task->set_next_run_time($record->nextruntime);
296 if (isset($record->customised)) {
297 $task->set_customised($record->customised);
299 if (isset($record->component)) {
300 $task->set_component($record->component);
302 $task->set_blocking(!empty($record->blocking));
303 if (isset($record->minute)) {
304 $task->set_minute($record->minute);
306 if (isset($record->hour)) {
307 $task->set_hour($record->hour);
309 if (isset($record->day)) {
310 $task->set_day($record->day);
312 if (isset($record->month)) {
313 $task->set_month($record->month);
315 if (isset($record->dayofweek)) {
316 $task->set_day_of_week($record->dayofweek);
318 if (isset($record->faildelay)) {
319 $task->set_fail_delay($record->faildelay);
321 if (isset($record->disabled)) {
322 $task->set_disabled($record->disabled);
329 * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
330 * Do not execute tasks loaded from this function - they have not been locked.
331 * @param string $componentname - The name of the component to load the tasks for.
332 * @return \core\task\scheduled_task[]
334 public static function load_scheduled_tasks_for_component($componentname) {
338 // We are just reading - so no locks required.
339 $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
340 foreach ($records as $record) {
341 $task = self::scheduled_task_from_record($record);
342 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
352 * This function load the scheduled task details for a given classname.
354 * @param string $classname
355 * @return \core\task\scheduled_task or false
357 public static function get_scheduled_task($classname) {
360 if (strpos($classname, '\\') !== 0) {
361 $classname = '\\' . $classname;
363 // We are just reading - so no locks required.
364 $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
368 return self::scheduled_task_from_record($record);
372 * This function load the default scheduled task details for a given classname.
374 * @param string $classname
375 * @return \core\task\scheduled_task or false
377 public static function get_default_scheduled_task($classname) {
378 $task = self::get_scheduled_task($classname);
379 $componenttasks = array();
381 // Safety check in case no task was found for the given classname.
383 $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
386 foreach ($componenttasks as $componenttask) {
387 if (get_class($componenttask) == get_class($task)) {
388 return $componenttask;
396 * This function will return a list of all the scheduled tasks that exist in the database.
398 * @return \core\task\scheduled_task[]
400 public static function get_all_scheduled_tasks() {
403 $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
406 foreach ($records as $record) {
407 $task = self::scheduled_task_from_record($record);
408 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
418 * This function will dispatch the next adhoc task in the queue. The task will be handed out
419 * with an open lock - possibly on the entire cron process. Make sure you call either
420 * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
422 * @param int $timestart
423 * @return \core\task\adhoc_task or null if not found
425 public static function get_next_adhoc_task($timestart) {
427 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
429 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
430 throw new \moodle_exception('locktimeout');
433 $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
434 $params = array('timestart1' => $timestart);
435 $records = $DB->get_records_select('task_adhoc', $where, $params);
437 foreach ($records as $record) {
439 if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) {
440 $classname = '\\' . $record->classname;
441 $task = self::adhoc_task_from_record($record);
442 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
448 $task->set_lock($lock);
449 if (!$task->is_blocking()) {
450 $cronlock->release();
452 $task->set_cron_lock($cronlock);
459 $cronlock->release();
464 * This function will dispatch the next scheduled task in the queue. The task will be handed out
465 * with an open lock - possibly on the entire cron process. Make sure you call either
466 * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
468 * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
469 * @return \core\task\scheduled_task or null
471 public static function get_next_scheduled_task($timestart) {
473 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
475 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
476 throw new \moodle_exception('locktimeout');
479 $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
480 AND (nextruntime IS NULL OR nextruntime < :timestart2)
482 $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
483 $records = $DB->get_records_select('task_scheduled', $where, $params);
485 $pluginmanager = \core_plugin_manager::instance();
487 foreach ($records as $record) {
489 if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
490 $classname = '\\' . $record->classname;
491 $task = self::scheduled_task_from_record($record);
492 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
498 $task->set_lock($lock);
500 // See if the component is disabled.
501 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
504 if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
505 mtrace($task->get_name().' skipped - the component '.$task->get_component().' is disabled');
511 if (!$task->is_blocking()) {
512 $cronlock->release();
514 $task->set_cron_lock($cronlock);
521 $cronlock->release();
526 * This function indicates that an adhoc task was not completed successfully and should be retried.
528 * @param \core\task\adhoc_task $task
530 public static function adhoc_task_failed(adhoc_task $task) {
532 $delay = $task->get_fail_delay();
534 // Reschedule task with exponential fall off for failing tasks.
541 // Max of 24 hour delay.
542 if ($delay > 86400) {
546 $classname = get_class($task);
547 if (strpos($classname, '\\') !== 0) {
548 $classname = '\\' . $classname;
551 $task->set_next_run_time(time() + $delay);
552 $task->set_fail_delay($delay);
553 $record = self::record_from_adhoc_task($task);
554 $DB->update_record('task_adhoc', $record);
556 if ($task->is_blocking()) {
557 $task->get_cron_lock()->release();
559 $task->get_lock()->release();
563 * This function indicates that an adhoc task was completed successfully.
565 * @param \core\task\adhoc_task $task
567 public static function adhoc_task_complete(adhoc_task $task) {
570 // Delete the adhoc task record - it is finished.
571 $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
573 // Reschedule and then release the locks.
574 if ($task->is_blocking()) {
575 $task->get_cron_lock()->release();
577 $task->get_lock()->release();
581 * This function indicates that a scheduled task was not completed successfully and should be retried.
583 * @param \core\task\scheduled_task $task
585 public static function scheduled_task_failed(scheduled_task $task) {
588 $delay = $task->get_fail_delay();
590 // Reschedule task with exponential fall off for failing tasks.
597 // Max of 24 hour delay.
598 if ($delay > 86400) {
602 $classname = get_class($task);
603 if (strpos($classname, '\\') !== 0) {
604 $classname = '\\' . $classname;
607 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
608 $record->nextruntime = time() + $delay;
609 $record->faildelay = $delay;
610 $DB->update_record('task_scheduled', $record);
612 if ($task->is_blocking()) {
613 $task->get_cron_lock()->release();
615 $task->get_lock()->release();
619 * This function indicates that a scheduled task was completed successfully and should be rescheduled.
621 * @param \core\task\scheduled_task $task
623 public static function scheduled_task_complete(scheduled_task $task) {
626 $classname = get_class($task);
627 if (strpos($classname, '\\') !== 0) {
628 $classname = '\\' . $classname;
630 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
632 $record->lastruntime = time();
633 $record->faildelay = 0;
634 $record->nextruntime = $task->get_next_scheduled_time();
636 $DB->update_record('task_scheduled', $record);
639 // Reschedule and then release the locks.
640 if ($task->is_blocking()) {
641 $task->get_cron_lock()->release();
643 $task->get_lock()->release();
647 * This function is used to indicate that any long running cron processes should exit at the
648 * next opportunity and restart. This is because something (e.g. DB changes) has changed and
649 * the static caches may be stale.
651 public static function clear_static_caches() {
653 // Do not use get/set config here because the caches cannot be relied on.
654 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
656 $record->value = time();
657 $DB->update_record('config', $record);
659 $record = new \stdClass();
660 $record->name = 'scheduledtaskreset';
661 $record->value = time();
662 $DB->insert_record('config', $record);
667 * Return true if the static caches have been cleared since $starttime.
668 * @param int $starttime The time this process started.
669 * @return boolean True if static caches need resetting.
671 public static function static_caches_cleared_since($starttime) {
673 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
674 return $record && (intval($record->value) > $starttime);