From a44f14c79e742a92a4f37ab0076b66031ec8406b Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Tue, 28 Nov 2023 10:55:35 +0700 Subject: [PATCH] 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