diff --git a/x-pack/legacy/plugins/maps/public/actions/map_actions.js b/x-pack/legacy/plugins/maps/public/actions/map_actions.js index f30457eddca03..fe6fbe9d8debf 100644 --- a/x-pack/legacy/plugins/maps/public/actions/map_actions.js +++ b/x-pack/legacy/plugins/maps/public/actions/map_actions.js @@ -17,6 +17,11 @@ import { getTooltipState } from '../selectors/map_selectors'; import { FLYOUT_STATE } from '../reducers/ui'; +import { + cancelRequest, + registerCancelCallback, + unregisterCancelCallback +} from '../reducers/non_serializable_instances'; import { updateFlyout } from '../actions/ui_actions'; import { SOURCE_DATA_ID_ORIGIN } from '../../common/constants'; @@ -64,7 +69,8 @@ function getLayerLoadingCallbacks(dispatch, layerId) { onLoadError: (dataId, requestToken, errorMessage) => dispatch(onDataLoadError(layerId, dataId, requestToken, errorMessage)), updateSourceData: (newData) => { dispatch(updateSourceDataRequest(layerId, newData)); - } + }, + registerCancelCallback: (requestToken, callback) => dispatch(registerCancelCallback(requestToken, callback)), }; } @@ -84,6 +90,16 @@ async function syncDataForAllLayers(getState, dispatch, dataFilters) { await Promise.all(syncs); } +export function cancelAllInFlightRequests() { + return (dispatch, getState) => { + getLayerList(getState()).forEach(layer => { + layer.getInFlightRequestTokens().forEach(requestToken => { + dispatch(cancelRequest(requestToken)); + }); + }); + }; +} + export function setMapInitError(errorMessage) { return { type: SET_MAP_INIT_ERROR, @@ -408,13 +424,20 @@ export function clearGoto() { } export function startDataLoad(layerId, dataId, requestToken, meta = {}) { - return ({ - meta, - type: LAYER_DATA_LOAD_STARTED, - layerId, - dataId, - requestToken - }); + return (dispatch, getState) => { + const layer = getLayerById(layerId, getState()); + if (layer) { + dispatch(cancelRequest(layer.getPrevRequestToken(dataId))); + } + + dispatch({ + meta, + type: LAYER_DATA_LOAD_STARTED, + layerId, + dataId, + requestToken + }); + }; } export function updateSourceDataRequest(layerId, newData) { @@ -432,6 +455,7 @@ export function updateSourceDataRequest(layerId, newData) { export function endDataLoad(layerId, dataId, requestToken, data, meta) { return async (dispatch) => { + dispatch(unregisterCancelCallback(requestToken)); dispatch(clearTooltipStateForLayer(layerId)); dispatch({ type: LAYER_DATA_LOAD_ENDED, @@ -453,6 +477,7 @@ export function endDataLoad(layerId, dataId, requestToken, data, meta) { export function onDataLoadError(layerId, dataId, requestToken, errorMessage) { return async (dispatch) => { + dispatch(unregisterCancelCallback(requestToken)); dispatch(clearTooltipStateForLayer(layerId)); dispatch({ type: LAYER_DATA_LOAD_ERROR, @@ -570,6 +595,10 @@ export function removeLayer(layerId) { if (!layerGettingRemoved) { return; } + + layerGettingRemoved.getInFlightRequestTokens().forEach(requestToken => { + dispatch(cancelRequest(requestToken)); + }); dispatch(clearTooltipStateForLayer(layerId)); layerGettingRemoved.destroy(); dispatch({ diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js index fc3641d3458de..a2308a638542a 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/index.js @@ -9,7 +9,7 @@ import { GisMap } from './view'; import { FLYOUT_STATE } from '../../reducers/ui'; import { exitFullScreen } from '../../actions/ui_actions'; import { getFlyoutDisplay, getIsFullScreen } from '../../selectors/ui_selectors'; -import { triggerRefreshTimer } from '../../actions/map_actions'; +import { triggerRefreshTimer, cancelAllInFlightRequests } from '../../actions/map_actions'; import { areLayersLoaded, getRefreshConfig, @@ -35,6 +35,7 @@ function mapDispatchToProps(dispatch) { return { triggerRefreshTimer: () => dispatch(triggerRefreshTimer()), exitFullScreen: () => dispatch(exitFullScreen()), + cancelAllInFlightRequests: () => dispatch(cancelAllInFlightRequests()), }; } diff --git a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js index 081f764978d78..9a88492adb246 100644 --- a/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js +++ b/x-pack/legacy/plugins/maps/public/connected_components/gis_map/view.js @@ -49,6 +49,7 @@ export class GisMap extends Component { componentWillUnmount() { this._isMounted = false; this._clearRefreshTimer(); + this.props.cancelAllInFlightRequests(); } // Reporting uses both a `data-render-complete` attribute and a DOM event listener to determine diff --git a/x-pack/legacy/plugins/maps/public/layers/layer.js b/x-pack/legacy/plugins/maps/public/layers/layer.js index 56aaae30aaf34..ab1af73634c60 100644 --- a/x-pack/legacy/plugins/maps/public/layers/layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/layer.js @@ -52,7 +52,7 @@ export class AbstractLayer { } destroy() { - if(this._source) { + if (this._source) { this._source.destroy(); } } @@ -251,6 +251,24 @@ export class AbstractLayer { return this._source.renderSourceSettingsEditor({ onChange }); }; + getPrevRequestToken(dataId) { + const prevDataRequest = this.getDataRequest(dataId); + if (!prevDataRequest) { + return; + } + + return prevDataRequest.getRequestToken(); + } + + getInFlightRequestTokens() { + if (!this._dataRequests) { + return []; + } + + const requestTokens = this._dataRequests.map(dataRequest => dataRequest.getRequestToken()); + return _.compact(requestTokens); + } + getSourceDataRequest() { return this.getDataRequest(SOURCE_DATA_ID_ORIGIN); } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js index b52ebc98b8c5d..8416ef5709e30 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js @@ -183,14 +183,18 @@ export class ESGeoGridSource extends AbstractESSource { }); } - async getGeoJsonWithMeta(layerName, searchFilters) { + async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { const indexPattern = await this._getIndexPattern(); const searchSource = await this._makeSearchSource(searchFilters, 0); const aggConfigs = new AggConfigs(indexPattern, this._makeAggConfigs(searchFilters.geogridPrecision), aggSchemas.all); searchSource.setField('aggs', aggConfigs.toDsl()); - const esResponse = await this._runEsQuery(layerName, searchSource, i18n.translate('xpack.maps.source.esGrid.inspectorDescription', { - defaultMessage: 'Elasticsearch geo grid aggregation request' - })); + const esResponse = await this._runEsQuery( + layerName, + searchSource, + registerCancelCallback, + i18n.translate('xpack.maps.source.esGrid.inspectorDescription', { + defaultMessage: 'Elasticsearch geo grid aggregation request' + })); const tabifiedResp = tabifyAggResponse(aggConfigs, esResponse); const { featureCollection } = convertToGeoJson({ diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js index d5c612fd1093c..6d924dfc46569 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js @@ -183,7 +183,7 @@ export class ESPewPewSource extends AbstractESSource { return Math.min(targetGeotileLevel, MAX_GEOTILE_LEVEL); } - async getGeoJsonWithMeta(layerName, searchFilters) { + async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { const indexPattern = await this._getIndexPattern(); const metricAggConfigs = this.getMetricFields().map(metric => { const metricAggConfig = { @@ -233,9 +233,13 @@ export class ESPewPewSource extends AbstractESSource { } }); - const esResponse = await this._runEsQuery(layerName, searchSource, i18n.translate('xpack.maps.source.pewPew.inspectorDescription', { - defaultMessage: 'Source-destination connections request' - })); + const esResponse = await this._runEsQuery( + layerName, + searchSource, + registerCancelCallback, + i18n.translate('xpack.maps.source.pewPew.inspectorDescription', { + defaultMessage: 'Source-destination connections request' + })); const { featureCollection } = convertToLines(esResponse); diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js index feebb50eeca42..6947333409515 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_search_source/es_search_source.js @@ -135,7 +135,7 @@ export class ESSearchSource extends AbstractESSource { ]; } - async _getTopHits(layerName, searchFilters) { + async _getTopHits(layerName, searchFilters, registerCancelCallback) { const { topHitsSplitField, topHitsTimeField, @@ -185,7 +185,7 @@ export class ESSearchSource extends AbstractESSource { } }); - const resp = await this._runEsQuery(layerName, searchSource, 'Elasticsearch document top hits request'); + const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document top hits request'); let hasTrimmedResults = false; const allHits = []; @@ -209,7 +209,7 @@ export class ESSearchSource extends AbstractESSource { }; } - async _getSearchHits(layerName, searchFilters) { + async _getSearchHits(layerName, searchFilters, registerCancelCallback) { const searchSource = await this._makeSearchSource(searchFilters, ES_SIZE_LIMIT); // Setting "fields" instead of "source: { includes: []}" // because SearchSource automatically adds the following by default @@ -218,7 +218,7 @@ export class ESSearchSource extends AbstractESSource { // By setting "fields", SearchSource removes all of defaults searchSource.setField('fields', searchFilters.fieldNames); - const resp = await this._runEsQuery(layerName, searchSource, 'Elasticsearch document request'); + const resp = await this._runEsQuery(layerName, searchSource, registerCancelCallback, 'Elasticsearch document request'); return { hits: resp.hits.hits, @@ -233,10 +233,10 @@ export class ESSearchSource extends AbstractESSource { return !!(useTopHits && topHitsSplitField && topHitsTimeField); } - async getGeoJsonWithMeta(layerName, searchFilters) { + async getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback) { const { hits, meta } = this._isTopHits() - ? await this._getTopHits(layerName, searchFilters) - : await this._getSearchHits(layerName, searchFilters); + ? await this._getTopHits(layerName, searchFilters, registerCancelCallback) + : await this._getSearchHits(layerName, searchFilters, registerCancelCallback); const indexPattern = await this._getIndexPattern(); const unusedMetaFields = indexPattern.metaFields.filter(metaField => { diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js index 1ff0d973ccbe0..48aa9df47b2f4 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_source.js @@ -20,6 +20,7 @@ import { ESAggMetricTooltipProperty } from '../tooltips/es_aggmetric_tooltip_pro import uuid from 'uuid/v4'; import { copyPersistentState } from '../../reducers/util'; import { ES_GEO_FIELD_TYPE } from '../../../common/constants'; +import { DataRequestAbortError } from '../util/data_request'; export class AbstractESSource extends AbstractVectorSource { @@ -131,7 +132,12 @@ export class AbstractESSource extends AbstractVectorSource { } - async _runEsQuery(requestName, searchSource, requestDescription) { + async _runEsQuery(requestName, searchSource, registerCancelCallback, requestDescription) { + const cancel = () => { + searchSource.cancelQueued(); + }; + registerCancelCallback(cancel); + try { return await fetchSearchSourceAndRecordWithInspector({ inspectorAdapters: this._inspectorAdapters, @@ -141,6 +147,10 @@ export class AbstractESSource extends AbstractVectorSource { requestDesc: requestDescription }); } catch(error) { + if (error.name === 'AbortError') { + throw new DataRequestAbortError(); + } + throw new Error(i18n.translate('xpack.maps.source.esSource.requestFailedErrorMessage', { defaultMessage: `Elasticsearch search request failed, error: {message}`, values: { message: error.message } diff --git a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js index 14760c7c50647..5d876dbbd011f 100644 --- a/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js +++ b/x-pack/legacy/plugins/maps/public/layers/sources/es_term_source.js @@ -90,7 +90,7 @@ export class ESTermSource extends AbstractESSource { return `${metricLabel} of ${this._descriptor.indexPatternTitle}:${this._descriptor.term}`; } - async getPropertiesMap(searchFilters, leftSourceName, leftFieldName) { + async getPropertiesMap(searchFilters, leftSourceName, leftFieldName, registerCancelCallback) { if (!this.hasCompleteConfig()) { return []; @@ -104,7 +104,7 @@ export class ESTermSource extends AbstractESSource { const requestName = `${this._descriptor.indexPatternTitle}.${this._descriptor.term}`; const requestDesc = this._getRequestDescription(leftSourceName, leftFieldName); - const rawEsData = await this._runEsQuery(requestName, searchSource, requestDesc); + const rawEsData = await this._runEsQuery(requestName, searchSource, registerCancelCallback, requestDesc); const metricPropertyNames = configStates .filter(configState => { diff --git a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js b/x-pack/legacy/plugins/maps/public/layers/util/data_request.js index da2e88ec44ae8..95b82aa292884 100644 --- a/x-pack/legacy/plugins/maps/public/layers/util/data_request.js +++ b/x-pack/legacy/plugins/maps/public/layers/util/data_request.js @@ -37,5 +37,15 @@ export class DataRequest { return this._descriptor.dataId; } + getRequestToken() { + return this._descriptor.dataRequestToken; + } + +} + +export class DataRequestAbortError extends Error { + constructor() { + super(); + } } diff --git a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js index 5033979843ab6..7f7875d7fb995 100644 --- a/x-pack/legacy/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/legacy/plugins/maps/public/layers/vector_layer.js @@ -22,6 +22,7 @@ import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; import { isRefreshOnlyQuery } from './util/is_refresh_only_query'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { DataRequestAbortError } from './util/data_request'; const VISIBILITY_FILTER_CLAUSE = ['all', [ @@ -348,7 +349,7 @@ export class VectorLayer extends AbstractLayer { && !updateDueToSourceMetaChange; } - async _syncJoin({ join, startLoading, stopLoading, onLoadError, dataFilters }) { + async _syncJoin({ join, startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const joinSource = join.getRightJoinSource(); const sourceDataId = join.getSourceId(); @@ -376,7 +377,11 @@ export class VectorLayer extends AbstractLayer { const leftSourceName = await this.getSourceName(); const { propertiesMap - } = await joinSource.getPropertiesMap(searchFilters, leftSourceName, join.getLeftFieldName()); + } = await joinSource.getPropertiesMap( + searchFilters, + leftSourceName, + join.getLeftFieldName(), + registerCancelCallback.bind(null, requestToken)); stopLoading(sourceDataId, requestToken, propertiesMap); return { dataHasChanged: true, @@ -384,7 +389,9 @@ export class VectorLayer extends AbstractLayer { propertiesMap: propertiesMap, }; } catch (e) { - onLoadError(sourceDataId, requestToken, `Join error: ${e.message}`); + if (!(e instanceof DataRequestAbortError)) { + onLoadError(sourceDataId, requestToken, `Join error: ${e.message}`); + } return { dataHasChanged: true, join: join, @@ -393,9 +400,9 @@ export class VectorLayer extends AbstractLayer { } } - async _syncJoins({ startLoading, stopLoading, onLoadError, dataFilters }) { + async _syncJoins(syncContext) { const joinSyncs = this.getValidJoins().map(async join => { - return this._syncJoin({ join, startLoading, stopLoading, onLoadError, dataFilters }); + return this._syncJoin({ join, ...syncContext }); }); return await Promise.all(joinSyncs); @@ -457,7 +464,7 @@ export class VectorLayer extends AbstractLayer { } } - async _syncSource({ startLoading, stopLoading, onLoadError, dataFilters }) { + async _syncSource({ startLoading, stopLoading, onLoadError, registerCancelCallback, dataFilters }) { const requestToken = Symbol(`layer-source-refresh:${ this.getId()} - source`); @@ -474,7 +481,10 @@ export class VectorLayer extends AbstractLayer { try { startLoading(SOURCE_DATA_ID_ORIGIN, requestToken, searchFilters); const layerName = await this.getDisplayName(); - const { data: featureCollection, meta } = await this._source.getGeoJsonWithMeta(layerName, searchFilters); + const { + data: featureCollection, + meta + } = await this._source.getGeoJsonWithMeta(layerName, searchFilters, registerCancelCallback.bind(null, requestToken)); this._assignIdsToFeatures(featureCollection); stopLoading(SOURCE_DATA_ID_ORIGIN, requestToken, featureCollection, meta); return { @@ -482,7 +492,9 @@ export class VectorLayer extends AbstractLayer { featureCollection: featureCollection }; } catch (error) { - onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); + if (!(error instanceof DataRequestAbortError)) { + onLoadError(SOURCE_DATA_ID_ORIGIN, requestToken, error.message); + } return { refreshed: false }; @@ -516,18 +528,18 @@ export class VectorLayer extends AbstractLayer { } - async syncData({ startLoading, stopLoading, onLoadError, dataFilters, updateSourceData }) { - if (!this.isVisible() || !this.showAtZoomLevel(dataFilters.zoom)) { + async syncData(syncContext) { + if (!this.isVisible() || !this.showAtZoomLevel(syncContext.dataFilters.zoom)) { return; } - const sourceResult = await this._syncSource({ startLoading, stopLoading, onLoadError, dataFilters }); + const sourceResult = await this._syncSource(syncContext); if (!sourceResult.featureCollection || !sourceResult.featureCollection.features.length) { return; } - const joinStates = await this._syncJoins({ startLoading, stopLoading, onLoadError, dataFilters }); - await this._performInnerJoins(sourceResult, joinStates, updateSourceData); + const joinStates = await this._syncJoins(syncContext); + await this._performInnerJoins(sourceResult, joinStates, syncContext.updateSourceData); } diff --git a/x-pack/legacy/plugins/maps/public/reducers/non_serializable_instances.js b/x-pack/legacy/plugins/maps/public/reducers/non_serializable_instances.js index 5aa5298b29547..5fb09b8c87006 100644 --- a/x-pack/legacy/plugins/maps/public/reducers/non_serializable_instances.js +++ b/x-pack/legacy/plugins/maps/public/reducers/non_serializable_instances.js @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import _ from 'lodash'; import chrome from 'ui/chrome'; import { RequestAdapter } from 'ui/inspector/adapters'; import { MapAdapter } from '../inspector/adapters/map_adapter'; +const REGISTER_CANCEL_CALLBACK = 'REGISTER_CANCEL_CALLBACK'; +const UNREGISTER_CANCEL_CALLBACK = 'UNREGISTER_CANCEL_CALLBACK'; + function createInspectorAdapters() { const inspectorAdapters = { requests: new RequestAdapter(), @@ -20,18 +22,67 @@ function createInspectorAdapters() { } // Reducer -export function nonSerializableInstances(state) { +export function nonSerializableInstances(state, action = {}) { if (!state) { return { inspectorAdapters: createInspectorAdapters(), + cancelRequestCallbacks: new Map(), // key is request token, value is cancel callback }; } - // state is read only and provides access to non-serializeable object instances - return state; + switch (action.type) { + case REGISTER_CANCEL_CALLBACK: + state.cancelRequestCallbacks.set(action.requestToken, action.callback); + return { + ...state, + }; + case UNREGISTER_CANCEL_CALLBACK: + state.cancelRequestCallbacks.delete(action.requestToken); + return { + ...state, + }; + default: + return state; + + + } } // Selectors export const getInspectorAdapters = ({ nonSerializableInstances }) => { - return _.get(nonSerializableInstances, 'inspectorAdapters', {}); + return nonSerializableInstances.inspectorAdapters; +}; + +export const getCancelRequestCallbacks = ({ nonSerializableInstances }) => { + return nonSerializableInstances.cancelRequestCallbacks; +}; + +// Actions +export const registerCancelCallback = (requestToken, callback) => { + return { + type: REGISTER_CANCEL_CALLBACK, + requestToken, + callback, + }; +}; + +export const unregisterCancelCallback = (requestToken) => { + return { + type: UNREGISTER_CANCEL_CALLBACK, + requestToken, + }; +}; + +export const cancelRequest = (requestToken) => { + return (dispatch, getState) => { + if (!requestToken) { + return; + } + + const cancelCallback = getCancelRequestCallbacks(getState()).get(requestToken); + if (cancelCallback) { + cancelCallback(); + dispatch(unregisterCancelCallback(requestToken)); + } + }; };