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 $tasks = self::load_default_scheduled_tasks_for_component($componentname);
90 foreach ($tasks as $taskid => $task) {
91 $classname = get_class($task);
92 if (strpos($classname, '\\') !== 0) {
93 $classname = '\\' . $classname;
96 if ($currenttask = self::get_scheduled_task($classname)) {
97 if ($currenttask->is_customised()) {
98 // If there is an existing task with a custom schedule, do not override it.
102 // Update the record from the default task data.
103 self::configure_scheduled_task($task);
105 // Ensure that the first run follows the schedule.
106 $task->set_next_run_time($task->get_next_scheduled_time());
108 // Insert the new task in the database.
109 $record = self::record_from_scheduled_task($task);
110 $DB->insert_record('task_scheduled', $record);
116 * Queue an adhoc task to run in the background.
118 * @param \core\task\adhoc_task $task - The new adhoc task information to store.
119 * @return boolean - True if the config was saved.
121 public static function queue_adhoc_task(adhoc_task $task) {
124 $record = self::record_from_adhoc_task($task);
125 // Schedule it immediately.
126 $record->nextruntime = time() - 1;
127 $result = $DB->insert_record('task_adhoc', $record);
133 * Change the default configuration for a scheduled task.
134 * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
136 * @param \core\task\scheduled_task $task - The new scheduled task information to store.
137 * @return boolean - True if the config was saved.
139 public static function configure_scheduled_task(scheduled_task $task) {
142 $classname = get_class($task);
143 if (strpos($classname, '\\') !== 0) {
144 $classname = '\\' . $classname;
147 $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
149 $record = self::record_from_scheduled_task($task);
150 $record->id = $original->id;
151 $record->nextruntime = $task->get_next_scheduled_time();
152 $result = $DB->update_record('task_scheduled', $record);
158 * Utility method to create a DB record from a scheduled task.
160 * @param \core\task\scheduled_task $task
163 public static function record_from_scheduled_task($task) {
164 $record = new \stdClass();
165 $record->classname = get_class($task);
166 if (strpos($record->classname, '\\') !== 0) {
167 $record->classname = '\\' . $record->classname;
169 $record->component = $task->get_component();
170 $record->blocking = $task->is_blocking();
171 $record->customised = $task->is_customised();
172 $record->lastruntime = $task->get_last_run_time();
173 $record->nextruntime = $task->get_next_run_time();
174 $record->faildelay = $task->get_fail_delay();
175 $record->hour = $task->get_hour();
176 $record->minute = $task->get_minute();
177 $record->day = $task->get_day();
178 $record->dayofweek = $task->get_day_of_week();
179 $record->month = $task->get_month();
180 $record->disabled = $task->get_disabled();
186 * Utility method to create a DB record from an adhoc task.
188 * @param \core\task\adhoc_task $task
191 public static function record_from_adhoc_task($task) {
192 $record = new \stdClass();
193 $record->classname = get_class($task);
194 if (strpos($record->classname, '\\') !== 0) {
195 $record->classname = '\\' . $record->classname;
197 $record->id = $task->get_id();
198 $record->component = $task->get_component();
199 $record->blocking = $task->is_blocking();
200 $record->nextruntime = $task->get_next_run_time();
201 $record->faildelay = $task->get_fail_delay();
202 $record->customdata = $task->get_custom_data_as_string();
208 * Utility method to create an adhoc task from a DB record.
210 * @param \stdClass $record
211 * @return \core\task\adhoc_task
213 public static function adhoc_task_from_record($record) {
214 $classname = $record->classname;
215 if (strpos($classname, '\\') !== 0) {
216 $classname = '\\' . $classname;
218 if (!class_exists($classname)) {
219 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
222 $task = new $classname;
223 if (isset($record->nextruntime)) {
224 $task->set_next_run_time($record->nextruntime);
226 if (isset($record->id)) {
227 $task->set_id($record->id);
229 if (isset($record->component)) {
230 $task->set_component($record->component);
232 $task->set_blocking(!empty($record->blocking));
233 if (isset($record->faildelay)) {
234 $task->set_fail_delay($record->faildelay);
236 if (isset($record->customdata)) {
237 $task->set_custom_data_as_string($record->customdata);
244 * Utility method to create a task from a DB record.
246 * @param \stdClass $record
247 * @return \core\task\scheduled_task
249 public static function scheduled_task_from_record($record) {
250 $classname = $record->classname;
251 if (strpos($classname, '\\') !== 0) {
252 $classname = '\\' . $classname;
254 if (!class_exists($classname)) {
255 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
258 /** @var \core\task\scheduled_task $task */
259 $task = new $classname;
260 if (isset($record->lastruntime)) {
261 $task->set_last_run_time($record->lastruntime);
263 if (isset($record->nextruntime)) {
264 $task->set_next_run_time($record->nextruntime);
266 if (isset($record->customised)) {
267 $task->set_customised($record->customised);
269 if (isset($record->component)) {
270 $task->set_component($record->component);
272 $task->set_blocking(!empty($record->blocking));
273 if (isset($record->minute)) {
274 $task->set_minute($record->minute);
276 if (isset($record->hour)) {
277 $task->set_hour($record->hour);
279 if (isset($record->day)) {
280 $task->set_day($record->day);
282 if (isset($record->month)) {
283 $task->set_month($record->month);
285 if (isset($record->dayofweek)) {
286 $task->set_day_of_week($record->dayofweek);
288 if (isset($record->faildelay)) {
289 $task->set_fail_delay($record->faildelay);
291 if (isset($record->disabled)) {
292 $task->set_disabled($record->disabled);
299 * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
300 * Do not execute tasks loaded from this function - they have not been locked.
301 * @param string $componentname - The name of the component to load the tasks for.
302 * @return \core\task\scheduled_task[]
304 public static function load_scheduled_tasks_for_component($componentname) {
308 // We are just reading - so no locks required.
309 $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
310 foreach ($records as $record) {
311 $task = self::scheduled_task_from_record($record);
312 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
322 * This function load the scheduled task details for a given classname.
324 * @param string $classname
325 * @return \core\task\scheduled_task or false
327 public static function get_scheduled_task($classname) {
330 if (strpos($classname, '\\') !== 0) {
331 $classname = '\\' . $classname;
333 // We are just reading - so no locks required.
334 $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
338 return self::scheduled_task_from_record($record);
342 * This function load the default scheduled task details for a given classname.
344 * @param string $classname
345 * @return \core\task\scheduled_task or false
347 public static function get_default_scheduled_task($classname) {
348 $task = self::get_scheduled_task($classname);
349 $componenttasks = array();
351 // Safety check in case no task was found for the given classname.
353 $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
356 foreach ($componenttasks as $componenttask) {
357 if (get_class($componenttask) == get_class($task)) {
358 return $componenttask;
366 * This function will return a list of all the scheduled tasks that exist in the database.
368 * @return \core\task\scheduled_task[]
370 public static function get_all_scheduled_tasks() {
373 $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
376 foreach ($records as $record) {
377 $task = self::scheduled_task_from_record($record);
378 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
388 * This function will dispatch the next adhoc task in the queue. The task will be handed out
389 * with an open lock - possibly on the entire cron process. Make sure you call either
390 * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
392 * @param int $timestart
393 * @return \core\task\adhoc_task or null if not found
395 public static function get_next_adhoc_task($timestart) {
397 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
399 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
400 throw new \moodle_exception('locktimeout');
403 $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
404 $params = array('timestart1' => $timestart);
405 $records = $DB->get_records_select('task_adhoc', $where, $params);
407 foreach ($records as $record) {
409 if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) {
410 $classname = '\\' . $record->classname;
411 $task = self::adhoc_task_from_record($record);
412 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
418 $task->set_lock($lock);
419 if (!$task->is_blocking()) {
420 $cronlock->release();
422 $task->set_cron_lock($cronlock);
429 $cronlock->release();
434 * This function will dispatch the next scheduled task in the queue. The task will be handed out
435 * with an open lock - possibly on the entire cron process. Make sure you call either
436 * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
438 * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
439 * @return \core\task\scheduled_task or null
441 public static function get_next_scheduled_task($timestart) {
443 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
445 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
446 throw new \moodle_exception('locktimeout');
449 $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
450 AND (nextruntime IS NULL OR nextruntime < :timestart2)
452 ORDER BY lastruntime, id ASC";
453 $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
454 $records = $DB->get_records_select('task_scheduled', $where, $params);
456 $pluginmanager = \core_plugin_manager::instance();
458 foreach ($records as $record) {
460 if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
461 $classname = '\\' . $record->classname;
462 $task = self::scheduled_task_from_record($record);
463 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
469 $task->set_lock($lock);
471 // See if the component is disabled.
472 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
475 if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
481 if (!$task->is_blocking()) {
482 $cronlock->release();
484 $task->set_cron_lock($cronlock);
491 $cronlock->release();
496 * This function indicates that an adhoc task was not completed successfully and should be retried.
498 * @param \core\task\adhoc_task $task
500 public static function adhoc_task_failed(adhoc_task $task) {
502 $delay = $task->get_fail_delay();
504 // Reschedule task with exponential fall off for failing tasks.
511 // Max of 24 hour delay.
512 if ($delay > 86400) {
516 $classname = get_class($task);
517 if (strpos($classname, '\\') !== 0) {
518 $classname = '\\' . $classname;
521 $task->set_next_run_time(time() + $delay);
522 $task->set_fail_delay($delay);
523 $record = self::record_from_adhoc_task($task);
524 $DB->update_record('task_adhoc', $record);
526 if ($task->is_blocking()) {
527 $task->get_cron_lock()->release();
529 $task->get_lock()->release();
533 * This function indicates that an adhoc task was completed successfully.
535 * @param \core\task\adhoc_task $task
537 public static function adhoc_task_complete(adhoc_task $task) {
540 // Delete the adhoc task record - it is finished.
541 $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
543 // Reschedule and then release the locks.
544 if ($task->is_blocking()) {
545 $task->get_cron_lock()->release();
547 $task->get_lock()->release();
551 * This function indicates that a scheduled task was not completed successfully and should be retried.
553 * @param \core\task\scheduled_task $task
555 public static function scheduled_task_failed(scheduled_task $task) {
558 $delay = $task->get_fail_delay();
560 // Reschedule task with exponential fall off for failing tasks.
567 // Max of 24 hour delay.
568 if ($delay > 86400) {
572 $classname = get_class($task);
573 if (strpos($classname, '\\') !== 0) {
574 $classname = '\\' . $classname;
577 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
578 $record->nextruntime = time() + $delay;
579 $record->faildelay = $delay;
580 $DB->update_record('task_scheduled', $record);
582 if ($task->is_blocking()) {
583 $task->get_cron_lock()->release();
585 $task->get_lock()->release();
589 * This function indicates that a scheduled task was completed successfully and should be rescheduled.
591 * @param \core\task\scheduled_task $task
593 public static function scheduled_task_complete(scheduled_task $task) {
596 $classname = get_class($task);
597 if (strpos($classname, '\\') !== 0) {
598 $classname = '\\' . $classname;
600 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
602 $record->lastruntime = time();
603 $record->faildelay = 0;
604 $record->nextruntime = $task->get_next_scheduled_time();
606 $DB->update_record('task_scheduled', $record);
609 // Reschedule and then release the locks.
610 if ($task->is_blocking()) {
611 $task->get_cron_lock()->release();
613 $task->get_lock()->release();
617 * This function is used to indicate that any long running cron processes should exit at the
618 * next opportunity and restart. This is because something (e.g. DB changes) has changed and
619 * the static caches may be stale.
621 public static function clear_static_caches() {
623 // Do not use get/set config here because the caches cannot be relied on.
624 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
626 $record->value = time();
627 $DB->update_record('config', $record);
629 $record = new \stdClass();
630 $record->name = 'scheduledtaskreset';
631 $record->value = time();
632 $DB->insert_record('config', $record);
637 * Return true if the static caches have been cleared since $starttime.
638 * @param int $starttime The time this process started.
639 * @return boolean True if static caches need resetting.
641 public static function static_caches_cleared_since($starttime) {
643 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
644 return $record && (intval($record->value) > $starttime);