[Profiling] Auto-abort requests (#142750) (#142817)

(cherry picked from commit c05190e516)

Co-authored-by: Dario Gieselaar <dario.gieselaar@elastic.co>
This commit is contained in:
Kibana Machine 2022-10-06 02:22:27 -06:00 committed by GitHub
parent 195c2622ce
commit 237173dfbb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 204 additions and 134 deletions

View file

@ -43,35 +43,40 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
services: { fetchElasticFlamechart },
} = useProfilingDependencies();
const state = useTimeRangeAsync(() => {
return Promise.all([
fetchElasticFlamechart({
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
kuery,
}),
comparisonTimeRange.start && comparisonTimeRange.end
? fetchElasticFlamechart({
timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000,
timeTo: new Date(comparisonTimeRange.end).getTime() / 1000,
kuery: comparisonKuery,
})
: Promise.resolve(undefined),
]).then(([primaryFlamegraph, comparisonFlamegraph]) => {
return {
primaryFlamegraph,
comparisonFlamegraph,
};
});
}, [
timeRange.start,
timeRange.end,
kuery,
comparisonTimeRange.start,
comparisonTimeRange.end,
comparisonKuery,
fetchElasticFlamechart,
]);
const state = useTimeRangeAsync(
({ http }) => {
return Promise.all([
fetchElasticFlamechart({
http,
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
kuery,
}),
comparisonTimeRange.start && comparisonTimeRange.end
? fetchElasticFlamechart({
http,
timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000,
timeTo: new Date(comparisonTimeRange.end).getTime() / 1000,
kuery: comparisonKuery,
})
: Promise.resolve(undefined),
]).then(([primaryFlamegraph, comparisonFlamegraph]) => {
return {
primaryFlamegraph,
comparisonFlamegraph,
};
});
},
[
timeRange.start,
timeRange.end,
kuery,
comparisonTimeRange.start,
comparisonTimeRange.end,
comparisonKuery,
fetchElasticFlamechart,
]
);
const { data } = state;
@ -173,7 +178,6 @@ export function FlameGraphsView({ children }: { children: React.ReactElement })
<AsyncComponent {...state} style={{ height: '100%' }} size="xl">
<FlameGraph
id="flamechart"
height={'100%'}
primaryFlamegraph={data?.primaryFlamegraph}
comparisonFlamegraph={data?.comparisonFlamegraph}
comparisonMode={comparisonMode}

View file

