19275: Fix for race conditions in the full text search bar 19275-Race-condition-in-search-bar-auto-suggest
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 28 Oct 2022 13:25:18 +0000 (15:25 +0200)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Fri, 28 Oct 2022 13:25:18 +0000 (15:25 +0200)
Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

src/services/common-service/common-service.ts
src/services/groups-service/groups-service.ts
src/store/search-bar/search-bar-actions.ts
src/views-components/search-bar/search-bar.tsx

index bdae87ab4089a48e769c34fed83f6c0998418ba9..b8e7dc679c21222badea95db2b1be7a9bb4e2138 100644 (file)
@@ -87,11 +87,13 @@ export class CommonService<T> {
                 return mapKeys ? CommonService.mapResponseKeys(response) : response.data;
             })
             .catch(({ response }) => {
-                actions.progressFn(reqId, false);
-                const errors = CommonService.mapResponseKeys(response) as Errors;
-                errors.status = response.status;
-                actions.errorFn(reqId, errors, showErrors);
-                throw errors;
+                if (response) {
+                    actions.progressFn(reqId, false);
+                    const errors = CommonService.mapResponseKeys(response) as Errors;
+                    errors.status = response.status;
+                    actions.errorFn(reqId, errors, showErrors);
+                    throw errors;
+                }
             });
     }
 
index dc6a798cf1c48c0ae4fa168c707a5266cc13495a..b69483cb3b2a45e6f74d6b2e0befcfc6753537e4 100644 (file)
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { CancelToken } from 'axios';
 import { snakeCase, camelCase } from "lodash";
 import { CommonResourceService } from 'services/common-service/common-resource-service';
 import { ListResults, ListArguments } from 'services/common-service/common-service';
@@ -41,7 +42,7 @@ export class GroupsService<T extends GroupResource = GroupResource> extends Tras
         super(serverApi, "groups", actions);
     }
 
-    async contents(uuid: string, args: ContentsArguments = {}, session?: Session): Promise<ListResults<GroupContentsResource>> {
+    async contents(uuid: string, args: ContentsArguments = {}, session?: Session, cancelToken?: CancelToken): Promise<ListResults<GroupContentsResource>> {
         const { filters, order, ...other } = args;
         const params = {
             ...other,
@@ -56,6 +57,10 @@ export class GroupsService<T extends GroupResource = GroupResource> extends Tras
             cfg.headers = { 'Authorization': 'Bearer ' + session.token };
         }
 
+        if (cancelToken) {
+            cfg.cancelToken = cancelToken;
+        }
+
         const response = await CommonResourceService.defaultResponse(
             this.serverApi.get(this.resourceType + pathUrl, cfg), this.actions, false
         );
index 7d76ec69a4af957672f27a72aed08b68b8d1c48b..8b03ddd765e3cfe7b9002f97f0e4fd8b371a6be7 100644 (file)
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import axios from "axios";
 import { ofType, unionize, UnionOf } from "common/unionize";
 import { GroupContentsResource, GroupContentsResourcePrefix } from 'services/groups-service/groups-service';
 import { Dispatch } from 'redux';
@@ -62,13 +63,13 @@ export const loadRecentQueries = () =>
         return recentQueries;
     };
 
-export const searchData = (searchValue: string) =>
+export const searchData = (searchValue: string, useCancel = false) =>
     async (dispatch: Dispatch, getState: () => RootState) => {
         const currentView = getState().searchBar.currentView;
         dispatch(searchResultsPanelActions.CLEAR());
         dispatch(searchBarActions.SET_SEARCH_VALUE(searchValue));
         if (searchValue.length > 0) {
-            dispatch<any>(searchGroups(searchValue, 5));
+            dispatch<any>(searchGroups(searchValue, 5, useCancel));
             if (currentView === SearchView.BASIC) {
                 dispatch(searchBarActions.CLOSE_SEARCH_VIEW());
                 dispatch(navigateToSearchResults(searchValue));
@@ -203,26 +204,41 @@ export const submitData = (event: React.FormEvent<HTMLFormElement>) =>
         }
     };
 
-
-const searchGroups = (searchValue: string, limit: number) =>
+let cancelTokens: any[] = [];
+const searchGroups = (searchValue: string, limit: number, useCancel = false) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const currentView = getState().searchBar.currentView;
 
-        if (searchValue || currentView === SearchView.ADVANCED) {
-            const { cluster: clusterId } = getAdvancedDataFromQuery(searchValue);
-            const sessions = getSearchSessions(clusterId, getState().auth.sessions);
-            const lists: ListResults<GroupContentsResource>[] = await Promise.all(sessions.map(session => {
-                const filters = queryToFilters(searchValue, session.apiRevision);
-                return services.groupsService.contents('', {
-                    filters,
-                    limit,
-                    recursive: true
-                }, session);
-            }));
-
-            const items = lists.reduce((items, list) => items.concat(list.items), [] as GroupContentsResource[]);
-            dispatch(searchBarActions.SET_SEARCH_RESULTS(items));
+        if (cancelTokens.length > 0 && useCancel) {
+            cancelTokens.forEach(cancelToken => (cancelToken as any).cancel('New search request triggered.'));
+            cancelTokens = [];
         }
+
+        setTimeout(async () => {
+            if (searchValue || currentView === SearchView.ADVANCED) {
+                const { cluster: clusterId } = getAdvancedDataFromQuery(searchValue);
+                const sessions = getSearchSessions(clusterId, getState().auth.sessions);
+                const lists: ListResults<GroupContentsResource>[] = await Promise.all(sessions.map((session, index) => {
+                    cancelTokens.push(axios.CancelToken.source());
+                    const filters = queryToFilters(searchValue, session.apiRevision);
+                    return services.groupsService.contents('', {
+                        filters,
+                        limit,
+                        recursive: true
+                    }, session, cancelTokens[index].token);
+                }));
+    
+                cancelTokens = [];
+    
+                const items = lists.reduce((items, list) => items.concat(list.items), [] as GroupContentsResource[]);
+    
+                if (lists.filter(list => !!(list as any).items).length !== lists.length) {
+                    dispatch(searchBarActions.SET_SEARCH_RESULTS([]));
+                } else {
+                    dispatch(searchBarActions.SET_SEARCH_RESULTS(items));
+                }
+            }
+        }, 10);
     };
 
 const buildQueryFromKeyMap = (data: any, keyMap: string[][]) => {
index 327644ed81fe6d3ebcafca5bbee5665cac9b1643..6a4d2a620e1fbf8ccd2cb3d4a209130cf75ce07e 100644 (file)
@@ -38,7 +38,7 @@ const mapStateToProps = ({ searchBar, form }: RootState): SearchBarDataProps =>
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): SearchBarActionProps => ({
-    onSearch: (valueSearch: string) => dispatch<any>(searchData(valueSearch)),
+    onSearch: (valueSearch: string) => dispatch<any>(searchData(valueSearch, true)),
     onChange: (event: React.ChangeEvent<HTMLInputElement>) => dispatch<any>(changeData(event.target.value)),
     onSetView: (currentView: string) => dispatch(goToView(currentView)),
     onSubmit: (event: React.FormEvent<HTMLFormElement>) => dispatch<any>(submitData(event)),