weekly release 2.7dev
[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.
43 * @return array(core\task\scheduled_task) - List of scheduled tasks for this component.
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
182 * @return stdClass
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();
201
202 return $record;
203 }
204
205 /**
206 * Utility method to create a DB record from an adhoc task.
207 *
208 * @param \core\task\adhoc_task $task
209 * @return stdClass
210 */
211 public static function record_from_adhoc_task($task) {
212 $record = new \stdClass();
213 $record->classname = get_class($task);
214 if (strpos($record->classname, '\\') !== 0) {
215 $record->classname = '\\' . $record->classname;
216 }
217 $record->id = $task->get_id();
218 $record->component = $task->get_component();
219 $record->blocking = $task->is_blocking();
220 $record->nextruntime = $task->get_next_run_time();
221 $record->faildelay = $task->get_fail_delay();
222 $record->customdata = $task->get_custom_data();
223
224 return $record;
225 }
226
227 /**
228 * Utility method to create an adhoc task from a DB record.
229 *
230 * @param stdClass $record
231 * @return \core\task\adhoc_task
232 */
233 public static function adhoc_task_from_record($record) {
234 $classname = $record->classname;
235 if (strpos($classname, '\\') !== 0) {
236 $classname = '\\' . $classname;
237 }
238 if (!class_exists($classname)) {
239 return false;
240 }
241 $task = new $classname;
242 if (isset($record->nextruntime)) {
243 $task->set_next_run_time($record->nextruntime);
244 }
245 if (isset($record->id)) {
246 $task->set_id($record->id);
247 }
248 if (isset($record->component)) {
249 $task->set_component($record->component);
250 }
251 $task->set_blocking(!empty($record->blocking));
252 if (isset($record->faildelay)) {
253 $task->set_fail_delay($record->faildelay);
254 }
255 if (isset($record->customdata)) {
256 $task->set_custom_data($record->customdata);
257 }
258
259 return $task;
260 }
261
262 /**
263 * Utility method to create a task from a DB record.
264 *
265 * @param stdClass $record
266 * @return \core\task\scheduled_task
267 */
268 public static function scheduled_task_from_record($record) {
269 $classname = $record->classname;
270 if (strpos($classname, '\\') !== 0) {
271 $classname = '\\' . $classname;
272 }
273 if (!class_exists($classname)) {
274 return false;
275 }
276 $task = new $classname;
277 if (isset($record->lastruntime)) {
278 $task->set_last_run_time($record->lastruntime);
279 }
280 if (isset($record->nextruntime)) {
281 $task->set_next_run_time($record->nextruntime);
282 }
283 if (isset($record->customised)) {
284 $task->set_customised($record->customised);
285 }
286 if (isset($record->component)) {
287 $task->set_component($record->component);
288 }
289 $task->set_blocking(!empty($record->blocking));
290 if (isset($record->minute)) {
291 $task->set_minute($record->minute);
292 }
293 if (isset($record->hour)) {
294 $task->set_hour($record->hour);
295 }
296 if (isset($record->day)) {
297 $task->set_day($record->day);
298 }
299 if (isset($record->month)) {
300 $task->set_month($record->month);
301 }
302 if (isset($record->dayofweek)) {
303 $task->set_day_of_week($record->dayofweek);
304 }
305 if (isset($record->faildelay)) {
306 $task->set_fail_delay($record->faildelay);
307 }
308
309 return $task;
310 }
311
312 /**
313 * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
314 * Do not execute tasks loaded from this function - they have not been locked.
315 * @param string $componentname - The name of the component to load the tasks for.
316 * @return array(core\task\scheduled_task)
317 */
318 public static function load_scheduled_tasks_for_component($componentname) {
319 global $DB;
320
321 $tasks = array();
322 // We are just reading - so no locks required.
323 $records = $DB->get_records('task_scheduled', array('componentname' => $componentname), 'classname', '*', IGNORE_MISSING);
324 foreach ($records as $record) {
325 $task = self::scheduled_task_from_record($record);
326 $tasks[] = $task;
327 }
328
329 return $tasks;
330 }
331
332 /**
333 * This function load the scheduled task details for a given classname.
334 *
335 * @return core\task\scheduled_task or false
336 */
337 public static function get_scheduled_task($classname) {
338 global $DB;
339
340 if (strpos($classname, '\\') !== 0) {
341 $classname = '\\' . $classname;
342 }
343 // We are just reading - so no locks required.
344 $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
345 if (!$record) {
346 return false;
347 }
348 return self::scheduled_task_from_record($record);
349 }
350
351 /**
352 * This function load the default scheduled task details for a given classname.
353 *
354 * @return core\task\scheduled_task or false
355 */
356 public static function get_default_scheduled_task($classname) {
357 $task = self::get_scheduled_task($classname);
358
359 $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
360
361 foreach ($componenttasks as $componenttask) {
362 if (get_class($componenttask) == get_class($task)) {
363 return $componenttask;
364 }
365 }
366
367 return false;
368 }
369
370 /**
371 * This function will return a list of all the scheduled tasks that exist in the database.
372 *
373 * @return array(core\task\scheduled_task) or null
374 */
375 public static function get_all_scheduled_tasks() {
376 global $DB;
377
378 $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
379 $tasks = array();
380
381 foreach ($records as $record) {
382 $task = self::scheduled_task_from_record($record);
383 $tasks[] = $task;
384 }
385
386 return $tasks;
387 }
388
389 /**
390 * This function will dispatch the next adhoc task in the queue. The task will be handed out
391 * with an open lock - possibly on the entire cron process. Make sure you call either
392 * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
393 *
394 * @return core\task\adhoc_task or null
395 */
396 public static function get_next_adhoc_task($timestart) {
397 global $DB;
398 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
399
400 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
401 throw new \moodle_exception('locktimeout');
402 }
403
404 $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
405 $params = array('timestart1' => $timestart);
406 $records = $DB->get_records_select('task_adhoc', $where, $params);
407
408 foreach ($records as $record) {
409
410 if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) {
411 $classname = '\\' . $record->classname;
412 $task = self::adhoc_task_from_record($record);
413
414 $task->set_lock($lock);
415 if (!$task->is_blocking()) {
416 $cronlock->release();
417 } else {
418 $task->set_cron_lock($cronlock);
419 }
420 return $task;
421 }
422 }
423
424 // No tasks.
425 $cronlock->release();
426 return null;
427 }
428
429 /**
430 * This function will dispatch the next scheduled task in the queue. The task will be handed out
431 * with an open lock - possibly on the entire cron process. Make sure you call either
432 * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
433 *
434 * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
435 * @return core\task\scheduled_task or null
436 */
437 public static function get_next_scheduled_task($timestart) {
438 global $DB;
439 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
440
441 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
442 throw new \moodle_exception('locktimeout');
443 }
444
445 $where = '(lastruntime IS NULL OR lastruntime < :timestart1) AND (nextruntime IS NULL OR nextruntime < :timestart2)';
446 $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
447 $records = $DB->get_records_select('task_scheduled', $where, $params);
448
449 foreach ($records as $record) {
450
451 if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) {
452 $classname = '\\' . $record->classname;
453 $task = self::scheduled_task_from_record($record);
454
455 $task->set_lock($lock);
456 if (!$task->is_blocking()) {
457 $cronlock->release();
458 } else {
459 $task->set_cron_lock($cronlock);
460 }
461 return $task;
462 }
463 }
464
465 // No tasks.
466 $cronlock->release();
467 return null;
468 }
469
470 /**
471 * This function indicates that an adhoc task was not completed succesfully and should be retried.
472 *
473 * @param core\task\adhoc_task $task
474 */
475 public static function adhoc_task_failed(adhoc_task $task) {
476 global $DB;
477 $delay = $task->get_fail_delay();
478
479 // Reschedule task with exponential fall off for failing tasks.
480 if (empty($delay)) {
481 $delay = 60;
482 } else {
483 $delay *= 2;
484 }
485
486 // Max of 24 hour delay.
487 if ($delay > 86400) {
488 $delay = 86400;
489 }
490
491 $classname = get_class($task);
492 if (strpos($classname, '\\') !== 0) {
493 $classname = '\\' . $classname;
494 }
495
496 $task->set_next_run_time(time() + $delay);
497 $task->set_fail_delay($delay);
498 $record = self::record_from_adhoc_task($task);
499 $DB->update_record('task_adhoc', $record);
500
501 if ($task->is_blocking()) {
502 $task->get_cron_lock()->release();
503 }
504 $task->get_lock()->release();
505 }
506
507 /**
508 * This function indicates that an adhoc task was completed succesfully.
509 *
510 * @param core\task\adhoc_task $task
511 */
512 public static function adhoc_task_complete(adhoc_task $task) {
513 global $DB;
514
515 // Delete the adhoc task record - it is finished.
516 $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
517
518 // Reschedule and then release the locks.
519 if ($task->is_blocking()) {
520 $task->get_cron_lock()->release();
521 }
522 $task->get_lock()->release();
523 }
524
525 /**
526 * This function indicates that a scheduled task was not completed succesfully and should be retried.
527 *
528 * @param core\task\scheduled_task $task
529 */
530 public static function scheduled_task_failed(scheduled_task $task) {
531 global $DB;
532
533 $delay = $task->get_fail_delay();
534
535 // Reschedule task with exponential fall off for failing tasks.
536 if (empty($delay)) {
537 $delay = 60;
538 } else {
539 $delay *= 2;
540 }
541
542 // Max of 24 hour delay.
543 if ($delay > 86400) {
544 $delay = 86400;
545 }
546
547 $classname = get_class($task);
548 if (strpos($classname, '\\') !== 0) {
549 $classname = '\\' . $classname;
550 }
551
552 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
553 $record->nextruntime = time() + $delay;
554 $record->faildelay = $delay;
555 $DB->update_record('task_scheduled', $record);
556
557 if ($task->is_blocking()) {
558 $task->get_cron_lock()->release();
559 }
560 $task->get_lock()->release();
561 }
562
563 /**
564 * This function indicates that a scheduled task was completed succesfully and should be rescheduled.
565 *
566 * @param core\task\scheduled_task $task
567 */
568 public static function scheduled_task_complete(scheduled_task $task) {
569 global $DB;
570
571 $classname = get_class($task);
572 if (strpos($classname, '\\') !== 0) {
573 $classname = '\\' . $classname;
574 }
575 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
576 if ($record) {
577 $record->lastruntime = time();
578 $record->faildelay = 0;
579 $record->nextruntime = $task->get_next_scheduled_time();
580
581 $DB->update_record('task_scheduled', $record);
582 }
583
584 // Reschedule and then release the locks.
585 if ($task->is_blocking()) {
586 $task->get_cron_lock()->release();
587 }
588 $task->get_lock()->release();
589 }
590
591 /**
592 * This function is used to indicate that any long running cron processes should exit at the
593 * next opportunity and restart. This is because something (e.g. DB changes) has changed and
594 * the static caches may be stale.
595 */
596 public static function clear_static_caches() {
597 global $DB;
598 // Do not use get/set config here because the caches cannot be relied on.
599 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
600 if ($record) {
601 $record->value = time();
602 $DB->update_record('config', $record);
603 } else {
604 $record = new \stdClass();
605 $record->name = 'scheduledtaskreset';
606 $record->value = time();
607 $DB->insert_record('config', $record);
608 }
609 }
610
611 /**
612 * Return true if the static caches have been cleared since $starttime.
613 * @param int $starttime The time this process started.
614 * @return boolean True if static caches need reseting.
615 */
616 public static function static_caches_cleared_since($starttime) {
617 global $DB;
618 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
619 return $record && intval($record->value) > $starttime;
620 }
621}