Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved lambda manager #6734

Merged
merged 15 commits into from
Aug 24, 2023
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,6 +30,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- 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>)

Expand Down
2 changes: 1 addition & 1 deletion cvat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"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": {
Expand Down
80 changes: 54 additions & 26 deletions cvat-core/src/lambda-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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),
};
}

Expand Down
14 changes: 11 additions & 3 deletions cvat-ui/src/actions/models-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
),
Expand Down Expand Up @@ -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}`),
),
);
Expand All @@ -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),
);
});
}
Expand Down
6 changes: 4 additions & 2 deletions cvat-ui/src/components/task-page/task-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand Down Expand Up @@ -64,6 +65,7 @@ function TaskPageComponent(): JSX.Element {

useEffect(() => {
receieveTask();
dispatch(getInferenceStatusAsync());
mounted.current = true;
return () => {
mounted.current = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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>
</>
Expand Down
13 changes: 8 additions & 5 deletions cvat-ui/src/components/tasks-page/task-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface TaskItemProps {

interface State {
importingState: {
state: RQStatus;
state: RQStatus | null;
message: string;
progress: number;
} | null;
Expand All @@ -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,
},
Expand Down Expand Up @@ -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';
}

Expand All @@ -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>
Expand Down
6 changes: 4 additions & 2 deletions cvat-ui/src/reducers/models-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
4 changes: 2 additions & 2 deletions cvat/apps/lambda_manager/tests/test_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,9 @@ def test_api_v2_lambda_requests_delete_finished_request(self):
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")
Expand Down
Loading
Loading