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 $scheduledtask->set_component($componentname);
70 $scheduledtasks[] = $scheduledtask;
73 return $scheduledtasks;
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.
81 * @param string $componentname - The frankenstyle component name.
83 public static function reset_scheduled_tasks_for_component($componentname) {
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');
90 $tasks = self::load_default_scheduled_tasks_for_component($componentname);
93 foreach ($tasks as $task) {
94 $classname = get_class($task);
95 if (strpos($classname, '\\') !== 0) {
96 $classname = '\\' . $classname;
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();
104 $cronlock->release();
105 throw new \moodle_exception('locktimeout');
107 $tasklocks[] = $lock;
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);
117 // Release the locks.
118 foreach ($tasklocks as $tasklock) {
119 $tasklock->release();
122 $cronlock->release();
126 * Queue an adhoc task to run in the background.
128 * @param \core\task\adhoc_task $task - The new adhoc task information to store.
129 * @return boolean - True if the config was saved.
131 public static function queue_adhoc_task(adhoc_task $task) {
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);
143 * Change the default configuration for a scheduled task.
144 * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
146 * @param \core\task\scheduled_task $task - The new scheduled task information to store.
147 * @return boolean - True if the config was saved.
149 public static function configure_scheduled_task(scheduled_task $task) {
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');
157 $classname = get_class($task);
158 if (strpos($classname, '\\') !== 0) {
159 $classname = '\\' . $classname;
161 if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
162 $cronlock->release();
163 throw new \moodle_exception('locktimeout');
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);
174 $cronlock->release();
179 * Utility method to create a DB record from a scheduled task.
181 * @param \core\task\scheduled_task $task
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;
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();
207 * Utility method to create a DB record from an adhoc task.
209 * @param \core\task\adhoc_task $task
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;
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();
229 * Utility method to create an adhoc task from a DB record.
231 * @param \stdClass $record
232 * @return \core\task\adhoc_task
234 public static function adhoc_task_from_record($record) {
235 $classname = $record->classname;
236 if (strpos($classname, '\\') !== 0) {
237 $classname = '\\' . $classname;
239 if (!class_exists($classname)) {
242 $task = new $classname;
243 if (isset($record->nextruntime)) {
244 $task->set_next_run_time($record->nextruntime);
246 if (isset($record->id)) {
247 $task->set_id($record->id);
249 if (isset($record->component)) {
250 $task->set_component($record->component);
252 $task->set_blocking(!empty($record->blocking));
253 if (isset($record->faildelay)) {
254 $task->set_fail_delay($record->faildelay);
256 if (isset($record->customdata)) {
257 $task->set_custom_data_as_string($record->customdata);
264 * Utility method to create a task from a DB record.
266 * @param \stdClass $record
267 * @return \core\task\scheduled_task
269 public static function scheduled_task_from_record($record) {
270 $classname = $record->classname;
271 if (strpos($classname, '\\') !== 0) {
272 $classname = '\\' . $classname;
274 if (!class_exists($classname)) {
277 /** @var \core\task\scheduled_task $task */
278 $task = new $classname;
279 if (isset($record->lastruntime)) {
280 $task->set_last_run_time($record->lastruntime);
282 if (isset($record->nextruntime)) {
283 $task->set_next_run_time($record->nextruntime);
285 if (isset($record->customised)) {
286 $task->set_customised($record->customised);
288 if (isset($record->component)) {
289 $task->set_component($record->component);
291 $task->set_blocking(!empty($record->blocking));
292 if (isset($record->minute)) {
293 $task->set_minute($record->minute);
295 if (isset($record->hour)) {
296 $task->set_hour($record->hour);
298 if (isset($record->day)) {
299 $task->set_day($record->day);
301 if (isset($record->month)) {
302 $task->set_month($record->month);
304 if (isset($record->dayofweek)) {
305 $task->set_day_of_week($record->dayofweek);
307 if (isset($record->faildelay)) {
308 $task->set_fail_delay($record->faildelay);
310 if (isset($record->disabled)) {
311 $task->set_disabled($record->disabled);
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[]
323 public static function load_scheduled_tasks_for_component($componentname) {
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);
338 * This function load the scheduled task details for a given classname.
340 * @param string $classname
341 * @return \core\task\scheduled_task or false
343 public static function get_scheduled_task($classname) {
346 if (strpos($classname, '\\') !== 0) {
347 $classname = '\\' . $classname;
349 // We are just reading - so no locks required.
350 $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
354 return self::scheduled_task_from_record($record);
358 * This function load the default scheduled task details for a given classname.
360 * @param string $classname
361 * @return \core\task\scheduled_task or false
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;
378 * This function will return a list of all the scheduled tasks that exist in the database.
380 * @return \core\task\scheduled_task[]
382 public static function get_all_scheduled_tasks() {
385 $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
388 foreach ($records as $record) {
389 $task = self::scheduled_task_from_record($record);
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.
401 * @param int $timestart
402 * @return \core\task\adhoc_task or null if not found
404 public static function get_next_adhoc_task($timestart) {
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');
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();
426 $task->set_cron_lock($cronlock);
433 $cronlock->release();
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.
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
445 public static function get_next_scheduled_task($timestart) {
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');
453 $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
454 AND (nextruntime IS NULL OR nextruntime < :timestart2)
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());
473 if (!$task->get_run_if_component_disabled() && !$plugininfo->is_enabled()) {
479 if (!$task->is_blocking()) {
480 $cronlock->release();
482 $task->set_cron_lock($cronlock);
489 $cronlock->release();
494 * This function indicates that an adhoc task was not completed successfully and should be retried.
496 * @param \core\task\adhoc_task $task
498 public static function adhoc_task_failed(adhoc_task $task) {
500 $delay = $task->get_fail_delay();
502 // Reschedule task with exponential fall off for failing tasks.
509 // Max of 24 hour delay.
510 if ($delay > 86400) {
514 $classname = get_class($task);
515 if (strpos($classname, '\\') !== 0) {
516 $classname = '\\' . $classname;
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();
527 $task->get_lock()->release();
531 * This function indicates that an adhoc task was completed successfully.
533 * @param \core\task\adhoc_task $task
535 public static function adhoc_task_complete(adhoc_task $task) {
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();
545 $task->get_lock()->release();
549 * This function indicates that a scheduled task was not completed successfully and should be retried.
551 * @param \core\task\scheduled_task $task
553 public static function scheduled_task_failed(scheduled_task $task) {
556 $delay = $task->get_fail_delay();
558 // Reschedule task with exponential fall off for failing tasks.
565 // Max of 24 hour delay.
566 if ($delay > 86400) {
570 $classname = get_class($task);
571 if (strpos($classname, '\\') !== 0) {
572 $classname = '\\' . $classname;
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();
583 $task->get_lock()->release();
587 * This function indicates that a scheduled task was completed successfully and should be rescheduled.
589 * @param \core\task\scheduled_task $task
591 public static function scheduled_task_complete(scheduled_task $task) {
594 $classname = get_class($task);
595 if (strpos($classname, '\\') !== 0) {
596 $classname = '\\' . $classname;
598 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
600 $record->lastruntime = time();
601 $record->faildelay = 0;
602 $record->nextruntime = $task->get_next_scheduled_time();
604 $DB->update_record('task_scheduled', $record);
607 // Reschedule and then release the locks.
608 if ($task->is_blocking()) {
609 $task->get_cron_lock()->release();
611 $task->get_lock()->release();
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.
619 public static function clear_static_caches() {
621 // Do not use get/set config here because the caches cannot be relied on.
622 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
624 $record->value = time();
625 $DB->update_record('config', $record);
627 $record = new \stdClass();
628 $record->name = 'scheduledtaskreset';
629 $record->value = time();
630 $DB->insert_record('config', $record);
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.
639 public static function static_caches_cleared_since($starttime) {
641 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
642 return $record && (intval($record->value) > $starttime);