15672: Rate limiting for data explorer refresh
authorPeter Amstutz <peter.amstutz@curii.com>
Wed, 8 Jan 2020 20:11:57 +0000 (15:11 -0500)
committerPeter Amstutz <peter.amstutz@curii.com>
Wed, 8 Jan 2020 20:11:57 +0000 (15:11 -0500)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

src/store/data-explorer/data-explorer-action.ts
src/store/data-explorer/data-explorer-middleware-service.ts
src/store/data-explorer/data-explorer-middleware.test.ts
src/store/data-explorer/data-explorer-middleware.ts
src/store/data-explorer/data-explorer-reducer.ts
src/store/processes/processes-actions.ts
src/websocket/websocket.ts

index 546ec8f3679512e24f00c1563cf4ad67d6f9a5fc..80b743a221e3cf9404b9b68296482b7667874cc7 100644 (file)
@@ -6,10 +6,17 @@ import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { DataColumns, DataTableFetchMode } from "~/components/data-table/data-table";
 import { DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree';
 
+export enum DataTableRequestState {
+    IDLE,
+    PENDING,
+    NEED_REFRESH
+}
+
 export const dataExplorerActions = unionize({
     CLEAR: ofType<{ id: string }>(),
     RESET_PAGINATION: ofType<{ id: string }>(),
     REQUEST_ITEMS: ofType<{ id: string, criteriaChanged?: boolean }>(),
+    REQUEST_STATE: ofType<{ id: string, criteriaChanged?: boolean }>(),
     SET_FETCH_MODE: ofType<({ id: string, fetchMode: DataTableFetchMode })>(),
     SET_COLUMNS: ofType<{ id: string, columns: DataColumns<any> }>(),
     SET_FILTERS: ofType<{ id: string, columnName: string, filters: DataTableFilters }>(),
@@ -20,6 +27,7 @@ export const dataExplorerActions = unionize({
     TOGGLE_COLUMN: ofType<{ id: string, columnName: string }>(),
     TOGGLE_SORT: ofType<{ id: string, columnName: string }>(),
     SET_EXPLORER_SEARCH_VALUE: ofType<{ id: string, searchValue: string }>(),
+    SET_REQUEST_STATE: ofType<{ id: string, requestState: DataTableRequestState }>(),
 });
 
 export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
@@ -51,4 +59,6 @@ export const bindDataExplorerActions = (id: string) => ({
         dataExplorerActions.TOGGLE_SORT({ ...payload, id }),
     SET_EXPLORER_SEARCH_VALUE: (payload: { searchValue: string }) =>
         dataExplorerActions.SET_EXPLORER_SEARCH_VALUE({ ...payload, id }),
+    SET_REQUEST_STATE: (payload: { requestState: DataTableRequestState }) =>
+        dataExplorerActions.SET_REQUEST_STATE({ ...payload, id })
 });
index 57fd0b59e34f759f7a2b0573613a78f9b7847a20..219e76030a14e12470bafa55442094b9c6239f74 100644 (file)
@@ -25,7 +25,7 @@ export abstract class DataExplorerMiddlewareService {
         return getDataExplorerColumnFilters(columns, columnName);
     }
 
-    abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean): void;
+    abstract requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean): Promise<void>;
 }
 
 export const getDataExplorerColumnFilters = <T>(columns: DataColumns<T>, columnName: string): DataTableFilters => {
index 5d729ce2d600290cb2f9d05acb268db861ad9eba..4a858c272ef82f158f65734dba1f862434ff955c 100644 (file)
@@ -202,7 +202,7 @@ class ServiceMock extends DataExplorerMiddlewareService {
     constructor(private config: {
         id: string,
         columns: DataColumns<any>,
-        requestItems: (api: MiddlewareAPI) => void
+        requestItems: (api: MiddlewareAPI) => Promise<void>
     }) {
         super(config.id);
     }
@@ -211,7 +211,8 @@ class ServiceMock extends DataExplorerMiddlewareService {
         return this.config.columns;
     }
 
-    requestItems(api: MiddlewareAPI) {
+    requestItems(api: MiddlewareAPI): Promise<void> {
         this.config.requestItems(api);
+        return Promise.resolve();
     }
 }
index e377f3410fef8ba215747fcd5dbb41f16931e10f..cc9c1a72f529312060f0a517fa347881d5a6600a 100644 (file)
@@ -3,8 +3,12 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+import { Dispatch } from 'redux';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from '~/services/services';
 import { Middleware } from "redux";
-import { dataExplorerActions, bindDataExplorerActions } from "./data-explorer-action";
+import { dataExplorerActions, bindDataExplorerActions, DataTableRequestState } from "./data-explorer-action";
+import { getDataExplorer } from "./data-explorer-reducer";
 import { DataExplorerMiddlewareService } from "./data-explorer-middleware-service";
 
 export const dataExplorerMiddleware = (service: DataExplorerMiddlewareService): Middleware => api => next => {
@@ -37,7 +41,37 @@ export const dataExplorerMiddleware = (service: DataExplorerMiddlewareService):
                 api.dispatch(actions.REQUEST_ITEMS(true));
             }),
             REQUEST_ITEMS: handleAction(({ criteriaChanged }) => {
-                service.requestItems(api, criteriaChanged);
+                api.dispatch<any>(async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+                    while (true) {
+                        let de = getDataExplorer(getState().dataExplorer, service.getId());
+                        switch (de.requestState) {
+                            case DataTableRequestState.IDLE:
+                                // Start a new request.
+                                try {
+                                    dispatch(actions.SET_REQUEST_STATE({ requestState: DataTableRequestState.PENDING }));
+                                    await service.requestItems(api, criteriaChanged);
+                                } catch {
+                                    dispatch(actions.SET_REQUEST_STATE({ requestState: DataTableRequestState.NEED_REFRESH }));
+                                }
+                                // Now check if the state is still PENDING, if it moved to NEED_REFRESH
+                                // then we need to reissue requestItems
+                                de = getDataExplorer(getState().dataExplorer, service.getId());
+                                const complete = (de.requestState === DataTableRequestState.PENDING);
+                                dispatch(actions.SET_REQUEST_STATE({ requestState: DataTableRequestState.IDLE }));
+                                if (complete) {
+                                    return;
+                                }
+                                break;
+                            case DataTableRequestState.PENDING:
+                                // State is PENDING, move it to NEED_REFRESH so that when the current request finishes it starts a new one.
+                                dispatch(actions.SET_REQUEST_STATE({ requestState: DataTableRequestState.NEED_REFRESH }));
+                                return;
+                            case DataTableRequestState.NEED_REFRESH:
+                                // Nothing to do right now.
+                                return;
+                        }
+                    }
+                });
             }),
             default: () => next(action)
         });
