From 25b97bbac1a4e37feac617ae2a68676f283f1661 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 1 Mar 2022 13:33:44 +0300 Subject: [PATCH] [Timelion] Cancel discarded searches (#125255) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/timelion_expression_input.tsx | 12 ++++-- .../helpers/timelion_request_handler.ts | 17 +++++++-- .../timelion/public/timelion_vis_fn.ts | 38 ++++++++++++------- .../timelion/public/timelion_vis_renderer.tsx | 18 +++++---- .../server/series_functions/es/es.test.js | 10 ++++- .../server/series_functions/es/index.js | 12 +++++- 6 files changed, 76 insertions(+), 31 deletions(-) diff --git a/src/plugins/vis_types/timelion/public/components/timelion_expression_input.tsx b/src/plugins/vis_types/timelion/public/components/timelion_expression_input.tsx index adaf87bfccd78..597d1a10dd051 100644 --- a/src/plugins/vis_types/timelion/public/components/timelion_expression_input.tsx +++ b/src/plugins/vis_types/timelion/public/components/timelion_expression_input.tsx @@ -83,11 +83,17 @@ function TimelionExpressionInput({ value, setValue }: TimelionExpressionInputPro ); useEffect(() => { + const abortController = new AbortController(); if (kibana.services.http) { - kibana.services.http.get('../api/timelion/functions').then((data) => { - functionList.current = data; - }); + kibana.services.http + .get('../api/timelion/functions', { signal: abortController.signal }) + .then((data) => { + functionList.current = data; + }); } + return () => { + abortController.abort(); + }; }, [kibana.services.http]); return ( diff --git a/src/plugins/vis_types/timelion/public/helpers/timelion_request_handler.ts b/src/plugins/vis_types/timelion/public/helpers/timelion_request_handler.ts index b613379d4d7c4..a8807cfe87b8a 100644 --- a/src/plugins/vis_types/timelion/public/helpers/timelion_request_handler.ts +++ b/src/plugins/vis_types/timelion/public/helpers/timelion_request_handler.ts @@ -54,7 +54,10 @@ export function getTimelionRequestHandler({ uiSettings, http, timefilter, -}: TimelionVisDependencies) { + expressionAbortSignal, +}: TimelionVisDependencies & { + expressionAbortSignal: AbortSignal; +}) { const timezone = getTimezone(uiSettings); return async function ({ @@ -74,6 +77,12 @@ export function getTimelionRequestHandler({ }): Promise { const dataSearch = getDataSearch(); const expression = visParams.expression; + const abortController = new AbortController(); + const expressionAbortHandler = function () { + abortController.abort(); + }; + + expressionAbortSignal.addEventListener('abort', expressionAbortHandler); if (!expression) { throw new Error( @@ -98,9 +107,7 @@ export function getTimelionRequestHandler({ const untrackSearch = dataSearch.session.isCurrentSession(searchSessionId) && dataSearch.session.trackSearch({ - abort: () => { - // TODO: support search cancellations - }, + abort: () => abortController.abort(), }); try { @@ -124,6 +131,7 @@ export function getTimelionRequestHandler({ }), }), context: executionContext, + signal: abortController.signal, }); } catch (e) { if (e && e.body) { @@ -142,6 +150,7 @@ export function getTimelionRequestHandler({ // call `untrack` if this search still belongs to current session untrackSearch(); } + expressionAbortSignal.removeEventListener('abort', expressionAbortHandler); } }; } diff --git a/src/plugins/vis_types/timelion/public/timelion_vis_fn.ts b/src/plugins/vis_types/timelion/public/timelion_vis_fn.ts index adf6c58f1cfc8..4bb195312f3b7 100644 --- a/src/plugins/vis_types/timelion/public/timelion_vis_fn.ts +++ b/src/plugins/vis_types/timelion/public/timelion_vis_fn.ts @@ -18,7 +18,7 @@ import { KibanaContext, Query, TimeRange } from '../../../data/public'; type Input = KibanaContext | null; type Output = Promise>; export interface TimelionRenderValue { - visData: TimelionSuccessResponse; + visData?: TimelionSuccessResponse; visType: 'timelion'; visParams: TimelionVisParams; } @@ -65,10 +65,12 @@ export const getTimelionVisualizationConfig = ( required: false, }, }, - async fn(input, args, { getSearchSessionId, getExecutionContext, variables }) { + async fn( + input, + args, + { getSearchSessionId, getExecutionContext, variables, abortSignal: expressionAbortSignal } + ) { const { getTimelionRequestHandler } = await import('./async_services'); - const timelionRequestHandler = getTimelionRequestHandler(dependencies); - const visParams = { expression: args.expression, interval: args.interval, @@ -77,17 +79,25 @@ export const getTimelionVisualizationConfig = ( (variables?.embeddableTitle as string) ?? getExecutionContext?.()?.description, }; + let visData: TimelionRenderValue['visData']; + + if (!expressionAbortSignal.aborted) { + const timelionRequestHandler = getTimelionRequestHandler({ + ...dependencies, + expressionAbortSignal, + }); - const response = await timelionRequestHandler({ - timeRange: get(input, 'timeRange') as TimeRange, - query: get(input, 'query') as Query, - filters: get(input, 'filters') as Filter[], - visParams, - searchSessionId: getSearchSessionId(), - executionContext: getExecutionContext(), - }); + visData = await timelionRequestHandler({ + timeRange: get(input, 'timeRange') as TimeRange, + query: get(input, 'query') as Query, + filters: get(input, 'filters') as Filter[], + visParams, + searchSessionId: getSearchSessionId(), + executionContext: getExecutionContext(), + }); - response.visType = TIMELION_VIS_NAME; + visData.visType = TIMELION_VIS_NAME; + } return { type: 'render', @@ -95,7 +105,7 @@ export const getTimelionVisualizationConfig = ( value: { visParams, visType: TIMELION_VIS_NAME, - visData: response, + visData, }, }; }, diff --git a/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx index 3552f6097d466..e865c3c8d7fb2 100644 --- a/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_types/timelion/public/timelion_vis_renderer.tsx @@ -33,7 +33,7 @@ export const getTimelionVisRenderer: ( unmountComponentAtNode(domNode); }); - const [seriesList] = visData.sheet; + const seriesList = visData?.sheet[0]; const showNoResult = !seriesList || !seriesList.list.length; const VisComponent = deps.uiSettings.get(UI_SETTINGS.LEGACY_CHARTS_LIBRARY, false) @@ -62,13 +62,15 @@ export const getTimelionVisRenderer: ( - + {seriesList && ( + + )} , diff --git a/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js b/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js index 9c0dac6f6975a..6f718a6ec8e16 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_types/timelion/server/series_functions/es/es.test.js @@ -28,6 +28,12 @@ describe('es', () => { getIndexPatternsService: () => ({ find: async () => [], }), + request: { + events: { + aborted$: of(), + }, + body: {}, + }, }; } @@ -46,9 +52,11 @@ describe('es', () => { }); test('should call data search with sessionId, isRestore and isStored', async () => { + const baseTlConfig = stubRequestAndServer({ rawResponse: esResponse }); tlConfig = { - ...stubRequestAndServer({ rawResponse: esResponse }), + ...baseTlConfig, request: { + ...baseTlConfig.request, body: { searchSession: { sessionId: '1', diff --git a/src/plugins/vis_types/timelion/server/series_functions/es/index.js b/src/plugins/vis_types/timelion/server/series_functions/es/index.js index d613818d7c3e3..a86ee64f00568 100644 --- a/src/plugins/vis_types/timelion/server/series_functions/es/index.js +++ b/src/plugins/vis_types/timelion/server/series_functions/es/index.js @@ -12,6 +12,12 @@ import Datasource from '../../lib/classes/datasource'; import buildRequest from './lib/build_request'; import toSeriesList from './lib/agg_response_to_series_list'; +function getRequestAbortedSignal(aborted$) { + const controller = new AbortController(); + aborted$.subscribe(() => controller.abort()); + return controller.signal; +} + export default new Datasource('es', { hideFitArg: true, args: [ @@ -107,13 +113,17 @@ export default new Datasource('es', { const body = buildRequest(config, tlConfig, scriptFields, runtimeFields, esShardTimeout); + // User may abort the request without waiting for the results + // we need to handle this scenario by aborting underlying server requests + const abortSignal = getRequestAbortedSignal(tlConfig.request.events.aborted$); + const resp = await tlConfig.context.search .search( body, { ...tlConfig.request?.body.searchSession, }, - tlConfig.context + { ...tlConfig.context, abortSignal } ) .toPromise();