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 $task) {
97 $classname = get_class($task);
98 if (strpos($classname, '\\') !== 0) {
99 $classname = '\\' . $classname;
102 if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
103 // Could not get all the locks required - release all locks and fail.
104 foreach ($tasklocks as $tasklock) {
105 $tasklock->release();
107 $cronlock->release();
108 throw new \moodle_exception('locktimeout');
110 $tasklocks[] = $lock;
113 // Got a lock on cron and all the tasks for this component, time to reset the config.
114 $DB->delete_records('task_scheduled', array('component' => $componentname));
115 foreach ($tasks as $task) {
116 $record = self::record_from_scheduled_task($task);
117 $DB->insert_record('task_scheduled', $record);
120 // Release the locks.
121 foreach ($tasklocks as $tasklock) {
122 $tasklock->release();
125 $cronlock->release();
129 * Queue an adhoc task to run in the background.
131 * @param \core\task\adhoc_task $task - The new adhoc task information to store.
132 * @return boolean - True if the config was saved.
134 public static function queue_adhoc_task(adhoc_task $task) {
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);
146 * Change the default configuration for a scheduled task.
147 * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
149 * @param \core\task\scheduled_task $task - The new scheduled task information to store.
150 * @return boolean - True if the config was saved.
152 public static function configure_scheduled_task(scheduled_task $task) {
154 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
156 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) {
157 throw new \moodle_exception('locktimeout');
160 $classname = get_class($task);
161 if (strpos($classname, '\\') !== 0) {
162 $classname = '\\' . $classname;
164 if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) {
165 $cronlock->release();
166 throw new \moodle_exception('locktimeout');
169 $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
171 $record = self::record_from_scheduled_task($task);
172 $record->id = $original->id;
173 $record->nextruntime = $task->get_next_scheduled_time();
174 $result = $DB->update_record('task_scheduled', $record);
177 $cronlock->release();
182 * Utility method to create a DB record from a scheduled task.
184 * @param \core\task\scheduled_task $task
187 public static function record_from_scheduled_task($task) {
188 $record = new \stdClass();
189 $record->classname = get_class($task);
190 if (strpos($record->classname, '\\') !== 0) {
191 $record->classname = '\\' . $record->classname;
193 $record->component = $task->get_component();
194 $record->blocking = $task->is_blocking();
195 $record->customised = $task->is_customised();
196 $record->lastruntime = $task->get_last_run_time();
197 $record->nextruntime = $task->get_next_run_time();
198 $record->faildelay = $task->get_fail_delay();
199 $record->hour = $task->get_hour();
200 $record->minute = $task->get_minute();
201 $record->day = $task->get_day();
202 $record->dayofweek = $task->get_day_of_week();
203 $record->month = $task->get_month();
204 $record->disabled = $task->get_disabled();
210 * Utility method to create a DB record from an adhoc task.
212 * @param \core\task\adhoc_task $task
215 public static function record_from_adhoc_task($task) {
216 $record = new \stdClass();
217 $record->classname = get_class($task);
218 if (strpos($record->classname, '\\') !== 0) {
219 $record->classname = '\\' . $record->classname;
221 $record->id = $task->get_id();
222 $record->component = $task->get_component();
223 $record->blocking = $task->is_blocking();
224 $record->nextruntime = $task->get_next_run_time();
225 $record->faildelay = $task->get_fail_delay();
226 $record->customdata = $task->get_custom_data_as_string();
232 * Utility method to create an adhoc task from a DB record.
234 * @param \stdClass $record
235 * @return \core\task\adhoc_task
237 public static function adhoc_task_from_record($record) {
238 $classname = $record->classname;
239 if (strpos($classname, '\\') !== 0) {
240 $classname = '\\' . $classname;
242 if (!class_exists($classname)) {
243 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
246 $task = new $classname;
247 if (isset($record->nextruntime)) {
248 $task->set_next_run_time($record->nextruntime);
250 if (isset($record->id)) {
251 $task->set_id($record->id);
253 if (isset($record->component)) {
254 $task->set_component($record->component);
256 $task->set_blocking(!empty($record->blocking));
257 if (isset($record->faildelay)) {
258 $task->set_fail_delay($record->faildelay);
260 if (isset($record->customdata)) {
261 $task->set_custom_data_as_string($record->customdata);
268 * Utility method to create a task from a DB record.
270 * @param \stdClass $record
271 * @return \core\task\scheduled_task
273 public static function scheduled_task_from_record($record) {
274 $classname = $record->classname;
275 if (strpos($classname, '\\') !== 0) {
276 $classname = '\\' . $classname;
278 if (!class_exists($classname)) {
279 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
282 /** @var \core\task\scheduled_task $task */
283 $task = new $classname;
284 if (isset($record->lastruntime)) {
285 $task->set_last_run_time($record->lastruntime);
287 if (isset($record->nextruntime)) {
288 $task->set_next_run_time($record->nextruntime);
290 if (isset($record->customised)) {
291 $task->set_customised($record->customised);
293 if (isset($record->component)) {
294 $task->set_component($record->component);
296 $task->set_blocking(!empty($record->blocking));
297 if (isset($record->minute)) {
298 $task->set_minute($record->minute);
300 if (isset($record->hour)) {
301 $task->set_hour($record->hour);
303 if (isset($record->day)) {
304 $task->set_day($record->day);
306 if (isset($record->month)) {
307 $task->set_month($record->month);
309 if (isset($record->dayofweek)) {
310 $task->set_day_of_week($record->dayofweek);
312 if (isset($record->faildelay)) {
313 $task->set_fail_delay($record->faildelay);
315 if (isset($record->disabled)) {
316 $task->set_disabled($record->disabled);
323 * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
324 * Do not execute tasks loaded from this function - they have not been locked.
325 * @param string $componentname - The name of the component to load the tasks for.
326 * @return \core\task\scheduled_task[]
328 public static function load_scheduled_tasks_for_component($componentname) {
332 // We are just reading - so no locks required.
333 $records = $DB->get_records('task_scheduled', array('componentname' => $componentname), 'classname', '*', IGNORE_MISSING);
334 foreach ($records as $record) {
335 $task = self::scheduled_task_from_record($record);
336 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
346 * This function load the scheduled task details for a given classname.
348 * @param string $classname
349 * @return \core\task\scheduled_task or false
351 public static function get_scheduled_task($classname) {
354 if (strpos($classname, '\\') !== 0) {
355 $classname = '\\' . $classname;
357 // We are just reading - so no locks required.
358 $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
362 return self::scheduled_task_from_record($record);
366 * This function load the default scheduled task details for a given classname.
368 * @param string $classname
369 * @return \core\task\scheduled_task or false
371 public static function get_default_scheduled_task($classname) {
372 $task = self::get_scheduled_task($classname);
373 $componenttasks = array();
375 // Safety check in case no task was found for the given classname.
377 $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
380 foreach ($componenttasks as $componenttask) {
381 if (get_class($componenttask) == get_class($task)) {
382 return $componenttask;
390 * This function will return a list of all the scheduled tasks that exist in the database.
392 * @return \core\task\scheduled_task[]
394 public static function get_all_scheduled_tasks() {
397 $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
400 foreach ($records as $record) {
401 $task = self::scheduled_task_from_record($record);
402 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
412 * This function will dispatch the next adhoc task in the queue. The task will be handed out
413 * with an open lock - possibly on the entire cron process. Make sure you call either
414 * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
416 * @param int $timestart
417 * @return \core\task\adhoc_task or null if not found
419 public static function get_next_adhoc_task($timestart) {
421 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
423 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
424 throw new \moodle_exception('locktimeout');
427 $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
428 $params = array('timestart1' => $timestart);
429 $records = $DB->get_records_select('task_adhoc', $where, $params);
431 foreach ($records as $record) {
433 if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) {
434 $classname = '\\' . $record->classname;
435 $task = self::adhoc_task_from_record($record);
436 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
442 $task->set_lock($lock);
443 if (!$task->is_blocking()) {
444 $cronlock->release();
446 $task->set_cron_lock($cronlock);
453 $cronlock->release();
458 * This function will dispatch the next scheduled task in the queue. The task will be handed out
459 * with an open lock - possibly on the entire cron process. Make sure you call either
460 * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
462 * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
463 * @return \core\task\scheduled_task or null
465 public static function get_next_scheduled_task($timestart) {
467 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
469 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
470 throw new \moodle_exception('locktimeout');
473 $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
474 AND (nextruntime IS NULL OR nextruntime < :timestart2)
476 $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
477 $records = $DB->get_records_select('task_scheduled', $where, $params);
479 $pluginmanager = \core_plugin_manager::instance();
481 foreach ($records as $record) {
483 if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
484 $classname = '\\' . $record->classname;
485 $task = self::scheduled_task_from_record($record);
486 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
492 $task->set_lock($lock);
494 // See if the component is disabled.
495 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
498 if (!$task->get_run_if_component_disabled() && !$plugininfo->is_enabled()) {
504 if (!$task->is_blocking()) {
505 $cronlock->release();
507 $task->set_cron_lock($cronlock);
514 $cronlock->release();
519 * This function indicates that an adhoc task was not completed successfully and should be retried.
521 * @param \core\task\adhoc_task $task
523 public static function adhoc_task_failed(adhoc_task $task) {
525 $delay = $task->get_fail_delay();
527 // Reschedule task with exponential fall off for failing tasks.
534 // Max of 24 hour delay.
535 if ($delay > 86400) {
539 $classname = get_class($task);
540 if (strpos($classname, '\\') !== 0) {
541 $classname = '\\' . $classname;
544 $task->set_next_run_time(time() + $delay);
545 $task->set_fail_delay($delay);
546 $record = self::record_from_adhoc_task($task);
547 $DB->update_record('task_adhoc', $record);
549 if ($task->is_blocking()) {
550 $task->get_cron_lock()->release();
552 $task->get_lock()->release();
556 * This function indicates that an adhoc task was completed successfully.
558 * @param \core\task\adhoc_task $task
560 public static function adhoc_task_complete(adhoc_task $task) {
563 // Delete the adhoc task record - it is finished.
564 $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
566 // Reschedule and then release the locks.
567 if ($task->is_blocking()) {
568 $task->get_cron_lock()->release();
570 $task->get_lock()->release();
574 * This function indicates that a scheduled task was not completed successfully and should be retried.
576 * @param \core\task\scheduled_task $task
578 public static function scheduled_task_failed(scheduled_task $task) {
581 $delay = $task->get_fail_delay();
583 // Reschedule task with exponential fall off for failing tasks.
590 // Max of 24 hour delay.
591 if ($delay > 86400) {
595 $classname = get_class($task);
596 if (strpos($classname, '\\') !== 0) {
597 $classname = '\\' . $classname;
600 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
601 $record->nextruntime = time() + $delay;
602 $record->faildelay = $delay;
603 $DB->update_record('task_scheduled', $record);
605 if ($task->is_blocking()) {
606 $task->get_cron_lock()->release();
608 $task->get_lock()->release();
612 * This function indicates that a scheduled task was completed successfully and should be rescheduled.
614 * @param \core\task\scheduled_task $task
616 public static function scheduled_task_complete(scheduled_task $task) {
619 $classname = get_class($task);
620 if (strpos($classname, '\\') !== 0) {
621 $classname = '\\' . $classname;
623 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
625 $record->lastruntime = time();
626 $record->faildelay = 0;
627 $record->nextruntime = $task->get_next_scheduled_time();
629 $DB->update_record('task_scheduled', $record);
632 // Reschedule and then release the locks.
633 if ($task->is_blocking()) {
634 $task->get_cron_lock()->release();
636 $task->get_lock()->release();
640 * This function is used to indicate that any long running cron processes should exit at the
641 * next opportunity and restart. This is because something (e.g. DB changes) has changed and
642 * the static caches may be stale.
644 public static function clear_static_caches() {
646 // Do not use get/set config here because the caches cannot be relied on.
647 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
649 $record->value = time();
650 $DB->update_record('config', $record);
652 $record = new \stdClass();
653 $record->name = 'scheduledtaskreset';
654 $record->value = time();
655 $DB->insert_record('config', $record);
660 * Return true if the static caches have been cleared since $starttime.
661 * @param int $starttime The time this process started.
662 * @return boolean True if static caches need resetting.
664 public static function static_caches_cleared_since($starttime) {
666 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
667 return $record && (intval($record->value) > $starttime);