Skip to content

Commit

Permalink
MDL-70975 task: Support for running adhoc tasks by id
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
srdjan-catalyst committed Apr 5, 2023
1 parent fba0658 commit 9405b5a
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 30 deletions.
32 changes: 20 additions & 12 deletions admin/cli/adhoc_task.php
Expand Up @@ -30,13 +30,14 @@

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',
Expand All @@ -51,6 +52,9 @@
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.
Expand All @@ -63,9 +67,11 @@
-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;

Expand All @@ -91,10 +97,6 @@
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);
Expand All @@ -111,8 +113,6 @@
set_debugging(DEBUG_DEVELOPER, true);
}

$checklimits = empty($options['ignorelimits']);

core_php_time_limit::raise();

// Increase memory limit.
Expand All @@ -121,10 +121,18 @@
// Emulate normal session - we use admin account by default.
\core\cron::setup_user();

\core\local\cli\shutdown::script_supports_graceful_exit();
$humantimenow = date('r', time());
$keepalive = (int)$options['keep-alive'];
mtrace("Server Time: {$humantimenow}\n");

\core\local\cli\shutdown::script_supports_graceful_exit();
if (!empty($options['id'])) {
$taskid = (int) $options['id'];
\core\cron::run_adhoc_task($taskid);
} elseif (!empty($options['execute'])) {

mtrace("Server Time: {$humantimenow}\n");
\core\cron::run_adhoc_tasks(time(), $keepalive, $checklimits);
$checklimits = empty($options['ignorelimits']);

$keepalive = (int)$options['keep-alive'];

\core\cron::run_adhoc_tasks(time(), $keepalive, $checklimits);
}
3 changes: 3 additions & 0 deletions lang/en/moodle.php
Expand Up @@ -1163,6 +1163,8 @@
$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';
Expand Down Expand Up @@ -2354,6 +2356,7 @@
$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';
Expand Down
15 changes: 15 additions & 0 deletions lib/classes/cron.php
Expand Up @@ -327,6 +327,21 @@ public static function run_adhoc_tasks(
}
}

/**
* 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.
*
Expand Down
80 changes: 63 additions & 17 deletions lib/classes/task/manager.php
Expand Up @@ -474,7 +474,7 @@ public static function get_scheduled_task($classname) {
* 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;
Expand Down Expand Up @@ -645,10 +645,10 @@ public static function ensure_adhoc_task_qos(array $records): array {
*
* @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');
Expand Down Expand Up @@ -797,21 +797,9 @@ function ($a, $b) use ($ordering) {
}
}

// 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]);
Expand Down Expand Up @@ -880,6 +868,64 @@ function (string $class, int $limit, int $index): array {
);
}

/**
* 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
Expand Down
11 changes: 10 additions & 1 deletion lib/tests/task/adhoc_task_test.php
Expand Up @@ -75,6 +75,7 @@ public function test_get_next_adhoc_task_now() {
* 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);
Expand All @@ -87,17 +88,25 @@ public function test_get_next_adhoc_task_fail_retry() {

// 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.
Expand Down

0 comments on commit 9405b5a

Please sign in to comment.