diff --git a/admin/tool/task/clear_fail_delay.php b/admin/tool/task/clear_fail_delay.php new file mode 100644 index 00000000000..d820dda7265 --- /dev/null +++ b/admin/tool/task/clear_fail_delay.php @@ -0,0 +1,69 @@ +. + +/** + * Script clears the fail delay for a task and reschedules its next execution. + * + * @package tool_task + * @copyright 2017 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('NO_OUTPUT_BUFFERING', true); + +require('../../../config.php'); + +require_once($CFG->libdir.'/cronlib.php'); + +// Basic security checks. +require_login(); +$context = context_system::instance(); +require_capability('moodle/site:config', $context); + +// Get task and check the parameter is valid. +$taskname = required_param('task', PARAM_RAW_TRIMMED); +$task = \core\task\manager::get_scheduled_task($taskname); +if (!$task) { + print_error('cannotfindinfo', 'error', $taskname); +} + +// If actually doing the clear, then carry out the task and redirect to the scheduled task page. +if (optional_param('confirm', 0, PARAM_INT)) { + require_sesskey(); + + \core\task\manager::clear_fail_delay($task); + + redirect(new moodle_url('/admin/tool/task/scheduledtasks.php')); +} + +// Start output. +$PAGE->set_url(new moodle_url('/admin/tool/task/schedule_task.php')); +$PAGE->set_context($context); +$PAGE->navbar->add(get_string('scheduledtasks', 'tool_task'), new moodle_url('/admin/tool/task/scheduledtasks.php')); +$PAGE->navbar->add(s($task->get_name())); +$PAGE->navbar->add(get_string('clear')); +echo $OUTPUT->header(); + +// The initial request just shows the confirmation page; we don't do anything further unless +// they confirm. +echo $OUTPUT->confirm(get_string('clearfaildelay_confirm', 'tool_task', $task->get_name()), + new single_button(new moodle_url('/admin/tool/task/clear_fail_delay.php', + array('task' => $taskname, 'confirm' => 1, 'sesskey' => sesskey())), + get_string('clear')), + new single_button(new moodle_url('/admin/tool/task/scheduledtasks.php'), + get_string('cancel'), false)); + +echo $OUTPUT->footer(); diff --git a/admin/tool/task/lang/en/tool_task.php b/admin/tool/task/lang/en/tool_task.php index 2f382b04ce7..107e01b5e83 100644 --- a/admin/tool/task/lang/en/tool_task.php +++ b/admin/tool/task/lang/en/tool_task.php @@ -25,6 +25,7 @@ $string['asap'] = 'ASAP'; $string['backtoscheduledtasks'] = 'Back to scheduled tasks'; $string['blocking'] = 'Blocking'; +$string['clearfaildelay_confirm'] = 'Are you sure you want to clear the fail delay for task \'{$a}\'? After clearing the delay, the task will run according to its normal schedule.'; $string['component'] = 'Component'; $string['corecomponent'] = 'Core'; $string['default'] = 'Default'; diff --git a/admin/tool/task/renderer.php b/admin/tool/task/renderer.php index 575aa662201..d612355dc9b 100644 --- a/admin/tool/task/renderer.php +++ b/admin/tool/task/renderer.php @@ -112,6 +112,14 @@ class tool_task_renderer extends plugin_renderer_base { get_string('runnow', 'tool_task')), 'task-runnow'); } + $clearfail = ''; + if ($task->get_fail_delay()) { + $clearfail = html_writer::div(html_writer::link( + new moodle_url('/admin/tool/task/clear_fail_delay.php', + array('task' => get_class($task), 'sesskey' => sesskey())), + get_string('clear')), 'task-clearfaildelay'); + } + $row = new html_table_row(array( $namecell, $componentcell, @@ -123,7 +131,7 @@ class tool_task_renderer extends plugin_renderer_base { new html_table_cell($task->get_day()), new html_table_cell($task->get_day_of_week()), new html_table_cell($task->get_month()), - new html_table_cell($task->get_fail_delay()), + new html_table_cell($task->get_fail_delay() . $clearfail), new html_table_cell($customised))); // Cron-style values must always be LTR. diff --git a/admin/tool/task/styles.css b/admin/tool/task/styles.css index 771467498fc..0e846ce3c5c 100644 --- a/admin/tool/task/styles.css +++ b/admin/tool/task/styles.css @@ -10,6 +10,7 @@ direction: ltr; } -#page-admin-tool-task-scheduledtasks .task-runnow { +#page-admin-tool-task-scheduledtasks .task-runnow, +#page-admin-tool-task-scheduledtasks .task-clearfaildelay { font-size: 0.75em; } diff --git a/admin/tool/task/tests/behat/behat_tool_task.php b/admin/tool/task/tests/behat/behat_tool_task.php new file mode 100644 index 00000000000..0a0afe148b9 --- /dev/null +++ b/admin/tool/task/tests/behat/behat_tool_task.php @@ -0,0 +1,53 @@ +. + +/** + * Behat step definitions for scheduled task administration. + * + * @package tool_task + * @copyright 2017 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/../../../../../lib/behat/behat_base.php'); + +/** + * Behat step definitions for scheduled task administration. + * + * @package tool_task + * @copyright 2017 The Open University + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_tool_task extends behat_base { + + /** + * Set a fake fail delay for a scheduled task. + * + * @Given /^the scheduled task "(?P[^"]+)" has a fail delay of "(?P\d+)" seconds$/ + * @param string $task Task classname + * @param int $seconds Fail delay time in seconds + */ + public function scheduled_task_has_fail_delay_seconds($task, $seconds) { + global $DB; + $id = $DB->get_field('task_scheduled', 'id', ['classname' => $task], IGNORE_MISSING); + if (!$id) { + throw new Exception('Unknown scheduled task: ' . $task); + } + $DB->set_field('task_scheduled', 'faildelay', $seconds, ['id' => $id]); + } +} diff --git a/admin/tool/task/tests/behat/clear_fail_delay.feature b/admin/tool/task/tests/behat/clear_fail_delay.feature new file mode 100644 index 00000000000..474468d42c4 --- /dev/null +++ b/admin/tool/task/tests/behat/clear_fail_delay.feature @@ -0,0 +1,25 @@ +@tool @tool_task +Feature: Clear scheduled task fail delay + In order to stop failures from delaying a scheduled task run + As an admin + I need to be able to clear the fail delay on a task + + Background: + Given the scheduled task "\core\task\send_new_user_passwords_task" has a fail delay of "60" seconds + And I log in as "admin" + And I navigate to "Scheduled tasks" node in "Site administration > Server" + + Scenario: Clear fail delay + When I click on "Clear" "text" in the "Send new user passwords" "table_row" + And I should see "Are you sure you want to clear the fail delay" + And I press "Clear" + + Then I should not see "60" in the "Send new user passwords" "table_row" + And I should not see "Clear" in the "Send new user passwords" "table_row" + + Scenario: Cancel clearing the fail delay + When I click on "Clear" "text" in the "Send new user passwords" "table_row" + And I press "Cancel" + + Then I should see "60" in the "Send new user passwords" "table_row" + And I should see "Clear" in the "Send new user passwords" "table_row" diff --git a/lib/classes/task/manager.php b/lib/classes/task/manager.php index d7f4a11e5cf..fe4124a39e5 100644 --- a/lib/classes/task/manager.php +++ b/lib/classes/task/manager.php @@ -89,10 +89,7 @@ class manager { $validtasks = array(); foreach ($tasks as $taskid => $task) { - $classname = get_class($task); - if (strpos($classname, '\\') !== 0) { - $classname = '\\' . $classname; - } + $classname = self::get_canonical_class_name($task); $validtasks[] = $classname; @@ -188,10 +185,7 @@ class manager { public static function configure_scheduled_task(scheduled_task $task) { global $DB; - $classname = get_class($task); - if (strpos($classname, '\\') !== 0) { - $classname = '\\' . $classname; - } + $classname = self::get_canonical_class_name($task); $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST); @@ -211,10 +205,7 @@ class manager { */ public static function record_from_scheduled_task($task) { $record = new \stdClass(); - $record->classname = get_class($task); - if (strpos($record->classname, '\\') !== 0) { - $record->classname = '\\' . $record->classname; - } + $record->classname = self::get_canonical_class_name($task); $record->component = $task->get_component(); $record->blocking = $task->is_blocking(); $record->customised = $task->is_customised(); @@ -239,10 +230,7 @@ class manager { */ public static function record_from_adhoc_task($task) { $record = new \stdClass(); - $record->classname = get_class($task); - if (strpos($record->classname, '\\') !== 0) { - $record->classname = '\\' . $record->classname; - } + $record->classname = self::get_canonical_class_name($task); $record->id = $task->get_id(); $record->component = $task->get_component(); $record->blocking = $task->is_blocking(); @@ -261,10 +249,7 @@ class manager { * @return \core\task\adhoc_task */ public static function adhoc_task_from_record($record) { - $classname = $record->classname; - if (strpos($classname, '\\') !== 0) { - $classname = '\\' . $classname; - } + $classname = self::get_canonical_class_name($record->classname); if (!class_exists($classname)) { debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER); return false; @@ -301,10 +286,7 @@ class manager { * @return \core\task\scheduled_task */ public static function scheduled_task_from_record($record) { - $classname = $record->classname; - if (strpos($classname, '\\') !== 0) { - $classname = '\\' . $classname; - } + $classname = self::get_canonical_class_name($record->classname); if (!class_exists($classname)) { debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER); return false; @@ -381,9 +363,7 @@ class manager { public static function get_scheduled_task($classname) { global $DB; - if (strpos($classname, '\\') !== 0) { - $classname = '\\' . $classname; - } + $classname = self::get_canonical_class_name($classname); // We are just reading - so no locks required. $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING); if (!$record) { @@ -401,9 +381,7 @@ class manager { public static function get_adhoc_tasks($classname) { global $DB; - if (strpos($classname, '\\') !== 0) { - $classname = '\\' . $classname; - } + $classname = self::get_canonical_class_name($classname); // We are just reading - so no locks required. $records = $DB->get_records('task_adhoc', array('classname' => $classname)); @@ -601,10 +579,7 @@ class manager { $delay = 86400; } - $classname = get_class($task); - if (strpos($classname, '\\') !== 0) { - $classname = '\\' . $classname; - } + $classname = self::get_canonical_class_name($task); $task->set_next_run_time(time() + $delay); $task->set_fail_delay($delay); @@ -657,10 +632,7 @@ class manager { $delay = 86400; } - $classname = get_class($task); - if (strpos($classname, '\\') !== 0) { - $classname = '\\' . $classname; - } + $classname = self::get_canonical_class_name($task); $record = $DB->get_record('task_scheduled', array('classname' => $classname)); $record->nextruntime = time() + $delay; @@ -673,6 +645,23 @@ class manager { $task->get_lock()->release(); } + /** + * Clears the fail delay for the given task and updates its next run time based on the schedule. + * + * @param scheduled_task $task Task to reset + * @throws \dml_exception If there is a database error + */ + public static function clear_fail_delay(scheduled_task $task) { + global $DB; + + $record = new \stdClass(); + $record->id = $DB->get_field('task_scheduled', 'id', + ['classname' => self::get_canonical_class_name($task)]); + $record->nextruntime = $task->get_next_scheduled_time(); + $record->faildelay = 0; + $DB->update_record('task_scheduled', $record); + } + /** * This function indicates that a scheduled task was completed successfully and should be rescheduled. * @@ -681,10 +670,7 @@ class manager { public static function scheduled_task_complete(scheduled_task $task) { global $DB; - $classname = get_class($task); - if (strpos($classname, '\\') !== 0) { - $classname = '\\' . $classname; - } + $classname = self::get_canonical_class_name($task); $record = $DB->get_record('task_scheduled', array('classname' => $classname)); if ($record) { $record->lastruntime = time(); @@ -731,4 +717,21 @@ class manager { $record = $DB->get_record('config', array('name'=>'scheduledtaskreset')); return $record && (intval($record->value) > $starttime); } + + /** + * Gets class name for use in database table. Always begins with a \. + * + * @param string|task_base $taskorstring Task object or a string + */ + protected static function get_canonical_class_name($taskorstring) { + if (is_string($taskorstring)) { + $classname = $taskorstring; + } else { + $classname = get_class($taskorstring); + } + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + return $classname; + } } diff --git a/lib/tests/scheduled_task_test.php b/lib/tests/scheduled_task_test.php index 2dc039c2ba6..adc9b4143c1 100644 --- a/lib/tests/scheduled_task_test.php +++ b/lib/tests/scheduled_task_test.php @@ -467,4 +467,38 @@ class core_scheduled_task_testcase extends advanced_testcase { // There should only be two items in the array, '.' and '..'. $this->assertEquals(2, count($filesarray)); } + + /** + * Test that the function to clear the fail delay from a task works correctly. + */ + public function test_clear_fail_delay() { + + $this->resetAfterTest(); + + // Get an example task to use for testing. Task is set to run every minute by default. + $taskname = '\core\task\send_new_user_passwords_task'; + + // Pretend task started running and then failed 3 times. + $before = time(); + $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); + for ($i = 0; $i < 3; $i ++) { + $task = \core\task\manager::get_scheduled_task($taskname); + $lock = $cronlockfactory->get_lock('\\' . get_class($task), 10); + $task->set_lock($lock); + \core\task\manager::scheduled_task_failed($task); + } + + // Confirm task is now delayed by several minutes. + $task = \core\task\manager::get_scheduled_task($taskname); + $this->assertEquals(240, $task->get_fail_delay()); + $this->assertGreaterThan($before + 230, $task->get_next_run_time()); + + // Clear the fail delay and re-get the task. + \core\task\manager::clear_fail_delay($task); + $task = \core\task\manager::get_scheduled_task($taskname); + + // There should be no delay and it should run within the next minute. + $this->assertEquals(0, $task->get_fail_delay()); + $this->assertLessThan($before + 70, $task->get_next_run_time()); + } }