[Search] Add cancelation logic to search example (#118176)

This commit is contained in:
Anton Dosov 2021-11-30 12:35:07 +01:00 committed by GitHub
parent 6d20bf39fd
commit b8a7370156
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 136 additions and 42 deletions

View file

@ -47,6 +47,7 @@ import {
isErrorResponse,
} from '../../../../src/plugins/data/public';
import { IMyStrategyResponse } from '../../common/types';
import { AbortError } from '../../../../src/plugins/kibana_utils/common';
interface SearchExamplesAppDeps {
notifications: CoreStart['notifications'];
@ -102,6 +103,8 @@ export const SearchExamplesApp = ({
IndexPatternField | null | undefined
>();
const [request, setRequest] = useState<Record<string, any>>({});
const [isLoading, setIsLoading] = useState<boolean>(false);
const [currentAbortController, setAbortController] = useState<AbortController>();
const [rawResponse, setRawResponse] = useState<Record<string, any>>({});
const [selectedTab, setSelectedTab] = useState(0);
@ -187,16 +190,23 @@ export const SearchExamplesApp = ({
...(strategy ? { get_cool: getCool } : {}),
};
const abortController = new AbortController();
setAbortController(abortController);
// Submit the search request using the `data.search` service.
setRequest(req.params.body);
const searchSubscription$ = data.search
setIsLoading(true);
data.search
.search(req, {
strategy,
sessionId,
abortSignal: abortController.signal,
})
.subscribe({
next: (res) => {
if (isCompleteResponse(res)) {
setIsLoading(false);
setResponse(res);
const avgResult: number | undefined = res.rawResponse.aggregations
? // @ts-expect-error @elastic/elasticsearch no way to declare a type for aggregation in the search response
@ -226,7 +236,6 @@ export const SearchExamplesApp = ({
toastLifeTimeMs: 300000,
}
);
searchSubscription$.unsubscribe();
if (res.warning) {
notifications.toasts.addWarning({
title: 'Warning',
@ -236,14 +245,20 @@ export const SearchExamplesApp = ({
} else if (isErrorResponse(res)) {
// TODO: Make response error status clearer
notifications.toasts.addDanger('An error has occurred');
searchSubscription$.unsubscribe();
}
},
error: (e) => {
notifications.toasts.addDanger({
title: 'Failed to run search',
text: e.message,
});
setIsLoading(false);
if (e instanceof AbortError) {
notifications.toasts.addWarning({
title: e.message,
});
} else {
notifications.toasts.addDanger({
title: 'Failed to run search',
text: e.message,
});
}
},
});
};
@ -286,7 +301,12 @@ export const SearchExamplesApp = ({
}
setRequest(searchSource.getSearchRequestBody());
const { rawResponse: res } = await searchSource.fetch$().toPromise();
const abortController = new AbortController();
setAbortController(abortController);
setIsLoading(true);
const { rawResponse: res } = await searchSource
.fetch$({ abortSignal: abortController.signal })
.toPromise();
setRawResponse(res);
const message = <EuiText>Searched {res.hits.total} documents.</EuiText>;
@ -301,7 +321,18 @@ export const SearchExamplesApp = ({
);
} catch (e) {
setRawResponse(e.body);
notifications.toasts.addWarning(`An error has occurred: ${e.message}`);
if (e instanceof AbortError) {
notifications.toasts.addWarning({
title: e.message,
});
} else {
notifications.toasts.addDanger({
title: 'Failed to run search',
text: e.message,
});
}
} finally {
setIsLoading(false);
}
};
@ -329,32 +360,44 @@ export const SearchExamplesApp = ({
},
};
const abortController = new AbortController();
setAbortController(abortController);
// Submit the search request using the `data.search` service.
setRequest(req.params);
const searchSubscription$ = data.search
setIsLoading(true);
data.search
.search(req, {
strategy: 'fibonacciStrategy',
abortSignal: abortController.signal,
})
.subscribe({
next: (res) => {
setResponse(res);
if (isCompleteResponse(res)) {
setIsLoading(false);
notifications.toasts.addSuccess({
title: 'Query result',
text: 'Query finished',
});
searchSubscription$.unsubscribe();
} else if (isErrorResponse(res)) {
setIsLoading(false);
// TODO: Make response error status clearer
notifications.toasts.addWarning('An error has occurred');
searchSubscription$.unsubscribe();
}
},
error: (e) => {
notifications.toasts.addDanger({
title: 'Failed to run search',
text: e.message,
});
setIsLoading(false);
if (e instanceof AbortError) {
notifications.toasts.addWarning({
title: e.message,
});
} else {
notifications.toasts.addDanger({
title: 'Failed to run search',
text: e.message,
});
}
},
});
};
@ -365,17 +408,32 @@ export const SearchExamplesApp = ({
const onServerClickHandler = async () => {
if (!indexPattern || !selectedNumericField) return;
const abortController = new AbortController();
setAbortController(abortController);
setIsLoading(true);
try {
const res = await http.get(SERVER_SEARCH_ROUTE_PATH, {
query: {
index: indexPattern.title,
field: selectedNumericField!.name,
},
signal: abortController.signal,
});
notifications.toasts.addSuccess(`Server returned ${JSON.stringify(res)}`);
} catch (e) {
notifications.toasts.addDanger('Failed to run search');
if (e?.name === 'AbortError') {
notifications.toasts.addWarning({
title: e.message,
});
} else {
notifications.toasts.addDanger({
title: 'Failed to run search',
text: e.message,
});
}
} finally {
setIsLoading(false);
}
};
@ -721,6 +779,11 @@ export const SearchExamplesApp = ({
strategy. This request does not take the configuration of{' '}
<EuiCode>TopNavMenu</EuiCode> into account, but you could pass those down to the
server as well.
<br />
When executing search on the server, make sure to cancel the search in case user
cancels corresponding network request. This could happen in case user re-runs a
query or leaves the page without waiting for the result. Cancellation API is similar
on client and server and use `AbortController`.
<EuiSpacer />
<EuiButtonEmpty size="xs" onClick={onServerClickHandler} iconType="play">
<FormattedMessage
@ -737,6 +800,15 @@ export const SearchExamplesApp = ({
selectedTab={reqTabs[selectedTab]}
onTabClick={(tab) => setSelectedTab(reqTabs.indexOf(tab))}
/>
<EuiSpacer />
{currentAbortController && isLoading && (
<EuiButtonEmpty size="xs" onClick={() => currentAbortController?.abort()}>
<FormattedMessage
id="searchExamples.abortButtonText"
defaultMessage="Abort request"
/>
</EuiButtonEmpty>
)}
</EuiFlexItem>
</EuiFlexGrid>
</EuiPageContentBody>

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { Observable } from 'rxjs';
import { IEsSearchRequest } from 'src/plugins/data/server';
import { schema } from '@kbn/config-schema';
import { IEsSearchResponse } from 'src/plugins/data/common';
@ -26,36 +27,51 @@ export function registerServerSearchRoute(router: IRouter<DataRequestHandlerCont
},
async (context, request, response) => {
const { index, field } = request.query;
// Run a synchronous search server side, by enforcing a high keepalive and waiting for completion.
// If you wish to run the search with polling (in basic+), you'd have to poll on the search API.
// Please reach out to the @app-arch-team if you need this to be implemented.
const res = await context
.search!.search(
{
params: {
index,
body: {
aggs: {
'1': {
avg: {
field,
// User may abort the request without waiting for the results
// we need to handle this scenario by aborting underlying server requests
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
try {
const res = await context
.search!.search(
{
params: {
index,
body: {
aggs: {
'1': {
avg: {
field,
},
},
},
},
},
waitForCompletionTimeout: '5m',
keepAlive: '5m',
},
} as IEsSearchRequest,
{}
)
.toPromise();
} as IEsSearchRequest,
{ abortSignal }
)
.toPromise();
return response.ok({
body: {
aggs: (res as IEsSearchResponse).rawResponse.aggregations,
},
});
return response.ok({
body: {
aggs: (res as IEsSearchResponse).rawResponse.aggregations,
},
});
} catch (e) {
return response.customError({
statusCode: e.statusCode ?? 500,
body: {
message: e.message,
},
});
}
}
);
}
function getRequestAbortedSignal(aborted$: Observable<void>): AbortSignal {
const controller = new AbortController();
aborted$.subscribe(() => controller.abort());
return controller.signal;
}