未验证 提交 4a5aa283 编写于 作者: B Boris Sekachev 提交者: GitHub

Improved lambda manager (#6734)

* First status check request after 20 seconds may confuse users making
them think that something going wrong
* Fixed a reason of a big amount status check requests (repeated calls
for listen() method) when user go from Tasks -> any other page -> Tasks
-> any other page -> Task -> any other page -> Task, etc not closing the
tab.
* Fixed bug when the progress invisible if the app initialized on task
page
* Fixed 403 code when user tries to cancel automatic annotation
* Added missed schema endpoint
* Added more informative progress (for queued/in progress/fail):

<img width="756" alt="image"
src="https://github.com/opencv/cvat/assets/40690378/f01c45a8-a460-48c0-bec4-88266bb65a2c">
<img width="980" alt="image"
src="https://github.com/opencv/cvat/assets/40690378/f9e2c5ad-c2b7-4c43-8214-34236f38103b">
上级 8cc5dde6
......@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Response code for empty cloud storage preview 204 -> 404 (<https://github.com/opencv/cvat/pull/6727>)
- Organization now opened immediately after it is created (<https://github.com/opencv/cvat/pull/6705>)
- More responsive automatic annotation progress bar (<https://github.com/opencv/cvat/pull/6734>)
- Improved message when invite more users to an organization (<https://github.com/opencv/cvat/pull/6731>)
### Deprecated
......@@ -30,6 +31,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Exporting project when its tasks has not data (<https://github.com/opencv/cvat/pull/6658>)
- Removing job assignee (<https://github.com/opencv/cvat/pull/6712>)
- Fixed switching from organization to sandbox while getting a resource (<https://github.com/opencv/cvat/pull/6689>)
- You do not have permissions when user is cancelling automatic annotation (<https://github.com/opencv/cvat/pull/6734>)
- Automatic annotation progress bar is invisible if the app initialized on the task page
(<https://github.com/opencv/cvat/pull/6734>)
- Extra status check requests for automatic annotation (<https://github.com/opencv/cvat/pull/6734>)
- \[SDK\]: `FileExistsError` exception raised on Windows when a dataset is loaded from cache
(<https://github.com/opencv/cvat/pull/6722>)
......
{
"name": "cvat-core",
"version": "11.0.2",
"version": "11.0.3",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "src/api.ts",
"scripts": {
......
......@@ -22,7 +22,11 @@ interface ModelProxy {
}
class LambdaManager {
private listening: any;
private listening: Record<number, {
onUpdate: ((status: RQStatus, progress: number, message?: string) => void)[];
functionID: string;
timeout: number | null;
}>;
private cachedList: any;
constructor() {
......@@ -114,41 +118,65 @@ class LambdaManager {
await LambdaManager.getModelProxy(model).cancel(requestID);
}
async listen(requestID, functionID, onUpdate): Promise<void> {
async listen(
requestID: string,
functionID: string,
callback: (status: RQStatus, progress: number, message?: string) => void,
): Promise<void> {
const model = this.cachedList.find((_model) => _model.id === functionID);
if (!model) {
throw new ArgumentError('Incorrect Function Id provided');
throw new ArgumentError('Incorrect function Id provided');
}
if (requestID in this.listening) {
this.listening[requestID].onUpdate.push(callback);
// already listening, avoid sending extra requests
return;
}
const timeoutCallback = async (): Promise<void> => {
try {
this.listening[requestID].timeout = null;
const response = await LambdaManager.getModelProxy(model).status(requestID);
if (response.status === RQStatus.QUEUED || response.status === RQStatus.STARTED) {
onUpdate(response.status, response.progress || 0);
this.listening[requestID].timeout = setTimeout(timeoutCallback, 20000);
} else {
if (response.status === RQStatus.FINISHED) {
onUpdate(response.status, response.progress || 100);
const timeoutCallback = (): void => {
LambdaManager.getModelProxy(model).status(requestID).then((response) => {
const { status } = response;
if (requestID in this.listening) {
// check it was not cancelled
const { onUpdate } = this.listening[requestID];
if ([RQStatus.QUEUED, RQStatus.STARTED].includes(status)) {
onUpdate.forEach((update) => update(status, response.progress || 0));
this.listening[requestID].timeout = window
.setTimeout(timeoutCallback, status === RQStatus.QUEUED ? 30000 : 10000);
} else {
onUpdate(response.status, response.progress || 0, response.exc_info || '');
delete this.listening[requestID];
if (status === RQStatus.FINISHED) {
onUpdate
.forEach((update) => update(status, response.progress || 100));
} else {
onUpdate
.forEach((update) => update(status, response.progress || 0, response.exc_info || ''));
}
}
delete this.listening[requestID];
}
} catch (error) {
onUpdate(
RQStatus.UNKNOWN,
0,
`Could not get a status of the request ${requestID}. ${error.toString()}`,
);
}
}).catch((error) => {
if (requestID in this.listening) {
// check it was not cancelled
const { onUpdate } = this.listening[requestID];
onUpdate
.forEach((update) => update(
RQStatus.UNKNOWN,
0,
`Could not get a status of the request ${requestID}. ${error.toString()}`,
));
}
}).finally(() => {
if (requestID in this.listening) {
this.listening[requestID].timeout = null;
}
});
};
this.listening[requestID] = {
onUpdate,
onUpdate: [callback],
functionID,
timeout: setTimeout(timeoutCallback, 20000),
timeout: window.setTimeout(timeoutCallback),
};
}
......
......@@ -63,9 +63,10 @@ export const modelsActions = {
activeInference,
})
),
getInferenceStatusFailed: (taskID: number, error: any) => (
getInferenceStatusFailed: (taskID: number, activeInference: ActiveInference, error: any) => (
createAction(ModelsActionTypes.GET_INFERENCE_STATUS_FAILED, {
taskID,
activeInference,
error,
})
),
......@@ -169,6 +170,13 @@ function listen(inferenceMeta: InferenceMeta, dispatch: (action: ModelsActions)
dispatch(
modelsActions.getInferenceStatusFailed(
taskID,
{
status,
progress,
functionID,
error: message,
id: requestID,
},
new Error(`Inference status for the task ${taskID} is ${status}. ${message}`),
),
);
......@@ -189,12 +197,12 @@ function listen(inferenceMeta: InferenceMeta, dispatch: (action: ModelsActions)
.catch((error: Error) => {
dispatch(
modelsActions.getInferenceStatusFailed(taskID, {
status: 'unknown',
status: RQStatus.UNKNOWN,
progress: 0,
error: error.toString(),
id: requestID,
functionID,
}),
}, error),
);
});
}
......
......@@ -6,17 +6,18 @@
import './styles.scss';
import React, { useEffect, useRef, useState } from 'react';
import { useHistory, useParams } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import { Row, Col } from 'antd/lib/grid';
import Spin from 'antd/lib/spin';
import Result from 'antd/lib/result';
import notification from 'antd/lib/notification';
import { getInferenceStatusAsync } from 'actions/models-actions';
import { getCore, Task, Job } from 'cvat-core-wrapper';
import JobListComponent from 'components/task-page/job-list';
import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog';
import CVATLoadingSpinner from 'components/common/loading-spinner';
import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import { useSelector } from 'react-redux';
import { CombinedState } from 'reducers';
import TopBarComponent from './top-bar';
import DetailsComponent from './details';
......@@ -26,7 +27,7 @@ const core = getCore();
function TaskPageComponent(): JSX.Element {
const history = useHistory();
const id = +useParams<{ id: string }>().id;
const dispatch = useDispatch();
const [taskInstance, setTaskInstance] = useState<Task | null>(null);
const [fetchingTask, setFetchingTask] = useState(true);
const [updatingTask, setUpdatingTask] = useState(false);
......@@ -64,6 +65,7 @@ function TaskPageComponent(): JSX.Element {
useEffect(() => {
receieveTask();
dispatch(getInferenceStatusAsync());
mounted.current = true;
return () => {
mounted.current = false;
......
......@@ -4,12 +4,13 @@
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import { CloseOutlined } from '@ant-design/icons';
import { CloseOutlined, LoadingOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text';
import Progress from 'antd/lib/progress';
import Modal from 'antd/lib/modal';
import CVATTooltip from 'components/common/cvat-tooltip';
import { RQStatus } from 'cvat-core-wrapper';
import { ActiveInference } from 'reducers';
interface Props {
......@@ -21,13 +22,49 @@ export default function AutomaticAnnotationProgress(props: Props): JSX.Element |
const { activeInference, cancelAutoAnnotation } = props;
if (!activeInference) return null;
let textType: 'success' | 'danger' = 'success';
if ([RQStatus.FAILED, RQStatus.UNKNOWN].includes(activeInference.status)) {
textType = 'danger';
}
return (
<>
<Row justify='space-between' align='bottom'>
<Col span={22} className='cvat-task-item-progress-wrapper'>
<div>
<Text strong type='secondary'>
Automatic annotation
<Text
type={activeInference.status === RQStatus.QUEUED ? undefined : textType}
strong
>
{((): JSX.Element => {
if (activeInference.status === RQStatus.QUEUED) {
return (
<>
Automatic annotation request queued
<LoadingOutlined />
</>
);
}
if (activeInference.status === RQStatus.STARTED) {
return (
<>
Automatic annotation is in progress
<LoadingOutlined />
</>
);
}
if (activeInference.status === RQStatus.FAILED) {
return (<>Automatic annotation failed</>);
}
if (activeInference.status === RQStatus.UNKNOWN) {
return (<>Unknown status received</>);
}
return <>Automatic annotation accomplisted</>;
})()}
</Text>
</div>
<Progress
......@@ -42,23 +79,25 @@ export default function AutomaticAnnotationProgress(props: Props): JSX.Element |
/>
</Col>
<Col span={1} className='close-auto-annotation-icon'>
<CVATTooltip title='Cancel automatic annotation'>
<CloseOutlined
onClick={() => {
Modal.confirm({
title: 'You are going to cancel automatic annotation?',
content: 'Reached progress will be lost. Continue?',
okButtonProps: {
type: 'primary',
danger: true,
},
onOk() {
cancelAutoAnnotation();
},
});
}}
/>
</CVATTooltip>
{ activeInference.status !== RQStatus.FAILED && (
<CVATTooltip title='Cancel automatic annotation'>
<CloseOutlined
onClick={() => {
Modal.confirm({
title: 'You are going to cancel automatic annotation?',
content: 'Reached progress will be lost. Continue?',
okButtonProps: {
type: 'primary',
danger: true,
},
onOk() {
cancelAutoAnnotation();
},
});
}}
/>
</CVATTooltip>
)}
</Col>
</Row>
</>
......
......@@ -32,7 +32,7 @@ export interface TaskItemProps {
interface State {
importingState: {
state: RQStatus;
state: RQStatus | null;
message: string;
progress: number;
} | null;
......@@ -47,7 +47,7 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
this.#isUnmounted = false;
this.state = {
importingState: taskInstance.size > 0 ? null : {
state: RQStatus.UNKNOWN,
state: null,
message: 'Request current progress',
progress: 0,
},
......@@ -139,7 +139,7 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
if (importingState) {
let textType: 'success' | 'danger' = 'success';
if (importingState.state === RQStatus.FAILED) {
if (!!importingState.state && [RQStatus.FAILED, RQStatus.UNKNOWN].includes(importingState.state)) {
textType = 'danger';
}
......@@ -148,9 +148,12 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
<Row>
<Col span={24} className='cvat-task-item-progress-wrapper'>
<div>
<Text strong type={importingState.state === RQStatus.UNKNOWN ? undefined : textType}>
<Text
strong
type={[RQStatus.QUEUED, null].includes(importingState.state) ? undefined : textType}
>
{`\u2022 ${importingState.message || importingState.state}`}
{ [RQStatus.QUEUED, RQStatus.STARTED]
{ !!importingState.state && [RQStatus.QUEUED, RQStatus.STARTED]
.includes(importingState.state) && <LoadingOutlined /> }
</Text>
</div>
......
......@@ -147,11 +147,13 @@ export default function (state = defaultState, action: ModelsActions | AuthActio
}
case ModelsActionTypes.GET_INFERENCE_STATUS_FAILED: {
const { inferences } = state;
delete inferences[action.payload.taskID];
return {
...state,
inferences: { ...inferences },
inferences: {
...inferences,
[action.payload.taskID]: action.payload.activeInference,
},
};
}
case ModelsActionTypes.CANCEL_INFERENCE_SUCCESS: {
......
......@@ -434,9 +434,9 @@ class LambdaTestCases(_LambdaTestCaseBase):
response = self._post_request(f'{LAMBDA_REQUESTS_PATH}', self.admin, data)
id_request = response.data["id"]
response = self._delete_request(f'{LAMBDA_REQUESTS_PATH}/{id_request}', self.user)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
response = self._get_request(f'{LAMBDA_REQUESTS_PATH}/{id_request}', self.user)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@skip("Fail: add mock")
......
......@@ -924,7 +924,7 @@ class FunctionViewSet(viewsets.ViewSet):
'200': FunctionCallSerializer
}
),
delete=extend_schema(
destroy=extend_schema(
operation_id='lambda_delete_requests',
summary='Method cancels the request',
parameters=[
......@@ -1000,7 +1000,7 @@ class RequestViewSet(viewsets.ViewSet):
return response_serializer.data
@return_response(status.HTTP_204_NO_CONTENT)
def delete(self, request, pk):
def destroy(self, request, pk):
self.check_object_permissions(request, pk)
queue = LambdaQueue()
rq_job = queue.fetch_job(pk)
......
......@@ -2774,6 +2774,27 @@ paths:
schema:
$ref: '#/components/schemas/FunctionCall'
description: ''
delete:
operationId: lambda_delete_requests
summary: Method cancels the request
parameters:
- in: path
name: id
schema:
type: string
description: Request id
required: true
tags:
- lambda
security:
- sessionAuth: []
csrfAuth: []
tokenAuth: []
- signatureAuth: []
- basicAuth: []
responses:
'204':
description: No response body
/api/memberships:
get:
operationId: memberships_list
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册