Skip to content

Commit 9bc66ed

Browse files
Wylie Conlonkibanamachine
Wylie Conlon
andauthored
[Lens] Faster field existence failures by adding timeouts (#97188)
* [Lens] Faster field existence failures by adding timeouts * Increase shard timeout and add timeout-specific warning * Fix types * Fix import * Hide field info when in error state, but not timeout Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
1 parent 563e4e6 commit 9bc66ed

File tree

8 files changed

+181
-50
lines changed

8 files changed

+181
-50
lines changed

x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx

+25
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import React, { ChangeEvent, ReactElement } from 'react';
99
import { createMockedDragDropContext } from './mocks';
1010
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
1111
import { InnerIndexPatternDataPanel, IndexPatternDataPanel, MemoizedDataPanel } from './datapanel';
12+
import { FieldList } from './field_list';
1213
import { FieldItem } from './field_item';
1314
import { NoFieldsCallout } from './no_fields_callout';
1415
import { act } from 'react-dom/test-utils';
@@ -713,6 +714,30 @@ describe('IndexPattern Data Panel', () => {
713714
expect(wrapper.find(NoFieldsCallout).length).toEqual(2);
714715
});
715716

717+
it('should not allow field details when error', () => {
718+
const wrapper = mountWithIntl(
719+
<InnerIndexPatternDataPanel {...props} existenceFetchFailed={true} />
720+
);
721+
722+
expect(wrapper.find(FieldList).prop('fieldGroups')).toEqual(
723+
expect.objectContaining({
724+
AvailableFields: expect.objectContaining({ hideDetails: true }),
725+
})
726+
);
727+
});
728+
729+
it('should allow field details when timeout', () => {
730+
const wrapper = mountWithIntl(
731+
<InnerIndexPatternDataPanel {...props} existenceFetchTimeout={true} />
732+
);
733+
734+
expect(wrapper.find(FieldList).prop('fieldGroups')).toEqual(
735+
expect.objectContaining({
736+
AvailableFields: expect.objectContaining({ hideDetails: false }),
737+
})
738+
);
739+
});
740+
716741
it('should filter down by name', () => {
717742
const wrapper = mountWithIntl(<InnerIndexPatternDataPanel {...props} />);
718743
act(() => {

x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx

+11-4
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ export function IndexPatternDataPanel({
230230
onUpdateIndexPattern={onUpdateIndexPattern}
231231
existingFields={state.existingFields}
232232
existenceFetchFailed={state.existenceFetchFailed}
233+
existenceFetchTimeout={state.existenceFetchTimeout}
233234
dropOntoWorkspace={dropOntoWorkspace}
234235
hasSuggestionForField={hasSuggestionForField}
235236
/>
@@ -271,6 +272,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
271272
indexPatternRefs,
272273
indexPatterns,
273274
existenceFetchFailed,
275+
existenceFetchTimeout,
274276
query,
275277
dateRange,
276278
filters,
@@ -297,6 +299,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
297299
charts: ChartsPluginSetup;
298300
indexPatternFieldEditor: IndexPatternFieldEditorStart;
299301
existenceFetchFailed?: boolean;
302+
existenceFetchTimeout?: boolean;
300303
}) {
301304
const [localState, setLocalState] = useState<DataPanelState>({
302305
nameFilter: '',
@@ -314,7 +317,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
314317
(type) => type in fieldTypeNames
315318
);
316319

317-
const fieldInfoUnavailable = existenceFetchFailed || currentIndexPattern.hasRestrictions;
320+
const fieldInfoUnavailable =
321+
existenceFetchFailed || existenceFetchTimeout || currentIndexPattern.hasRestrictions;
318322

319323
const editPermission = indexPatternFieldEditor.userPermissions.editIndexPattern();
320324

@@ -389,7 +393,8 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
389393
}),
390394
isAffectedByGlobalFilter: !!filters.length,
391395
isAffectedByTimeFilter: true,
392-
hideDetails: fieldInfoUnavailable,
396+
// Show details on timeout but not failure
397+
hideDetails: fieldInfoUnavailable && !existenceFetchTimeout,
393398
defaultNoFieldsMessage: i18n.translate('xpack.lens.indexPatterns.noAvailableDataLabel', {
394399
defaultMessage: `There are no available fields that contain data.`,
395400
}),
@@ -438,11 +443,12 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
438443
return fieldGroupDefinitions;
439444
}, [
440445
allFields,
441-
existingFields,
442-
currentIndexPattern,
443446
hasSyncedExistingFields,
444447
fieldInfoUnavailable,
445448
filters.length,
449+
existenceFetchTimeout,
450+
currentIndexPattern,
451+
existingFields,
446452
]);
447453

448454
const fieldGroups: FieldGroups = useMemo(() => {
@@ -794,6 +800,7 @@ export const InnerIndexPatternDataPanel = function InnerIndexPatternDataPanel({
794800
filter={filter}
795801
currentIndexPatternId={currentIndexPatternId}
796802
existenceFetchFailed={existenceFetchFailed}
803+
existenceFetchTimeout={existenceFetchTimeout}
797804
existFieldsInIndex={!!allFields.length}
798805
dropOntoWorkspace={dropOntoWorkspace}
799806
hasSuggestionForField={hasSuggestionForField}

x-pack/plugins/lens/public/indexpattern_datasource/field_list.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export const FieldList = React.memo(function FieldList({
4545
exists,
4646
fieldGroups,
4747
existenceFetchFailed,
48+
existenceFetchTimeout,
4849
fieldProps,
4950
hasSyncedExistingFields,
5051
filter,
@@ -60,6 +61,7 @@ export const FieldList = React.memo(function FieldList({
6061
fieldProps: FieldItemSharedProps;
6162
hasSyncedExistingFields: boolean;
6263
existenceFetchFailed?: boolean;
64+
existenceFetchTimeout?: boolean;
6365
filter: {
6466
nameFilter: string;
6567
typeFilter: string[];
@@ -194,6 +196,7 @@ export const FieldList = React.memo(function FieldList({
194196
);
195197
}}
196198
showExistenceFetchError={existenceFetchFailed}
199+
showExistenceFetchTimeout={existenceFetchTimeout}
197200
renderCallout={
198201
<NoFieldsCallout
199202
isAffectedByGlobalFilter={fieldGroup.isAffectedByGlobalFilter}

x-pack/plugins/lens/public/indexpattern_datasource/fields_accordion.tsx

+40-19
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface FieldsAccordionProps {
5050
renderCallout: JSX.Element;
5151
exists: (field: IndexPatternField) => boolean;
5252
showExistenceFetchError?: boolean;
53+
showExistenceFetchTimeout?: boolean;
5354
hideDetails?: boolean;
5455
groupIndex: number;
5556
dropOntoWorkspace: DatasourceDataPanelProps['dropOntoWorkspace'];
@@ -73,6 +74,7 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({
7374
exists,
7475
hideDetails,
7576
showExistenceFetchError,
77+
showExistenceFetchTimeout,
7678
groupIndex,
7779
dropOntoWorkspace,
7880
hasSuggestionForField,
@@ -133,25 +135,44 @@ export const FieldsAccordion = memo(function InnerFieldsAccordion({
133135
}, [label, helpTooltip]);
134136

135137
const extraAction = useMemo(() => {
136-
return showExistenceFetchError ? (
137-
<EuiIconTip
138-
aria-label={i18n.translate('xpack.lens.indexPattern.existenceErrorAriaLabel', {
139-
defaultMessage: 'Existence fetch failed',
140-
})}
141-
type="alert"
142-
color="warning"
143-
content={i18n.translate('xpack.lens.indexPattern.existenceErrorLabel', {
144-
defaultMessage: "Field information can't be loaded",
145-
})}
146-
/>
147-
) : hasLoaded ? (
148-
<EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}>
149-
{fieldsCount}
150-
</EuiNotificationBadge>
151-
) : (
152-
<EuiLoadingSpinner size="m" />
153-
);
154-
}, [showExistenceFetchError, hasLoaded, isFiltered, fieldsCount]);
138+
if (showExistenceFetchError) {
139+
return (
140+
<EuiIconTip
141+
aria-label={i18n.translate('xpack.lens.indexPattern.existenceErrorAriaLabel', {
142+
defaultMessage: 'Existence fetch failed',
143+
})}
144+
type="alert"
145+
color="warning"
146+
content={i18n.translate('xpack.lens.indexPattern.existenceErrorLabel', {
147+
defaultMessage: "Field information can't be loaded",
148+
})}
149+
/>
150+
);
151+
}
152+
if (showExistenceFetchTimeout) {
153+
return (
154+
<EuiIconTip
155+
aria-label={i18n.translate('xpack.lens.indexPattern.existenceTimeoutAriaLabel', {
156+
defaultMessage: 'Existence fetch timed out',
157+
})}
158+
type="clock"
159+
color="warning"
160+
content={i18n.translate('xpack.lens.indexPattern.existenceTimeoutLabel', {
161+
defaultMessage: 'Field information took too long',
162+
})}
163+
/>
164+
);
165+
}
166+
if (hasLoaded) {
167+
return (
168+
<EuiNotificationBadge size="m" color={isFiltered ? 'accent' : 'subdued'}>
169+
{fieldsCount}
170+
</EuiNotificationBadge>
171+
);
172+
}
173+
174+
return <EuiLoadingSpinner size="m" />;
175+
}, [showExistenceFetchError, showExistenceFetchTimeout, hasLoaded, isFiltered, fieldsCount]);
155176

156177
return (
157178
<EuiAccordion

x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts

+52
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
injectReferences,
1818
} from './loader';
1919
import { IndexPatternsContract } from '../../../../../src/plugins/data/public';
20+
import { HttpFetchError } from '../../../../../src/core/public';
2021
import {
2122
IndexPatternPersistedState,
2223
IndexPatternPrivateState,
@@ -877,6 +878,7 @@ describe('loader', () => {
877878
foo: 'bar',
878879
isFirstExistenceFetch: false,
879880
existenceFetchFailed: false,
881+
existenceFetchTimeout: false,
880882
existingFields: {
881883
'1': { ip1_field_1: true, ip1_field_2: true },
882884
'2': { ip2_field_1: true, ip2_field_2: true },
@@ -957,6 +959,56 @@ describe('loader', () => {
957959
}) as IndexPatternPrivateState;
958960

959961
expect(newState.existenceFetchFailed).toEqual(true);
962+
expect(newState.existenceFetchTimeout).toEqual(false);
963+
expect(newState.existingFields['1']).toEqual({
964+
field1: true,
965+
field2: true,
966+
});
967+
});
968+
969+
it('should set all fields to available and existence error flag if the request times out', async () => {
970+
const setState = jest.fn();
971+
const fetchJson = (jest.fn((path: string) => {
972+
return new Promise((resolve, reject) => {
973+
reject(
974+
new HttpFetchError(
975+
'timeout',
976+
'name',
977+
({} as unknown) as Request,
978+
({ status: 408 } as unknown) as Response
979+
)
980+
);
981+
});
982+
}) as unknown) as HttpHandler;
983+
984+
const args = {
985+
dateRange: { fromDate: '1900-01-01', toDate: '2000-01-01' },
986+
fetchJson,
987+
indexPatterns: [
988+
{
989+
id: '1',
990+
title: '1',
991+
hasRestrictions: false,
992+
fields: [{ name: 'field1' }, { name: 'field2' }] as IndexPatternField[],
993+
},
994+
],
995+
setState,
996+
dslQuery,
997+
showNoDataPopover: jest.fn(),
998+
currentIndexPatternTitle: 'abc',
999+
isFirstExistenceFetch: false,
1000+
};
1001+
1002+
await syncExistingFields(args);
1003+
1004+
const [fn] = setState.mock.calls[0];
1005+
const newState = fn({
1006+
foo: 'bar',
1007+
existingFields: {},
1008+
}) as IndexPatternPrivateState;
1009+
1010+
expect(newState.existenceFetchFailed).toEqual(false);
1011+
expect(newState.existenceFetchTimeout).toEqual(true);
9601012
expect(newState.existingFields['1']).toEqual({
9611013
field1: true,
9621014
field2: true,

x-pack/plugins/lens/public/indexpattern_datasource/loader.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -445,16 +445,18 @@ export async function syncExistingFields({
445445
...state,
446446
isFirstExistenceFetch: false,
447447
existenceFetchFailed: false,
448+
existenceFetchTimeout: false,
448449
existingFields: emptinessInfo.reduce((acc, info) => {
449450
acc[info.indexPatternTitle] = booleanMap(info.existingFieldNames);
450451
return acc;
451452
}, state.existingFields),
452453
}));
453454
} catch (e) {
454-
// show all fields as available if fetch failed
455+
// show all fields as available if fetch failed or timed out
455456
setState((state) => ({
456457
...state,
457-
existenceFetchFailed: true,
458+
existenceFetchFailed: e.res?.status !== 408,
459+
existenceFetchTimeout: e.res?.status === 408,
458460
existingFields: indexPatterns.reduce((acc, pattern) => {
459461
acc[pattern.title] = booleanMap(pattern.fields.map((field) => field.name));
460462
return acc;

x-pack/plugins/lens/public/indexpattern_datasource/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export interface IndexPatternPrivateState {
8787
existingFields: Record<string, Record<string, boolean>>;
8888
isFirstExistenceFetch: boolean;
8989
existenceFetchFailed?: boolean;
90+
existenceFetchTimeout?: boolean;
9091
}
9192

9293
export interface IndexPatternRef {

x-pack/plugins/lens/server/routes/existing_fields.ts

+45-25
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,15 @@ export async function existingFieldsRoute(setup: CoreSetup<PluginStartContract>,
6868
}),
6969
});
7070
} catch (e) {
71+
if (e instanceof errors.TimeoutError) {
72+
logger.info(`Field existence check timed out on ${req.params.indexPatternId}`);
73+
// 408 is Request Timeout
74+
return res.customError({ statusCode: 408, body: e.message });
75+
}
7176
logger.info(
72-
`Field existence check failed: ${isBoomError(e) ? e.output.payload.message : e.message}`
77+
`Field existence check failed on ${req.params.indexPatternId}: ${
78+
isBoomError(e) ? e.output.payload.message : e.message
79+
}`
7380
);
7481
if (e instanceof errors.ResponseError && e.statusCode === 404) {
7582
return res.notFound({ body: e.message });
@@ -182,31 +189,44 @@ async function fetchIndexPatternStats({
182189

183190
const scriptedFields = fields.filter((f) => f.isScript);
184191
const runtimeFields = fields.filter((f) => f.runtimeField);
185-
const { body: result } = await client.search({
186-
index,
187-
body: {
188-
size: SAMPLE_SIZE,
189-
query,
190-
sort: timeFieldName && fromDate && toDate ? [{ [timeFieldName]: 'desc' }] : [],
191-
fields: ['*'],
192-
_source: false,
193-
runtime_mappings: runtimeFields.reduce((acc, field) => {
194-
if (!field.runtimeField) return acc;
195-
// @ts-expect-error @elastic/elasticsearch StoredScript.language is required
196-
acc[field.name] = field.runtimeField;
197-
return acc;
198-
}, {} as Record<string, estypes.RuntimeField>),
199-
script_fields: scriptedFields.reduce((acc, field) => {
200-
acc[field.name] = {
201-
script: {
202-
lang: field.lang!,
203-
source: field.script!,
204-
},
205-
};
206-
return acc;
207-
}, {} as Record<string, estypes.ScriptField>),
192+
const { body: result } = await client.search(
193+
{
194+
index,
195+
body: {
196+
size: SAMPLE_SIZE,
197+
query,
198+
// Sorted queries are usually able to skip entire shards that don't match
199+
sort: timeFieldName && fromDate && toDate ? [{ [timeFieldName]: 'desc' }] : [],
200+
fields: ['*'],
201+
_source: false,
202+
runtime_mappings: runtimeFields.reduce((acc, field) => {
203+
if (!field.runtimeField) return acc;
204+
// @ts-expect-error @elastic/elasticsearch StoredScript.language is required
205+
acc[field.name] = field.runtimeField;
206+
return acc;
207+
}, {} as Record<string, estypes.RuntimeField>),
208+
script_fields: scriptedFields.reduce((acc, field) => {
209+
acc[field.name] = {
210+
script: {
211+
lang: field.lang!,
212+
source: field.script!,
213+
},
214+
};
215+
return acc;
216+
}, {} as Record<string, estypes.ScriptField>),
217+
// Small improvement because there is overhead in counting
218+
track_total_hits: false,
219+
// Per-shard timeout, must be lower than overall. Shards return partial results on timeout
220+
timeout: '4500ms',
221+
},
208222
},
209-
});
223+
{
224+
// Global request timeout. Will cancel the request if exceeded. Overrides the elasticsearch.requestTimeout
225+
requestTimeout: '5000ms',
226+
// Fails fast instead of retrying- default is to retry
227+
maxRetries: 0,
228+
}
229+
);
210230
return result.hits.hits;
211231
}
212232

0 commit comments

Comments
 (0)