Merge branch 'MDL-47480-master' of git://github.com/ankitagarwal/moodle
[moodle.git] / lib / classes / task / manager.php
CommitLineData
309ae892
DW
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.
b7f7c3bc 43 * @return \core\task\scheduled_task[] - List of scheduled tasks for this component.
309ae892
DW
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 require_once($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);
a0ac4060
DW
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 }
309ae892
DW
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;
309ae892
DW
88 $tasks = self::load_default_scheduled_tasks_for_component($componentname);
89
852ff037 90 foreach ($tasks as $taskid => $task) {
309ae892
DW
91 $classname = get_class($task);
92 if (strpos($classname, '\\') !== 0) {
93 $classname = '\\' . $classname;
94 }
95
0af336ef
FM
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.
99 continue;
309ae892 100 }
309ae892 101
0af336ef
FM
102 // Update the record from the default task data.
103 self::configure_scheduled_task($task);
104 } else {
105 // Ensure that the first run follows the schedule.
106 $task->set_next_run_time($task->get_next_scheduled_time());
309ae892 107
0af336ef
FM
108 // Insert the new task in the database.
109 $record = self::record_from_scheduled_task($task);
110 $DB->insert_record('task_scheduled', $record);
111 }
309ae892 112 }
309ae892
DW
113 }
114
115 /**
116 * Queue an adhoc task to run in the background.
117 *
118 * @param \core\task\adhoc_task $task - The new adhoc task information to store.
119 * @return boolean - True if the config was saved.
120 */
121 public static function queue_adhoc_task(adhoc_task $task) {
122 global $DB;
123
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);
128
129 return $result;
130 }
131
132 /**
133 * Change the default configuration for a scheduled task.
134 * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
135 *
136 * @param \core\task\scheduled_task $task - The new scheduled task information to store.
137 * @return boolean - True if the config was saved.
138 */
139 public static function configure_scheduled_task(scheduled_task $task) {
140 global $DB;
309ae892
DW
141
142 $classname = get_class($task);
143 if (strpos($classname, '\\') !== 0) {
144 $classname = '\\' . $classname;
145 }
309ae892
DW
146
147 $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
148
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);
153
309ae892
DW
154 return $result;
155 }
156
157 /**
158 * Utility method to create a DB record from a scheduled task.
159 *
160 * @param \core\task\scheduled_task $task
b7f7c3bc 161 * @return \stdClass
309ae892
DW
162 */
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;
168 }
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();
0a5aa65b 180 $record->disabled = $task->get_disabled();
309ae892
DW
181
182 return $record;
183 }
184
185 /**
186 * Utility method to create a DB record from an adhoc task.
187 *
188 * @param \core\task\adhoc_task $task
b7f7c3bc 189 * @return \stdClass
309ae892
DW
190 */
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;
196 }
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();
ab57368a 202 $record->customdata = $task->get_custom_data_as_string();
309ae892
DW
203
204 return $record;
205 }
206
207 /**
208 * Utility method to create an adhoc task from a DB record.
209 *
b7f7c3bc 210 * @param \stdClass $record
309ae892
DW
211 * @return \core\task\adhoc_task
212 */
213 public static function adhoc_task_from_record($record) {
214 $classname = $record->classname;
215 if (strpos($classname, '\\') !== 0) {
216 $classname = '\\' . $classname;
217 }
218 if (!class_exists($classname)) {
a0ac4060 219 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
309ae892
DW
220 return false;
221 }
222 $task = new $classname;
223 if (isset($record->nextruntime)) {
224 $task->set_next_run_time($record->nextruntime);
225 }
226 if (isset($record->id)) {
227 $task->set_id($record->id);
228 }
229 if (isset($record->component)) {
230 $task->set_component($record->component);
231 }
232 $task->set_blocking(!empty($record->blocking));
233 if (isset($record->faildelay)) {
234 $task->set_fail_delay($record->faildelay);
235 }
236 if (isset($record->customdata)) {
ab57368a 237 $task->set_custom_data_as_string($record->customdata);
309ae892
DW
238 }
239
240 return $task;
241 }
242
243 /**
244 * Utility method to create a task from a DB record.
245 *
b7f7c3bc 246 * @param \stdClass $record
309ae892
DW
247 * @return \core\task\scheduled_task
248 */
249 public static function scheduled_task_from_record($record) {
250 $classname = $record->classname;
251 if (strpos($classname, '\\') !== 0) {
252 $classname = '\\' . $classname;
253 }
254 if (!class_exists($classname)) {
a0ac4060 255 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
309ae892
DW
256 return false;
257 }
b7f7c3bc 258 /** @var \core\task\scheduled_task $task */
309ae892
DW
259 $task = new $classname;
260 if (isset($record->lastruntime)) {
261 $task->set_last_run_time($record->lastruntime);
262 }
263 if (isset($record->nextruntime)) {
264 $task->set_next_run_time($record->nextruntime);
265 }
266 if (isset($record->customised)) {
267 $task->set_customised($record->customised);
268 }
269 if (isset($record->component)) {
270 $task->set_component($record->component);
271 }
272 $task->set_blocking(!empty($record->blocking));
273 if (isset($record->minute)) {
274 $task->set_minute($record->minute);
275 }
276 if (isset($record->hour)) {
277 $task->set_hour($record->hour);
278 }
279 if (isset($record->day)) {
280 $task->set_day($record->day);
281 }
282 if (isset($record->month)) {
283 $task->set_month($record->month);
284 }
285 if (isset($record->dayofweek)) {
286 $task->set_day_of_week($record->dayofweek);
287 }
288 if (isset($record->faildelay)) {
289 $task->set_fail_delay($record->faildelay);
290 }
0a5aa65b
291 if (isset($record->disabled)) {
292 $task->set_disabled($record->disabled);
293 }
309ae892
DW
294
295 return $task;
296 }
297
298 /**
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.
b7f7c3bc 302 * @return \core\task\scheduled_task[]
309ae892
DW
303 */
304 public static function load_scheduled_tasks_for_component($componentname) {
305 global $DB;
306
307 $tasks = array();
308 // We are just reading - so no locks required.
852ff037 309 $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
309ae892
DW
310 foreach ($records as $record) {
311 $task = self::scheduled_task_from_record($record);
a0ac4060
DW
312 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
313 if ($task) {
314 $tasks[] = $task;
315 }
309ae892
DW
316 }
317
318 return $tasks;
319 }
320
321 /**
322 * This function load the scheduled task details for a given classname.
323 *
b7f7c3bc
324 * @param string $classname
325 * @return \core\task\scheduled_task or false
309ae892
DW
326 */
327 public static function get_scheduled_task($classname) {
328 global $DB;
329
330 if (strpos($classname, '\\') !== 0) {
331 $classname = '\\' . $classname;
332 }
333 // We are just reading - so no locks required.
334 $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
335 if (!$record) {
336 return false;
337 }
338 return self::scheduled_task_from_record($record);
339 }
340
341 /**
342 * This function load the default scheduled task details for a given classname.
343 *
b7f7c3bc
344 * @param string $classname
345 * @return \core\task\scheduled_task or false
309ae892
DW
346 */
347 public static function get_default_scheduled_task($classname) {
348 $task = self::get_scheduled_task($classname);
a0ac4060 349 $componenttasks = array();
309ae892 350
a0ac4060
DW
351 // Safety check in case no task was found for the given classname.
352 if ($task) {
353 $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
354 }
309ae892
DW
355
356 foreach ($componenttasks as $componenttask) {
357 if (get_class($componenttask) == get_class($task)) {
358 return $componenttask;
359 }
360 }
361
362 return false;
363 }
364
365 /**
366 * This function will return a list of all the scheduled tasks that exist in the database.
367 *
b7f7c3bc 368 * @return \core\task\scheduled_task[]
309ae892
DW
369 */
370 public static function get_all_scheduled_tasks() {
371 global $DB;
372
373 $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
374 $tasks = array();
375
376 foreach ($records as $record) {
377 $task = self::scheduled_task_from_record($record);
a0ac4060
DW
378 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
379 if ($task) {
380 $tasks[] = $task;
381 }
309ae892
DW
382 }
383
384 return $tasks;
385 }
386
387 /**
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.
391 *
b7f7c3bc
392 * @param int $timestart
393 * @return \core\task\adhoc_task or null if not found
309ae892
DW
394 */
395 public static function get_next_adhoc_task($timestart) {
396 global $DB;
397 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
398
399 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
400 throw new \moodle_exception('locktimeout');
401 }
402
403 $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
404 $params = array('timestart1' => $timestart);
405 $records = $DB->get_records_select('task_adhoc', $where, $params);
406
407 foreach ($records as $record) {
408
409 if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) {
410 $classname = '\\' . $record->classname;
411 $task = self::adhoc_task_from_record($record);
a0ac4060
DW
412 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
413 if (!$task) {
414 $lock->release();
415 continue;
416 }
309ae892
DW
417
418 $task->set_lock($lock);
419 if (!$task->is_blocking()) {
420 $cronlock->release();
421 } else {
422 $task->set_cron_lock($cronlock);
423 }
424 return $task;
425 }
426 }
427
428 // No tasks.
429 $cronlock->release();
430 return null;
431 }
432
433 /**
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.
437 *
438 * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
b7f7c3bc 439 * @return \core\task\scheduled_task or null
309ae892
DW
440 */
441 public static function get_next_scheduled_task($timestart) {
442 global $DB;
443 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
444
445 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
446 throw new \moodle_exception('locktimeout');
447 }
448
0a5aa65b
449 $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
450 AND (nextruntime IS NULL OR nextruntime < :timestart2)
c44b4213
AA
451 AND disabled = 0
452 ORDER BY lastruntime, id ASC";
309ae892
DW
453 $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
454 $records = $DB->get_records_select('task_scheduled', $where, $params);
455
fd57c17e
DW
456 $pluginmanager = \core_plugin_manager::instance();
457
309ae892
DW
458 foreach ($records as $record) {
459
460 if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
461 $classname = '\\' . $record->classname;
462 $task = self::scheduled_task_from_record($record);
a0ac4060
DW
463 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
464 if (!$task) {
465 $lock->release();
466 continue;
467 }
309ae892
DW
468
469 $task->set_lock($lock);
fd57c17e
DW
470
471 // See if the component is disabled.
472 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
473
474 if ($plugininfo) {
5429afc0 475 if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
fd57c17e
DW
476 $lock->release();
477 continue;
478 }
479 }
480
309ae892
DW
481 if (!$task->is_blocking()) {
482 $cronlock->release();
483 } else {
484 $task->set_cron_lock($cronlock);
485 }
486 return $task;
487 }
488 }
489
490 // No tasks.
491 $cronlock->release();
492 return null;
493 }
494
495 /**
b7f7c3bc 496 * This function indicates that an adhoc task was not completed successfully and should be retried.
309ae892 497 *
b7f7c3bc 498 * @param \core\task\adhoc_task $task
309ae892
DW
499 */
500 public static function adhoc_task_failed(adhoc_task $task) {
501 global $DB;
502 $delay = $task->get_fail_delay();
503
504 // Reschedule task with exponential fall off for failing tasks.
505 if (empty($delay)) {
506 $delay = 60;
507 } else {
508 $delay *= 2;
509 }
510
511 // Max of 24 hour delay.
512 if ($delay > 86400) {
513 $delay = 86400;
514 }
515
516 $classname = get_class($task);
517 if (strpos($classname, '\\') !== 0) {
518 $classname = '\\' . $classname;
519 }
520
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);
525
526 if ($task->is_blocking()) {
527 $task->get_cron_lock()->release();
528 }
529 $task->get_lock()->release();
530 }
531
532 /**
b7f7c3bc 533 * This function indicates that an adhoc task was completed successfully.
309ae892 534 *
b7f7c3bc 535 * @param \core\task\adhoc_task $task
309ae892
DW
536 */
537 public static function adhoc_task_complete(adhoc_task $task) {
538 global $DB;
539
540 // Delete the adhoc task record - it is finished.
541 $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
542
543 // Reschedule and then release the locks.
544 if ($task->is_blocking()) {
545 $task->get_cron_lock()->release();
546 }
547 $task->get_lock()->release();
548 }
549
550 /**
b7f7c3bc 551 * This function indicates that a scheduled task was not completed successfully and should be retried.
309ae892 552 *
b7f7c3bc 553 * @param \core\task\scheduled_task $task
309ae892
DW
554 */
555 public static function scheduled_task_failed(scheduled_task $task) {
556 global $DB;
557
558 $delay = $task->get_fail_delay();
559
560 // Reschedule task with exponential fall off for failing tasks.
561 if (empty($delay)) {
562 $delay = 60;
563 } else {
564 $delay *= 2;
565 }
566
567 // Max of 24 hour delay.
568 if ($delay > 86400) {
569 $delay = 86400;
570 }
571
572 $classname = get_class($task);
573 if (strpos($classname, '\\') !== 0) {
574 $classname = '\\' . $classname;
575 }
576
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);
581
582 if ($task->is_blocking()) {
583 $task->get_cron_lock()->release();
584 }
585 $task->get_lock()->release();
586 }
587
588 /**
b7f7c3bc 589 * This function indicates that a scheduled task was completed successfully and should be rescheduled.
309ae892 590 *
b7f7c3bc 591 * @param \core\task\scheduled_task $task
309ae892
DW
592 */
593 public static function scheduled_task_complete(scheduled_task $task) {
594 global $DB;
595
596 $classname = get_class($task);
597 if (strpos($classname, '\\') !== 0) {
598 $classname = '\\' . $classname;
599 }
600 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
601 if ($record) {
602 $record->lastruntime = time();
603 $record->faildelay = 0;
604 $record->nextruntime = $task->get_next_scheduled_time();
605
606 $DB->update_record('task_scheduled', $record);
607 }
608
609 // Reschedule and then release the locks.
610 if ($task->is_blocking()) {
611 $task->get_cron_lock()->release();
612 }
613 $task->get_lock()->release();
614 }
615
616 /**
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.
620 */
621 public static function clear_static_caches() {
622 global $DB;
623 // Do not use get/set config here because the caches cannot be relied on.
624 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
625 if ($record) {
626 $record->value = time();
627 $DB->update_record('config', $record);
628 } else {
629 $record = new \stdClass();
630 $record->name = 'scheduledtaskreset';
631 $record->value = time();
632 $DB->insert_record('config', $record);
633 }
634 }
635
636 /**
637 * Return true if the static caches have been cleared since $starttime.
638 * @param int $starttime The time this process started.
b7f7c3bc 639 * @return boolean True if static caches need resetting.
309ae892
DW
640 */
641 public static function static_caches_cleared_since($starttime) {
642 global $DB;
643 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
b7f7c3bc 644 return $record && (intval($record->value) > $starttime);
309ae892
DW
645 }
646}