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
This commit is contained in:
Srdjan
2023-04-05 10:55:18 +10:00
parent fba0658777
commit 9405b5aa6d
5 changed files with 112 additions and 31 deletions
+21 -13
View File
@@ -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 = <<<EOT
Ad hoc cron tasks.
@@ -63,9 +67,11 @@ Options:
-k, --keep-alive=N Keep this script alive for N seconds and poll for new adhoc tasks
-i --ignorelimits Ignore task_adhoc_concurrency_limit and task_adhoc_max_runtime limits
-f, --force Run even if cron is disabled
--id Run (failed) task with id
Example:
\$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --execute
\$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --id=123456
EOT;
@@ -91,10 +97,6 @@ if (moodle_needs_upgrading()) {
exit(1);
}
if (empty($options['execute'])) {
exit(0);
}
if (!get_config('core', 'cron_enabled') && !$options['force']) {
mtrace('Cron is disabled. Use --force to override.');
exit(1);
@@ -111,8 +113,6 @@ if (!empty($CFG->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);
}
+3
View File
@@ -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';
+15
View File
@@ -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.
*
+63 -17
View File
@@ -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
+10 -1
View File
@@ -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.