index fc7438a577a0a7e936128fdfcf39b523ef59b48d..7705a891d8f23e34bf40b96d355ccf0cee4d3bce 100644 (file)
@@ -8,7 +8,7 @@ import {
     SortDirection,
     toggleSortDirection
 } from "~/components/data-table/data-column";
-import { DataExplorerAction, dataExplorerActions } from "./data-explorer-action";
+import { DataExplorerAction, dataExplorerActions, DataTableRequestState } from "./data-explorer-action";
 import { DataColumns, DataTableFetchMode } from "~/components/data-table/data-table";
 import { DataTableFilters } from "~/components/data-table-filters/data-table-filters-tree";
 
@@ -22,6 +22,7 @@ export interface DataExplorer {
     rowsPerPageOptions: number[];
     searchValue: string;
     working?: boolean;
+    requestState: DataTableRequestState;
 }
 
 export const initialDataExplorer: DataExplorer = {
@@ -32,7 +33,8 @@ export const initialDataExplorer: DataExplorer = {
     page: 0,
     rowsPerPage: 50,
     rowsPerPageOptions: [50, 100, 200, 500],
-    searchValue: ""
+    searchValue: "",
+    requestState: DataTableRequestState.IDLE
 };
 
 export type DataExplorerState = Record<string, DataExplorer>;
@@ -75,6 +77,9 @@ export const dataExplorerReducer = (state: DataExplorerState = {}, action: DataE
         SET_EXPLORER_SEARCH_VALUE: ({ id, searchValue }) =>
             update(state, id, explorer => ({ ...explorer, searchValue })),
 
+        SET_REQUEST_STATE: ({ id, requestState }) =>
+            update(state, id, explorer => ({ ...explorer, requestState })),
+
         TOGGLE_SORT: ({ id, columnName }) =>
             update(state, id, mapColumns(toggleSort(columnName))),
 
index d3d715ad91782bce0c55b76ebc7af028edae4360..bb60378c5bb9308a2922f24667300e0a69f4348f 100644 (file)
@@ -123,5 +123,3 @@ export const removeProcessPermanently = (uuid: string) =>
         dispatch(projectPanelActions.REQUEST_ITEMS());
         dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
     };
-
-
index d10414616a359982c1e9d77d010e8327062fe620..1a5964b516eddc4b59ba1e7b6c72029b9ffb7c51 100644 (file)
@@ -8,11 +8,12 @@ import { Config } from '~/common/config';
 import { WebSocketService } from './websocket-service';
 import { ResourceEventMessage } from './resource-event-message';
 import { ResourceKind } from '~/models/resource';
-import { loadProcess } from '~/store/processes/processes-actions';
-import { loadContainers } from '~/store/processes/processes-actions';
+// import { loadProcess } from '~/store/processes/processes-actions';
+// import { loadContainers } from '~/store/processes/processes-actions';
 import { LogEventType } from '~/models/log';
 import { addProcessLogsPanelItem } from '../store/process-logs-panel/process-logs-panel-actions';
-import { FilterBuilder } from "~/services/api/filter-builder";
+// import { FilterBuilder } from "~/services/api/filter-builder";
+import { subprocessPanelActions } from "~/store/subprocess-panel/subprocess-panel-actions";
 
 export const initWebSocket = (config: Config, authService: AuthService, store: RootStore) => {
     if (config.websocketUrl) {
@@ -28,15 +29,16 @@ const messageListener = (store: RootStore) => (message: ResourceEventMessage) =>
     if (message.eventType === LogEventType.CREATE || message.eventType === LogEventType.UPDATE) {
         switch (message.objectKind) {
             case ResourceKind.CONTAINER_REQUEST:
-                return store.dispatch(loadProcess(message.objectUuid));
+            // return store.dispatch(loadProcess(message.objectUuid));
             case ResourceKind.CONTAINER:
-                return store.dispatch(loadContainers(
-                    new FilterBuilder().addIn('uuid', [message.objectUuid]).getFilters()
-                ));
+                // return store.dispatch(loadContainers(
+                //     new FilterBuilder().addIn('uuid', [message.objectUuid]).getFilters()
+                // ));
+                store.dispatch(subprocessPanelActions.REQUEST_ITEMS());
             default:
                 return;
         }
     } else {
-        return store.dispatch(addProcessLogsPanelItem(message as ResourceEventMessage<{text: string}>));
+        return store.dispatch(addProcessLogsPanelItem(message as ResourceEventMessage<{ text: string }>));
     }
 };