From 470d59d31f5d277de799875308b20922890dfb7a Mon Sep 17 00:00:00 2001
From: sam marshall <s.marshall@open.ac.uk>
Date: Tue, 19 Dec 2017 13:31:14 +0000
Subject: [PATCH] MDL-61063 Scheduled tasks: Add link to clear fail delay

---
 admin/tool/task/clear_fail_delay.php          | 69 +++++++++++++++
 admin/tool/task/lang/en/tool_task.php         |  1 +
 admin/tool/task/renderer.php                  | 10 ++-
 admin/tool/task/styles.css                    |  3 +-
 .../tool/task/tests/behat/behat_tool_task.php | 53 +++++++++++
 .../task/tests/behat/clear_fail_delay.feature | 25 ++++++
 lib/classes/task/manager.php                  | 87 ++++++++++---------
 lib/tests/scheduled_task_test.php             | 34 ++++++++
 8 files changed, 238 insertions(+), 44 deletions(-)
 create mode 100644 admin/tool/task/clear_fail_delay.php
 create mode 100644 admin/tool/task/tests/behat/behat_tool_task.php
 create mode 100644 admin/tool/task/tests/behat/clear_fail_delay.feature

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 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * 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<task_name>[^"]+)" has a fail delay of "(?P<seconds_number>\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());
+    }
 }