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