Merge branch 'MDL-67648-master' of https://github.com/cameron1729/moodle

This commit is contained in:
Ilya Tregubov 2022-05-27 15:41:29 +06:00
commit 0460147cb5
7 changed files with 482 additions and 154 deletions

View File

@ -36,6 +36,31 @@ define('CORE_TASK_TASKS_FILENAME', 'db/tasks.php');
*/
class manager {
/**
* @var int Used to tell the adhoc task queue to fairly distribute tasks.
*/
const ADHOC_TASK_QUEUE_MODE_DISTRIBUTING = 0;
/**
* @var int Used to tell the adhoc task queue to try and fill unused capacity.
*/
const ADHOC_TASK_QUEUE_MODE_FILLING = 1;
/**
* @var array A cached queue of adhoc tasks
*/
public static $miniqueue;
/**
* @var int The last recorded number of unique adhoc tasks.
*/
public static $numtasks;
/**
* @var string Used to determine if the adhoc task queue is distributing or filling capacity.
*/
public static $mode;
/**
* Given a component name, will load the list of tasks in the db/tasks.php file for that component.
*
@ -542,8 +567,13 @@ class manager {
*
* @param array $records array of task records
* @param array $records array of same task records shuffled
* @deprecated since Moodle 4.1 MDL-67648 - please do not use this method anymore.
* @todo MDL-74843 This method will be deleted in Moodle 4.5
* @see \core\task\manager::get_next_adhoc_task
*/
public static function ensure_adhoc_task_qos(array $records): array {
debugging('The method \core\task\manager::ensure_adhoc_task_qos is deprecated.
Please use \core\task\manager::get_next_adhoc_task instead.', DEBUG_DEVELOPER);
$count = count($records);
if ($count == 0) {
@ -621,16 +651,114 @@ class manager {
public static function get_next_adhoc_task($timestart, $checklimits = true) {
global $DB;
$where = '(nextruntime IS NULL OR nextruntime < :timestart1)';
$params = array('timestart1' => $timestart);
$records = $DB->get_records_select('task_adhoc', $where, $params, 'nextruntime ASC, id ASC', '*', 0, 2000);
$records = self::ensure_adhoc_task_qos($records);
$concurrencylimit = get_config('core', 'task_adhoc_concurrency_limit');
$cachedqueuesize = 1200;
$uniquetasksinqueue = array_map(
['\core\task\manager', 'adhoc_task_from_record'],
$DB->get_records_sql(
'SELECT classname FROM {task_adhoc} WHERE nextruntime < :timestart GROUP BY classname',
['timestart' => $timestart]
)
);
if (!isset(self::$numtasks) || self::$numtasks !== count($uniquetasksinqueue)) {
self::$numtasks = count($uniquetasksinqueue);
self::$miniqueue = [];
}
$concurrencylimits = [];
if ($checklimits) {
$concurrencylimits = array_map(
function ($task) {
return $task->get_concurrency_limit();
},
$uniquetasksinqueue
);
}
/*
* The maximum number of cron runners that an individual task is allowed to use.
* For example if the concurrency limit is 20 and there are 5 unique types of tasks
* in the queue, each task should not be allowed to consume more than 3 (i.e., ⌊20/6).
* The + 1 is needed to prevent the queue from becoming full of only one type of class.
* i.e., if it wasn't there and there were 20 tasks of the same type in the queue, every
* runner would become consumed with the same (potentially long-running task) and no more
* tasks can run. This way, some resources are always available if some new types
* of tasks enter the queue.
*
* We use the short-ternary to force the value to 1 in the case when the number of tasks
* exceeds the runners (e.g., there are 8 tasks and 4 runners, ⌊4/(8+1) = 0).
*/
$slots = floor($concurrencylimit / (count($uniquetasksinqueue) + 1)) ?: 1;
if (empty(self::$miniqueue)) {
self::$mode = self::ADHOC_TASK_QUEUE_MODE_DISTRIBUTING;
self::$miniqueue = self::get_candidate_adhoc_tasks(
$timestart,
$cachedqueuesize,
$slots,
$concurrencylimits
);
}
// The query to cache tasks is expensive on big data sets, so we use this cheap
// query to get the ordering (which is the interesting part about the main query)
// We can use this information to filter the cache and also order it.
$runningtasks = $DB->get_records_sql(
'SELECT classname, COALESCE(COUNT(*), 0) running, MIN(timestarted) earliest
FROM {task_adhoc}
WHERE timestarted IS NOT NULL
AND nextruntime < :timestart
GROUP BY classname
ORDER BY running ASC, earliest DESC',
['timestart' => $timestart]
);
/*
* Each runner has a cache, so the same task can be in multiple runners' caches.
* We need to check that each task we have cached hasn't gone over its fair number
* of slots. This filtering is only applied during distributing mode as when we are
* filling capacity we intend for fast tasks to go over their slot limit.
*/
if (self::$mode === self::ADHOC_TASK_QUEUE_MODE_DISTRIBUTING) {
self::$miniqueue = array_filter(
self::$miniqueue,
function (\stdClass $task) use ($runningtasks, $slots) {
return !array_key_exists($task->classname, $runningtasks) || $runningtasks[$task->classname]->running < $slots;
}
);
}
/*
* If this happens that means each task has consumed its fair share of capacity, but there's still
* runners left over (and we are one of them). Fetch tasks without checking slot limits.
*/
if (empty(self::$miniqueue) && array_sum(array_column($runningtasks, 'running')) < $concurrencylimit) {
self::$mode = self::ADHOC_TASK_QUEUE_MODE_FILLING;
self::$miniqueue = self::get_candidate_adhoc_tasks(
$timestart,
$cachedqueuesize,
false,
$concurrencylimits
);
}
// Used below to order the cache.
$ordering = array_flip(array_keys($runningtasks));
// Order the queue so it's consistent with the ordering from the DB.
usort(
self::$miniqueue,
function ($a, $b) use ($ordering) {
return ($ordering[$a->classname] ?? -1) - ($ordering[$b->classname] ?? -1);
}
);
$cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
$skipclasses = array();
foreach ($records as $record) {
foreach (self::$miniqueue as $taskid => $record) {
if (in_array($record->classname, $skipclasses)) {
// Skip the task if it can't be started due to per-task concurrency limit.
@ -643,6 +771,7 @@ class manager {
$record = $DB->get_record('task_adhoc', array('id' => $record->id));
if (!$record) {
$lock->release();
unset(self::$miniqueue[$taskid]);
continue;
}
@ -650,6 +779,7 @@ class manager {
// Safety check in case the task in the DB does not match a real class (maybe something was uninstalled).
if (!$task) {
$lock->release();
unset(self::$miniqueue[$taskid]);
continue;
}
@ -661,6 +791,7 @@ class manager {
// Unable to obtain a concurrency lock.
mtrace("Skipping $record->classname adhoc task class as the per-task limit of $tasklimit is reached.");
$skipclasses[] = $record->classname;
unset(self::$miniqueue[$taskid]);
$lock->release();
continue;
}
@ -679,13 +810,76 @@ class manager {
} else {
$task->set_cron_lock($cronlock);
}
unset(self::$miniqueue[$taskid]);
return $task;
} else {
unset(self::$miniqueue[$taskid]);
}
}
return null;
}
/**
* Return a list of candidate adhoc tasks to run.
*
* @param int $timestart Only return tasks where nextruntime is less than this value
* @param int $limit Limit the list to this many results
* @param int|null $runmax Only return tasks that have less than this value currently running
* @param array $pertasklimits An array of classname => limit specifying how many instance of a task may be returned
* @return array Array of candidate tasks
*/
public static function get_candidate_adhoc_tasks(
int $timestart,
int $limit,
?int $runmax,
array $pertasklimits = []
): array {
global $DB;
$pertaskclauses = array_map(
function (string $class, int $limit, int $index): array {
$limitcheck = $limit > 0 ? " AND COALESCE(run.running, 0) < :running_$index" : "";
$limitparam = $limit > 0 ? ["running_$index" => $limit] : [];
return [
"sql" => "(q.classname = :classname_$index" . $limitcheck . ")",
"params" => ["classname_$index" => $class] + $limitparam
];
},
array_keys($pertasklimits),
$pertasklimits,
$pertasklimits ? range(1, count($pertasklimits)) : []
);
$pertasksql = implode(" OR ", array_column($pertaskclauses, 'sql'));
$pertaskparams = $pertaskclauses ? array_merge(...array_column($pertaskclauses, 'params')) : [];
$params = ['timestart' => $timestart] +
($runmax ? ['runmax' => $runmax] : []) +
$pertaskparams;
return $DB->get_records_sql(
"SELECT q.id, q.classname, q.timestarted, COALESCE(run.running, 0) running, run.earliest
FROM {task_adhoc} q
LEFT JOIN (
SELECT classname, COUNT(*) running, MIN(timestarted) earliest
FROM {task_adhoc} run
WHERE timestarted IS NOT NULL
GROUP BY classname
) run ON run.classname = q.classname
WHERE nextruntime < :timestart
AND q.timestarted IS NULL " .
(!empty($pertasksql) ? "AND (" . $pertasksql . ") " : "") .
($runmax ? "AND (COALESCE(run.running, 0)) < :runmax " : "") .
"ORDER BY COALESCE(run.running, 0) ASC, run.earliest DESC, q.nextruntime ASC, q.id ASC",
$params,
0,
$limit
);
}
/**
* 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

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/db" VERSION="20220510" COMMENT="XMLDB file for core Moodle tables"
<XMLDB PATH="lib/db" VERSION="20220524" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
@ -3448,6 +3448,7 @@
</KEYS>
<INDEXES>
<INDEX NAME="nextruntime_idx" UNIQUE="false" FIELDS="nextruntime"/>
<INDEX NAME="timestarted_idx" UNIQUE="false" FIELDS="timestarted"/>
</INDEXES>
</TABLE>
<TABLE NAME="task_log" COMMENT="The log table for all tasks">

View File

@ -4515,5 +4515,20 @@ privatefiles,moodle|/user/files.php';
upgrade_main_savepoint(true, 2022052500.00);
}
if ($oldversion < 2022052700.01) {
// Define index timestarted_idx (not unique) to be added to task_adhoc.
$table = new xmldb_table('task_adhoc');
$index = new xmldb_index('timestarted_idx', XMLDB_INDEX_NOTUNIQUE, ['timestarted']);
// Conditionally launch add index timestarted_idx.
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2022052700.01);
}
return true;
}

View File

@ -26,14 +26,76 @@
namespace core\task;
defined('MOODLE_INTERNAL') || die();
/**
* Test class.
*
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Cameron Ball <cameron@cameron1729.xyz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adhoc_test_task extends \core\task\adhoc_task {
/**
* Constructor.
*
* @param int|null $nextruntime Next run time
* @param int|null $timestarted Time started
*/
public function __construct(?int $nextruntime = null, ?int $timestarted = null) {
if ($nextruntime) {
$this->set_next_run_time($nextruntime);
}
if ($timestarted) {
$this->set_timestarted($timestarted);
}
}
/**
* Execute.
*/
public function execute() {
}
}
class adhoc_test2_task extends \core\task\adhoc_task {
public function execute() {
}
/**
* Test class.
*
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Cameron Ball <cameron@cameron1729.xyz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adhoc_test2_task extends adhoc_test_task {
}
/**
* Test class.
*
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Cameron Ball <cameron@cameron1729.xyz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adhoc_test3_task extends adhoc_test_task {
}
/**
* Test class.
*
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Cameron Ball <cameron@cameron1729.xyz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adhoc_test4_task extends adhoc_test_task {
}
/**
* Test class.
*
* @copyright 2022 Catalyst IT Australia Pty Ltd
* @author Cameron Ball <cameron@cameron1729.xyz>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class adhoc_test5_task extends adhoc_test_task {
}
class scheduled_test_task extends \core\task\scheduled_task {

View File

@ -22,7 +22,10 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core\task;
defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/fixtures/task_fixtures.php');
/**
* This file contains the unit tests for the task manager.
@ -30,168 +33,216 @@ defined('MOODLE_INTERNAL') || die();
* @copyright 2019 Brendan Heywood <brendan@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class core_task_manager_testcase extends advanced_testcase {
class task_manager_test extends \advanced_testcase {
public function test_ensure_adhoc_task_qos_provider() {
/**
* Data provider for test_get_candidate_adhoc_tasks.
*
* @return array
*/
public function test_get_candidate_adhoc_tasks_provider(): array {
return [
[
[],
[],
],
// A queue with a lopside initial load that needs to be staggered.
[
[
(object)['id' => 1, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 2, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 3, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 4, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 10, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 11, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 12, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 13, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 14, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 15, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
],
[
(object)['id' => 1, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 2, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 3, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 4, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 10, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 11, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 12, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 13, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 14, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 15, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
],
],
// The same lopsided queue but now the first item is gone.
[
[
(object)['id' => 2, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 3, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 4, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
],
[
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 2, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 3, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 4, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
],
],
// The same lopsided queue but now the first two items is gone.
[
[
(object)['id' => 3, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 4, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
],
[
(object)['id' => 3, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 4, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
],
],
// The same lopsided queue but now the first three items are gone.
[
[
(object)['id' => 4, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
],
[
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 4, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
'concurrencylimit' => 5,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null)
],
'expected' => [
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class
]
],
[
[
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
],
[
(object)['id' => 5, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 7, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 6, 'classname' => '\core\task\asynchronous_backup_task'],
(object)['id' => 8, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
(object)['id' => 9, 'classname' => '\tool_dataprivacy\task\process_data_request_task'],
'concurrencylimit' => 5,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null)
],
'expected' => [
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class,
adhoc_test_task::class
]
],
[
'concurrencylimit' => 1,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null)
],
'expected' => []
],
[
'concurrencylimit' => 2,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null)
],
'expected' => []
],
[
'concurrencylimit' => 2,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, null)
],
'expected' => [adhoc_test3_task::class]
],
[
'concurrencylimit' => 2,
'limit' => 2,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test_task(time() - 20, null),
new adhoc_test2_task(time() - 20, null),
],
'expected' => [
adhoc_test_task::class,
adhoc_test_task::class
]
],
[
'concurrencylimit' => 2,
'limit' => 2,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test2_task(time() - 20, null),
],
'expected' => [
adhoc_test2_task::class
]
],
[
'concurrencylimit' => 3,
'limit' => 100,
'pertasklimits' => [],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, null),
new adhoc_test3_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, null),
new adhoc_test4_task(time() - 20, time()),
new adhoc_test4_task(time() - 20, time()),
new adhoc_test4_task(time() - 20, null),
new adhoc_test5_task(time() - 20, time()),
new adhoc_test5_task(time() - 20, time()),
new adhoc_test5_task(time() - 20, null),
],
'expected' => [
adhoc_test_task::class,
adhoc_test2_task::class,
adhoc_test3_task::class,
adhoc_test4_task::class,
adhoc_test5_task::class
]
],
[
'concurrencylimit' => 3,
'limit' => 100,
'pertasklimits' => [
'adhoc_test_task' => 2,
'adhoc_test2_task' => 2,
'adhoc_test3_task' => 2,
'adhoc_test4_task' => 2,
'adhoc_test5_task' => 2
],
'tasks' => [
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, time()),
new adhoc_test_task(time() - 20, null),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, time()),
new adhoc_test2_task(time() - 20, null),
new adhoc_test3_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, time()),
new adhoc_test3_task(time() - 20, null),
new adhoc_test4_task(time() - 20, time()),
new adhoc_test4_task(time() - 20, time()),
new adhoc_test4_task(time() - 20, null),
new adhoc_test5_task(time() - 20, time()),
new adhoc_test5_task(time() - 20, time()),
new adhoc_test5_task(time() - 20, null),
],
'expected' => []
]
];
}
/**
* Reduces a list of tasks into a simpler string
* Test that the candidate adhoc tasks are returned in the right order.
*
* @param array $input array of tasks
* @return string list of task ids
*/
function flatten($tasks) {
$list = '';
foreach ($tasks as $id => $task) {
$list .= ' ' . $task->id;
}
return $list;
}
/**
* Test that the Quality of Service reordering works.
* @dataProvider test_get_candidate_adhoc_tasks_provider
*
* @dataProvider test_ensure_adhoc_task_qos_provider
*
* @param array $input array of tasks
* @param array $expected array of reordered tasks
* @param int $concurrencylimit The max number of runners each task can consume
* @param int $limit SQL limit
* @param array $pertasklimits Per-task limits
* @param array $tasks Array of tasks to put in DB and retrieve
* @param array $expected Array of expected classnames
* @return void
* @covers \manager::get_candidate_adhoc_tasks
*/
public function test_ensure_adhoc_task_qos(array $input, array $expected) {
public function test_get_candidate_adhoc_tasks(
int $concurrencylimit,
int $limit,
array $pertasklimits,
array $tasks,
array $expected
): void {
$this->resetAfterTest();
$result = \core\task\manager::ensure_adhoc_task_qos($input);
foreach ($tasks as $task) {
manager::queue_adhoc_task($task);
}
$result = $this->flatten($result);
$expected = $this->flatten($expected);
$this->assertEquals($expected, $result);
$candidates = manager::get_candidate_adhoc_tasks(time(), $limit, $concurrencylimit, $pertasklimits);
$this->assertEquals(
array_map(
function(string $classname): string {
return '\\' . $classname;
},
$expected
),
array_column($candidates, 'classname')
);
}
}

View File

@ -1,6 +1,11 @@
This files describes API changes in core libraries and APIs,
information provided here is intended especially for developers.
=== 4.1 ===
* The method ensure_adhoc_task_qos() in lib/classes/task/manager.php has been deprecated, please use get_next_adhoc_task()
instead.
=== 4.0 ===
* To better detect wrong floats (like, for example, unformatted, using local-dependent separators ones) a number of

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2022052700.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2022052700.01; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.1dev (Build: 20220527)'; // Human-friendly version name