Skip to content

Commit 9fecf8d

Browse files
authored
Cancel visualize fetches when navigating away or re-fetching (#42035)
* add AbortSignal to interpreter * adding AbortSignal to visualize_loader * adding AbortSignal to embeddables and dashboard * passing AbortSignal to courier request handler * Remove abort signal from dashboard and move to handler, and abort fetches when necessary * Remove the rest of the references to abort signal in dashboard * Revert changes to dashboard_app * Remove code related to canceling visualize requests and only keep stuff for canceling interpreter * Use createError * Cancel in-progress fetches when a new one is requested or when leaving the page * Update with cancel methods and make visualize data loader work * Remove unnecessary call to * Fix tests * Remove cancel from data loaders * Fix search source to always either resolve or reject in fetch() * Fix naming * Update search request to reject on abort, and don't render errors in the UI * Update tests * Add cancellation to input control * Cancel histogram fetches * Update esaggs typings * Add cancellation for search embeddables * Abort post-flight requests
1 parent b0df7a8 commit 9fecf8d

File tree

19 files changed

+157
-48
lines changed

19 files changed

+157
-48
lines changed

src/legacy/core_plugins/input_control_vis/public/control/control.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export class Control {
6060
throw new Error('fetch method not defined, subclass are required to implement');
6161
}
6262

63+
destroy() {
64+
throw new Error('destroy method not defined, subclass are required to implement');
65+
}
66+
6367
format = (value) => {
6468
const field = this.filterManager.getField();
6569
if (field) {

src/legacy/core_plugins/input_control_vis/public/control/list_control_factory.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ const termsAgg = ({ field, size, direction, query }) => {
6868
class ListControl extends Control {
6969

7070
fetch = async (query) => {
71+
// Abort any in-progress fetch
72+
if (this.abortController) {
73+
this.abortController.abort();
74+
}
75+
this.abortController = new AbortController();
76+
7177
const indexPattern = this.filterManager.getIndexPattern();
7278
if (!indexPattern) {
7379
this.disable(noIndexPatternMsg(this.controlParams.indexPattern));
@@ -114,13 +120,18 @@ class ListControl extends Control {
114120
indexPattern,
115121
aggs,
116122
this.useTimeFilter,
117-
ancestorFilters);
123+
ancestorFilters
124+
);
125+
this.abortController.signal.addEventListener('abort', () => searchSource.cancelQueued());
118126

119127
this.lastQuery = query;
120128
let resp;
121129
try {
122130
resp = await searchSource.fetch();
123131
} catch(error) {
132+
// If the fetch was aborted then no need to surface this error in the UI
133+
if (error.name === 'AbortError') return;
134+
124135
this.disable(i18n.translate('inputControl.listControl.unableToFetchTooltip', {
125136
defaultMessage: 'Unable to fetch terms, error: {errorMessage}',
126137
values: { errorMessage: error.message }
@@ -148,6 +159,10 @@ class ListControl extends Control {
148159
this.disabledReason = '';
149160
}
150161

162+
destroy() {
163+
if (this.abortController) this.abortController.abort();
164+
}
165+
151166
hasValue() {
152167
return typeof this.value !== 'undefined' && this.value.length > 0;
153168
}

src/legacy/core_plugins/input_control_vis/public/control/range_control_factory.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ const minMaxAgg = (field) => {
5050
class RangeControl extends Control {
5151

5252
async fetch() {
53+
// Abort any in-progress fetch
54+
if (this.abortController) {
55+
this.abortController.abort();
56+
}
57+
this.abortController = new AbortController();
58+
5359
const indexPattern = this.filterManager.getIndexPattern();
5460
if (!indexPattern) {
5561
this.disable(noIndexPatternMsg(this.controlParams.indexPattern));
@@ -60,11 +66,15 @@ class RangeControl extends Control {
6066

6167
const aggs = minMaxAgg(indexPattern.fields.byName[fieldName]);
6268
const searchSource = createSearchSource(this.kbnApi, null, indexPattern, aggs, this.useTimeFilter);
69+
this.abortController.signal.addEventListener('abort', () => searchSource.cancelQueued());
6370

6471
let resp;
6572
try {
6673
resp = await searchSource.fetch();
6774
} catch(error) {
75+
// If the fetch was aborted then no need to surface this error in the UI
76+
if (error.name === 'AbortError') return;
77+
6878
this.disable(i18n.translate('inputControl.rangeControl.unableToFetchTooltip', {
6979
defaultMessage: 'Unable to fetch range min and max, error: {errorMessage}',
7080
values: { errorMessage: error.message }
@@ -84,6 +94,10 @@ class RangeControl extends Control {
8494
this.max = max;
8595
this.enable = true;
8696
}
97+
98+
destroy() {
99+
if (this.abortController) this.abortController.abort();
100+
}
87101
}
88102

89103
export async function rangeControlFactory(controlParams, kbnApi, useTimeFilter) {

src/legacy/core_plugins/input_control_vis/public/vis_controller.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,13 @@ class VisController {
4242
this.controls = [];
4343
this.controls = await this.initControls();
4444
this.drawVis();
45-
return;
4645
}
47-
return;
4846
}
4947

5048
destroy() {
5149
this.updateSubsciption.unsubscribe();
5250
unmountComponentAtNode(this.el);
51+
this.controls.forEach(control => control.destroy());
5352
}
5453

5554
drawVis = () => {

src/legacy/core_plugins/interpreter/public/functions/esaggs.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const esaggs = (): ExpressionFunction<typeof name, Context, Arguments, Re
8787
help: '',
8888
},
8989
},
90-
async fn(context, args, handlers) {
90+
async fn(context, args, { inspectorAdapters, abortSignal }) {
9191
const $injector = await chrome.dangerouslyGetActiveInjector();
9292
const Private: Function = $injector.get('Private');
9393
const indexPatterns = Private(IndexPatternsProvider);
@@ -112,14 +112,15 @@ export const esaggs = (): ExpressionFunction<typeof name, Context, Arguments, Re
112112
forceFetch: true,
113113
metricsAtAllLevels: args.metricsAtAllLevels,
114114
partialRows: args.partialRows,
115-
inspectorAdapters: handlers.inspectorAdapters,
115+
inspectorAdapters,
116116
queryFilter,
117+
abortSignal: (abortSignal as unknown) as AbortSignal,
117118
});
118119

119120
const table: KibanaDatatable = {
120121
type: 'kibana_datatable',
121122
rows: response.rows,
122-
columns: response.columns.map(column => {
123+
columns: response.columns.map((column: any) => {
123124
const cleanedColumn: KibanaDatatableColumn = {
124125
id: column.id,
125126
name: column.name,

src/legacy/core_plugins/kibana/public/discover/controllers/discover.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,9 @@ function discoverController(
761761
})
762762
.then(onResults)
763763
.catch((error) => {
764+
// If the request was aborted then no need to surface this error in the UI
765+
if (error instanceof Error && error.name === 'AbortError') return;
766+
764767
const fetchError = getPainlessError(error);
765768

766769
if (fetchError) {

src/legacy/core_plugins/kibana/public/discover/embeddable/search_embeddable.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ export class SearchEmbeddable extends Embeddable<SearchInput, SearchOutput>
191191
if (this.autoRefreshFetchSubscription) {
192192
this.autoRefreshFetchSubscription.unsubscribe();
193193
}
194+
this.savedSearch.searchSource.cancelQueued();
194195
}
195196

196197
private initializeSearchScope() {
@@ -270,6 +271,10 @@ export class SearchEmbeddable extends Embeddable<SearchInput, SearchOutput>
270271
if (!this.searchScope) return;
271272

272273
const { searchSource } = this.savedSearch;
274+
275+
// Abort any in-progress requests
276+
searchSource.cancelQueued();
277+
273278
searchSource.setField('size', config.get('discover:sampleSize'));
274279
searchSource.setField('sort', getSort(this.searchScope.sort, this.searchScope.indexPattern));
275280

@@ -304,6 +309,9 @@ export class SearchEmbeddable extends Embeddable<SearchInput, SearchOutput>
304309
this.searchScope!.totalHitCount = resp.hits.total;
305310
});
306311
} catch (error) {
312+
// If the fetch was aborted, no need to surface this in the UI
313+
if (error.name === 'AbortError') return;
314+
307315
toastNotifications.addError(error, {
308316
title: i18n.translate('kbn.embeddable.errorTitle', {
309317
defaultMessage: 'Error fetching data',

src/legacy/ui/public/agg_types/buckets/histogram.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ export const histogramBucketAgg = new BucketAggType({
7676
{
7777
name: 'interval',
7878
editorComponent: NumberIntervalParamEditor,
79-
modifyAggConfigOnSearchRequestStart(aggConfig, searchSource) {
79+
modifyAggConfigOnSearchRequestStart(aggConfig, searchSource, searchRequest) {
8080
const field = aggConfig.getField();
8181
const aggBody = field.scripted
8282
? { script: { source: field.script, lang: field.lang } }
8383
: { field: field.name };
8484

85-
return searchSource
85+
const childSearchSource = searchSource
8686
.createChild()
8787
.setField('size', 0)
8888
.setField('aggs', {
@@ -92,15 +92,19 @@ export const histogramBucketAgg = new BucketAggType({
9292
minAgg: {
9393
min: aggBody
9494
}
95-
})
96-
.fetch()
95+
});
96+
97+
searchRequest.whenAborted(() => childSearchSource.cancelQueued());
98+
99+
return childSearchSource.fetch()
97100
.then((resp) => {
98101
aggConfig.setAutoBounds({
99102
min: _.get(resp, 'aggregations.minAgg.value'),
100103
max: _.get(resp, 'aggregations.maxAgg.value')
101104
});
102105
})
103-
.catch(() => {
106+
.catch(e => {
107+
if (e.name === 'AbortError') return;
104108
toastNotifications.addWarning(i18n.translate('common.ui.aggTypes.histogram.missingMaxMinValuesWarning', {
105109
// eslint-disable-next-line max-len
106110
defaultMessage: 'Unable to retrieve max and min values to auto-scale histogram buckets. This may lead to poor visualization performance.'

src/legacy/ui/public/agg_types/buckets/terms.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ export const termsBucketAgg = new BucketAggType({
7676
};
7777
},
7878
createFilter: createFilterTerms,
79-
postFlightRequest: async (resp, aggConfigs, aggConfig, searchSource, inspectorAdapters) => {
79+
postFlightRequest: async (resp, aggConfigs, aggConfig, searchSource, inspectorAdapters, abortSignal) => {
8080
if (!resp.aggregations) return resp;
8181
const nestedSearchSource = searchSource.createChild();
8282
if (aggConfig.params.otherBucket) {
8383
const filterAgg = buildOtherBucketAgg(aggConfigs, aggConfig, resp);
8484
if (!filterAgg) return resp;
85+
if (abortSignal) {
86+
abortSignal.addEventListener('abort', () => nestedSearchSource.cancelQueued());
87+
}
8588

8689
nestedSearchSource.setField('aggs', filterAgg);
8790

src/legacy/ui/public/courier/fetch/request/search_request/search_request.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,13 @@ export function SearchRequestProvider(Promise) {
168168

169169
abort() {
170170
this._markStopped();
171-
this.defer = null;
172171
this.aborted = true;
173-
this.abortedDefer.resolve();
172+
const error = new Error('The request was aborted.');
173+
error.name = 'AbortError';
174+
this.abortedDefer.resolve(error);
174175
this.abortedDefer = null;
176+
this.defer.reject(error);
177+
this.defer = null;
175178
}
176179

177180
whenAborted(cb) {

0 commit comments

Comments
 (0)