From 9405b5aa6d1d7fdf7f366a0fcc538aa4e0c4292b Mon Sep 17 00:00:00 2001 From: Srdjan Date: Mon, 21 Mar 2022 16:27:46 +1000 Subject: [PATCH] MDL-70975 task: Support for running adhoc tasks by id * CLI adhoc_task.php: new option --id * cron::run_adhoc_task($taskid) for running tasks by id * core\task\manager::get_adhoc_task($taskid) for retreival/locking --- admin/cli/adhoc_task.php | 34 ++++++++----- lang/en/moodle.php | 3 ++ lib/classes/cron.php | 15 ++++++ lib/classes/task/manager.php | 80 +++++++++++++++++++++++------- lib/tests/task/adhoc_task_test.php | 11 +++- 5 files changed, 112 insertions(+), 31 deletions(-) diff --git a/admin/cli/adhoc_task.php b/admin/cli/adhoc_task.php index e6ccff48bca..2b96501dbac 100644 --- a/admin/cli/adhoc_task.php +++ b/admin/cli/adhoc_task.php @@ -30,13 +30,14 @@ require_once("{$CFG->libdir}/clilib.php"); list($options, $unrecognized) = cli_get_params( [ - 'execute' => false, 'help' => false, - 'keep-alive' => 0, 'showsql' => false, 'showdebugging' => false, + 'execute' => false, + 'keep-alive' => 0, 'ignorelimits' => false, 'force' => false, + 'id' => null, ], [ 'h' => 'help', 'e' => 'execute', @@ -51,6 +52,9 @@ if ($unrecognized) { cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); } +if ($options['id']) { + $options['execute'] = true; +} if ($options['help'] or empty($options['execute'])) { $help = <<showcrondebugging)) { set_debugging(DEBUG_DEVELOPER, true); } -$checklimits = empty($options['ignorelimits']); - core_php_time_limit::raise(); // Increase memory limit. @@ -121,10 +121,18 @@ raise_memory_limit(MEMORY_EXTRA); // Emulate normal session - we use admin account by default. \core\cron::setup_user(); -$humantimenow = date('r', time()); -$keepalive = (int)$options['keep-alive']; - \core\local\cli\shutdown::script_supports_graceful_exit(); - +$humantimenow = date('r', time()); mtrace("Server Time: {$humantimenow}\n"); -\core\cron::run_adhoc_tasks(time(), $keepalive, $checklimits); + +if (!empty($options['id'])) { + $taskid = (int) $options['id']; + \core\cron::run_adhoc_task($taskid); +} elseif (!empty($options['execute'])) { + + $checklimits = empty($options['ignorelimits']); + + $keepalive = (int)$options['keep-alive']; + + \core\cron::run_adhoc_tasks(time(), $keepalive, $checklimits); +} diff --git a/lang/en/moodle.php b/lang/en/moodle.php index 23f99b9dfaf..18449e927d7 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1163,6 +1163,8 @@ $string['interestslist'] = 'List of interests'; $string['interestslist_help'] = 'Enter your interests, one by one, which will then be displayed on your profile page as tags.'; $string['invalidemail'] = 'Invalid email address'; $string['invalidlogin'] = 'Invalid login, please try again'; +$string['invalidtaskid'] = 'Invalid task ID'; +$string['invalidtaskclassname'] = 'Invalid task class {$a}'; $string['invalidusername'] = 'The username can only contain alphanumeric lowercase characters (letters and numbers), underscore (_), hyphen (-), period (.) or at symbol (@).'; $string['invalidusernameupload'] = 'Invalid username'; $string['ip_address'] = 'IP address'; @@ -2354,6 +2356,7 @@ $string['withdisablednote'] = '{$a} (disabled)'; $string['withoutuserdata'] = 'without user data'; $string['withselectedusers'] = 'With selected users...'; $string['withuserdata'] = 'with user data'; +$string['wontrunfuturescheduledtask'] = "Won't run task that hasn't failed and is scheduled to run in the future"; $string['wordforstudent'] = 'Your word for Student'; $string['wordforstudenteg'] = 'eg Student, Participant etc'; $string['wordforstudents'] = 'Your word for Students'; diff --git a/lib/classes/cron.php b/lib/classes/cron.php index 10f62c18a50..334b29cbacb 100644 --- a/lib/classes/cron.php +++ b/lib/classes/cron.php @@ -327,6 +327,21 @@ class cron { } } + /** + * Execute a (failed) adhoc task. + * + * @param int $taskid + */ + public static function run_adhoc_task(int $taskid): void { + $task = \core\task\manager::get_adhoc_task($taskid); + if (!$task->get_fail_delay() && $task->get_next_run_time() > time()) { + throw new \moodle_exception('wontrunfuturescheduledtask'); + } + + self::run_inner_adhoc_task($task); + self::set_process_title("Running adhoc task $taskid"); + } + /** * Shared code that handles running of a single scheduled task within the cron. * diff --git a/lib/classes/task/manager.php b/lib/classes/task/manager.php index 35c8751f809..0f851edec1c 100644 --- a/lib/classes/task/manager.php +++ b/lib/classes/task/manager.php @@ -474,7 +474,7 @@ class manager { * This function load the adhoc tasks for a given classname. * * @param string $classname - * @return \core\task\adhoc_task[] + * @return array */ public static function get_adhoc_tasks($classname) { global $DB; @@ -645,10 +645,10 @@ class manager { * * @param int $timestart * @param bool $checklimits Should we check limits? - * @return \core\task\adhoc_task or null if not found + * @return \core\task\adhoc_task|null * @throws \moodle_exception */ - public static function get_next_adhoc_task($timestart, $checklimits = true) { + public static function get_next_adhoc_task(int $timestart, bool $checklimits = true): ?adhoc_task { global $DB; $concurrencylimit = get_config('core', 'task_adhoc_concurrency_limit'); @@ -797,21 +797,9 @@ class manager { } } - // The global cron lock is under the most contention so request it - // as late as possible and release it as soon as possible. - if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) { - $lock->release(); - throw new \moodle_exception('locktimeout'); - } - - $task->set_lock($lock); - if (!$task->is_blocking()) { - $cronlock->release(); - } else { - $task->set_cron_lock($cronlock); - } - + self::set_locks($task, $lock, $cronlockfactory); unset(self::$miniqueue[$taskid]); + return $task; } else { unset(self::$miniqueue[$taskid]); @@ -880,6 +868,64 @@ class manager { ); } + /** + * This function will get a (failed) adhoc task by id. The task will be handed out + * with an open lock - possibly on the entire cron process. Make sure you call either + * {@see ::adhoc_task_failed} or {@see ::adhoc_task_complete} to release the lock and reschedule the task. + * + * @param int $taskid + * @return \core\task\adhoc_task|null + * @throws \moodle_exception + */ + public static function get_adhoc_task(int $taskid): ?adhoc_task { + global $DB; + + $record = $DB->get_record('task_adhoc', array('id' => $taskid)); + if (!$record) { + throw new \moodle_exception('invalidtaskid'); + } + + $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); + + if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 0)) { + $task = self::adhoc_task_from_record($record); + // Safety check in case the task in the DB does not match a real class (maybe something was uninstalled). + if (!$task) { + $lock->release(); + throw new \moodle_exception('invalidtaskclassname'); + } + + self::set_locks($task, $lock, $cronlockfactory); + return $task; + } + + return null; + } + + /** + * This function will set locks on the task. + * + * @param \core\task\adhoc_task $task + * @param \core\lock\lock $lock task lock + * @param \core\lock\lock_factory $cronlockfactory + * @throws \moodle_exception + */ + private static function set_locks($task, $lock, $cronlockfactory): void { + // The global cron lock is under the most contention so request it + // as late as possible and release it as soon as possible. + if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) { + $lock->release(); + throw new \moodle_exception('locktimeout'); + } + + $task->set_lock($lock); + if (!$task->is_blocking()) { + $cronlock->release(); + } else { + $task->set_cron_lock($cronlock); + } + } + /** * This function will dispatch the next scheduled task in the queue. The task will be handed out * with an open lock - possibly on the entire cron process. Make sure you call either diff --git a/lib/tests/task/adhoc_task_test.php b/lib/tests/task/adhoc_task_test.php index 197ef611443..7b646d946ae 100644 --- a/lib/tests/task/adhoc_task_test.php +++ b/lib/tests/task/adhoc_task_test.php @@ -75,6 +75,7 @@ class adhoc_task_test extends \advanced_testcase { * Test adhoc task failure retry backoff. * * @covers ::get_next_adhoc_task + * @covers ::get_adhoc_task */ public function test_get_next_adhoc_task_fail_retry() { $this->resetAfterTest(true); @@ -87,17 +88,25 @@ class adhoc_task_test extends \advanced_testcase { // Get it from the scheduler, execute it, and mark it as failed. $task = manager::get_next_adhoc_task($now); + $taskid = $task->get_id(); $task->execute(); manager::adhoc_task_failed($task); // The task will not be returned immediately. $this->assertNull(manager::get_next_adhoc_task($now)); - // Should get the adhoc task (retry after delay). + // Should get the adhoc task (retry after delay). Fail it again. $task = manager::get_next_adhoc_task($now + 120); $this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task); + $this->assertEquals($taskid, $task->get_id()); $task->execute(); + manager::adhoc_task_failed($task); + // Should get the adhoc task immediately. + $task = manager::get_adhoc_task($taskid); + $this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task); + $this->assertEquals($taskid, $task->get_id()); + $task->execute(); manager::adhoc_task_complete($task); // Should not get any task.