diff --git a/changelog.d/20241022_121638_klakhov_fix_request_status_crush.md b/changelog.d/20241022_121638_klakhov_fix_request_status_crush.md new file mode 100644 index 000000000000..082b59a70d4f --- /dev/null +++ b/changelog.d/20241022_121638_klakhov_fix_request_status_crush.md @@ -0,0 +1,4 @@ +### Fixed + +- Requests page crush with `Cannot read property 'target' of undefined` error + () diff --git a/cvat-core/package.json b/cvat-core/package.json index 782d74c15b65..a769b74bf78c 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "15.2.0", + "version": "15.2.1", "type": "module", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", diff --git a/cvat-core/src/core-types.ts b/cvat-core/src/core-types.ts index e44a354cb5bd..c05b7b6ba4a5 100644 --- a/cvat-core/src/core-types.ts +++ b/cvat-core/src/core-types.ts @@ -2,7 +2,9 @@ // // SPDX-License-Identifier: MIT -import { ModelKind, ModelReturnType, ShapeType } from './enums'; +import { + ModelKind, ModelReturnType, RQStatus, ShapeType, +} from './enums'; export interface ModelAttribute { name: string; @@ -54,4 +56,10 @@ export interface SerializedModel { updated_date?: string; } +export interface UpdateStatusData { + status: RQStatus; + progress: number; + message: string; +} + export type PaginatedResource = T[] & { count: number }; diff --git a/cvat-core/src/request.ts b/cvat-core/src/request.ts index 66ae49b4c96b..1935f78b2f0c 100644 --- a/cvat-core/src/request.ts +++ b/cvat-core/src/request.ts @@ -6,10 +6,10 @@ import { RQStatus } from './enums'; import User from './user'; import { SerializedRequest } from './server-response-types'; -type Operation = { +export type RequestOperation = { target: string; type: string; - format: string; + format: string | null; jobID: number | null; taskID: number | null; projectID: number | null; @@ -44,9 +44,7 @@ export class Request { this.#finishedDate = initialData.finished_date; this.#expiryDate = initialData.expiry_date; - if (initialData.owner) { - this.#owner = new User(initialData.owner); - } + this.#owner = new User(initialData.owner); } get id(): string { @@ -57,7 +55,7 @@ export class Request { return this.#status.toLowerCase() as RQStatus; } - get progress(): number { + get progress(): number | undefined { return this.#progress; } @@ -65,7 +63,7 @@ export class Request { return this.#message; } - get operation(): Operation { + get operation(): RequestOperation { return { target: this.#operation.target, type: this.#operation.type, @@ -77,11 +75,11 @@ export class Request { }; } - get url(): string { + get url(): string | undefined { return this.#resultUrl; } - get resultID(): number { + get resultID(): number | undefined { return this.#resultID; } @@ -89,19 +87,49 @@ export class Request { return this.#createdDate; } - get startedDate(): string { + get startedDate(): string | undefined { return this.#startedDate; } - get finishedDate(): string { + get finishedDate(): string | undefined { return this.#finishedDate; } - get expiryDate(): string { + get expiryDate(): string | undefined { return this.#expiryDate; } get owner(): User { return this.#owner; } + + public toJSON(): SerializedRequest { + const result: SerializedRequest = { + id: this.#id, + status: this.#status, + operation: { + target: this.#operation.target, + type: this.#operation.type, + format: this.#operation.format, + job_id: this.#operation.job_id, + task_id: this.#operation.task_id, + project_id: this.#operation.project_id, + function_id: this.#operation.function_id, + }, + progress: this.#progress, + message: this.#message, + result_url: this.#resultUrl, + result_id: this.#resultID, + created_date: this.#createdDate, + started_date: this.#startedDate, + finished_date: this.#finishedDate, + expiry_date: this.#expiryDate, + owner: { + id: this.#owner.id, + username: this.#owner.username, + }, + }; + + return result; + } } diff --git a/cvat-core/src/requests-manager.ts b/cvat-core/src/requests-manager.ts index c348923e68bc..711073988955 100644 --- a/cvat-core/src/requests-manager.ts +++ b/cvat-core/src/requests-manager.ts @@ -122,14 +122,15 @@ class RequestsManager { } } catch (error) { if (requestID in this.listening) { - const { onUpdate } = this.listening[requestID]; - - onUpdate - .forEach((update) => update(new Request({ - id: requestID, - status: RQStatus.FAILED, - message: `Could not get a status of the request ${requestID}. ${error.toString()}`, - }))); + const { onUpdate, request } = this.listening[requestID]; + if (request) { + onUpdate + .forEach((update) => update(new Request({ + ...request.toJSON(), + status: RQStatus.FAILED, + message: `Could not get a status of the request ${requestID}. ${error.toString()}`, + }))); + } reject(error); } } diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index eb9c15ce64b9..37f2337c0e52 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -21,7 +21,7 @@ import { SerializedQualityReportData, APIQualityReportsFilter, SerializedAnalyticsReport, APIAnalyticsReportFilter, SerializedRequest, SerializedJobValidationLayout, SerializedTaskValidationLayout, } from './server-response-types'; -import { PaginatedResource } from './core-types'; +import { PaginatedResource, UpdateStatusData } from './core-types'; import { Request } from './request'; import { Storage } from './storage'; import { SerializedEvent } from './event'; @@ -1069,7 +1069,7 @@ type LongProcessListener = Record, taskDataSpec: any, - onUpdate: (request: Request) => void, + onUpdate: (request: Request | UpdateStatusData) => void, ): Promise<{ taskID: number, rqID: string }> { const { backendAPI, origin } = config; // keep current default params to 'freeze" them during this request @@ -1104,11 +1104,11 @@ async function createTask( let response = null; - onUpdate(new Request({ + onUpdate({ status: RQStatus.UNKNOWN, progress: 0, message: 'CVAT is creating your task', - })); + }); try { response = await Axios.post(`${backendAPI}/tasks`, taskSpec, { @@ -1118,11 +1118,11 @@ async function createTask( throw generateError(errorData); } - onUpdate(new Request({ + onUpdate({ status: RQStatus.UNKNOWN, progress: 0, message: 'CVAT is uploading task data to the server', - })); + }); async function bulkUpload(taskId, files) { const fileBulks = files.reduce((fileGroups, file) => { @@ -1142,11 +1142,11 @@ async function createTask( taskData.append(`client_files[${idx}]`, element); } const percentage = totalSentSize / totalSize; - onUpdate(new Request({ + onUpdate({ status: RQStatus.UNKNOWN, progress: percentage, message: 'CVAT is uploading task data to the server', - })); + }); await Axios.post(`${backendAPI}/tasks/${taskId}/data`, taskData, { ...params, headers: { 'Upload-Multiple': true }, @@ -1170,11 +1170,11 @@ async function createTask( const uploadConfig = { endpoint: `${origin}${backendAPI}/tasks/${response.data.id}/data/`, onUpdate: (percentage) => { - onUpdate(new Request({ + onUpdate({ status: RQStatus.UNKNOWN, progress: percentage, message: 'CVAT is uploading task data to the server', - })); + }); }, chunkSize, totalSize, @@ -2250,16 +2250,32 @@ async function getRequestsList(): Promise> } } +// Temporary solution for server availability problems +const retryTimeouts = [5000, 10000, 15000]; async function getRequestStatus(rqID: string): Promise { const { backendAPI } = config; + let retryCount = 0; + let lastError = null; - try { - const response = await Axios.get(`${backendAPI}/requests/${rqID}`); + while (retryCount < 3) { + try { + const response = await Axios.get(`${backendAPI}/requests/${rqID}`); - return response.data; - } catch (errorData) { - throw generateError(errorData); + return response.data; + } catch (errorData) { + lastError = generateError(errorData); + const { response } = errorData; + if (response && [502, 503, 504].includes(response.status)) { + const timeout = retryTimeouts[retryCount]; + await new Promise((resolve) => { setTimeout(resolve, timeout); }); + retryCount++; + } else { + throw generateError(errorData); + } + } } + + throw lastError; } async function cancelRequest(requestID): Promise { diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index af6cd760ed40..4bf7a482bccb 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -504,25 +504,26 @@ export interface SerializedAPISchema { } export interface SerializedRequest { - id?: string; + id: string; + message: string; status: string; - operation?: { + operation: { target: string; type: string; - format: string; + format: string | null; job_id: number | null; task_id: number | null; project_id: number | null; + function_id: string | null; }; progress?: number; - message: string; result_url?: string; result_id?: number; - created_date?: string; + created_date: string; started_date?: string; finished_date?: string; expiry_date?: string; - owner?: any; + owner: any; } export interface SerializedJobValidationLayout { diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 369d0c9d5393..1c2194250155 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -756,12 +756,12 @@ export function implementTask(Task: typeof TaskClass): typeof TaskClass { const { taskID, rqID } = await serverProxy.tasks.create( taskSpec, taskDataSpec, - options?.requestStatusCallback || (() => {}), + options?.updateStatusCallback || (() => {}), ); await requestsManager.listen(rqID, { callback: (request: Request) => { - options?.requestStatusCallback(request); + options?.updateStatusCallback(request); if (request.status === RQStatus.FAILED) { serverProxy.tasks.delete(taskID, config.organization.organizationSlug || null); } diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 8ecef7e0e632..1164ae0c07de 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -29,6 +29,7 @@ import logger from './logger'; import Issue from './issue'; import ObjectState from './object-state'; import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; +import { UpdateStatusData } from './core-types'; function buildDuplicatedAPI(prototype) { Object.defineProperties(prototype, { @@ -1141,7 +1142,7 @@ export class Task extends Session { async save( fields: Record = {}, - options?: { requestStatusCallback?: (request: Request) => void }, + options?: { updateStatusCallback?: (updateData: Request | UpdateStatusData) => void }, ): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.save, fields, options); return result; diff --git a/cvat-ui/src/actions/requests-actions.ts b/cvat-ui/src/actions/requests-actions.ts index 6a62e7cecf7c..1f3972746e7e 100644 --- a/cvat-ui/src/actions/requests-actions.ts +++ b/cvat-ui/src/actions/requests-actions.ts @@ -3,12 +3,21 @@ // SPDX-License-Identifier: MIT import { ActionUnion, createAction } from 'utils/redux'; -import { RequestsQuery, RequestsState } from 'reducers'; +import { CombinedState, RequestsQuery, RequestsState } from 'reducers'; import { Request, ProjectOrTaskOrJob, getCore, RQStatus, } from 'cvat-core-wrapper'; +import { Store } from 'redux'; +import { getCVATStore } from 'cvat-store'; const core = getCore(); +let store: null | Store = null; +function getStore(): Store { + if (store === null) { + store = getCVATStore(); + } + return store; +} export enum RequestsActionsTypes { GET_REQUESTS = 'GET_REQUESTS', @@ -79,7 +88,7 @@ export function updateRequestProgress(request: Request, dispatch: (action: Reque ); } -export function shouldListenForProgress(rqID: string | undefined, state: RequestsState): boolean { +export function shouldListenForProgress(rqID: string | void, state: RequestsState): boolean { return ( typeof rqID === 'string' && (!state.requests[rqID] || [RQStatus.FINISHED, RQStatus.FAILED].includes(state.requests[rqID]?.status)) @@ -89,13 +98,13 @@ export function shouldListenForProgress(rqID: string | undefined, state: Request export function listen( requestID: string, dispatch: (action: RequestsActions) => void, - initialRequest?: Request, ) : Promise { + const { requests } = getStore().getState().requests; return core.requests .listen(requestID, { callback: (updatedRequest) => { updateRequestProgress(updatedRequest, dispatch); }, - initialRequest, + initialRequest: requests[requestID], }); } diff --git a/cvat-ui/src/actions/requests-async-actions.ts b/cvat-ui/src/actions/requests-async-actions.ts index 04a5ffd0a5c5..06a137eafd28 100644 --- a/cvat-ui/src/actions/requests-async-actions.ts +++ b/cvat-ui/src/actions/requests-async-actions.ts @@ -8,7 +8,9 @@ import { getCore, RQStatus, Request, Project, Task, Job, } from 'cvat-core-wrapper'; import { listenExportBackupAsync, listenExportDatasetAsync } from './export-actions'; -import { RequestInstanceType, listen, requestsActions } from './requests-actions'; +import { + RequestInstanceType, listen, requestsActions, +} from './requests-actions'; import { listenImportBackupAsync, listenImportDatasetAsync } from './import-actions'; const core = getCore(); @@ -28,6 +30,7 @@ export function getRequestsAsync(query: RequestsQuery): ThunkAction { try { const requests = await core.requests.list(); + dispatch(requestsActions.getRequestsSuccess(requests)); requests .filter((request: Request) => [RQStatus.STARTED, RQStatus.QUEUED].includes(request.status)) @@ -80,7 +83,6 @@ export function getRequestsAsync(query: RequestsQuery): ThunkAction { } } }); - dispatch(requestsActions.getRequestsSuccess(requests)); } catch (error) { dispatch(requestsActions.getRequestsFailed(error)); } diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index 60e4da022ef2..d15f033f1e6f 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -6,7 +6,7 @@ import { AnyAction } from 'redux'; import { TasksQuery, StorageLocation } from 'reducers'; import { - getCore, RQStatus, Storage, Task, + getCore, RQStatus, Storage, Task, UpdateStatusData, Request, } from 'cvat-core-wrapper'; import { filterNull } from 'utils/filter-null'; import { ThunkDispatch, ThunkAction } from 'utils/redux'; @@ -274,10 +274,10 @@ ThunkAction { taskInstance.remoteFiles = data.files.remote; try { const savedTask = await taskInstance.save(extras, { - requestStatusCallback(request) { - let { message } = request; + updateStatusCallback(updateData: Request | UpdateStatusData) { + let { message } = updateData; + const { status, progress } = updateData; let helperMessage = ''; - const { status, progress } = request; if (!message) { if ([RQStatus.QUEUED, RQStatus.STARTED].includes(status)) { message = 'CVAT queued the task to import'; @@ -291,7 +291,7 @@ ThunkAction { } } onProgress?.(`${message} ${progress ? `${Math.floor(progress * 100)}%` : ''}. ${helperMessage}`); - if (request.id) updateRequestProgress(request, dispatch); + if (updateData instanceof Request) updateRequestProgress(updateData, dispatch); }, }); diff --git a/cvat-ui/src/components/requests-page/request-card.tsx b/cvat-ui/src/components/requests-page/request-card.tsx index 980af114563a..52c109e3822c 100644 --- a/cvat-ui/src/components/requests-page/request-card.tsx +++ b/cvat-ui/src/components/requests-page/request-card.tsx @@ -100,11 +100,15 @@ function constructTimestamps(request: Request): JSX.Element { ); } case RQStatus.FAILED: { - return ( + return (request.startedDate ? ( {`Started by ${request.owner.username} on ${started}`} - ); + ) : ( + + {`Enqueued by ${request.owner.username} on ${created}`} + + )); } case RQStatus.STARTED: { return ( diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index 275cedcc8ab9..94b70373a1c7 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -17,6 +17,7 @@ import { import { SerializedAttribute, SerializedLabel, SerializedAPISchema, } from 'cvat-core/src/server-response-types'; +import { UpdateStatusData } from 'cvat-core/src/core-types'; import { Job, Task } from 'cvat-core/src/session'; import Project from 'cvat-core/src/project'; import QualityReport, { QualitySummary } from 'cvat-core/src/quality-report'; @@ -41,7 +42,7 @@ import { Dumper } from 'cvat-core/src/annotation-formats'; import { Event } from 'cvat-core/src/event'; import { APIWrapperEnterOptions } from 'cvat-core/src/plugins'; import BaseSingleFrameAction, { ActionParameterType, FrameSelectionType } from 'cvat-core/src/annotations-actions'; -import { Request } from 'cvat-core/src/request'; +import { Request, RequestOperation } from 'cvat-core/src/request'; const cvat: CVATCore = _cvat; @@ -120,4 +121,6 @@ export type { CVATCore, SerializedAPISchema, ProjectOrTaskOrJob, + RequestOperation, + UpdateStatusData, };