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