@ -145,7 +145,6 @@ function FlameGraphTooltip({
export interface FlameGraphProps {
id: string;
height: number | string;
comparisonMode: FlameGraphComparisonMode;
primaryFlamegraph?: ElasticFlameGraph;
comparisonFlamegraph?: ElasticFlameGraph;
@ -153,7 +152,6 @@ export interface FlameGraphProps {
export const FlameGraph: React.FC<FlameGraphProps> = ({
id,
height,
comparisonMode,
primaryFlamegraph,
comparisonFlamegraph,

View file

@ -42,28 +42,36 @@ export function FunctionsView({ children }: { children: React.ReactElement }) {
services: { fetchTopNFunctions },
} = useProfilingDependencies();
const state = useTimeRangeAsync(() => {
return fetchTopNFunctions({
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
startIndex: 0,
endIndex: 1000,
kuery,
});
}, [timeRange.start, timeRange.end, kuery, fetchTopNFunctions]);
const state = useTimeRangeAsync(
({ http }) => {
return fetchTopNFunctions({
http,
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
startIndex: 0,
endIndex: 1000,
kuery,
});
},
[timeRange.start, timeRange.end, kuery, fetchTopNFunctions]
);
const comparisonState = useTimeRangeAsync(() => {
if (!comparisonTimeRange.start || !comparisonTimeRange.end) {
return undefined;
}
return fetchTopNFunctions({
timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000,
timeTo: new Date(comparisonTimeRange.end).getTime() / 1000,
startIndex: 0,
endIndex: 1000,
kuery: comparisonKuery,
});
}, [comparisonTimeRange.start, comparisonTimeRange.end, comparisonKuery, fetchTopNFunctions]);
const comparisonState = useTimeRangeAsync(
({ http }) => {
if (!comparisonTimeRange.start || !comparisonTimeRange.end) {
return undefined;
}
return fetchTopNFunctions({
http,
timeFrom: new Date(comparisonTimeRange.start).getTime() / 1000,
timeTo: new Date(comparisonTimeRange.end).getTime() / 1000,
startIndex: 0,
endIndex: 1000,
kuery: comparisonKuery,
});
},
[comparisonTimeRange.start, comparisonTimeRange.end, comparisonKuery, fetchTopNFunctions]
);
const routePath = useProfilingRoutePath() as
| '/functions'

View file

@ -50,29 +50,33 @@ export function StackTracesView() {
rangeTo,
});
const state = useTimeRangeAsync(() => {
if (!topNType) {
return Promise.resolve({ charts: [], metadata: {} });
}
return fetchTopN({
type: topNType,
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
kuery,
}).then((response: TopNResponse) => {
const totalCount = response.TotalCount;
const samples = response.TopN;
const charts = groupSamplesByCategory({
samples,
totalCount,
metadata: response.Metadata,
labels: response.Labels,
const state = useTimeRangeAsync(
({ http }) => {
if (!topNType) {
return Promise.resolve({ charts: [], metadata: {} });
}
return fetchTopN({
http,
type: topNType,
timeFrom: new Date(timeRange.start).getTime() / 1000,
timeTo: new Date(timeRange.end).getTime() / 1000,
kuery,
}).then((response: TopNResponse) => {
const totalCount = response.TotalCount;
const samples = response.TopN;
const charts = groupSamplesByCategory({
samples,
totalCount,
metadata: response.Metadata,
labels: response.Labels,
});
return {
charts,
};
});
return {
charts,
};
});
}, [topNType, timeRange.start, timeRange.end, fetchTopN, kuery]);
},
[topNType, timeRange.start, timeRange.end, fetchTopN, kuery]
);
const [highlightedSubchart, setHighlightedSubchart] = useState<TopNSubchart | undefined>(
undefined

View file

@ -4,8 +4,10 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { HttpStart } from '@kbn/core-http-browser';
import { useEffect, useState } from 'react';
import { HttpFetchOptions, HttpHandler, HttpStart } from '@kbn/core-http-browser';
import { AbortError } from '@kbn/kibana-utils-plugin/common';
import { useEffect, useRef, useState } from 'react';
import { Overwrite, ValuesType } from 'utility-types';
import { useProfilingDependencies } from '../components/contexts/profiling_dependencies/use_profiling_dependencies';
export enum AsyncStatus {
@ -20,8 +22,22 @@ export interface AsyncState<T> {
status: AsyncStatus;
}
const HTTP_METHODS = ['fetch', 'get', 'post', 'put', 'delete', 'patch'] as const;
type HttpMethod = ValuesType<typeof HTTP_METHODS>;
type AutoAbortedHttpMethod = (
path: string,
options: Omit<HttpFetchOptions, 'signal'>
) => ReturnType<HttpHandler>;
export type AutoAbortedHttpService = Overwrite<
HttpStart,
Record<HttpMethod, AutoAbortedHttpMethod>
>;
export type UseAsync = <T>(
fn: ({ http }: { http: HttpStart }) => Promise<T> | undefined,
fn: ({ http }: { http: AutoAbortedHttpService }) => Promise<T> | undefined,
dependencies: any[]
) => AsyncState<T>;
@ -37,8 +53,30 @@ export const useAsync: UseAsync = (fn, dependencies) => {
const { data, error } = asyncState;
const controllerRef = useRef(new AbortController());
useEffect(() => {
const returnValue = fn({ http });
controllerRef.current.abort();
controllerRef.current = new AbortController();
const autoAbortedMethods = {} as Record<HttpMethod, AutoAbortedHttpMethod>;
for (const key of HTTP_METHODS) {
autoAbortedMethods[key] = (path, options) => {
return http[key](path, { ...options, signal: controllerRef.current.signal }).catch(
(err) => {
if (err.name === 'AbortError') {
// return never-resolving promise
return new Promise(() => {});
}
throw err;
}
);
};
}
const returnValue = fn({ http: { ...http, ...autoAbortedMethods } });
if (returnValue === undefined) {
setAsyncState({
@ -63,13 +101,23 @@ export const useAsync: UseAsync = (fn, dependencies) => {
});
returnValue.catch((nextError) => {
if (nextError instanceof AbortError) {
return;
}
setAsyncState({
status: AsyncStatus.Settled,
error: nextError,
});
throw nextError;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [http, ...dependencies]);
useEffect(() => {
return () => {
controllerRef.current.abort();
};
}, []);
return asyncState;
};

View file

@ -90,7 +90,7 @@ export class ProfilingPlugin implements Plugin {
unknown
];
const profilingFetchServices = getServices(coreStart);
const profilingFetchServices = getServices();
const { renderApp } = await import('./app');
function pushKueryToSubject(location: Location) {

View file

@ -4,21 +4,23 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { CoreStart, HttpFetchQuery } from '@kbn/core/public';
import { HttpFetchQuery } from '@kbn/core/public';
import { getRoutePaths } from '../common';
import { BaseFlameGraph, createFlameGraph, ElasticFlameGraph } from '../common/flamegraph';
import { TopNFunctions } from '../common/functions';
import { TopNResponse } from '../common/topn';
import { AutoAbortedHttpService } from './hooks/use_async';
export interface Services {
fetchTopN: (params: {
http: AutoAbortedHttpService;
type: string;
timeFrom: number;
timeTo: number;
kuery: string;
}) => Promise<TopNResponse>;
fetchTopNFunctions: (params: {
http: AutoAbortedHttpService;
timeFrom: number;
timeTo: number;
startIndex: number;
@ -26,42 +28,31 @@ export interface Services {
kuery: string;
}) => Promise<TopNFunctions>;
fetchElasticFlamechart: (params: {
http: AutoAbortedHttpService;
timeFrom: number;
timeTo: number;
kuery: string;
}) => Promise<ElasticFlameGraph>;
}
export function getServices(core: CoreStart): Services {
export function getServices(): Services {
const paths = getRoutePaths();
return {
fetchTopN: async ({ type, timeFrom, timeTo, kuery }) => {
fetchTopN: async ({ http, type, timeFrom, timeTo, kuery }) => {
try {
const query: HttpFetchQuery = {
timeFrom,
timeTo,
kuery,
};
return await core.http.get(`${paths.TopN}/${type}`, { query });
return await http.get(`${paths.TopN}/${type}`, { query });
} catch (e) {
return e;
}
},
fetchTopNFunctions: async ({
timeFrom,
timeTo,
startIndex,
endIndex,
kuery,
}: {
timeFrom: number;
timeTo: number;
startIndex: number;
endIndex: number;
kuery: string;
}) => {
fetchTopNFunctions: async ({ http, timeFrom, timeTo, startIndex, endIndex, kuery }) => {
try {
const query: HttpFetchQuery = {
timeFrom,
@ -70,28 +61,20 @@ export function getServices(core: CoreStart): Services {
endIndex,
kuery,
};
return await core.http.get(paths.TopNFunctions, { query });
return await http.get(paths.TopNFunctions, { query });
} catch (e) {
return e;
}
},
fetchElasticFlamechart: async ({
timeFrom,
timeTo,
kuery,
}: {
timeFrom: number;
timeTo: number;
kuery: string;
}) => {
fetchElasticFlamechart: async ({ http, timeFrom, timeTo, kuery }) => {
try {
const query: HttpFetchQuery = {
timeFrom,
timeTo,
kuery,
};
const baseFlamegraph: BaseFlameGraph = await core.http.get(paths.Flamechart, { query });
const baseFlamegraph = (await http.get(paths.Flamechart, { query })) as BaseFlameGraph;
return createFlameGraph(baseFlamegraph);
} catch (e) {
return e;

View file

@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema';
import { RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import { createCalleeTree } from '../../common/callee';
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
import { createBaseFlameGraph } from '../../common/flamegraph';
import { createProfilingEsClient } from '../utils/create_profiling_es_client';
import { withProfilingSpan } from '../utils/with_profiling_span';
@ -71,14 +72,8 @@ export function registerFlameChartSearchRoute({ router, logger }: RouteRegisterP
logger.info('returning payload response to client');
return response.ok({ body: flamegraph });
} catch (e) {
logger.error(e);
return response.customError({
statusCode: e.statusCode ?? 500,
body: {
message: e.message,
},
});
} catch (error) {
return handleRouteHandlerError({ error, logger, response });
}
}
);

View file

@ -9,6 +9,7 @@ import { schema, TypeOf } from '@kbn/config-schema';
import { RouteRegisterParameters } from '.';
import { getRoutePaths } from '../../common';
import { createTopNFunctions } from '../../common/functions';
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
import { createProfilingEsClient } from '../utils/create_profiling_es_client';
import { withProfilingSpan } from '../utils/with_profiling_span';
import { getClient } from './compat';
@ -72,14 +73,8 @@ export function registerTopNFunctionsSearchRoute({ router, logger }: RouteRegist
return response.ok({
body: topNFunctions,
});
} catch (e) {
logger.error(e);
return response.customError({
statusCode: e.statusCode ?? 500,
body: {
message: e.message,
},
});
} catch (error) {
return handleRouteHandlerError({ error, logger, response });
}
}
);

View file

@ -14,6 +14,7 @@ import { computeBucketWidthFromTimeRangeAndBucketCount } from '../../common/hist
import { groupStackFrameMetadataByStackTrace, StackTraceID } from '../../common/profiling';
import { getFieldNameForTopNType, TopNType } from '../../common/stack_traces';
import { createTopNSamples, getTopNAggregationRequest, TopNResponse } from '../../common/topn';
import { handleRouteHandlerError } from '../utils/handle_route_error_handler';
import { ProfilingRequestHandlerContext } from '../types';
import { createProfilingEsClient, ProfilingESClient } from '../utils/create_profiling_es_client';
import { withProfilingSpan } from '../utils/with_profiling_span';
@ -189,15 +190,8 @@ export function queryTopNCommon(
kuery,
}),
});
} catch (e) {
logger.error(e);
return response.customError({
statusCode: e.statusCode ?? 500,
body: {
message: 'Profiling TopN request failed: ' + e.message + '; full error ' + e.toString(),
},
});
} catch (error) {
return handleRouteHandlerError({ error, logger, response });
}
}
);

View file

@ -0,0 +1,41 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { KibanaResponseFactory } from '@kbn/core-http-server';
import { Logger } from '@kbn/logging';
import { WrappedElasticsearchClientError } from '@kbn/observability-plugin/server';
import { errors } from '@elastic/elasticsearch';
export function handleRouteHandlerError({
error,
logger,
response,
}: {
error: any;
response: KibanaResponseFactory;
logger: Logger;
}) {
if (
error instanceof WrappedElasticsearchClientError &&
error.originalError instanceof errors.RequestAbortedError
) {
return response.custom({
statusCode: 499,
body: {
message: 'Client closed request',
},
});
}
logger.error(error);
return response.customError({
statusCode: error.statusCode ?? 500,
body: {
message: error.message,
},
});
}