[Webportal] Support Job Priority in job-list (#5525)

* update hivedScheduler.jobPriorityClass spell in db controller

* add jobPriority to frameworkConverter

* update

* update

* update

* update

* update swagger

* add default jobPriority support

* fix

* fix

* add job priority to table and ordering

* add priority filter

* update

* fix

* fix

* fix lint
This commit is contained in:
Yi Yi 2021-06-10 14:23:50 +08:00 коммит произвёл GitHub
Родитель 6625542870
Коммит 5153a4d9a6
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 169 добавлений и 14 удалений

Просмотреть файл

@ -186,7 +186,7 @@ class Snapshot {
); );
const jobPriority = _.get( const jobPriority = _.get(
loadedConfig, loadedConfig,
'extras.hivedscheduler.jobPriorityClass', 'extras.hivedScheduler.jobPriorityClass',
null, null,
); );
// Job status change notification // Job status change notification

Просмотреть файл

@ -17,10 +17,11 @@ info:
version 2.2.4: support sorting by completionTime in get the list of jobs version 2.2.4: support sorting by completionTime in get the list of jobs
version 2.2.5: add alert related api version 2.2.5: add alert related api
version 2.2.6: update type of taskUid version 2.2.6: update type of taskUid
version 2.2.7: add jobPriority list jobs parameter
license: license:
name: MIT License name: MIT License
url: "https://github.com/microsoft/pai/blob/master/LICENSE" url: "https://github.com/microsoft/pai/blob/master/LICENSE"
version: 2.2.6 version: 2.2.7
externalDocs: externalDocs:
description: Find out more about OpenPAI description: Find out more about OpenPAI
url: "https://github.com/microsoft/pai" url: "https://github.com/microsoft/pai"
@ -1150,6 +1151,11 @@ paths:
description: filter jobs with tags. When multiple tags are specified, every job selected should have none of these tags description: filter jobs with tags. When multiple tags are specified, every job selected should have none of these tags
schema: schema:
type: string type: string
- name: jobPriority
in: query
description: filter jobs with jobPriority, fields include oppo, test, prod, and default (default means jobPriorityClass in job config is null)
schema:
type: string
- name: offset - name: offset
in: query in: query
description: list job offset description: list job offset
@ -1164,7 +1170,7 @@ paths:
in: query in: query
description: 'order of job list. description: 'order of job list.
It follows the format <field>,<ASC|DESC>, default value is "submissionTime,DESC". It follows the format <field>,<ASC|DESC>, default value is "submissionTime,DESC".
Available fields include: jobName, submissionTime, username, vc, retries, totalTaskNumber, totalGpuNumber, state, completionTime. Available fields include: jobName, submissionTime, username, vc, retries, totalTaskNumber, totalGpuNumber, state, completionTime, jobPriority.
CompletionTime maybe null for some jobs, these jobs will be returned at the end of the list when sorting by ASC order & at the beginning when sorting by DESC order.' CompletionTime maybe null for some jobs, these jobs will be returned at the end of the list when sorting by ASC order & at the beginning when sorting by DESC order.'
schema: schema:
type: string type: string
@ -1197,6 +1203,7 @@ paths:
completedTime: 0 completedTime: 0
appExitCode: 0 appExitCode: 0
virtualCluster: unknown virtualCluster: unknown
jobPriority: prod
"500": "500":
$ref: "#/components/responses/UnknownError" $ref: "#/components/responses/UnknownError"
"/api/v2/jobs/{user}~{job}": "/api/v2/jobs/{user}~{job}":
@ -2223,6 +2230,10 @@ components:
debugId: debugId:
type: string type: string
description: md5 hash name for the job in framework controller, used for debug purpose description: md5 hash name for the job in framework controller, used for debug purpose
jobPriority:
type: string
nullable: true
description: job priority
required: required:
- name - name
- username - username

Просмотреть файл

