[search source] remove is_partial check (#164506)

Closes https://github.com/elastic/kibana/issues/164893

### Background

"is partial" has 2 meanings
1) Results are incomplete because search is still running
2) Search is finished. Results are incomplete because there are shard
failures (either in local or remote clusters)

[async
search](https://www.elastic.co/guide/en/elasticsearch/reference/current/async-search.html)
defines 2 flags.
1) `is_running`: Whether the search is still being executed or it has
completed
2) `is_partial`: When the query is no longer running, indicates whether
the search failed or was successfully completed on all shards. While the
query is being executed, is_partial is always set to true
**note**: there is a bug in async search where requests to only local
clusters return `is_partial:false` when there are shard errors on the
local cluster. See
https://github.com/elastic/elasticsearch/issues/98725. This should be
resolved in 8.11

Kibana's existing search implementation does not align with
Elasticsearch's `is_running` and `is_partial` flags. Kibana defines "is
partial" as definition "1)". Elasticsearch async search defines "is
partial" as definition "2)".

This PR aligns Kibana's usage of "is partial" with Elasticsearch's
definition. This required the following changes
1) `isErrorResponse` renamed to `isAbortedResponse`. Method no longer
returns true when `!response.isRunning && !!response.isPartial`. Kibana
handles results with incomplete data. **Note** removed export of
`isErrorResponse` from data plugin since its use outside of data plugin
does not make sense.
2) Replace `isPartialResponse` with `isRunningResponse`. This aligns
Kibana's definition with Elasticsearch async search flags.
3) Remove `isCompleteResponse`. The word "complete" is ambiguous. Does
it mean the search is finished (no longer running)? Or does it mean the
search has all results and there are no shard failures?

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Jatin Kathuria <jatin.kathuria@elastic.co>
Co-authored-by: Patryk Kopyciński <contact@patrykkopycinski.com>
This commit is contained in:
Nathan Reese 2023-10-02 13:25:08 -06:00 committed by GitHub
parent 236eff4769
commit b5bcf69022
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 192 additions and 441 deletions

View file

