[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

@ -129,6 +129,12 @@ setTimeout(() => {
}, 1000);
```
<DocCallOut color="danger" title="Cancel your searches if results are no longer needed">
Users might no longer be interested in search results. For example, they might start a new search
or leave your app without waiting for the results. You should handle such cases by using
`AbortController` with search API.
</DocCallOut>
#### Search strategies
By default, the search service uses the DSL query and aggregation syntax and returns the response from Elasticsearch as is. It also provides several additional basic strategies, such as Async DSL (`x-pack` default) and EQL.

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;
}