@ -81,6 +81,22 @@ const list = asyncHandler(async (req, res) => {
{ virtualCluster: { [Op.substring]: req.query.keyword } }, { virtualCluster: { [Op.substring]: req.query.keyword } },
]; ];
} }
if ('jobPriority' in req.query) {
const jobPriorityFilter = req.query.jobPriority.split(',');
const index = jobPriorityFilter.indexOf('default');
if (index !== -1) {
jobPriorityFilter.splice(index, 1);
if (filters[Op.or] === undefined) {
filters[Op.or] = [];
}
filters[Op.or].push({ jobPriority: { [Op.is]: null } });
if (jobPriorityFilter.length > 0) {
filters[Op.or].push({ jobPriority: jobPriorityFilter });
}
} else {
filters.jobPriority = jobPriorityFilter;
}
}
if ('order' in req.query) { if ('order' in req.query) {
const [field, ordering] = req.query.order.split(','); const [field, ordering] = req.query.order.split(',');
if ( if (
@ -94,6 +110,7 @@ const list = asyncHandler(async (req, res) => {
'totalGpuNumber', 'totalGpuNumber',
'state', 'state',
'completionTime', 'completionTime',
'jobPriority',
].includes(field) ].includes(field)
) { ) {
if (ordering === 'ASC' || ordering === 'DESC') { if (ordering === 'ASC' || ordering === 'DESC') {
@ -106,6 +123,10 @@ const list = asyncHandler(async (req, res) => {
const orderingWithNulls = const orderingWithNulls =
ordering === 'ASC' ? 'ASC NULLS LAST' : 'DESC NULLS FIRST'; ordering === 'ASC' ? 'ASC NULLS LAST' : 'DESC NULLS FIRST';
order.push(['completionTime', orderingWithNulls]); order.push(['completionTime', orderingWithNulls]);
} else if (field === 'jobPriority') {
const orderingWithNulls =
ordering === 'ASC' ? 'ASC NULLS LAST' : 'DESC NULLS FIRST';
order.push(['jobPriority', orderingWithNulls]);
} else { } else {
order.push([field, ordering]); order.push([field, ordering]);
} }
@ -129,6 +150,7 @@ const list = asyncHandler(async (req, res) => {
'totalGpuNumber', 'totalGpuNumber',
'totalTaskNumber', 'totalTaskNumber',
'totalTaskRoleNumber', 'totalTaskRoleNumber',
'jobPriority',
'retries', 'retries',
'retryDelayTime', 'retryDelayTime',
'platformRetries', 'platformRetries',

Просмотреть файл

@ -75,6 +75,7 @@ const convertFrameworkSummary = (framework) => {
totalGpuNumber: framework.totalGpuNumber, totalGpuNumber: framework.totalGpuNumber,
totalTaskNumber: framework.totalTaskNumber, totalTaskNumber: framework.totalTaskNumber,
totalTaskRoleNumber: framework.totalTaskRoleNumber, totalTaskRoleNumber: framework.totalTaskRoleNumber,
jobPriority: framework.jobPriority,
}; };
}; };
@ -187,6 +188,7 @@ const convertFrameworkDetail = async (
debugId: frameworkWithLatestAttempt.metadata.name, debugId: frameworkWithLatestAttempt.metadata.name,
name: jobName, name: jobName,
tags: tags.reduce((arr, curr) => [...arr, curr.name], []), tags: tags.reduce((arr, curr) => [...arr, curr.name], []),
jobPriority: frameworkWithLatestAttempt.jobPriority,
jobStatus: { jobStatus: {
username: userName, username: userName,
state: convertState( state: convertState(

Просмотреть файл

@ -337,6 +337,7 @@ const convertToJobAttempt = async (framework) => {
0, 0,
); );
const totalTaskRoleNumber = framework.spec.taskRoles.length; const totalTaskRoleNumber = framework.spec.taskRoles.length;
const jobPriority = framework.jobPriority;
const diagnostics = completionStatus ? completionStatus.diagnostics : null; const diagnostics = completionStatus ? completionStatus.diagnostics : null;
const exitDiagnostics = generateExitDiagnostics(diagnostics); const exitDiagnostics = generateExitDiagnostics(diagnostics);
const appExitTriggerMessage = const appExitTriggerMessage =
@ -417,6 +418,7 @@ const convertToJobAttempt = async (framework) => {
totalGpuNumber, totalGpuNumber,
totalTaskNumber, totalTaskNumber,
totalTaskRoleNumber, totalTaskRoleNumber,
jobPriority,
taskRoles, taskRoles,
}; };
}; };

Просмотреть файл

@ -11,11 +11,13 @@ class Filter {
*/ */
constructor( constructor(
keyword = '', keyword = '',
priorities = new Set(),
users = new Set(), users = new Set(),
virtualClusters = new Set(), virtualClusters = new Set(),
statuses = new Set(), statuses = new Set(),
) { ) {
this.keyword = keyword; this.keyword = keyword;
this.priorities = priorities;
this.users = users; this.users = users;
this.virtualClusters = virtualClusters; this.virtualClusters = virtualClusters;
this.statuses = statuses; this.statuses = statuses;
@ -26,6 +28,7 @@ class Filter {
save() { save() {
const content = JSON.stringify({ const content = JSON.stringify({
users: Array.from(this.users), users: Array.from(this.users),
priorities: Array.from(this.priorities),
virtualClusters: Array.from(this.virtualClusters), virtualClusters: Array.from(this.virtualClusters),
statuses: Array.from(this.statuses), statuses: Array.from(this.statuses),
keyword: this.keyword, keyword: this.keyword,
@ -36,10 +39,19 @@ class Filter {
load() { load() {
try { try {
const content = window.localStorage.getItem(LOCAL_STORAGE_KEY); const content = window.localStorage.getItem(LOCAL_STORAGE_KEY);
const { users, virtualClusters, statuses, keyword } = JSON.parse(content); const {
priorities,
users,
virtualClusters,
statuses,
keyword,
} = JSON.parse(content);
if (Array.isArray(users)) { if (Array.isArray(users)) {
this.users = new Set(users); this.users = new Set(users);
} }
if (Array.isArray(priorities)) {
this.priorities = new Set(priorities);
}
if (Array.isArray(virtualClusters)) { if (Array.isArray(virtualClusters)) {
this.virtualClusters = new Set(virtualClusters); this.virtualClusters = new Set(virtualClusters);
} }
@ -53,7 +65,7 @@ class Filter {
} }
apply() { apply() {
const { keyword, users, virtualClusters, statuses } = this; const { keyword, priorities, users, virtualClusters, statuses } = this;
const query = {}; const query = {};
if (keyword && keyword !== '') { if (keyword && keyword !== '') {
@ -62,6 +74,22 @@ class Filter {
if (users && users.size > 0) { if (users && users.size > 0) {
query.username = Array.from(users).join(','); query.username = Array.from(users).join(',');
} }
if (priorities && priorities.size > 0) {
query.jobPriority = Array.from(priorities)
.map(priority => {
switch (priority) {
case 'Opportunistic':
return 'oppo';
case 'Product':
return 'prod';
case 'Test':
return 'test';
default:
return 'default';
}
})
.join(',');
}
if (virtualClusters && virtualClusters.size > 0) { if (virtualClusters && virtualClusters.size > 0) {
query.vc = Array.from(virtualClusters).join(','); query.vc = Array.from(virtualClusters).join(',');
} }

Просмотреть файл

@ -40,6 +40,7 @@ export default class Ordering {
'status', 'status',
'taskCount', 'taskCount',
'gpuCount', 'gpuCount',
'jobPriority',
].includes(field) ].includes(field)
) { ) {
this.field = field; this.field = field;
@ -78,6 +79,8 @@ export default class Ordering {
query = 'totalTaskNumber'; query = 'totalTaskNumber';
} else if (field === 'gpuCount') { } else if (field === 'gpuCount') {
query = 'totalGpuNumber'; query = 'totalGpuNumber';
} else if (field === 'jobPriority') {
query = 'jobPriority';
} }
return { order: `${query},${descending ? 'DESC' : 'ASC'}` }; return { order: `${query},${descending ? 'DESC' : 'ASC'}` };

Просмотреть файл

@ -187,6 +187,26 @@ export default function Table() {
headerClassName: FontClassNames.medium, headerClassName: FontClassNames.medium,
isResizable: true, isResizable: true,
}); });
const priorityColumn = applySortProps({
key: 'jobPriority',
minWidth: 95,
name: 'Priority',
className: FontClassNames.mediumPlus,
headerClassName: FontClassNames.medium,
isResizable: true,
onRender(job) {
switch (job.jobPriority) {
case 'oppo':
return 'Opportunistic';
case 'test':
return 'Test';
case 'prod':
return 'Product';
default:
return 'Default';
}
},
});
const statusColumn = applySortProps({ const statusColumn = applySortProps({
key: 'status', key: 'status',
minWidth: 100, minWidth: 100,
@ -272,6 +292,7 @@ export default function Table() {
retriesColumn, retriesColumn,
taskCountColumn, taskCountColumn,
gpuCountColumn, gpuCountColumn,
priorityColumn,
statusColumn, statusColumn,
actionsColumn, actionsColumn,
]; ];

Просмотреть файл

@ -41,8 +41,14 @@ function KeywordSearchBox() {
const { filter, setFilter } = useContext(Context); const { filter, setFilter } = useContext(Context);
function onKeywordChange(keyword) { function onKeywordChange(keyword) {
const { users, virtualClusters, statuses } = filter; const { priorities, users, virtualClusters, statuses } = filter;
const newFilter = new Filter(keyword, users, virtualClusters, statuses); const newFilter = new Filter(
keyword,
priorities,
users,
virtualClusters,
statuses,
);
setFilter(newFilter); setFilter(newFilter);
} }
@ -76,6 +82,13 @@ function TopBar() {
Failed: true, Failed: true,
}; };
const priorityItems = {
Product: true,
Test: true,
Opportunistic: true,
Default: true,
};
const { refreshJobs, selectedJobs, stopJob, filter, setFilter } = useContext( const { refreshJobs, selectedJobs, stopJob, filter, setFilter } = useContext(
Context, Context,
); );
@ -127,11 +140,17 @@ function TopBar() {
} }
setVirtualClusters(vcs); setVirtualClusters(vcs);
const allValidVC = Object.keys(data); const allValidVC = Object.keys(data);
const { keyword, users, virtualClusters, statuses } = filter; const {
keyword,
priorities,
users,
virtualClusters,
statuses,
} = filter;
const filterVC = new Set( const filterVC = new Set(
allValidVC.filter(vc => virtualClusters.has(vc)), allValidVC.filter(vc => virtualClusters.has(vc)),
); );
setFilter(new Filter(keyword, users, filterVC, statuses)); setFilter(new Filter(keyword, priorities, users, filterVC, statuses));
} else { } else {
const data = await response.json().catch(() => { const data = await response.json().catch(() => {
throw new Error( throw new Error(
@ -289,6 +308,33 @@ function TopBar() {
> >
<KeywordSearchBox /> <KeywordSearchBox />
<Stack horizontal> <Stack horizontal>
<FilterButton
styles={{ root: { backgroundColor: 'transparent' } }}
text='Priority'
iconProps={{ iconName: 'Sort' }}
items={Object.keys(priorityItems)}
selectedItems={Array.from(filter.priorities)}
onSelect={priorities => {
const {
keyword,
userFilter,
virtualClusters,
statuses,
} = filter;
const priorityFilter = new Set(priorities);
setFilter(
new Filter(
keyword,
priorityFilter,
userFilter,
virtualClusters,
statuses,
),
);
}}
searchBox
clearButton
/>
<FilterButton <FilterButton
styles={{ root: { backgroundColor: 'transparent' } }} styles={{ root: { backgroundColor: 'transparent' } }}
text='User' text='User'
@ -296,14 +342,25 @@ function TopBar() {
items={userItems} items={userItems}
selectedItems={selectedItems} selectedItems={selectedItems}
onSelect={users => { onSelect={users => {
const { keyword, virtualClusters, statuses } = filter; const {
keyword,
priorities,
virtualClusters,
statuses,
} = filter;
const userFilter = new Set(users); const userFilter = new Set(users);
if (userFilter.has(CURRENT_USER_KEY)) { if (userFilter.has(CURRENT_USER_KEY)) {
userFilter.delete(CURRENT_USER_KEY); userFilter.delete(CURRENT_USER_KEY);
userFilter.add(currentUser); userFilter.add(currentUser);
} }
setFilter( setFilter(
new Filter(keyword, userFilter, virtualClusters, statuses), new Filter(
keyword,
priorities,
userFilter,
virtualClusters,
statuses,
),
); );
}} }}
searchBox searchBox
@ -316,10 +373,11 @@ function TopBar() {
items={Object.keys(virtualClusters)} items={Object.keys(virtualClusters)}
selectedItems={Array.from(filter.virtualClusters)} selectedItems={Array.from(filter.virtualClusters)}
onSelect={virtualClusters => { onSelect={virtualClusters => {
const { keyword, users, statuses } = filter; const { keyword, priorities, users, statuses } = filter;
setFilter( setFilter(
new Filter( new Filter(
keyword, keyword,
priorities,
users, users,
new Set(virtualClusters), new Set(virtualClusters),
statuses, statuses,
@ -335,10 +393,11 @@ function TopBar() {
items={Object.keys(statuses)} items={Object.keys(statuses)}
selectedItems={Array.from(filter.statuses)} selectedItems={Array.from(filter.statuses)}
onSelect={statuses => { onSelect={statuses => {
const { keyword, users, virtualClusters } = filter; const { keyword, priorities, users, virtualClusters } = filter;
setFilter( setFilter(
new Filter( new Filter(
keyword, keyword,
priorities,
users, users,
virtualClusters, virtualClusters,
new Set(statuses), new Set(statuses),

Просмотреть файл

@ -44,7 +44,11 @@ export default function JobList() {
const initialFilter = useMemo(() => { const initialFilter = useMemo(() => {
const query = querystring.parse(location.search.replace(/^\?/, '')); const query = querystring.parse(location.search.replace(/^\?/, ''));
if (['vcName', 'status', 'user', 'keyword'].some(x => !isEmpty(query[x]))) { if (
['vcName', 'status', 'user', 'jobPriority', 'keyword'].some(
x => !isEmpty(query[x]),
)
) {
const queryFilter = new Filter(); const queryFilter = new Filter();
if (query.vcName) { if (query.vcName) {
queryFilter.virtualClusters = new Set([query.vcName]); queryFilter.virtualClusters = new Set([query.vcName]);
@ -55,6 +59,9 @@ export default function JobList() {
if (query.user) { if (query.user) {
queryFilter.users = new Set([query.user]); queryFilter.users = new Set([query.user]);
} }
if (query.jobPriority) {
queryFilter.priorities = new Set([query.jobPriority]);
}
if (query.keyword) { if (query.keyword) {
queryFilter.keyword = query.user; queryFilter.keyword = query.user;
} }