MDL-46881 core: Allow adhoc tasks to be rescheduled
[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;
b2f6f88d 58 include($file);
309ae892
DW
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 88 $tasks = self::load_default_scheduled_tasks_for_component($componentname);
b2f6f88d 89 $validtasks = array();
309ae892 90
852ff037 91 foreach ($tasks as $taskid => $task) {
470d59d3 92 $classname = self::get_canonical_class_name($task);
309ae892 93
b2f6f88d
FM
94 $validtasks[] = $classname;
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 }
b2f6f88d
FM
113
114 // Delete any task that is not defined in the component any more.
115 $sql = "component = :component";
116 $params = array('component' => $componentname);
117 if (!empty($validtasks)) {
118 list($insql, $inparams) = $DB->get_in_or_equal($validtasks, SQL_PARAMS_NAMED, 'param', false);
119 $sql .= ' AND classname ' . $insql;
120 $params = array_merge($params, $inparams);
121 }
122 $DB->delete_records_select('task_scheduled', $sql, $params);
309ae892
DW
123 }
124
96e1633f
MG
125 /**
126 * Checks if the task with the same classname, component and customdata is already scheduled
127 *
128 * @param adhoc_task $task
129 * @return bool
130 */
131 protected static function task_is_scheduled($task) {
b51b6969
AN
132 return false !== self::get_queued_adhoc_task_record($task);
133 }
134
135 /**
136 * Checks if the task with the same classname, component and customdata is already scheduled
137 *
138 * @param adhoc_task $task
139 * @return bool
140 */
141 protected static function get_queued_adhoc_task_record($task) {
96e1633f 142 global $DB;
b51b6969 143
96e1633f
MG
144 $record = self::record_from_adhoc_task($task);
145 $params = [$record->classname, $record->component, $record->customdata];
146 $sql = 'classname = ? AND component = ? AND ' .
147 $DB->sql_compare_text('customdata', \core_text::strlen($record->customdata) + 1) . ' = ?';
7e4c4b6f
AN
148
149 if ($record->userid) {
150 $params[] = $record->userid;
151 $sql .= " AND userid = ? ";
152 }
b51b6969
AN
153 return $DB->get_record_select('task_adhoc', $sql, $params);
154 }
155
156 /**
157 * Schedule a new task, or reschedule an existing adhoc task which has matching data.
158 *
159 * Only a task matching the same user, classname, component, and customdata will be rescheduled.
160 * If these values do not match exactly then a new task is scheduled.
161 *
162 * @param \core\task\adhoc_task $task - The new adhoc task information to store.
163 * @since Moodle 3.7
164 */
165 public static function reschedule_or_queue_adhoc_task(adhoc_task $task) : void {
166 global $DB;
167
168 if ($existingrecord = self::get_queued_adhoc_task_record($task)) {
169 // Only update the next run time if it is explicitly set on the task.
170 $nextruntime = $task->get_next_run_time();
171 if ($nextruntime && ($existingrecord->nextruntime != $nextruntime)) {
172 $DB->set_field('task_adhoc', 'nextruntime', $nextruntime, ['id' => $existingrecord->id]);
173 }
174 } else {
175 // There is nothing queued yet. Just queue as normal.
176 self::queue_adhoc_task($task);
177 }
96e1633f
MG
178 }
179
309ae892
DW
180 /**
181 * Queue an adhoc task to run in the background.
182 *
183 * @param \core\task\adhoc_task $task - The new adhoc task information to store.
b51b6969 184 * @param bool $checkforexisting - If set to true and the task with the same user, classname, component and customdata
96e1633f 185 * is already scheduled then it will not schedule a new task. Can be used only for ASAP tasks.
309ae892
DW
186 * @return boolean - True if the config was saved.
187 */
96e1633f 188 public static function queue_adhoc_task(adhoc_task $task, $checkforexisting = false) {
309ae892
DW
189 global $DB;
190
7e4c4b6f
AN
191 if ($userid = $task->get_userid()) {
192 // User found. Check that they are suitable.
193 \core_user::require_active_user(\core_user::get_user($userid, '*', MUST_EXIST), true, true);
194 }
195
309ae892 196 $record = self::record_from_adhoc_task($task);
dfb9daea
DNA
197 // Schedule it immediately if nextruntime not explicitly set.
198 if (!$task->get_next_run_time()) {
199 $record->nextruntime = time() - 1;
200 }
96e1633f
MG
201
202 // Check if the same task is already scheduled.
203 if ($checkforexisting && self::task_is_scheduled($task)) {
204 return false;
205 }
206
207 // Queue the task.
309ae892
DW
208 $result = $DB->insert_record('task_adhoc', $record);
209
210 return $result;
211 }
212
213 /**
214 * Change the default configuration for a scheduled task.
215 * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}.
216 *
217 * @param \core\task\scheduled_task $task - The new scheduled task information to store.
218 * @return boolean - True if the config was saved.
219 */
220 public static function configure_scheduled_task(scheduled_task $task) {
221 global $DB;
309ae892 222
470d59d3 223 $classname = self::get_canonical_class_name($task);
309ae892
DW
224
225 $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST);
226
227 $record = self::record_from_scheduled_task($task);
228 $record->id = $original->id;
229 $record->nextruntime = $task->get_next_scheduled_time();
230 $result = $DB->update_record('task_scheduled', $record);
231
309ae892
DW
232 return $result;
233 }
234
235 /**
236 * Utility method to create a DB record from a scheduled task.
237 *
238 * @param \core\task\scheduled_task $task
b7f7c3bc 239 * @return \stdClass
309ae892
DW
240 */
241 public static function record_from_scheduled_task($task) {
242 $record = new \stdClass();
470d59d3 243 $record->classname = self::get_canonical_class_name($task);
309ae892
DW
244 $record->component = $task->get_component();
245 $record->blocking = $task->is_blocking();
246 $record->customised = $task->is_customised();
247 $record->lastruntime = $task->get_last_run_time();
248 $record->nextruntime = $task->get_next_run_time();
249 $record->faildelay = $task->get_fail_delay();
250 $record->hour = $task->get_hour();
251 $record->minute = $task->get_minute();
252 $record->day = $task->get_day();
253 $record->dayofweek = $task->get_day_of_week();
254 $record->month = $task->get_month();
0a5aa65b 255 $record->disabled = $task->get_disabled();
309ae892
DW
256
257 return $record;
258 }
259
260 /**
261 * Utility method to create a DB record from an adhoc task.
262 *
263 * @param \core\task\adhoc_task $task
b7f7c3bc 264 * @return \stdClass
309ae892
DW
265 */
266 public static function record_from_adhoc_task($task) {
267 $record = new \stdClass();
470d59d3 268 $record->classname = self::get_canonical_class_name($task);
309ae892
DW
269 $record->id = $task->get_id();
270 $record->component = $task->get_component();
271 $record->blocking = $task->is_blocking();
272 $record->nextruntime = $task->get_next_run_time();
273 $record->faildelay = $task->get_fail_delay();
ab57368a 274 $record->customdata = $task->get_custom_data_as_string();
7e4c4b6f 275 $record->userid = $task->get_userid();
309ae892
DW
276
277 return $record;
278 }
279
280 /**
281 * Utility method to create an adhoc task from a DB record.
282 *
b7f7c3bc 283 * @param \stdClass $record
309ae892
DW
284 * @return \core\task\adhoc_task
285 */
286 public static function adhoc_task_from_record($record) {
470d59d3 287 $classname = self::get_canonical_class_name($record->classname);
309ae892 288 if (!class_exists($classname)) {
a0ac4060 289 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
309ae892
DW
290 return false;
291 }
292 $task = new $classname;
293 if (isset($record->nextruntime)) {
294 $task->set_next_run_time($record->nextruntime);
295 }
296 if (isset($record->id)) {
297 $task->set_id($record->id);
298 }
299 if (isset($record->component)) {
300 $task->set_component($record->component);
301 }
302 $task->set_blocking(!empty($record->blocking));
303 if (isset($record->faildelay)) {
304 $task->set_fail_delay($record->faildelay);
305 }
306 if (isset($record->customdata)) {
ab57368a 307 $task->set_custom_data_as_string($record->customdata);
309ae892
DW
308 }
309
7e4c4b6f
AN
310 if (isset($record->userid)) {
311 $task->set_userid($record->userid);
312 }
313
309ae892
DW
314 return $task;
315 }
316
317 /**
318 * Utility method to create a task from a DB record.
319 *
b7f7c3bc 320 * @param \stdClass $record
309ae892
DW
321 * @return \core\task\scheduled_task
322 */
323 public static function scheduled_task_from_record($record) {
470d59d3 324 $classname = self::get_canonical_class_name($record->classname);
309ae892 325 if (!class_exists($classname)) {
a0ac4060 326 debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
309ae892
DW
327 return false;
328 }
b7f7c3bc 329 /** @var \core\task\scheduled_task $task */
309ae892
DW
330 $task = new $classname;
331 if (isset($record->lastruntime)) {
332 $task->set_last_run_time($record->lastruntime);
333 }
334 if (isset($record->nextruntime)) {
335 $task->set_next_run_time($record->nextruntime);
336 }
337 if (isset($record->customised)) {
338 $task->set_customised($record->customised);
339 }
340 if (isset($record->component)) {
341 $task->set_component($record->component);
342 }
343 $task->set_blocking(!empty($record->blocking));
344 if (isset($record->minute)) {
345 $task->set_minute($record->minute);
346 }
347 if (isset($record->hour)) {
348 $task->set_hour($record->hour);
349 }
350 if (isset($record->day)) {
351 $task->set_day($record->day);
352 }
353 if (isset($record->month)) {
354 $task->set_month($record->month);
355 }
356 if (isset($record->dayofweek)) {
357 $task->set_day_of_week($record->dayofweek);
358 }
359 if (isset($record->faildelay)) {
360 $task->set_fail_delay($record->faildelay);
361 }
0a5aa65b
362 if (isset($record->disabled)) {
363 $task->set_disabled($record->disabled);
364 }
309ae892
DW
365
366 return $task;
367 }
368
369 /**
370 * Given a component name, will load the list of tasks from the scheduled_tasks table for that component.
371 * Do not execute tasks loaded from this function - they have not been locked.
372 * @param string $componentname - The name of the component to load the tasks for.
b7f7c3bc 373 * @return \core\task\scheduled_task[]
309ae892
DW
374 */
375 public static function load_scheduled_tasks_for_component($componentname) {
376 global $DB;
377
378 $tasks = array();
379 // We are just reading - so no locks required.
852ff037 380 $records = $DB->get_records('task_scheduled', array('component' => $componentname), 'classname', '*', IGNORE_MISSING);
309ae892
DW
381 foreach ($records as $record) {
382 $task = self::scheduled_task_from_record($record);
a0ac4060
DW
383 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
384 if ($task) {
385 $tasks[] = $task;
386 }
309ae892
DW
387 }
388
389 return $tasks;
390 }
391
392 /**
393 * This function load the scheduled task details for a given classname.
394 *
b7f7c3bc
395 * @param string $classname
396 * @return \core\task\scheduled_task or false
309ae892
DW
397 */
398 public static function get_scheduled_task($classname) {
399 global $DB;
400
470d59d3 401 $classname = self::get_canonical_class_name($classname);
309ae892
DW
402 // We are just reading - so no locks required.
403 $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING);
404 if (!$record) {
405 return false;
406 }
407 return self::scheduled_task_from_record($record);
408 }
409
0f268f5d
RW
410 /**
411 * This function load the adhoc tasks for a given classname.
412 *
413 * @param string $classname
414 * @return \core\task\adhoc_task[]
415 */
416 public static function get_adhoc_tasks($classname) {
417 global $DB;
418
470d59d3 419 $classname = self::get_canonical_class_name($classname);
0f268f5d
RW
420 // We are just reading - so no locks required.
421 $records = $DB->get_records('task_adhoc', array('classname' => $classname));
422
423 return array_map(function($record) {
424 return self::adhoc_task_from_record($record);
425 }, $records);
426 }
427
309ae892
DW
428 /**
429 * This function load the default scheduled task details for a given classname.
430 *
b7f7c3bc
431 * @param string $classname
432 * @return \core\task\scheduled_task or false
309ae892
DW
433 */
434 public static function get_default_scheduled_task($classname) {
435 $task = self::get_scheduled_task($classname);
a0ac4060 436 $componenttasks = array();
309ae892 437
a0ac4060
DW
438 // Safety check in case no task was found for the given classname.
439 if ($task) {
440 $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component());
441 }
309ae892
DW
442
443 foreach ($componenttasks as $componenttask) {
444 if (get_class($componenttask) == get_class($task)) {
445 return $componenttask;
446 }
447 }
448
449 return false;
450 }
451
452 /**
453 * This function will return a list of all the scheduled tasks that exist in the database.
454 *
b7f7c3bc 455 * @return \core\task\scheduled_task[]
309ae892
DW
456 */
457 public static function get_all_scheduled_tasks() {
458 global $DB;
459
460 $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING);
461 $tasks = array();
462
463 foreach ($records as $record) {
464 $task = self::scheduled_task_from_record($record);
a0ac4060
DW
465 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
466 if ($task) {
467 $tasks[] = $task;
468 }
309ae892
DW
469 }
470
471 return $tasks;
472 }
473
474 /**
475 * This function will dispatch the next adhoc task in the queue. The task will be handed out
476 * with an open lock - possibly on the entire cron process. Make sure you call either
477 * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task.
478 *
b7f7c3bc
479 * @param int $timestart
480 * @return \core\task\adhoc_task or null if not found
309ae892
DW
481 */
482 public static function get_next_adhoc_task($timestart) {
483 global $DB;
484 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
485
486 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
487 throw new \moodle_exception('locktimeout');
488 }
489
490 $where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
491 $params = array('timestart1' => $timestart);
492 $records = $DB->get_records_select('task_adhoc', $where, $params);
493
494 foreach ($records as $record) {
495
504d569b 496 if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) {
309ae892 497 $classname = '\\' . $record->classname;
504d569b
BH
498
499 // Safety check, see if the task has been already processed by another cron run.
500 $record = $DB->get_record('task_adhoc', array('id' => $record->id));
501 if (!$record) {
502 $lock->release();
503 continue;
504 }
505
309ae892 506 $task = self::adhoc_task_from_record($record);
a0ac4060
DW
507 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
508 if (!$task) {
509 $lock->release();
510 continue;
511 }
309ae892
DW
512
513 $task->set_lock($lock);
514 if (!$task->is_blocking()) {
515 $cronlock->release();
516 } else {
517 $task->set_cron_lock($cronlock);
518 }
519 return $task;
520 }
521 }
522
523 // No tasks.
524 $cronlock->release();
525 return null;
526 }
527
528 /**
529 * This function will dispatch the next scheduled task in the queue. The task will be handed out
530 * with an open lock - possibly on the entire cron process. Make sure you call either
531 * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task.
532 *
533 * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this.
b7f7c3bc 534 * @return \core\task\scheduled_task or null
309ae892
DW
535 */
536 public static function get_next_scheduled_task($timestart) {
537 global $DB;
538 $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
539
540 if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) {
541 throw new \moodle_exception('locktimeout');
542 }
543
0a5aa65b
544 $where = "(lastruntime IS NULL OR lastruntime < :timestart1)
545 AND (nextruntime IS NULL OR nextruntime < :timestart2)
c44b4213
AA
546 AND disabled = 0
547 ORDER BY lastruntime, id ASC";
309ae892
DW
548 $params = array('timestart1' => $timestart, 'timestart2' => $timestart);
549 $records = $DB->get_records_select('task_scheduled', $where, $params);
550
fd57c17e
DW
551 $pluginmanager = \core_plugin_manager::instance();
552
309ae892
DW
553 foreach ($records as $record) {
554
504d569b 555 if ($lock = $cronlockfactory->get_lock(($record->classname), 0)) {
309ae892
DW
556 $classname = '\\' . $record->classname;
557 $task = self::scheduled_task_from_record($record);
a0ac4060
DW
558 // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
559 if (!$task) {
560 $lock->release();
561 continue;
562 }
309ae892
DW
563
564 $task->set_lock($lock);
fd57c17e
DW
565
566 // See if the component is disabled.
567 $plugininfo = $pluginmanager->get_plugin_info($task->get_component());
568
569 if ($plugininfo) {
5429afc0 570 if (($plugininfo->is_enabled() === false) && !$task->get_run_if_component_disabled()) {
fd57c17e
DW
571 $lock->release();
572 continue;
573 }
574 }
575
1dc462ba
MA
576 // Make sure the task data is unchanged.
577 if (!$DB->record_exists('task_scheduled', (array) $record)) {
578 $lock->release();
579 continue;
580 }
581
309ae892
DW
582 if (!$task->is_blocking()) {
583 $cronlock->release();
584 } else {
585 $task->set_cron_lock($cronlock);
586 }
587 return $task;
588 }
589 }
590
591 // No tasks.
592 $cronlock->release();
593 return null;
594 }
595
596 /**
b7f7c3bc 597 * This function indicates that an adhoc task was not completed successfully and should be retried.
309ae892 598 *
b7f7c3bc 599 * @param \core\task\adhoc_task $task
309ae892
DW
600 */
601 public static function adhoc_task_failed(adhoc_task $task) {
602 global $DB;
603 $delay = $task->get_fail_delay();
604
605 // Reschedule task with exponential fall off for failing tasks.
606 if (empty($delay)) {
607 $delay = 60;
608 } else {
609 $delay *= 2;
610 }
611
612 // Max of 24 hour delay.
613 if ($delay > 86400) {
614 $delay = 86400;
615 }
616
470d59d3 617 $classname = self::get_canonical_class_name($task);
309ae892
DW
618
619 $task->set_next_run_time(time() + $delay);
620 $task->set_fail_delay($delay);
621 $record = self::record_from_adhoc_task($task);
622 $DB->update_record('task_adhoc', $record);
623
624 if ($task->is_blocking()) {
625 $task->get_cron_lock()->release();
626 }
627 $task->get_lock()->release();
4b71596f
AN
628
629 // Finalise the log output.
8c69e86c 630 logmanager::finalise_log(true);
309ae892
DW
631 }
632
633 /**
b7f7c3bc 634 * This function indicates that an adhoc task was completed successfully.
309ae892 635 *
b7f7c3bc 636 * @param \core\task\adhoc_task $task
309ae892
DW
637 */
638 public static function adhoc_task_complete(adhoc_task $task) {
639 global $DB;
640
4b71596f 641 // Finalise the log output.
8c69e86c 642 logmanager::finalise_log();
4b71596f 643
309ae892
DW
644 // Delete the adhoc task record - it is finished.
645 $DB->delete_records('task_adhoc', array('id' => $task->get_id()));
646
647 // Reschedule and then release the locks.
648 if ($task->is_blocking()) {
649 $task->get_cron_lock()->release();
650 }
651 $task->get_lock()->release();
652 }
653
654 /**
b7f7c3bc 655 * This function indicates that a scheduled task was not completed successfully and should be retried.
309ae892 656 *
b7f7c3bc 657 * @param \core\task\scheduled_task $task
309ae892
DW
658 */
659 public static function scheduled_task_failed(scheduled_task $task) {
660 global $DB;
661
662 $delay = $task->get_fail_delay();
663
664 // Reschedule task with exponential fall off for failing tasks.
665 if (empty($delay)) {
666 $delay = 60;
667 } else {
668 $delay *= 2;
669 }
670
671 // Max of 24 hour delay.
672 if ($delay > 86400) {
673 $delay = 86400;
674 }
675
470d59d3 676 $classname = self::get_canonical_class_name($task);
309ae892
DW
677
678 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
679 $record->nextruntime = time() + $delay;
680 $record->faildelay = $delay;
681 $DB->update_record('task_scheduled', $record);
682
683 if ($task->is_blocking()) {
684 $task->get_cron_lock()->release();
685 }
686 $task->get_lock()->release();
4b71596f
AN
687
688 // Finalise the log output.
8c69e86c 689 logmanager::finalise_log(true);
309ae892
DW
690 }
691
470d59d3 692 /**
693 * Clears the fail delay for the given task and updates its next run time based on the schedule.
694 *
695 * @param scheduled_task $task Task to reset
696 * @throws \dml_exception If there is a database error
697 */
698 public static function clear_fail_delay(scheduled_task $task) {
699 global $DB;
700
701 $record = new \stdClass();
702 $record->id = $DB->get_field('task_scheduled', 'id',
703 ['classname' => self::get_canonical_class_name($task)]);
704 $record->nextruntime = $task->get_next_scheduled_time();
705 $record->faildelay = 0;
706 $DB->update_record('task_scheduled', $record);
707 }
708
309ae892 709 /**
b7f7c3bc 710 * This function indicates that a scheduled task was completed successfully and should be rescheduled.
309ae892 711 *
b7f7c3bc 712 * @param \core\task\scheduled_task $task
309ae892
DW
713 */
714 public static function scheduled_task_complete(scheduled_task $task) {
715 global $DB;
716
4b71596f 717 // Finalise the log output.
8c69e86c 718 logmanager::finalise_log();
4b71596f 719
470d59d3 720 $classname = self::get_canonical_class_name($task);
309ae892
DW
721 $record = $DB->get_record('task_scheduled', array('classname' => $classname));
722 if ($record) {
723 $record->lastruntime = time();
724 $record->faildelay = 0;
725 $record->nextruntime = $task->get_next_scheduled_time();
726
727 $DB->update_record('task_scheduled', $record);
728 }
729
730 // Reschedule and then release the locks.
731 if ($task->is_blocking()) {
732 $task->get_cron_lock()->release();
733 }
734 $task->get_lock()->release();
735 }
736
737 /**
738 * This function is used to indicate that any long running cron processes should exit at the
739 * next opportunity and restart. This is because something (e.g. DB changes) has changed and
740 * the static caches may be stale.
741 */
742 public static function clear_static_caches() {
743 global $DB;
744 // Do not use get/set config here because the caches cannot be relied on.
745 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
746 if ($record) {
747 $record->value = time();
748 $DB->update_record('config', $record);
749 } else {
750 $record = new \stdClass();
751 $record->name = 'scheduledtaskreset';
752 $record->value = time();
753 $DB->insert_record('config', $record);
754 }
755 }
756
757 /**
758 * Return true if the static caches have been cleared since $starttime.
759 * @param int $starttime The time this process started.
b7f7c3bc 760 * @return boolean True if static caches need resetting.
309ae892
DW
761 */
762 public static function static_caches_cleared_since($starttime) {
763 global $DB;
764 $record = $DB->get_record('config', array('name'=>'scheduledtaskreset'));
b7f7c3bc 765 return $record && (intval($record->value) > $starttime);
309ae892 766 }
470d59d3 767
768 /**
769 * Gets class name for use in database table. Always begins with a \.
770 *
771 * @param string|task_base $taskorstring Task object or a string
772 */
773 protected static function get_canonical_class_name($taskorstring) {
774 if (is_string($taskorstring)) {
775 $classname = $taskorstring;
776 } else {
777 $classname = get_class($taskorstring);
778 }
779 if (strpos($classname, '\\') !== 0) {
780 $classname = '\\' . $classname;
781 }
782 return $classname;
783 }
309ae892 784}