From a44f14c79e742a92a4f37ab0076b66031ec8406b Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Tue, 28 Nov 2023 10:55:35 +0700 Subject: [PATCH 1/4] MDL-79128 adhoc_task: Implement no-retry task Co-authored-by: Stevani Andolo Including in this commit: - Added a new counter called attemptsavailable for ad-hoc task - PHPUnit test for the new feature --- lib/classes/task/adhoc_task.php | 31 ++++++++++++ lib/classes/task/manager.php | 10 ++++ lib/db/install.xml | 3 +- lib/db/upgrade.php | 33 +++++++++++++ lib/tests/fixtures/task_fixtures.php | 19 ++++++++ lib/tests/task/adhoc_task_test.php | 72 ++++++++++++++++++++++++++++ lib/upgrade.txt | 5 ++ version.php | 2 +- 8 files changed, 173 insertions(+), 2 deletions(-) diff --git a/lib/classes/task/adhoc_task.php b/lib/classes/task/adhoc_task.php index 1036ced2bba..69c7db08ca9 100644 --- a/lib/classes/task/adhoc_task.php +++ b/lib/classes/task/adhoc_task.php @@ -46,6 +46,9 @@ abstract class adhoc_task extends task_base { /** @var \core\lock\lock The concurrency task lock for this task. */ private $concurrencylock = null; + /** @var integer|null $attemptsavailable - The remaining attempts of the task, or null for unlimited. */ + private $attemptsavailable = null; + /** * Provide default implementation of the task name for backward compatibility. Extending classes are expected to implement * this method to provide a descriptive name for the task (shown to admins) @@ -171,4 +174,32 @@ abstract class adhoc_task extends task_base { $this->concurrencylock->release(); } } + + /** + * Set the remaining attempts of the task. + * + * @param int|null $attemptsavailable Number of the remaining attempts of the task, or null for unlimited. + */ + public function set_attempts_available(?int $attemptsavailable): void { + $this->attemptsavailable = $attemptsavailable; + } + + /** + * Get the remaining attempts of the task. + * + * @return int|null Number of the remaining attempts of the task, or null for unlimited. + */ + public function get_attempts_available(): ?int { + return $this->attemptsavailable; + } + + /** + * Used to indicate if the task should be re-run if it fails. + * By default, tasks will be retried until they succeed, other tasks can override this method to change this behaviour. + * + * @return bool true if the task should be retried until it succeeds, false otherwise. + */ + public function retry_until_success(): bool { + return true; + } } diff --git a/lib/classes/task/manager.php b/lib/classes/task/manager.php index 90ea9450205..ccb88a28a91 100644 --- a/lib/classes/task/manager.php +++ b/lib/classes/task/manager.php @@ -229,6 +229,9 @@ class manager { $record->nextruntime = time() - 1; } + // Check if the task is allowed to be retried or not. + $record->attemptsavailable = $task->retry_until_success() ? null : 1; + // Check if the same task is already scheduled. if ($checkforexisting && self::task_is_scheduled($task)) { return false; @@ -311,6 +314,7 @@ class manager { $record->timestarted = $task->get_timestarted(); $record->hostname = $task->get_hostname(); $record->pid = $task->get_pid(); + $record->attemptsavailable = $task->get_attempts_available(); return $record; } @@ -357,6 +361,9 @@ class manager { if (isset($record->pid)) { $task->set_pid($record->pid); } + if (isset($record->attemptsavailable)) { + $task->set_attempts_available($record->attemptsavailable); + } return $task; } @@ -1106,6 +1113,9 @@ class manager { $task->set_pid(); $task->set_next_run_time(time() + $delay); $task->set_fail_delay($delay); + if (!$task->retry_until_success() && $task->get_attempts_available() > 0) { + $task->set_attempts_available($task->get_attempts_available() - 1); + } $record = self::record_from_adhoc_task($task); $DB->update_record('task_adhoc', $record); diff --git a/lib/db/install.xml b/lib/db/install.xml index 1a994f74a39..2cc45492767 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1,5 +1,5 @@ - @@ -3505,6 +3505,7 @@ + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index d0a47fcc39e..bbe8c64f329 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -864,5 +864,38 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2023120100.01); } + if ($oldversion < 2023121500.01) { + // Define field attemptsavailable to be added to task_adhoc. + $table = new xmldb_table('task_adhoc'); + $field = new xmldb_field( + name: 'attemptsavailable', + type: XMLDB_TYPE_INTEGER, + precision: '1', + unsigned: null, + notnull: null, + sequence: null, + default: null, + previous: 'pid', + ); + + // Conditionally launch add field attemptsavailable. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Set attemptsavailable to 0 for the tasks that have not been run before. + // Set attemptsavailable to 1 for the tasks that have been run and failed before. + $DB->execute(' + UPDATE {task_adhoc} + SET attemptsavailable = CASE + WHEN faildelay = 0 THEN 1 + WHEN faildelay > 0 THEN 0 + END + '); + + // Main savepoint reached. + upgrade_main_savepoint(true, 2023121500.01); + } + return true; } diff --git a/lib/tests/fixtures/task_fixtures.php b/lib/tests/fixtures/task_fixtures.php index efaaa98b54b..723aa36ef85 100644 --- a/lib/tests/fixtures/task_fixtures.php +++ b/lib/tests/fixtures/task_fixtures.php @@ -107,6 +107,25 @@ class adhoc_test4_task extends adhoc_test_task { class adhoc_test5_task extends adhoc_test_task { } +/** + * Test class for no-retry adhoc task. + * + * @package core + * @copyright 2023 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class no_retry_adhoc_task extends adhoc_test_task { + + /** + * Prevent the task from retrying. + * @return bool + */ + public function retry_until_success(): bool { + return false; + } + +} + class scheduled_test_task extends \core\task\scheduled_task { public function get_name() { return "Test task"; diff --git a/lib/tests/task/adhoc_task_test.php b/lib/tests/task/adhoc_task_test.php index dbd7033deb8..515fc722d35 100644 --- a/lib/tests/task/adhoc_task_test.php +++ b/lib/tests/task/adhoc_task_test.php @@ -142,6 +142,78 @@ class adhoc_task_test extends \advanced_testcase { $this->assertNull(manager::get_next_adhoc_task($now)); } + /** + * Test adhoc task failure retry backoff. + * + * @covers ::queue_adhoc_task + * @covers ::get_next_adhoc_task + * @covers ::adhoc_task_failed + */ + public function test_adhoc_task_with_retry_flag(): void { + global $DB; + $this->resetAfterTest(); + + $now = time(); + // Create a normal adhoc task. + $task = new adhoc_test_task(); + $taskid = manager::queue_adhoc_task(task: $task); + + // This is a normal task, so it should have unlimited attempts. The remaining available attempts should be null. + $attemptsavailable = $DB->get_field( + table: 'task_adhoc', + return: 'attemptsavailable', + conditions: ['id' => $taskid] + ); + $this->assertNull(actual: $attemptsavailable); + + // Get the task from the scheduler, execute it, and mark it as failed. + $task = manager::get_next_adhoc_task(timestart: $now); + $taskid = $task->get_id(); + $task->execute(); + manager::adhoc_task_failed(task: $task); + + // This is a normal task, so it should have unlimited attempts. The remaining available attempts should be null. + $attemptsavailable = $DB->get_field( + table: 'task_adhoc', + return: 'attemptsavailable', + conditions: ['id' => $taskid] + ); + $this->assertNull(actual: $attemptsavailable); + + // Create a no-retry adhoc task. + $now = time(); + $task = new no_retry_adhoc_task(); + $taskid = manager::queue_adhoc_task(task: $task); + + // This is no-retry task, so it should have only 1 attempt available. + $attemptsavailable = $DB->get_field( + table: 'task_adhoc', + return: 'attemptsavailable', + conditions: ['id' => $taskid] + ); + $this->assertEquals( + expected: 1, + actual: $attemptsavailable, + ); + + // Get the task from the scheduler, execute it, and mark it as failed. + $task = manager::get_next_adhoc_task(timestart: $now); + $taskid = $task->get_id(); + $task->execute(); + manager::adhoc_task_failed(task: $task); + + // This is no-retry task, the remaining available attempts should be reduced to 0. + $attemptsavailable = $DB->get_field( + table: 'task_adhoc', + return: 'attemptsavailable', + conditions: ['id' => $taskid] + ); + $this->assertEquals( + expected: 0, + actual: $attemptsavailable, + ); + } + /** * Test future adhoc task execution. * @covers ::get_next_adhoc_task diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 14746a6847b..5cb383ced38 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -19,6 +19,11 @@ information provided here is intended especially for developers. that are also members of these groups. * The options for `format_string()`, and `format_text()` are now checked for incorrectly passed context objects. Please note that this was never an accepted value but previously failed silently. +* The following functions have been added to adhoc_task class: + - retry_until_success(): Used to indicate if the task should be re-run if it fails + By default, tasks will be retried until they succeed, other tasks can override this method to change this behaviour. + - set_attempts_available(): Used to set the number of attempts available for the task + - get_attempts_available(): Used to get the number of attempts available for the task. === 4.3 === diff --git a/version.php b/version.php index e3af558e2bc..88b9f316404 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2023121500.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2023121500.01; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. $release = '4.4dev (Build: 20231215)'; // Human-friendly version name From 6c047c40b7b61a6d09ed70daf08bc20a4d3ae1f9 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Tue, 28 Nov 2023 11:47:22 +0700 Subject: [PATCH 2/4] MDL-79128 adhoc_task: Only rerun the allowed task Co-authored-by: Stevani Andolo --- lib/classes/cron.php | 3 ++ lib/classes/task/manager.php | 5 +++- lib/tests/task/adhoc_task_test.php | 47 +++++++++++++++++++++++++----- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/lib/classes/cron.php b/lib/classes/cron.php index 5a07780d10c..0b941cfce7e 100644 --- a/lib/classes/cron.php +++ b/lib/classes/cron.php @@ -363,6 +363,9 @@ class cron { $where .= ' AND classname = :classname'; $params['classname'] = \core\task\manager::get_canonical_class_name($classname); } + + // Only rerun the failed tasks that allow to be re-tried or have the remaining attempts available. + $where .= 'AND (attemptsavailable > 0 OR attemptsavailable IS NULL)'; $tasks = $DB->get_records_sql("SELECT * from {task_adhoc} WHERE $where", $params); foreach ($tasks as $t) { self::run_adhoc_task($t->id); diff --git a/lib/classes/task/manager.php b/lib/classes/task/manager.php index ccb88a28a91..e82215be6eb 100644 --- a/lib/classes/task/manager.php +++ b/lib/classes/task/manager.php @@ -785,6 +785,7 @@ class manager { 'SELECT classname, COALESCE(COUNT(*), 0) running, MIN(timestarted) earliest FROM {task_adhoc} WHERE timestarted IS NOT NULL + AND (attemptsavailable > 0 OR attemptsavailable IS NULL) AND nextruntime < :timestart GROUP BY classname ORDER BY running ASC, earliest DESC', @@ -939,10 +940,12 @@ class manager { SELECT classname, COUNT(*) running, MIN(timestarted) earliest FROM {task_adhoc} run WHERE timestarted IS NOT NULL + AND (attemptsavailable > 0 OR attemptsavailable IS NULL) GROUP BY classname ) run ON run.classname = q.classname WHERE nextruntime < :timestart - AND q.timestarted IS NULL " . + AND q.timestarted IS NULL + AND (q.attemptsavailable > 0 OR q.attemptsavailable IS NULL) " . (!empty($pertasksql) ? "AND (" . $pertasksql . ") " : "") . ($runmax ? "AND (COALESCE(run.running, 0)) < :runmax " : "") . "ORDER BY COALESCE(run.running, 0) ASC, run.earliest DESC, q.nextruntime ASC, q.id ASC", diff --git a/lib/tests/task/adhoc_task_test.php b/lib/tests/task/adhoc_task_test.php index 515fc722d35..2a8e78e834e 100644 --- a/lib/tests/task/adhoc_task_test.php +++ b/lib/tests/task/adhoc_task_test.php @@ -156,19 +156,19 @@ class adhoc_task_test extends \advanced_testcase { $now = time(); // Create a normal adhoc task. $task = new adhoc_test_task(); - $taskid = manager::queue_adhoc_task(task: $task); + $taskid1 = manager::queue_adhoc_task(task: $task); // This is a normal task, so it should have unlimited attempts. The remaining available attempts should be null. $attemptsavailable = $DB->get_field( table: 'task_adhoc', return: 'attemptsavailable', - conditions: ['id' => $taskid] + conditions: ['id' => $taskid1] ); $this->assertNull(actual: $attemptsavailable); // Get the task from the scheduler, execute it, and mark it as failed. $task = manager::get_next_adhoc_task(timestart: $now); - $taskid = $task->get_id(); + $taskid1 = $task->get_id(); $task->execute(); manager::adhoc_task_failed(task: $task); @@ -176,20 +176,20 @@ class adhoc_task_test extends \advanced_testcase { $attemptsavailable = $DB->get_field( table: 'task_adhoc', return: 'attemptsavailable', - conditions: ['id' => $taskid] + conditions: ['id' => $taskid1] ); $this->assertNull(actual: $attemptsavailable); // Create a no-retry adhoc task. $now = time(); $task = new no_retry_adhoc_task(); - $taskid = manager::queue_adhoc_task(task: $task); + $taskid2 = manager::queue_adhoc_task(task: $task); // This is no-retry task, so it should have only 1 attempt available. $attemptsavailable = $DB->get_field( table: 'task_adhoc', return: 'attemptsavailable', - conditions: ['id' => $taskid] + conditions: ['id' => $taskid2] ); $this->assertEquals( expected: 1, @@ -198,7 +198,7 @@ class adhoc_task_test extends \advanced_testcase { // Get the task from the scheduler, execute it, and mark it as failed. $task = manager::get_next_adhoc_task(timestart: $now); - $taskid = $task->get_id(); + $taskid2 = $task->get_id(); $task->execute(); manager::adhoc_task_failed(task: $task); @@ -206,12 +206,43 @@ class adhoc_task_test extends \advanced_testcase { $attemptsavailable = $DB->get_field( table: 'task_adhoc', return: 'attemptsavailable', - conditions: ['id' => $taskid] + conditions: ['id' => $taskid2] ); $this->assertEquals( expected: 0, actual: $attemptsavailable, ); + + // There will be two records in the task_adhoc table, one for each task. + $this->assertEquals( + expected: 2, + actual: $DB->count_records(table: 'task_adhoc') + ); + // But get_next_adhoc_task() should return only the allowed re-try task. + // The no-retry task should not be returned because it has no remaining attempts. + do { + $task = manager::get_next_adhoc_task(timestart: $now + 86400); + if ($task) { + manager::adhoc_task_failed(task: $task); + $this->assertEquals( + expected: $taskid1, + actual: $task->get_id(), + ); + } + } while ($task); + + // Mark the normal task as complete. + $task = manager::get_adhoc_task(taskid: $taskid1); + manager::adhoc_task_complete($task); + + // There will be one record in the task_adhoc table. + $this->assertEquals( + expected: 1, + actual: $DB->count_records(table: 'task_adhoc') + ); + + // But get_next_adhoc_task() should return nothing. + $this->assertNull(manager::get_next_adhoc_task(timestart: $now + 86400)); } /** From f13392d23001437510879c37f38d419678f070ac Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Tue, 28 Nov 2023 12:09:14 +0700 Subject: [PATCH 3/4] MDL-79128 adhoc_task: Implement failed ad-hoc task cleanup Co-authored-by: Stevani Andolo --- admin/settings/server.php | 11 +++ lang/en/admin.php | 2 + lib/classes/task/manager.php | 20 ++++ lib/classes/task/task_log_cleanup_task.php | 3 + lib/tests/task/adhoc_task_test.php | 104 +++++++++++++++++++++ 5 files changed, 140 insertions(+) diff --git a/admin/settings/server.php b/admin/settings/server.php index b215f47def8..c4a0e3e6b11 100644 --- a/admin/settings/server.php +++ b/admin/settings/server.php @@ -390,6 +390,17 @@ if ($hassiteconfig) { 30 * MINSECS ) ); + + $temp->add( + new admin_setting_configduration( + 'task_adhoc_failed_retention', + new lang_string('task_adhoc_failed_retention', 'admin'), + new lang_string('task_adhoc_failed_retention_desc', 'admin'), + \core\task\manager::ADHOC_TASK_FAILED_RETENTION, + WEEKSECS + ) + ); + $ADMIN->add('taskconfig', $temp); // Task log configuration. diff --git a/lang/en/admin.php b/lang/en/admin.php index 49f58af3289..4e639a7c8f9 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -1362,6 +1362,8 @@ $string['task_scheduled_max_runtime'] = 'Scheduled task runner lifetime'; $string['task_scheduled_max_runtime_desc'] = 'The age of a scheduled task runner before it is freed.'; $string['task_adhoc_concurrency_limit'] = 'Ad hoc task concurrency limit'; $string['task_adhoc_concurrency_limit_desc'] = 'The number of ad hoc task runners allowed to run concurrently. If the limit is high then scheduled tasks may not run regularly when there are lots of ad hoc tasks. A setting of 0 will disable processing of ad hoc tasks completely.'; +$string['task_adhoc_failed_retention'] = 'Failed ad hoc task retention period'; +$string['task_adhoc_failed_retention_desc'] = 'The maximum period that failed ad hoc tasks should remain in the queue once they have reached their retry limit.'; $string['task_adhoc_max_runtime'] = 'Ad hoc task runner lifetime'; $string['task_adhoc_max_runtime_desc'] = 'The age of an ad hoc task runner before it is freed. A low duration is recommended as there is no limit to the number of ad hoc tasks queued. If this number is too high and you have a large ad hoc task queue then scheduled tasks may not be run regularly.'; $string['task_endtime'] = 'End time'; diff --git a/lib/classes/task/manager.php b/lib/classes/task/manager.php index e82215be6eb..9d4a9e8dd63 100644 --- a/lib/classes/task/manager.php +++ b/lib/classes/task/manager.php @@ -49,6 +49,12 @@ class manager { */ const ADHOC_TASK_QUEUE_MODE_FILLING = 1; + /** + * @var int Used to set the retention period for adhoc tasks that have failed and to be cleaned up. + * The number is in week unit. The default value is 4 weeks. + */ + const ADHOC_TASK_FAILED_RETENTION = 4 * WEEKSECS; + /** * @var array A cached queue of adhoc tasks */ @@ -1717,4 +1723,18 @@ class manager { return null; } + + /** + * Clean up failed adhoc tasks. + */ + public static function clean_failed_adhoc_tasks(): void { + global $CFG, $DB; + $difftime = !empty($CFG->task_adhoc_failed_retention) ? + $CFG->task_adhoc_failed_retention : static::ADHOC_TASK_FAILED_RETENTION; + $DB->delete_records_select( + table: 'task_adhoc', + select: 'attemptsavailable = 0 AND timestarted < :time', + params: ['time' => time() - $difftime], + ); + } } diff --git a/lib/classes/task/task_log_cleanup_task.php b/lib/classes/task/task_log_cleanup_task.php index 5a6380fc939..6c8971cdb3a 100644 --- a/lib/classes/task/task_log_cleanup_task.php +++ b/lib/classes/task/task_log_cleanup_task.php @@ -53,5 +53,8 @@ class task_log_cleanup_task extends scheduled_task { if (is_a($logger, database_logger::class, true)) { $logger::cleanup(); } + + // Clean failed ad-hoc tasks. + manager::clean_failed_adhoc_tasks(); } } diff --git a/lib/tests/task/adhoc_task_test.php b/lib/tests/task/adhoc_task_test.php index 2a8e78e834e..0a726f39f56 100644 --- a/lib/tests/task/adhoc_task_test.php +++ b/lib/tests/task/adhoc_task_test.php @@ -245,6 +245,110 @@ class adhoc_task_test extends \advanced_testcase { $this->assertNull(manager::get_next_adhoc_task(timestart: $now + 86400)); } + /** + * Test adhoc task failure cleanup. + * + * @covers ::queue_adhoc_task + * @covers ::get_next_adhoc_task + * @covers ::adhoc_task_failed + * @covers ::clean_failed_adhoc_tasks + */ + public function test_adhoc_task_clean_up(): void { + global $DB, $CFG; + $this->resetAfterTest(); + + // Create two no-retry adhoc tasks. + $task1 = new no_retry_adhoc_task(); + $taskid1 = manager::queue_adhoc_task(task: $task1); + $task2 = new no_retry_adhoc_task(); + $taskid2 = manager::queue_adhoc_task(task: $task2); + + // Get the tasks and mark it as failed. + $task = manager::get_adhoc_task($taskid1); + manager::adhoc_task_failed(task: $task); + $task = manager::get_adhoc_task($taskid2); + manager::adhoc_task_failed(task: $task); + + // These are no-retry tasks, the remaining available attempts should be reduced to 0. + $this->assertEquals( + expected: 0, + actual: $DB->get_field( + table: 'task_adhoc', + return: 'attemptsavailable', + conditions: ['id' => $taskid1], + ), + ); + $this->assertEquals( + expected: 0, + actual: $DB->get_field( + table: 'task_adhoc', + return: 'attemptsavailable', + conditions: ['id' => $taskid2], + ), + ); + + // There will be two records in the task_adhoc table. + $this->assertEquals( + expected: 2, + actual: $DB->count_records(table: 'task_adhoc'), + ); + + // Clean up failed adhoc tasks. This will clean nothing because the tasks are not old enough. + manager::clean_failed_adhoc_tasks(); + + // There will be two records in the task_adhoc table. + $this->assertEquals( + expected: 2, + actual: $DB->count_records(table: 'task_adhoc'), + ); + + // Update the time of the task2 to be older more than 2 days. + $DB->set_field( + table: 'task_adhoc', + newfield: 'timestarted', + newvalue: time() - (DAYSECS * 2) - 10, // Plus 10 seconds to make sure it is older than 2 days. + conditions: ['id' => $taskid2], + ); + + // Clean up failed adhoc tasks. This will clean nothing because the tasks are not old enough. + manager::clean_failed_adhoc_tasks(); + + // There will be two records in the task_adhoc table. + $this->assertEquals( + expected: 2, + actual: $DB->count_records(table: 'task_adhoc'), + ); + + // Update the time of the task1 to be older than the cleanup time. + $DB->set_field( + table: 'task_adhoc', + newfield: 'timestarted', + newvalue: time() - $CFG->task_adhoc_failed_retention - 10, // Plus 10 seconds to make sure it is older than the retention time. + conditions: ['id' => $taskid1], + ); + + // Clean up failed adhoc tasks. task1 should be cleaned now. + manager::clean_failed_adhoc_tasks(); + + // There will be one record in the task_adhoc table. + $this->assertEquals( + expected: 1, + actual: $DB->count_records(table: 'task_adhoc'), + ); + + // Update the duration of the Failed ad hoc task retention period to one day. + $CFG->task_adhoc_failed_retention = DAYSECS; + + // Clean up failed adhoc tasks. task2 should be cleaned now. + manager::clean_failed_adhoc_tasks(); + + // The task_adhoc table should be empty now. + $this->assertEquals( + expected: 0, + actual: $DB->count_records(table: 'task_adhoc'), + ); + } + /** * Test future adhoc task execution. * @covers ::get_next_adhoc_task From bd3af64741e45ec763f153af1a3e59b917c0a697 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Tue, 28 Nov 2023 14:41:33 +0700 Subject: [PATCH 4/4] MDL-79128 adhoc_task: Added 'Never' status for ad-hoc task Next run Co-authored-by: Stevani Andolo --- admin/tool/task/renderer.php | 10 ++++++++-- lib/classes/task/manager.php | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/admin/tool/task/renderer.php b/admin/tool/task/renderer.php index a25f3117e2f..f61909d256a 100644 --- a/admin/tool/task/renderer.php +++ b/admin/tool/task/renderer.php @@ -119,7 +119,9 @@ class tool_task_renderer extends plugin_renderer_base { // Prepares the next run time cell contents. $nextrun = ''; - if ($stats['due'] > 0) { + if ($stats['stop']) { + $nextrun = get_string('never', 'admin'); + } else if ($stats['due'] > 0) { $nextrun = get_string('asap', 'tool_task'); } else if ($stats['nextruntime']) { $nextrun = userdate($stats['nextruntime']); @@ -271,7 +273,11 @@ class tool_task_renderer extends plugin_renderer_base { if (!$started) { $nextruntime = $task->get_next_run_time(); $due = $nextruntime < $now; - $nextrun = $due ? userdate($nextruntime) : get_string('asap', 'tool_task'); + if ($task->get_attempts_available() > 0) { + $nextrun = $due ? userdate($nextruntime) : get_string('asap', 'tool_task'); + } else { + $nextrun = get_string('never', 'admin'); + } if ($wantruntasks && ($faildelay || $due)) { $nextrun .= ' '.html_writer::div( diff --git a/lib/classes/task/manager.php b/lib/classes/task/manager.php index 9d4a9e8dd63..b57fd17922f 100644 --- a/lib/classes/task/manager.php +++ b/lib/classes/task/manager.php @@ -543,6 +543,7 @@ class manager { 'failed' => 0, 'running' => 0, 'due' => 0, + 'stop' => false, ]; } @@ -564,6 +565,11 @@ class manager { } } + // Mark the task as stopped if it has no attempts available. + if (isset($r->attemptsavailable) && $r->attemptsavailable == 0) { + $classsummary['stop'] = true; + } + $summary[$r->component][$r->classname] = $classsummary; } return $summary;