Merge branch 'MDL-70975-adhoc-carneval' of https://github.com/catalyst/moodle

This commit is contained in:
Ilya Tregubov
2023-04-13 09:07:58 +08:00
14 changed files with 973 additions and 75 deletions
+71 -27
View File
@@ -30,19 +30,25 @@ 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,
'classname' => null,
'taskslimit' => null,
'failed' => false,
], [
'h' => 'help',
'e' => 'execute',
'k' => 'keep-alive',
'i' => 'ignorelimits',
'f' => 'force',
'c' => 'classname',
'l' => 'taskslimit',
]
);
@@ -51,8 +57,7 @@ if ($unrecognized) {
cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
}
if ($options['help'] or empty($options['execute'])) {
$help = <<<EOT
$help = <<<EOT
Ad hoc cron tasks.
Options:
@@ -63,22 +68,32 @@ 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
-c, --classname Run tasks with a certain classname (FQN)
-l, --taskslimit=N Run at most N tasks
--failed Run only tasks that failed, ie those with a fail delay
Example:
Run all queued tasks:
\$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --execute
Run all queued tasks of specific class:
\$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --classname=\\\\core_course\\\\task\\\\course_delete_modules
Double backslash for the shell escape reasons.
Run a specific task:
\$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --id=123456
Run a specific task with debugging:
\$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --id=123456 --showsql --showdebugging
To profile a long running task:
\$sudo -u www-data /usr/bin/php admin/cli/adhoc_task.php --taskslimit=1 --classname='\\some\\class\\name' --ignorelimits
EOT;
if ($options['help']) {
echo $help;
die;
}
if ($options['showdebugging']) {
set_debugging(DEBUG_DEVELOPER, true);
}
if ($options['showsql']) {
$DB->set_debug(true);
exit(0);
}
if (CLI_MAINTENANCE) {
@@ -91,17 +106,18 @@ 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);
}
if (empty($options['keep-alive'])) {
$options['keep-alive'] = 0;
// Common debugging options.
if ($options['showdebugging']) {
set_debugging(DEBUG_DEVELOPER, true);
}
if ($options['showsql']) {
$DB->set_debug(true);
}
if (!empty($CFG->showcronsql)) {
@@ -111,8 +127,7 @@ if (!empty($CFG->showcrondebugging)) {
set_debugging(DEBUG_DEVELOPER, true);
}
$checklimits = empty($options['ignorelimits']);
// Process params.
core_php_time_limit::raise();
// Increase memory limit.
@@ -121,10 +136,39 @@ 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);
$classname = $options['classname'];
// Run a single adhoc task only, if requested.
if (!empty($options['id'])) {
$taskid = (int) $options['id'];
\core\cron::run_adhoc_task($taskid);
exit(0);
}
// Run all failed tasks.
if (!empty($options['failed'])) {
\core\cron::run_failed_adhoc_tasks($classname);
exit(0);
}
// Examine params and determine if we should run.
$execute = (bool) $options['execute'];
$keepalive = empty($options['keep-alive']) ? 0 : (int) $options['keep-alive'];
$taskslimit = empty($options['taskslimit']) ? null : (int) $options['taskslimit'];
$checklimits = empty($options['ignorelimits']);
if ($classname || $keepalive || $taskslimit) {
$execute = true;
}
// Output the help text if no criteria for running the adhoc tasks are given.
if (!$execute) {
echo $help;
exit(0);
}
\core\cron::run_adhoc_tasks(time(), $keepalive, $checklimits, null, $taskslimit, $classname);
+1 -1
View File
@@ -60,7 +60,7 @@ $report = system_report_factory::create(task_logs::class, context_system::instan
if (!empty($filter)) {
$report->set_filter_values([
'task_log:name_values' => $filter,
'task_log:name_values' => trim($filter, '\\'),
]);
}
+60
View File
@@ -0,0 +1,60 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Ad hoc task list.
*
* @package tool_task
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
admin_externalpage_setup('adhoctasks');
$failedonly = optional_param('failedonly', false, PARAM_BOOL);
$classname = optional_param('classname', null, PARAM_RAW);
$renderer = $PAGE->get_renderer('tool_task');
if ($classname) {
$pageurl = new moodle_url('/admin/tool/task/adhoctasks.php');
$PAGE->navbar->add(get_string('adhoctasks', 'tool_task'), $pageurl);
$PAGE->navbar->add($classname, $PAGE->url);
$tasks = core\task\manager::get_adhoc_tasks($classname, $failedonly);
echo $OUTPUT->header();
if (!get_config('core', 'cron_enabled')) {
echo $renderer->cron_disabled();
}
echo $renderer->adhoc_tasks_class_table($classname, $tasks, ['failedonly' => $failedonly]);
} else {
$summary = core\task\manager::get_adhoc_tasks_summary();
echo $OUTPUT->header();
if (!get_config('core', 'cron_enabled')) {
echo $renderer->cron_disabled();
}
echo $renderer->adhoc_tasks_summary_table($summary);
}
echo $OUTPUT->footer();
+21
View File
@@ -24,11 +24,17 @@
$string['adhoc'] = 'Ad hoc';
$string['adhoctaskid'] = 'Ad hoc task ID: {$a}';
$string['adhoctaskrun'] = 'Ad hoc task run initiated';
$string['adhoctasks'] = 'Ad hoc tasks';
$string['adhoctasksdue'] = 'Ad hoc tasks due';
$string['adhoctasksfailed'] = 'Ad hoc tasks failed';
$string['adhoctasksfuture'] = 'Future ad hoc tasks';
$string['adhoctasksrunning'] = 'Ad hoc tasks running';
$string['asap'] = 'ASAP';
$string['adhocempty'] = 'Ad hoc task queue is empty';
$string['adhocqueuesize'] = 'Ad hoc task queue has {$a} tasks';
$string['adhocqueueold'] = 'Oldest unprocessed task is {$a->age}, which is more than {$a->max}';
$string['backtoadhoctasks'] = 'Back to ad hoc tasks';
$string['backtoscheduledtasks'] = 'Back to scheduled tasks';
$string['blocking'] = 'Blocking';
$string['cannotfindthepathtothecli'] = 'Cannot find the path to the PHP CLI executable so task execution aborted. Set the \'Path to PHP CLI\' setting in Site administration / Server / System paths.';
@@ -51,31 +57,46 @@ $string['edittaskschedule'] = 'Edit task schedule: {$a}';
$string['enablerunnow'] = 'Allow \'Run now\' for scheduled tasks';
$string['enablerunnow_desc'] = 'Allows administrators to run a single scheduled task immediately, rather than waiting for it to run as scheduled. The feature requires \'Path to PHP CLI\' (pathtophp) to be set in System paths. The task runs on the web server, so you may wish to disable this feature to avoid potential performance issues.';
$string['faildelay'] = 'Fail delay';
$string['failed'] = 'Failed';
$string['fromcomponent'] = 'From component: {$a}';
$string['hostname'] = 'Host name';
$string['lastcronstart'] = 'Time since last cron run: {$a}';
$string['lastruntime'] = 'Last run';
$string['lastupdated'] = 'Last updated {$a}.';
$string['nextruntime'] = 'Next run';
$string['noclassname'] = 'Class name not specified';
$string['notasks'] = 'No tasks to run';
$string['payload'] = 'Payload';
$string['pid'] = 'PID';
$string['plugindisabled'] = 'Plugin disabled';
$string['pluginname'] = 'Scheduled task configuration';
$string['resettasktodefaults'] = 'Reset task schedule to defaults';
$string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task back to its original settings.';
$string['run_adhoctasks'] = 'Run ad hoc tasks';
$string['runningalltasks'] = 'Running all tasks';
$string['runningfailedtasks'] = 'Running failed tasks';
$string['runningtasks'] = 'Tasks running now';
$string['runnow'] = 'Run now';
$string['runagain'] = 'Run again';
$string['runadhoc_confirm'] = 'Tasks will run on the web server and may take some time to complete.';
$string['runadhoc'] = 'Run ad hoc tasks now?';
$string['runnow_confirm'] = 'Are you sure you want to run this task \'{$a}\' now? The task will run on the web server and may take some time to complete.';
$string['runclassname'] = 'Run all';
$string['runclassnamefailedonly'] = 'Run all failed';
$string['runpattern'] = 'Run pattern';
$string['scheduled'] = 'Scheduled';
$string['scheduledtasks'] = 'Scheduled tasks';
$string['scheduledtaskchangesdisabled'] = 'Modifications to the list of scheduled tasks have been prevented in Moodle configuration';
$string['slowtask'] = 'Task has run for longer than {$a}';
$string['showall'] = 'Show all';
$string['showfailedonly'] = 'Show failed only';
$string['showsummary'] = 'Show ad hoc tasks summary';
$string['started'] = 'Started';
$string['taskage'] = 'Run time';
$string['taskdetails'] = 'Tasks running for more than {$a->time} (max {$a->maxtime}): {$a->count}';
$string['taskdisabled'] = 'Task disabled';
$string['taskfailures'] = '{$a} task(s) failing';
$string['taskid'] = 'Task ID';
$string['tasklogs'] = 'Task logs';
$string['tasknofailures'] = 'There are no tasks failing';
$string['taskrunningtime'] = 'Task has run for {$a}';
+9
View File
@@ -38,3 +38,12 @@ function tool_task_status_checks() : array {
];
}
/**
* Function used to handle mtrace by outputting the text to normal browser window.
*
* @param string $message Message to output
* @param string $eol End of line character
*/
function tool_task_mtrace_wrapper(string $message, string $eol): void {
echo s($message . $eol);
}
+280 -3
View File
@@ -35,6 +35,279 @@ use core\task\scheduled_task;
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_task_renderer extends plugin_renderer_base {
/**
* This function will render a table with the summary of all adhoc tasks.
*
* @param array $summary
* @return string HTML to output.
*/
public function adhoc_tasks_summary_table(array $summary): string {
$adhocurl = '/admin/tool/task/adhoctasks.php';
$adhocrunurl = '/admin/tool/task/run_adhoctasks.php';
// Main tasks table.
$table = new html_table();
$table->caption = get_string('adhoctasks', 'tool_task');
$table->head = [
get_string('component', 'tool_task') . ' / ' . get_string('classname', 'tool_task'),
get_string('adhoctasksrunning', 'tool_task'),
get_string('adhoctasksdue', 'tool_task'),
get_string('adhoctasksfuture', 'tool_task'),
get_string('adhoctasksfailed', 'tool_task'),
get_string('nextruntime', 'tool_task'),
];
$table->attributes['class'] = 'admintable generaltable';
$table->colclasses = [];
// For each task entry (row) show action buttons/logs link depending on the user permissions.
$data = [];
$canruntasks = \core\task\manager::is_runnable() && get_config('tool_task', 'enablerunnow');
foreach ($summary as $component => $classes) {
// Component cell.
$componentcell = new html_table_cell($component);
$componentcell->header = true;
$componentcell->id = "tasks-$component";
$componentcell->colspan = 6;
$data[] = new html_table_row([$componentcell]);
foreach ($classes as $classname => $stats) {
// Task class cell.
$classbits = explode('\\', $classname);
$classcontent = html_writer::link(
new moodle_url($adhocurl, ['classname' => $classname]),
end($classbits)
);
$classcell = new html_table_cell($classcontent);
$classcell->header = true;
$classcell->class = "task-class-summary text-ltr";
$duecontent = $stats['due'];
if ($canruntasks && ($stats['due'] > 0 || $stats['failed'] > 0)) {
$duecontent .= html_writer::div(
html_writer::link(
new moodle_url(
$adhocrunurl,
['classname' => $classname]
),
get_string('runclassname', 'tool_task')
),
'task-runnow'
);
}
// Mark cell if has failed tasks.
$failed = $stats['failed'];
if ($canruntasks && $failed > 0) {
$failed .= html_writer::div(
html_writer::link(
new moodle_url(
$adhocrunurl,
['classname' => $classname, 'failedonly' => 1]
),
get_string('runclassnamefailedonly', 'tool_task')
),
'task-runnow'
);
}
$failedcell = new html_table_cell($failed);
if ($failed > 0) {
$failedcell->attributes['class'] = 'table-danger';
}
// Prepares the next run time cell contents.
$nextrun = '';
if ($stats['due'] > 0) {
$nextrun = get_string('asap', 'tool_task');
} else if ($stats['nextruntime']) {
$nextrun = userdate($stats['nextruntime']);
}
$data[] = new html_table_row([
$classcell,
new html_table_cell($stats['running']),
new html_table_cell($duecontent),
new html_table_cell($stats['count'] - $stats['running'] - $stats['due']),
$failedcell,
new html_table_cell($nextrun),
]);
}
}
$table->data = $data;
return html_writer::table($table);
}
/**
* This function will render a table with all the adhoc tasks for the class.
*
* @param string $classname
* @param array $tasks - list of all adhoc tasks.
* @param array|null $params
* @return string HTML to output.
*/
public function adhoc_tasks_class_table(string $classname, array $tasks, ?array $params = []): string {
$adhocurl = '/admin/tool/task/adhoctasks.php';
$adhocrunurl = '/admin/tool/task/run_adhoctasks.php';
$showloglink = \core\task\logmanager::has_log_report();
$failedonly = !empty($params['failedonly']);
$canruntasks = \core\task\manager::is_runnable() && get_config('tool_task', 'enablerunnow');
// Depending on the currently set parameters, set up toggle buttons.
$failedorall = html_writer::link(
new moodle_url(
$adhocurl,
array_merge($params, ['classname' => $classname, 'failedonly' => !$failedonly])
),
get_string($failedonly ? 'showall' : 'showfailedonly', 'tool_task')
);
// Main tasks table.
$table = $this->generate_adhoc_tasks_simple_table($tasks, $canruntasks);
$table->caption = "$classname "
. get_string($failedonly ? 'adhoctasksfailed' : 'adhoctasks', 'tool_task');
$table->head[3] .= " $failedorall"; // Spice up faildelay heading.
if ($showloglink) {
// Insert logs as the second col.
array_splice($table->head, 1, 0, [get_string('logs')]);
array_walk($table->data, function ($row, $idx) use ($classname) {
$loglink = '';
$faildelaycell = $row->cells[3];
if ($faildelaycell->attributes['class'] == 'table-danger') {
// Failed task.
$loglink = $this->output->action_icon(
\core\task\logmanager::get_url_for_task_class($classname),
new pix_icon('e/file-text', get_string('viewlogs', 'tool_task', $classname)
));
}
array_splice($row->cells, 1, 0, [new html_table_cell($loglink)]);
});
}
return html_writer::table($table)
. html_writer::div(
html_writer::link(
new moodle_url(
$adhocrunurl,
array_merge($params, ['classname' => $classname])
),
get_string('runclassname', 'tool_task')
),
'task-runnow'
)
. html_writer::div(
html_writer::link(
new moodle_url(
$adhocurl
),
get_string('showsummary', 'tool_task')
),
'task-show-summary'
);
}
/**
* This function will render a plain adhoc tasks table.
*
* @param array $tasks - list of adhoc tasks.
* @return string HTML to output.
*/
public function adhoc_tasks_simple_table(array $tasks): string {
$table = $this->generate_adhoc_tasks_simple_table($tasks);
return html_writer::table($table);
}
/**
* This function will render a plain adhoc tasks table.
*
* @param array $tasks - list of adhoc tasks.
* @param bool $wantruntasks add 'Run now' link
* @return html_table
*/
private function generate_adhoc_tasks_simple_table(array $tasks, bool $wantruntasks = false): html_table {
$adhocrunurl = '/admin/tool/task/run_adhoctasks.php';
$now = time();
$failedstr = get_string('failed', 'tool_task');
// Main tasks table.
$table = new html_table();
$table->caption = get_string('adhoctasks', 'tool_task');
$table->head = [
get_string('taskid', 'tool_task'),
get_string('nextruntime', 'tool_task'),
get_string('payload', 'tool_task'),
$failedstr
];
$table->attributes['class'] = 'generaltable';
$table->colclasses = [];
// For each task entry (row) show action buttons/logs link depending on the user permissions.
$data = [];
foreach ($tasks as $task) {
$taskid = $task->get_id();
$started = $task->get_timestarted();
// Task id cell.
$taskidcellcontent = html_writer::span($taskid, 'task-id');
$taskidcell = new html_table_cell($taskidcellcontent);
$taskidcell->header = true;
$taskidcell->id = "task-$taskid";
// Mark cell if task has failed.
$faildelay = $task->get_fail_delay();
$faildelaycell = new html_table_cell($faildelay ? $failedstr : '');
if ($faildelay) {
$faildelaycell->attributes['class'] = 'table-danger';
}
// Prepares the next run time cell contents.
$nextrun = get_string('started', 'tool_task');
if (!$started) {
$nextruntime = $task->get_next_run_time();
$due = $nextruntime < $now;
$nextrun = $due ? userdate($nextruntime) : get_string('asap', 'tool_task');
if ($wantruntasks && ($faildelay || $due)) {
$nextrun .= ' '.html_writer::div(
html_writer::link(
new moodle_url(
$adhocrunurl,
['id' => $taskid]
),
get_string('runnow', 'tool_task')
),
'task-runnow'
);
}
}
$data[] = new html_table_row([
$taskidcell,
new html_table_cell($nextrun),
new html_table_cell($task->get_custom_data_as_string()),
$faildelaycell,
]);
}
$table->data = $data;
return $table;
}
/**
* Displays a notification on ad hoc task run request.
*
* @return string HTML notification block for task initiated message
*/
public function adhoc_task_run(): string {
return $this->output->notification(get_string('adhoctaskrun', 'tool_task'), 'info');
}
/**
* This function will render one beautiful table with all the scheduled tasks.
*
@@ -121,10 +394,14 @@ class tool_task_renderer extends plugin_renderer_base {
$faildelaycell = new html_table_cell($task->get_fail_delay());
if ($task->get_fail_delay()) {
$faildelaycell->text .= html_writer::div(html_writer::link(
$faildelaycell->text .= html_writer::div(
$this->output->single_button(
new moodle_url('/admin/tool/task/clear_fail_delay.php',
['task' => $classname, 'sesskey' => sesskey()]),
get_string('clear')), 'task-clearfaildelay');
['task' => $classname]),
get_string('clear')
),
'task-runnow'
);
$faildelaycell->attributes['class'] = 'table-danger';
}
+157
View File
@@ -0,0 +1,157 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Web run ad hoc task(s)
*
* This script runs a group or a single ad hoc task from the web UI.
*
* @package tool_task
* @copyright Catalyst IT
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('NO_OUTPUT_BUFFERING', true);
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
admin_externalpage_setup('adhoctasks');
$runurl = '/admin/tool/task/run_adhoctasks.php';
$tasksurl = '/admin/tool/task/adhoctasks.php';
// Allow execution of single task. This requires login and has different rules.
$classname = optional_param('classname', null, PARAM_RAW);
$failedonly = optional_param('failedonly', false, PARAM_BOOL);
$taskid = optional_param('id', null, PARAM_INT);
$confirmed = optional_param('confirm', 0, PARAM_INT);
if (!\core\task\manager::is_runnable()) {
$redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
throw new moodle_exception('cannotfindthepathtothecli', 'tool_task', $redirecturl->out());
}
$params = ['classname' => $classname, 'failedonly' => $failedonly, 'id' => $taskid];
// Check input parameter id against all existing tasks.
if ($taskid) {
$record = $DB->get_record('task_adhoc', ['id' => $taskid]);
if (!$record) {
throw new \moodle_exception('invalidtaskid');
}
$classname = $record->classname;
$heading = "Run $classname task Id $taskid";
$tasks = [core\task\manager::adhoc_task_from_record($record)];
} else {
if (!$classname) {
throw new \moodle_exception('noclassname', 'tool_task');
}
$heading = "Run $classname " . ($failedonly ? "failed" : "all")." tasks";
$now = time();
$tasks = array_filter(
core\task\manager::get_adhoc_tasks($classname, $failedonly, true),
function ($t) use ($now) {
return $t->get_fail_delay() || $t->get_next_run_time() <= $now;
}
);
}
// Start output.
$context = context_system::instance();
$PAGE->set_context($context);
$PAGE->set_heading($SITE->fullname);
$PAGE->set_title($classname);
echo $OUTPUT->header();
echo $OUTPUT->heading($heading);
if (!$tasks) {
echo $OUTPUT->single_button($tasksurl,
get_string('notasks', 'tool_task'),
'get');
echo $OUTPUT->footer();
exit;
}
$renderer = $PAGE->get_renderer('tool_task');
if (!get_config('core', 'cron_enabled')) {
echo $renderer->cron_disabled();
}
echo $renderer->adhoc_tasks_simple_table($tasks);
// The initial request just shows the confirmation page; we don't do anything further unless
// they confirm.
if (!$confirmed) {
echo $OUTPUT->confirm(get_string('runadhoc_confirm', 'tool_task'),
new single_button(new moodle_url($runurl, array_merge($params, ['confirm' => 1])),
get_string('runadhoc', 'tool_task')),
new single_button(new moodle_url($tasksurl, $params),
get_string('cancel'), false));
echo $OUTPUT->footer();
exit;
}
// Action requires session key.
require_sesskey();
\core\session\manager::write_close();
// Prepare to handle output via mtrace.
$CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
// Run the specified tasks.
if ($taskid) {
$repeat = $DB->get_record('task_adhoc', ['id' => $taskid]);
echo html_writer::start_tag('pre');
\core\task\manager::run_adhoc_from_cli($taskid);
echo html_writer::end_tag('pre');
} else {
$repeat = core\task\manager::get_adhoc_tasks($classname, $failedonly, true);
// Run failed first (if any). We have to run them separately anyway,
// because faildelay is observed if failed flag is not true.
echo html_writer::tag('p', get_string('runningfailedtasks', 'tool_task'), ['class' => 'lead']);
echo html_writer::start_tag('pre');
\core\task\manager::run_all_adhoc_from_cli(true, $classname);
echo html_writer::end_tag('pre');
if (!$failedonly) {
echo html_writer::tag('p', get_string('runningalltasks', 'tool_task'), ['class' => 'lead']);
echo html_writer::start_tag('pre');
\core\task\manager::run_all_adhoc_from_cli(false, $classname);
echo html_writer::end_tag('pre');
}
}
if ($repeat) {
echo html_writer::div(
$OUTPUT->single_button(
new moodle_url($runurl, array_merge($params, ['confirm' => 1])),
get_string('runagain', 'tool_task')
)
);
}
echo html_writer::div(
html_writer::link(
new moodle_url($tasksurl, $taskid ? ['classname' => $classname] : []),
get_string('backtoadhoctasks', 'tool_task')
)
);
echo $OUTPUT->footer();
-10
View File
@@ -28,16 +28,6 @@ define('NO_OUTPUT_BUFFERING', true);
require('../../../config.php');
/**
* Function used to handle mtrace by outputting the text to normal browser window.
*
* @param string $message Message to output
* @param string $eol End of line character
*/
function tool_task_mtrace_wrapper($message, $eol) {
echo s($message . $eol);
}
// Allow execution of single task. This requires login and has different rules.
$taskname = required_param('task', PARAM_RAW_TRIMMED);
+9
View File
@@ -34,6 +34,15 @@ if ($hassiteconfig) {
)
);
$ADMIN->add(
'taskconfig',
new admin_externalpage(
'adhoctasks',
new lang_string('adhoctasks', 'tool_task'),
"$CFG->wwwroot/$CFG->admin/tool/task/adhoctasks.php"
)
);
$ADMIN->add(
'taskconfig',
new admin_externalpage(
+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';
+43 -1
View File
@@ -226,6 +226,8 @@ class cron {
* @param int $keepalive Keep this public static function alive for N seconds and poll for new adhoc tasks.
* @param bool $checklimits Should we check limits?
* @param null|int $startprocesstime The time this process started.
* @param int|null $maxtasks Limit number of tasks to run`
* @param null|string $classname Run only tasks of this class
* @throws \moodle_exception
*/
public static function run_adhoc_tasks(
@@ -233,6 +235,8 @@ class cron {
$keepalive = 0,
$checklimits = true,
?int $startprocesstime = null,
?int $maxtasks = null,
?string $classname = null,
): void {
// Allow a restriction on the number of adhoc task runners at once.
$cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
@@ -281,7 +285,7 @@ class cron {
}
try {
$task = \core\task\manager::get_next_adhoc_task(time(), $checklimits);
$task = \core\task\manager::get_next_adhoc_task(time(), $checklimits, $classname);
} catch (\Throwable $e) {
if ($adhoclock) {
// Release the adhoc task runner lock.
@@ -298,6 +302,9 @@ class cron {
self::run_inner_adhoc_task($task);
self::set_process_title("Waiting for next adhoc task");
$taskcount++;
if ($maxtasks && $taskcount >= $maxtasks) {
break;
}
unset($task);
} else {
$timeleft = $finishtime - time();
@@ -327,6 +334,41 @@ class cron {
}
}
/**
* Execute an 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");
}
/**
* Execute all failed adhoc tasks.
*
* @param string|null $classname Run only tasks of this class
*/
public static function run_failed_adhoc_tasks(?string $classname = null): void {
global $DB;
$where = 'faildelay > 0';
$params = [];
if ($classname) {
$where .= ' AND classname = :classname';
$params['classname'] = \core\task\manager::get_canonical_class_name($classname);
}
$tasks = $DB->get_records_sql("SELECT * from {task_adhoc} WHERE $where", $params);
foreach ($tasks as $t) {
self::run_adhoc_task($t->id);
}
}
/**
* Shared code that handles running of a single scheduled task within the cron.
*
+223 -32
View File
@@ -24,6 +24,9 @@
*/
namespace core\task;
use core\lock\lock;
use core\lock\lock_factory;
define('CORE_TASK_TASKS_FILENAME', 'db/tasks.php');
/**
* Collection of task related methods.
@@ -317,12 +320,12 @@ class manager {
*
* @param \stdClass $record
* @return \core\task\adhoc_task
* @throws \moodle_exception
*/
public static function adhoc_task_from_record($record) {
$classname = self::get_canonical_class_name($record->classname);
if (!class_exists($classname)) {
debugging("Failed to load task: " . $classname, DEBUG_DEVELOPER);
return false;
throw new \moodle_exception('invalidtaskclassname', '', '', $record->classname);
}
$task = new $classname;
if (isset($record->nextruntime)) {
@@ -474,18 +477,83 @@ class manager {
* This function load the adhoc tasks for a given classname.
*
* @param string $classname
* @return \core\task\adhoc_task[]
* @param bool $failedonly
* @param bool $skiprunning do not return tasks that are in the running state
* @return array
*/
public static function get_adhoc_tasks($classname) {
public static function get_adhoc_tasks(string $classname, bool $failedonly = false, bool $skiprunning = false): array {
global $DB;
$classname = self::get_canonical_class_name($classname);
// We are just reading - so no locks required.
$records = $DB->get_records('task_adhoc', array('classname' => $classname));
$conds[] = 'classname = ?';
$params[] = self::get_canonical_class_name($classname);
if ($failedonly) {
$conds[] = 'faildelay > 0';
}
if ($skiprunning) {
$conds[] = 'timestarted IS NULL';
}
// We are just reading - so no locks required.
$sql = 'SELECT * FROM {task_adhoc}';
if ($conds) {
$sql .= ' WHERE '.implode(' AND ', $conds);
}
$rs = $DB->get_records_sql($sql, $params);
return array_map(function($record) {
return self::adhoc_task_from_record($record);
}, $records);
}, $rs);
}
/**
* This function returns adhoc tasks summary per component classname
*
* @return array
*/
public static function get_adhoc_tasks_summary(): array {
global $DB;
$now = time();
$records = $DB->get_records('task_adhoc');
$summary = [];
foreach ($records as $r) {
if (!isset($summary[$r->component])) {
$summary[$r->component] = [];
}
if (isset($summary[$r->component][$r->classname])) {
$classsummary = $summary[$r->component][$r->classname];
} else {
$classsummary = [
'nextruntime' => null,
'count' => 0,
'failed' => 0,
'running' => 0,
'due' => 0,
];
}
$classsummary['count']++;
$nextruntime = (int)$r->nextruntime;
if (!$classsummary['nextruntime'] || $nextruntime < $classsummary['nextruntime']) {
$classsummary['nextruntime'] = $nextruntime;
}
if ((int)$r->timestarted > 0) {
$classsummary['running']++;
} else {
if ((int)$r->faildelay > 0) {
$classsummary['failed']++;
}
if ($nextruntime <= $now) {
$classsummary['due']++;
}
}
$summary[$r->component][$r->classname] = $classsummary;
}
return $summary;
}
/**
@@ -550,9 +618,10 @@ class manager {
$records = $DB->get_records_sql('SELECT * from {task_adhoc} WHERE faildelay > ?', [$delay]);
foreach ($records as $record) {
$task = self::adhoc_task_from_record($record);
if ($task) {
$tasks[] = $task;
try {
$tasks[] = self::adhoc_task_from_record($record);
} catch (\moodle_exception $e) {
debugging("Failed to load task: $record->classname", DEBUG_DEVELOPER, $e->getTrace());
}
}
return $tasks;
@@ -645,10 +714,11 @@ class manager {
*
* @param int $timestart
* @param bool $checklimits Should we check limits?
* @return \core\task\adhoc_task or null if not found
* @param string|null $classname Return only task of this class
* @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, ?string $classname = null): ?adhoc_task {
global $DB;
$concurrencylimit = get_config('core', 'task_adhoc_concurrency_limit');
@@ -760,6 +830,11 @@ class manager {
foreach (self::$miniqueue as $taskid => $record) {
if (!empty($classname) && $record->classname != self::get_canonical_class_name($classname)) {
// Skip the task if The class is specified, and doesn't match.
continue;
}
if (in_array($record->classname, $skipclasses)) {
// Skip the task if it can't be started due to per-task concurrency limit.
continue;
@@ -775,9 +850,11 @@ class manager {
continue;
}
$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) {
try {
$task = self::adhoc_task_from_record($record);
} catch (\moodle_exception $e) {
debugging("Failed to load task: $record->classname", DEBUG_DEVELOPER);
$lock->release();
unset(self::$miniqueue[$taskid]);
continue;
@@ -797,21 +874,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 +945,65 @@ class manager {
);
}
/**
* This function will get an 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', ['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)) {
// Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
try {
$task = self::adhoc_task_from_record($record);
} catch (\moodle_exception $e) {
$lock->release();
throw $e;
}
self::set_locks($task, $lock, $cronlockfactory);
return $task;
}
return null;
}
/**
* This function will set locks on the task.
*
* @param adhoc_task $task
* @param lock $lock task lock
* @param lock_factory $cronlockfactory
* @throws \moodle_exception
*/
private static function set_locks(adhoc_task $task, lock $lock, lock_factory $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
@@ -1307,7 +1431,7 @@ class manager {
*
* @param string|task_base $taskorstring Task object or a string
*/
protected static function get_canonical_class_name($taskorstring) {
public static function get_canonical_class_name($taskorstring) {
if (is_string($taskorstring)) {
$classname = $taskorstring;
} else {
@@ -1366,11 +1490,11 @@ class manager {
/**
* Executes a cron from web invocation using PHP CLI.
*
* @param \core\task\task_base $task Task that be executed via CLI.
* @param scheduled_task $task Task that be executed via CLI.
* @return bool
* @throws \moodle_exception
*/
public static function run_from_cli(\core\task\task_base $task):bool {
public static function run_from_cli(scheduled_task $task): bool {
global $CFG;
if (!self::is_runnable()) {
@@ -1398,6 +1522,73 @@ class manager {
return true;
}
/**
* Executes an ad hoc task from web invocation using PHP CLI.
*
* @param int $taskid Task to execute via CLI.
* @throws \moodle_exception
*/
public static function run_adhoc_from_cli(int $taskid) {
// Shell-escaped task name.
$taskarg = escapeshellarg("--id={$taskid}");
self::run_adhoc_from_cli_base($taskarg);
}
/**
* Executes ad hoc tasks from web invocation using PHP CLI.
*
* @param bool|null $failedonly
* @param string|null $classname Task class to execute via CLI.
* @throws \moodle_exception
*/
public static function run_all_adhoc_from_cli(?bool $failedonly = false, ?string $classname = null) {
$taskargs = [];
if ($failedonly) {
$taskargs[] = '--failed';
}
if ($classname) {
// Shell-escaped task select.
$taskargs[] = escapeshellarg("--classname={$classname}");
}
self::run_adhoc_from_cli_base($taskargs ? implode(' ', $taskargs) : '--execute');
}
/**
* Executes an ad hoc task from web invocation using PHP CLI.
*
* @param string $taskarg Task to execute via CLI.
* @throws \moodle_exception
*/
private static function run_adhoc_from_cli_base(string $taskarg): void {
global $CFG;
if (!self::is_runnable()) {
$redirecturl = new \moodle_url('/admin/settings.php', ['section' => 'systempaths']);
throw new \moodle_exception('cannotfindthepathtothecli', 'tool_task', $redirecturl->out());
}
// Shell-escaped path to the PHP binary.
$phpbinary = escapeshellarg(self::find_php_cli_path());
// Shell-escaped path CLI script.
$pathcomponents = [$CFG->dirroot, $CFG->admin, 'cli', 'adhoc_task.php'];
$scriptpath = escapeshellarg(implode(DIRECTORY_SEPARATOR, $pathcomponents));
// Build the CLI command.
$command = "{$phpbinary} {$scriptpath} {$taskarg} --force";
// We cannot run it in phpunit.
if (PHPUNIT_TEST) {
echo $command;
return;
}
// Execute it.
passthru($command);
}
/**
* For a given scheduled task record, this method will check to see if any overrides have
* been applied in config and return a copy of the record with any overridden values.
+87 -1
View File
@@ -53,6 +53,8 @@ class adhoc_task_test extends \advanced_testcase {
/**
* Test basic adhoc task execution.
*
* @covers ::get_next_adhoc_task
*/
public function test_get_next_adhoc_task_now() {
$this->resetAfterTest(true);
@@ -71,10 +73,38 @@ class adhoc_task_test extends \advanced_testcase {
manager::adhoc_task_complete($task);
}
/**
* Test basic adhoc task execution.
*
* @covers ::get_next_adhoc_task
*/
public function test_get_next_adhoc_task_class() {
$this->resetAfterTest(true);
// Create an adhoc task.
$task = new \core\task\adhoc_test_task();
// Queue it.
manager::queue_adhoc_task($task);
$now = time();
$classname = get_class($task);
// The task will not be returned.
$this->assertNull(manager::get_next_adhoc_task($now, true, "{$classname}notexists"));
// Get it from the scheduler.
$task = manager::get_next_adhoc_task($now, true, $classname);
$this->assertInstanceOf('\\core\\task\\adhoc_test_task', $task);
$task->execute();
manager::adhoc_task_complete($task);
}
/**
* 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 +117,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.
@@ -519,4 +557,52 @@ class adhoc_task_test extends \advanced_testcase {
$this->assertEquals('Task 1', $task->get_custom_data_as_string());
manager::adhoc_task_complete($task);
}
/**
* Test adhoc task run from CLI.
* @covers ::run_adhoc_from_cli
*/
public function test_run_adhoc_from_cli() {
$this->resetAfterTest(true);
$taskid = 1;
if (!manager::is_runnable()) {
$this->markTestSkipped("Cannot run tasks");
}
ob_start();
manager::run_adhoc_from_cli($taskid);
$output = ob_get_contents();
ob_end_clean();
$this->assertMatchesRegularExpression(
sprintf('!admin/cli/adhoc_task.php\W+--id=%d\W+--force!', $taskid),
$output
);
}
/**
* Test adhoc class run from CLI.
* @covers ::run_all_adhoc_from_cli
*/
public function test_run_all_adhoc_from_cli() {
$this->resetAfterTest(true);
$classname = 'fake';
if (!manager::is_runnable()) {
$this->markTestSkipped("Cannot run tasks");
}
ob_start();
manager::run_all_adhoc_from_cli(false, $classname);
$output = ob_get_contents();
ob_end_clean();
$this->assertMatchesRegularExpression(
sprintf('!admin/cli/adhoc_task.php\W+--classname=%s\W+--force!', $classname),
$output
);
}
}
+9
View File
@@ -129,6 +129,15 @@ information provided here is intended especially for developers.
needs to be rendered as is and not whitened out, the `.nofilter` CSS class needs to be applied to the icon.
* New moodle_url::export_params_for_template() returns the params as an array of key => value pairs. You should use this when
you want to pass URL params to a template. An example can be found in single button.
* Functions get_next_adhoc_task() and cron::run_adhoc_tasks() have additional parameter $classname to filter by the specified class.
* Function cron::run_adhoc_tasks() has additional parameter $number to limit the number of the tasks to run.
* Function get_adhoc_tasks() has additional parameter $failedonly to return only failed tasks.
* admin/cli/adhoc_task.php has additional parameters:
- id - to run individual tasks by id
- classname - to run tasks by classname
- taskslimit - to limit the number of tasks in one run
- failed - to limit the run to only the tasks that failed in their previous run
Can be mixed, apart from 'id' of course.
=== 4.1 ===