@ -29,7 +29,7 @@ import { IInspectorInfo } from '@kbn/data-plugin/common';
import {
DataPublicPluginStart,
IKibanaSearchResponse,
isCompleteResponse,
isRunningResponse,
} from '@kbn/data-plugin/public';
import { SearchResponseWarning } from '@kbn/data-plugin/public/search/types';
import type { DataView, DataViewField } from '@kbn/data-views-plugin/public';
@ -209,7 +209,7 @@ export const SearchExamplesApp = ({
})
.subscribe({
next: (res) => {
if (isCompleteResponse(res)) {
if (!isRunningResponse(res)) {
setIsLoading(false);
setResponse(res);
const aggResult: number | undefined = res.rawResponse.aggregations
@ -389,7 +389,7 @@ export const SearchExamplesApp = ({
.subscribe({
next: (res) => {
setResponse(res);
if (isCompleteResponse(res)) {
if (!isRunningResponse(res)) {
setIsLoading(false);
notifications.toasts.addSuccess({
title: 'Query result',

View file

@ -39,7 +39,7 @@ import {
DataPublicPluginStart,
IEsSearchRequest,
IEsSearchResponse,
isCompleteResponse,
isRunningResponse,
QueryState,
SearchSessionState,
} from '@kbn/data-plugin/public';
@ -706,7 +706,7 @@ function doSearch(
return lastValueFrom(
data.search.search(req, { sessionId }).pipe(
tap((res) => {
if (isCompleteResponse(res)) {
if (!isRunningResponse(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
res.rawResponse.aggregations[1]?.value ?? res.rawResponse.aggregations[2]?.value

View file

@ -26,7 +26,7 @@ import { CoreStart } from '@kbn/core/public';
import {
DataPublicPluginStart,
IKibanaSearchResponse,
isCompleteResponse,
isRunningResponse,
} from '@kbn/data-plugin/public';
import {
SQL_SEARCH_STRATEGY,
@ -66,7 +66,7 @@ export const SqlSearchExampleApp = ({ notifications, data }: SearchExamplesAppDe
})
.subscribe({
next: (res) => {
if (isCompleteResponse(res)) {
if (!isRunningResponse(res)) {
setIsLoading(false);
setResponse(res);
}

View file

@ -158,12 +158,12 @@ The `SearchSource` API is a convenient way to construct and run an Elasticsearch
One benefit of using the low-level search API, is partial response support, allowing for a better and more responsive user experience.
```.ts
import { isCompleteResponse } from '../plugins/data/public';
import { isRunningResponse } from '../plugins/data/public';
const search$ = data.search.search(request)
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
// Final result
search$.unsubscribe();
} else {

View file

@ -10,7 +10,7 @@ import { pollSearch } from './poll_search';
import { AbortError } from '@kbn/kibana-utils-plugin/common';
describe('pollSearch', () => {
function getMockedSearch$(resolveOnI = 1, finishWithError = false) {
function getMockedSearch$(resolveOnI = 1) {
let counter = 0;
return jest.fn().mockImplementation(() => {
counter++;
@ -19,7 +19,7 @@ describe('pollSearch', () => {
if (lastCall) {
resolve({
isRunning: false,
isPartial: finishWithError,
isPartial: false,
rawResponse: {},
});
} else {
@ -57,15 +57,6 @@ describe('pollSearch', () => {
expect(cancelFn).toBeCalledTimes(0);
});
test('Throws Error on ES error response', async () => {
const searchFn = getMockedSearch$(2, true);
const cancelFn = jest.fn();
const poll = pollSearch(searchFn, cancelFn).toPromise();
await expect(poll).rejects.toThrow(Error);
expect(searchFn).toBeCalledTimes(2);
expect(cancelFn).toBeCalledTimes(0);
});
test('Throws AbortError on empty response', async () => {
const searchFn = jest.fn().mockResolvedValue(undefined);
const cancelFn = jest.fn();

View file

@ -10,7 +10,7 @@ import { from, Observable, timer, defer, fromEvent, EMPTY } from 'rxjs';
import { expand, map, switchMap, takeUntil, takeWhile, tap } from 'rxjs/operators';
import { AbortError } from '@kbn/kibana-utils-plugin/common';
import type { IAsyncSearchOptions, IKibanaSearchResponse } from '..';
import { isErrorResponse, isPartialResponse } from '..';
import { isAbortResponse, isRunningResponse } from '..';
export const pollSearch = <Response extends IKibanaSearchResponse>(
search: () => Promise<Response>,
@ -57,11 +57,11 @@ export const pollSearch = <Response extends IKibanaSearchResponse>(
return timer(getPollInterval(elapsedTime)).pipe(switchMap(search));
}),
tap((response) => {
if (isErrorResponse(response)) {
throw response ? new Error('Received partial response') : new AbortError();
if (isAbortResponse(response)) {
throw new AbortError();
}
}),
takeWhile<Response>(isPartialResponse, true),
takeWhile<Response>(isRunningResponse, true),
takeUntil<Response>(aborted$)
);
});

View file

@ -103,13 +103,7 @@ import { getSearchParamsFromRequest, RequestFailure } from './fetch';
import type { FetchHandlers, SearchRequest } from './fetch';
import { getRequestInspectorStats, getResponseInspectorStats } from './inspect';
import {
getEsQueryConfig,
IKibanaSearchResponse,
isPartialResponse,
isCompleteResponse,
UI_SETTINGS,
} from '../..';
import { getEsQueryConfig, IKibanaSearchResponse, isRunningResponse, UI_SETTINGS } from '../..';
import { AggsStart } from '../aggs';
import { extractReferences } from './extract_references';
import {
@ -546,7 +540,7 @@ export class SearchSource {
// For testing timeout messages in UI, uncomment the next line
// response.rawResponse.timed_out = true;
return new Observable<IKibanaSearchResponse<unknown>>((obs) => {
if (isPartialResponse(response)) {
if (isRunningResponse(response)) {
obs.next(this.postFlightTransform(response));
} else {
if (!this.hasPostFlightRequests()) {
@ -582,7 +576,7 @@ export class SearchSource {
});
}),
map((response) => {
if (!isCompleteResponse(response)) {
if (isRunningResponse(response)) {
return response;
}
return onResponse(searchRequest, response, options);

View file

@ -6,210 +6,42 @@
* Side Public License, v 1.
*/
import { isErrorResponse, isCompleteResponse, isPartialResponse } from './utils';
import type { IKibanaSearchResponse } from './types';
import { isAbortResponse, isRunningResponse } from './utils';
describe('utils', () => {
describe('isErrorResponse', () => {
describe('isAbortResponse', () => {
it('returns `true` if the response is undefined', () => {
const isError = isErrorResponse();
const isError = isAbortResponse();
expect(isError).toBe(true);
});
it('returns `true` if the response is not running and partial', () => {
const isError = isErrorResponse({
isPartial: true,
isRunning: false,
rawResponse: {},
});
expect(isError).toBe(true);
});
it('returns `false` if the response is not running and partial and contains failure details', () => {
const isError = isErrorResponse({
isPartial: true,
isRunning: false,
rawResponse: {
took: 7,
timed_out: false,
_shards: {
total: 2,
successful: 1,
skipped: 0,
failed: 1,
failures: [
{
shard: 0,
index: 'remote:tmp-00002',
node: '9SNgMgppT2-6UHJNXwio3g',
reason: {
type: 'script_exception',
reason: 'runtime error',
script_stack: [
'org.elasticsearch.server@8.10.0/org.elasticsearch.search.lookup.LeafDocLookup.getFactoryForDoc(LeafDocLookup.java:148)',
'org.elasticsearch.server@8.10.0/org.elasticsearch.search.lookup.LeafDocLookup.get(LeafDocLookup.java:191)',
'org.elasticsearch.server@8.10.0/org.elasticsearch.search.lookup.LeafDocLookup.get(LeafDocLookup.java:32)',
"doc['bar'].value < 10",
' ^---- HERE',
],
script: "doc['bar'].value < 10",
lang: 'painless',
position: {
offset: 4,
start: 0,
end: 21,
},
caused_by: {
type: 'illegal_argument_exception',
reason: 'No field found for [bar] in mapping',
},
},
},
],
},
_clusters: {
total: 1,
successful: 1,
skipped: 0,
details: {
remote: {
status: 'partial',
indices: 'tmp-*',
took: 3,
timed_out: false,
_shards: {
total: 2,
successful: 1,
skipped: 0,
failed: 1,
},
failures: [
{
shard: 0,
index: 'remote:tmp-00002',
node: '9SNgMgppT2-6UHJNXwio3g',
reason: {
type: 'script_exception',
reason: 'runtime error',
script_stack: [
'org.elasticsearch.server@8.10.0/org.elasticsearch.search.lookup.LeafDocLookup.getFactoryForDoc(LeafDocLookup.java:148)',
'org.elasticsearch.server@8.10.0/org.elasticsearch.search.lookup.LeafDocLookup.get(LeafDocLookup.java:191)',
'org.elasticsearch.server@8.10.0/org.elasticsearch.search.lookup.LeafDocLookup.get(LeafDocLookup.java:32)',
"doc['bar'].value < 10",
' ^---- HERE',
],
script: "doc['bar'].value < 10",
lang: 'painless',
position: {
offset: 4,
start: 0,
end: 21,
},
caused_by: {
type: 'illegal_argument_exception',
reason: 'No field found for [bar] in mapping',
},
},
},
],
},
},
},
hits: {
total: {
value: 1,
relation: 'eq',
},
max_score: 0,
hits: [
{
_index: 'remote:tmp-00001',
_id: 'd8JNlYoBFqAcOBVnvdqx',
_score: 0,
_source: {
foo: 'bar',
bar: 1,
},
},
],
},
},
});
expect(isError).toBe(false);
});
it('returns `false` if the response is running and partial', () => {
const isError = isErrorResponse({
isPartial: true,
isRunning: true,
rawResponse: {},
});
expect(isError).toBe(false);
});
it('returns `false` if the response is complete', () => {
const isError = isErrorResponse({
isPartial: false,
isRunning: false,
rawResponse: {},
});
expect(isError).toBe(false);
});
});
describe('isCompleteResponse', () => {
it('returns `false` if the response is undefined', () => {
const isError = isCompleteResponse();
expect(isError).toBe(false);
});
it('returns `false` if the response is running and partial', () => {
const isError = isCompleteResponse({
isPartial: true,
isRunning: true,
rawResponse: {},
});
expect(isError).toBe(false);
});
it('returns `true` if the response is complete', () => {
const isError = isCompleteResponse({
isPartial: false,
isRunning: false,
rawResponse: {},
});
expect(isError).toBe(true);
});
it('returns `true` if the response does not indicate isRunning', () => {
const isError = isCompleteResponse({
rawResponse: {},
});
it('returns `true` if rawResponse is undefined', () => {
const isError = isAbortResponse({} as unknown as IKibanaSearchResponse);
expect(isError).toBe(true);
});
});
describe('isPartialResponse', () => {
describe('isRunningResponse', () => {
it('returns `false` if the response is undefined', () => {
const isError = isPartialResponse();
expect(isError).toBe(false);
const isRunning = isRunningResponse();
expect(isRunning).toBe(false);
});
it('returns `true` if the response is running and partial', () => {
const isError = isPartialResponse({
isPartial: true,
it('returns `true` if the response is running', () => {
const isRunning = isRunningResponse({
isRunning: true,
rawResponse: {},
});
expect(isError).toBe(true);
expect(isRunning).toBe(true);
});
it('returns `false` if the response is complete', () => {
const isError = isPartialResponse({
isPartial: false,
it('returns `false` if the response is finished running', () => {
const isRunning = isRunningResponse({
isRunning: false,
rawResponse: {},
});
expect(isError).toBe(false);
expect(isRunning).toBe(false);
});
});
});

View file

@ -10,45 +10,20 @@ import moment from 'moment-timezone';
import { AggTypesDependencies } from '..';
import type { IKibanaSearchResponse } from './types';
// TODO - investigate if this check is still needed
// There are no documented work flows where response or rawResponse is not returned
// Leaving check to prevent breaking changes until full investigation can be completed.
/**
* From https://github.com/elastic/elasticsearch/issues/55572: "When is_running is false, the query has stopped, which
* may happen due to ... the search failed, in which case is_partial is set to true to indicate that any results that
* may be included in the search response come only from a subset of the shards that the query should have hit."
* @returns true if response had an error while executing in ES
* @returns true if response is abort
*/
export const isErrorResponse = (response?: IKibanaSearchResponse) => {
return (
!response ||
!response.rawResponse ||
(!response.isRunning &&
!!response.isPartial &&
// See https://github.com/elastic/elasticsearch/pull/97731. For CCS with ccs_minimize_roundtrips=true, isPartial
// is true if the search is complete but there are shard failures. In that case, the _clusters.details section
// will have information about those failures. This will also likely be the behavior of CCS with
// ccs_minimize_roundtrips=false and non-CCS after https://github.com/elastic/elasticsearch/issues/98913 is
// resolved.
!response.rawResponse?._clusters?.details)
);
export const isAbortResponse = (response?: IKibanaSearchResponse) => {
return !response || !response.rawResponse;
};
/**
* @returns true if response is completed successfully
* @returns true if request is still running
*/
export const isCompleteResponse = (response?: IKibanaSearchResponse) => {
// Some custom search strategies do not indicate whether they are still running. In this case, assume it is complete.
if (response && !response.hasOwnProperty('isRunning')) {
return true;
}
return !isErrorResponse(response) && Boolean(response && !response.isRunning);
};
/**
* @returns true if request is still running an/d response contains partial results
*/
export const isPartialResponse = (response?: IKibanaSearchResponse) => {
return Boolean(response && response.isRunning && response.isPartial);
};
export const isRunningResponse = (response?: IKibanaSearchResponse) => response?.isRunning ?? false;
export const getUserTimeZone = (
getConfig: AggTypesDependencies['getConfig'],

View file

@ -197,7 +197,7 @@ export type {
} from './search';
export type { ISearchOptions } from '../common';
export { isErrorResponse, isCompleteResponse, isPartialResponse } from '../common';
export { isRunningResponse } from '../common';
// Search namespace
export const search = {

View file

@ -249,29 +249,6 @@ describe('SearchInterceptor', () => {
expect(error).not.toHaveBeenCalled();
});
test('should abort if request is partial and not running (ES graceful error)', async () => {
const responses = [
{
time: 10,
value: {
isPartial: true,
isRunning: false,
rawResponse: {},
id: 1,
},
},
];
mockFetchImplementation(responses);
const response = searchInterceptor.search({});
response.subscribe({ next, error });
await timeTravel(10);
expect(error).toHaveBeenCalled();
expect(error.mock.calls[0][0]).toBeInstanceOf(Error);
});
test('should abort on user abort', async () => {
const responses = [
{
@ -1005,30 +982,6 @@ describe('SearchInterceptor', () => {
expect(fetchMock).toBeCalledTimes(2);
});
test('should deliver error to all replays', async () => {
const responses = [
{
time: 10,
value: {
isPartial: true,
isRunning: false,
rawResponse: {},
id: 1,
},
},
];
mockFetchImplementation(responses);
searchInterceptor.search(basicReq, { sessionId }).subscribe({ next, error, complete });
searchInterceptor.search(basicReq, { sessionId }).subscribe({ next, error, complete });
await timeTravel(10);
expect(fetchMock).toBeCalledTimes(1);
expect(error).toBeCalledTimes(2);
expect(error.mock.calls[0][0].message).toEqual('Received partial response');
expect(error.mock.calls[1][0].message).toEqual('Received partial response');
});
test('should ignore anything outside params when hashing', async () => {
mockFetchImplementation(basicCompleteResponse);
@ -1055,6 +1008,25 @@ describe('SearchInterceptor', () => {
expect(fetchMock).toBeCalledTimes(1);
});
test('should deliver error to all replays', async () => {
const responses = [
{
time: 10,
value: {},
},
];
mockFetchImplementation(responses);
searchInterceptor.search(basicReq, { sessionId }).subscribe({ next, error, complete });
searchInterceptor.search(basicReq, { sessionId }).subscribe({ next, error, complete });
await timeTravel(10);
expect(fetchMock).toBeCalledTimes(1);
expect(error).toBeCalledTimes(2);
expect(error.mock.calls[0][0].message).toEqual('Aborted');
expect(error.mock.calls[1][0].message).toEqual('Aborted');
});
test('should ignore preference when hashing', async () => {
mockFetchImplementation(basicCompleteResponse);

View file

@ -51,7 +51,7 @@ import {
IAsyncSearchOptions,
IKibanaSearchRequest,
IKibanaSearchResponse,
isCompleteResponse,
isRunningResponse,
ISearchOptions,
ISearchOptionsSerializable,
pollSearch,
@ -312,7 +312,7 @@ export class SearchInterceptor {
tap((response) => {
id = response.id;
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
searchTracker?.complete();
}
}),

View file

@ -120,23 +120,17 @@ describe('SearchResponseCache', () => {
isPartial: true,
isRunning: true,
rawResponse: {
t: 1,
},
},
{
isPartial: true,
isRunning: false,
rawResponse: {
t: 2,
t: 'a'.repeat(1000),
},
},
{} as any,
]);
cache.set('123', wrapWithAbortController(err$));
const errHandler = jest.fn();
await err$.toPromise().catch(errHandler);
expect(errHandler).toBeCalledTimes(0);
expect(errHandler).toBeCalledTimes(1);
expect(cache.get('123')).toBeUndefined();
});

View file

@ -8,7 +8,7 @@
import { Observable, Subscription } from 'rxjs';
import { SearchAbortController } from './search_abort_controller';
import { IKibanaSearchResponse, isErrorResponse } from '../../../common';
import { IKibanaSearchResponse } from '../../../common';
interface ResponseCacheItem {
response$: Observable<IKibanaSearchResponse>;
@ -102,14 +102,14 @@ export class SearchResponseCache {
next: (r) => {
// TODO: avoid stringiying. Get the size some other way!
const newSize = new Blob([JSON.stringify(r)]).size;
if (this.byteToMb(newSize) < this.maxCacheSizeMB && !isErrorResponse(r)) {
if (this.byteToMb(newSize) < this.maxCacheSizeMB) {
this.setItem(key, {
...cacheItem,
size: newSize,
});
this.shrink();
} else {
// Single item is too large to be cached, or an error response returned.
// Single item is too large to be cached
// Evict and ignore.
this.deleteItem(key);
}

View file

@ -9,7 +9,7 @@
import { once, debounce } from 'lodash';
import type { CoreSetup, Logger } from '@kbn/core/server';
import type { IEsSearchResponse, ISearchOptions } from '../../../../common';
import { isCompleteResponse } from '../../../../common';
import { isRunningResponse } from '../../../../common';
import { CollectedUsage } from './register';
const SAVED_OBJECT_ID = 'search-telemetry';
@ -86,7 +86,7 @@ export function searchUsageObserver(
) {
return {
next(response: IEsSearchResponse) {
if (isRestore || !isCompleteResponse(response)) return;
if (isRestore || isRunningResponse(response)) return;
logger.debug(`trackSearchStatus:success, took:${response.rawResponse.took}`);
usage?.trackSuccess(response.rawResponse.took);
},

View file

@ -19,7 +19,7 @@ describe('getSearchStatus', () => {
};
});
test('returns an error status if search is partial and not running', async () => {
test('returns a complete status if search is partial and not running', async () => {
mockClient.asyncSearch.status.mockResolvedValue({
body: {
is_partial: true,
@ -28,7 +28,7 @@ describe('getSearchStatus', () => {
},
});
const res = await getSearchStatus(mockClient, '123');
expect(res.status).toBe(SearchStatus.ERROR);
expect(res.status).toBe(SearchStatus.COMPLETE);
});
test('returns an error status if completion_status is an error', async () => {

View file

@ -29,7 +29,7 @@ export async function getSearchStatus(
{ meta: true }
);
const response = apiResponse.body;
if ((response.is_partial && !response.is_running) || response.completion_status >= 400) {
if (response.completion_status >= 400) {
return {
status: SearchStatus.ERROR,
error: i18n.translate('data.search.statusError', {
@ -37,7 +37,7 @@ export async function getSearchStatus(
values: { searchId: asyncId, errorCode: response.completion_status },
}),
};
} else if (!response.is_partial && !response.is_running) {
} else if (!response.is_running) {
return {
status: SearchStatus.COMPLETE,
error: undefined,

View file

@ -266,7 +266,9 @@ describe('SQL search strategy', () => {
);
const esSearch = await sqlSearchStrategyProvider(mockSearchConfig, mockLogger);
await esSearch.search({ id: 'foo', params: { query: 'query' } }, {}, mockDeps).toPromise();
esSearch.search({ id: 'foo', params: { query: 'query' } }, {}, mockDeps);
// await next tick. esSearch.search will not resolve until `is_running: false`
await new Promise((resolve) => process.nextTick(resolve));
expect(mockSqlClearCursor).not.toHaveBeenCalled();
});

View file

@ -69,7 +69,7 @@ export const sqlSearchStrategyProvider = (
));
}
if (!body.is_partial && !body.is_running && body.cursor && !keepCursor) {
if (!body.is_running && body.cursor && !keepCursor) {
try {
await client.sql.clearCursor({ cursor: body.cursor });
} catch (error) {

View file

@ -8,7 +8,7 @@
import { i18n } from '@kbn/i18n';
import { filter, map } from 'rxjs/operators';
import { lastValueFrom } from 'rxjs';
import { isCompleteResponse, ISearchSource } from '@kbn/data-plugin/public';
import { isRunningResponse, ISearchSource } from '@kbn/data-plugin/public';
import { SAMPLE_SIZE_SETTING, buildDataTableRecordList } from '@kbn/discover-utils';
import type { EsHitRecord } from '@kbn/discover-utils/types';
import { getSearchResponseInterceptedWarnings } from '@kbn/search-response-warnings';
@ -62,7 +62,7 @@ export const fetchDocuments = (
disableWarningToasts: true,
})
.pipe(
filter((res) => isCompleteResponse(res)),
filter((res) => !isRunningResponse(res)),
map((res) => {
return buildDataTableRecordList(res.rawResponse.hits.hits as EsHitRecord[], dataView);
})

View file

@ -7,7 +7,7 @@
*/
import { textBasedQueryStateToAstWithValidation } from '@kbn/data-plugin/common';
import { isCompleteResponse } from '@kbn/data-plugin/public';
import { isRunningResponse } from '@kbn/data-plugin/public';
import { DataView, DataViewType } from '@kbn/data-views-plugin/public';
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import { Datatable, isExpressionValueError } from '@kbn/expressions-plugin/common';
@ -209,7 +209,7 @@ const fetchTotalHitsSearchSource = async ({
disableWarningToasts: true, // TODO: show warnings as a badge next to total hits number
})
.pipe(
filter((res) => isCompleteResponse(res)),
filter((res) => !isRunningResponse(res)),
map((res) => res.rawResponse.hits.total as number),
catchError((error: Error) => of(error))
);

View file

@ -9,7 +9,7 @@ import { useRef, useCallback, useMemo } from 'react';
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { isCompleteResponse } from '@kbn/data-plugin/public';
import { isRunningResponse } from '@kbn/data-plugin/public';
import { useStorage } from '@kbn/ml-local-storage';
import { createCategoryRequest } from '../../../common/api/log_categorization/create_category_request';
@ -83,7 +83,7 @@ export function useCategorizeRequest() {
)
.subscribe({
next: (result) => {
if (isCompleteResponse(result)) {
if (!isRunningResponse(result)) {
resolve(processCategoryResults(result, field, unwrap));
} else {
// partial results

View file

@ -6,7 +6,7 @@
*/
import { useCallback, useRef, useState } from 'react';
import { type IKibanaSearchResponse, isCompleteResponse } from '@kbn/data-plugin/common';
import { type IKibanaSearchResponse, isRunningResponse } from '@kbn/data-plugin/common';
import { tap } from 'rxjs/operators';
import { useAiopsAppContext } from './use_aiops_app_context';
@ -31,7 +31,7 @@ export function useCancellableSearch() {
)
.subscribe({
next: (result) => {
if (isCompleteResponse(result)) {
if (!isRunningResponse(result)) {
setIsFetching(false);
resolve(result);
} else {

View file

@ -11,7 +11,7 @@ import {
DataView,
IKibanaSearchRequest,
IKibanaSearchResponse,
isCompleteResponse,
isRunningResponse,
TimeRange,
} from '@kbn/data-plugin/common';
@ -415,7 +415,7 @@ export const AnalyticsCollectionExploreTableLogic = kea<
KibanaLogic.values.data.search.showError(e);
},
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
const { items, totalCount } = parseResponse(response);
actions.setItems(items);

View file

@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { ESSearchResponse } from '@kbn/es-types';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import { IInspectorInfo, isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/common';
import { IInspectorInfo, isRunningResponse } from '@kbn/data-plugin/common';
import { getInspectResponse } from '../../common/utils/get_inspect_response';
import { useInspectorContext } from '../contexts/inspector/use_inspector_context';
import { FETCH_STATUS, useFetcher } from './use_fetcher';
@ -42,7 +42,7 @@ export const useEsSearch = <DocumentSource extends unknown, TParams extends esty
)
.subscribe({
next: (result) => {
if (isCompleteResponse(result)) {
if (!isRunningResponse(result)) {
if (addInspectorRequest) {
addInspectorRequest({
data: {
@ -72,32 +72,30 @@ export const useEsSearch = <DocumentSource extends unknown, TParams extends esty
}
},
error: (err) => {
if (isErrorResponse(err)) {
// eslint-disable-next-line no-console
console.error(err);
if (addInspectorRequest) {
addInspectorRequest({
data: {
_inspect: [
getInspectResponse({
startTime,
esRequestParams: params,
esResponse: null,
esError: { originalError: err, name: err.name, message: err.message },
esRequestStatus: 2,
operationName: name,
kibanaRequest: {
route: {
path: '/internal/bsearch',
method: 'POST',
},
} as any,
}),
],
},
status: FETCH_STATUS.SUCCESS,
});
}
// eslint-disable-next-line no-console
console.error(err);
if (addInspectorRequest) {
addInspectorRequest({
data: {
_inspect: [
getInspectResponse({
startTime,
esRequestParams: params,
esResponse: null,
esError: { originalError: err, name: err.name, message: err.message },
esRequestStatus: 2,
operationName: name,
kibanaRequest: {
route: {
path: '/internal/bsearch',
method: 'POST',
},
} as any,
}),
],
},
status: FETCH_STATUS.SUCCESS,
});
}
},
});

View file

@ -12,7 +12,7 @@ import { useDispatch } from 'react-redux';
import { Subscription } from 'rxjs';
import type { DataView } from '@kbn/data-views-plugin/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type {
Inspect,
PaginationInputPaginated,
@ -246,7 +246,7 @@ export const useTimelineEventsHandler = ({
)
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
setTimelineResponse((prevResponse) => {
const newTimelineResponse = {
...prevResponse,

View file

@ -9,7 +9,7 @@ import type { Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { EventEnrichmentRequestOptionsInput } from '../../../../../common/api/search_strategy';
import type { CtiEventEnrichmentStrategyResponse } from '../../../../../common/search_strategy/security_solution/cti';
import { CtiQueries } from '../../../../../common/search_strategy/security_solution/cti';
@ -44,4 +44,4 @@ export const getEventEnrichment = ({
export const getEventEnrichmentComplete = (
props: GetEventEnrichmentProps
): Observable<CtiEventEnrichmentStrategyResponse> =>
getEventEnrichment(props).pipe(filter((response) => isCompleteResponse(response)));
getEventEnrichment(props).pipe(filter((response) => !isRunningResponse(response)));

View file

@ -10,7 +10,7 @@ import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { inputsModel } from '../../../store';
import { useKibana } from '../../../lib/kibana';
import type {
@ -77,7 +77,7 @@ export const useTimelineLastEventTime = ({
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
setLoading(false);
setTimelineLastEventTimeResponse((prevResponse) => ({
...prevResponse,

View file

@ -10,7 +10,7 @@ import { getOr, noop } from 'lodash/fp';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { MatrixHistogramRequestOptionsInput } from '../../../../common/api/search_strategy';
import type { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types';
import type { inputsModel } from '../../store';
@ -121,7 +121,7 @@ export const useMatrixHistogram = ({
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
const histogramBuckets: Buckets = getOr(
bucketEmpty,
MatrixHistogramTypeToAggName[histogramType],

View file

@ -102,7 +102,7 @@ describe('useSearchStrategy', () => {
useSearchStrategy<FactoryQueryTypes>({ ...userSearchStrategyProps, initialResult })
);
expect(result.current.result).toBe(initialResult);
expect(result.current.result).toEqual(initialResult);
});
it('calls start with the given request', () => {
@ -278,9 +278,7 @@ describe('useSearchStrategy', () => {
it('should handle search error', () => {
mockResponse.mockImplementation(() => {
throw new Error(
'simulated search response error, which could be 1) undefined response, 2) response without rawResponse, or 3) partial response'
);
throw new Error('simulated search error');
});
const { result } = renderHook(() => useSearch<FactoryQueryTypes>(factoryQueryType));

View file

@ -9,7 +9,7 @@ import { noop, omit } from 'lodash/fp';
import { useCallback, useEffect, useRef, useMemo } from 'react';
import type { Observable } from 'rxjs';
import { useObservable } from '@kbn/securitysolution-hook-utils';
import { isCompleteResponse, isErrorResponse } from '@kbn/data-plugin/public';
import { isRunningResponse } from '@kbn/data-plugin/public';
import { AbortError } from '@kbn/kibana-utils-plugin/common';
import * as i18n from './translations';
@ -64,7 +64,7 @@ export const useSearch = <QueryType extends FactoryQueryTypes>(
abortSignal,
}
)
.pipe(filter((response) => isCompleteResponse(response)));
.pipe(filter((response) => !isRunningResponse(response)));
observable.subscribe({
next: (response) => {
@ -158,7 +158,7 @@ export const useSearchStrategy = <QueryType extends FactoryQueryTypes>({
}, [abort]);
const [formattedResult, inspect] = useMemo(() => {
if (isErrorResponse(result)) {
if (!result) {
return [initialResult, EMPTY_INSPECT];
}
return [

View file

@ -10,7 +10,7 @@ import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { NetworkKpiDnsRequestOptionsInput } from '../../../../../../common/api/search_strategy';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import type { inputsModel } from '../../../../../common/store';
@ -86,7 +86,7 @@ export const useNetworkKpiDns = ({
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
setLoading(false);
setNetworkKpiDnsResponse((prevResponse) => ({
...prevResponse,

View file

@ -10,7 +10,7 @@ import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { NetworkKpiEventsRequestOptionsInput } from '../../../../../../common/api/search_strategy';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import type { inputsModel } from '../../../../../common/store';
@ -90,7 +90,7 @@ export const useNetworkKpiNetworkEvents = ({
)
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
setLoading(false);
setNetworkKpiNetworkEventsResponse((prevResponse) => ({
...prevResponse,

View file

@ -10,7 +10,7 @@ import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { NetworkKpiTlsHandshakesRequestOptionsInput } from '../../../../../../common/api/search_strategy';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import type { inputsModel } from '../../../../../common/store';
@ -89,7 +89,7 @@ export const useNetworkKpiTlsHandshakes = ({
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
setLoading(false);
setNetworkKpiTlsHandshakesResponse((prevResponse) => ({
...prevResponse,

View file

@ -10,7 +10,7 @@ import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { NetworkKpiUniqueFlowsRequestOptionsInput } from '../../../../../../common/api/search_strategy';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import type { inputsModel } from '../../../../../common/store';
@ -89,7 +89,7 @@ export const useNetworkKpiUniqueFlows = ({
)
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
setLoading(false);
setNetworkKpiUniqueFlowsResponse((prevResponse) => ({
...prevResponse,

View file

@ -10,7 +10,7 @@ import { noop } from 'lodash/fp';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { NetworkKpiUniquePrivateIpsRequestOptionsInput } from '../../../../../../common/api/search_strategy';
import { useAppToasts } from '../../../../../common/hooks/use_app_toasts';
import type { inputsModel } from '../../../../../common/store';
@ -99,7 +99,7 @@ export const useNetworkKpiUniquePrivateIps = ({
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
setLoading(false);
setNetworkKpiUniquePrivateIpsResponse((prevResponse) => ({
...prevResponse,

View file

@ -6,7 +6,7 @@
*/
import type { SearchResponse } from '@elastic/elasticsearch/lib/api/types';
import { isCompleteResponse, type ISearchStart, isErrorResponse } from '@kbn/data-plugin/public';
import { isRunningResponse, type ISearchStart } from '@kbn/data-plugin/public';
export interface AlertsQueryParams {
alertIds: string[];
@ -47,14 +47,17 @@ export const createFindAlerts =
},
{ abortSignal: signal }
)
.subscribe((response) => {
if (isCompleteResponse(response)) {
$subscription.unsubscribe();
resolve(response.rawResponse);
} else if (isErrorResponse(response)) {
.subscribe({
next: (response) => {
if (!isRunningResponse(response)) {
$subscription.unsubscribe();
resolve(response.rawResponse);
}
},
error: (err) => {
$subscription.unsubscribe();
reject(new Error(`Error while loading alerts`));
}
},
});
});
};

View file

@ -9,7 +9,7 @@ import { filter } from 'rxjs/operators';
import { useEffect, useState } from 'react';
import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { isCompleteResponse } from '@kbn/data-plugin/public';
import { isRunningResponse } from '@kbn/data-plugin/public';
import type { ThreatIntelSourceRequestOptionsInput } from '../../../../common/api/search_strategy';
import { useKibana } from '../../../common/lib/kibana';
import type {
@ -51,7 +51,7 @@ export const getTiDataSourcesComplete = (
): Observable<CtiDataSourceStrategyResponse> => {
return getTiDataSources(props).pipe(
filter((response) => {
return isCompleteResponse(response);
return !isRunningResponse(response);
})
);
};

View file

@ -11,7 +11,7 @@ import ReactDOM from 'react-dom';
import deepEqual from 'fast-deep-equal';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type { TimelineEventsDetailsRequestOptionsInput } from '@kbn/timelines-plugin/common';
import { EntityType } from '@kbn/timelines-plugin/common';
import type { EcsSecurityExtension as Ecs } from '@kbn/securitysolution-ecs';
@ -88,7 +88,7 @@ export const useTimelineEventsDetails = ({
)
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
Promise.resolve().then(() => {
ReactDOM.unstable_batchedUpdates(() => {
setLoading(false);

View file

@ -12,7 +12,7 @@ import { useDispatch } from 'react-redux';
import { Subscription } from 'rxjs';
import type { DataView } from '@kbn/data-plugin/common';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type {
TimelineEqlRequestOptionsInput,
TimelineEventsAllOptionsInput,
@ -245,7 +245,7 @@ export const useTimelineEventsHandler = ({
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
endTracking('success');
setLoading(false);
setTimelineResponse((prevResponse) => {

View file

@ -10,7 +10,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import deepEqual from 'fast-deep-equal';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/public';
import { isRunningResponse } from '@kbn/data-plugin/public';
import { TimelineEventsQueries } from '@kbn/timelines-plugin/common';
import type { inputsModel } from '../../../common/store';
import { useKibana } from '../../../common/lib/kibana';
@ -63,7 +63,7 @@ export const useTimelineKpis = ({
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
setLoading(false);
setTimelineKpiResponse(response);
searchSubscription$.current.unsubscribe();

View file

@ -5,11 +5,7 @@
* 2.0.
*/
import {
IKibanaSearchResponse,
isCompleteResponse,
isErrorResponse,
} from '@kbn/data-plugin/common';
import { IKibanaSearchResponse, isRunningResponse } from '@kbn/data-plugin/common';
import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ESSearchResponse } from '@kbn/es-types';
import { FETCH_STATUS } from '@kbn/observability-shared-plugin/public';
@ -40,7 +36,7 @@ export const executeEsQueryAPI = async ({
)
.subscribe({
next: (result) => {
if (isCompleteResponse(result)) {
if (!isRunningResponse(result)) {
if (addInspectorRequest) {
addInspectorRequest({
data: {
@ -70,33 +66,31 @@ export const executeEsQueryAPI = async ({
}
},
error: (err) => {
if (isErrorResponse(err)) {
// eslint-disable-next-line no-console
console.error(err);
reject(err);
if (addInspectorRequest) {
addInspectorRequest({
data: {
_inspect: [
getInspectResponse({
startTime,
esRequestParams: params,
esResponse: null,
esError: { originalError: err, name: err.name, message: err.message },
esRequestStatus: 2,
operationName: name,
kibanaRequest: {
route: {
path: '/internal/bsearch',
method: 'POST',
},
} as any,
}),
],
},
status: FETCH_STATUS.SUCCESS,
});
}
// eslint-disable-next-line no-console
console.error(err);
reject(err);
if (addInspectorRequest) {
addInspectorRequest({
data: {
_inspect: [
getInspectResponse({
startTime,
esRequestParams: params,
esResponse: null,
esError: { originalError: err, name: err.name, message: err.message },
esRequestStatus: 2,
operationName: name,
kibanaRequest: {
route: {
path: '/internal/bsearch',
method: 'POST',
},
} as any,
}),
],
},
status: FETCH_STATUS.SUCCESS,
});
}
},
});

View file

@ -9,7 +9,7 @@ import { useEffect, useState } from 'react';
import {
IEsSearchRequest,
IKibanaSearchResponse,
isCompleteResponse,
isRunningResponse,
} from '@kbn/data-plugin/common';
import { useKibana } from '../../../hooks/use_kibana';
import { useSourcererDataView } from './use_sourcerer_data_view';
@ -61,7 +61,7 @@ export const useIndicatorsTotalCount = () => {
.search<IEsSearchRequest, IKibanaSearchResponse<RawIndicatorsResponse>>(req)
.subscribe({
next: (res) => {
if (isCompleteResponse(res)) {
if (!isRunningResponse(res)) {
const returnedCount = res.rawResponse.hits.total || 0;
setCount(returnedCount);

View file

@ -8,7 +8,7 @@
import {
IEsSearchRequest,
IKibanaSearchResponse,
isCompleteResponse,
isRunningResponse,
} from '@kbn/data-plugin/common';
import { ISearchStart } from '@kbn/data-plugin/public';
import { RequestAdapter } from '@kbn/inspector-plugin/common';
@ -93,7 +93,7 @@ export const search = async <TResponse, T = {}>(
})
.subscribe({
next: (response) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
inspect.recordRequestCompletion(searchRequest, response);
resolve(response.rawResponse);
}

View file

@ -287,9 +287,7 @@ describe('useFetchAlerts', () => {
});
it('handles search error', () => {
const obs$ = throwError(
'simulated search response error, which could be 1) undefined response, 2) response without rawResponse, or 3) partial response'
);
const obs$ = throwError('simulated search error');
dataSearchMock.mockReturnValue(obs$);
const { result } = renderHook(() => useFetchAlerts(args));

View file

@ -12,7 +12,7 @@ import { noop } from 'lodash';
import { useCallback, useEffect, useReducer, useRef, useMemo } from 'react';
import { Subscription } from 'rxjs';
import { isCompleteResponse } from '@kbn/data-plugin/common';
import { isRunningResponse } from '@kbn/data-plugin/common';
import type {
RuleRegistrySearchRequest,
RuleRegistrySearchRequestPagination,
@ -206,7 +206,7 @@ const useFetchAlerts = ({
)
.subscribe({
next: (response: RuleRegistrySearchResponse) => {
if (isCompleteResponse(response)) {
if (!isRunningResponse(response)) {
const { rawResponse } = response;
inspectQuery.current = {
request: response?.inspect?.dsl ?? [],

View file

@ -8,7 +8,7 @@
import type { ESSearchResponse } from '@kbn/es-types';
import {
DataPublicPluginStart,
isCompleteResponse,
isRunningResponse,
} from '@kbn/data-plugin/public';
import { IKibanaSearchRequest } from '@kbn/data-plugin/common';
import {
@ -117,7 +117,7 @@ async function esQuery<T>(
})
.subscribe({
next: (result) => {
if (isCompleteResponse(result)) {
if (!isRunningResponse(result)) {
resolve(result.rawResponse as any);
search$.unsubscribe();
}