feat(sqllab): Add timeout on fetching query results (#29959)

This commit is contained in:
JUST.in DO IT 2024-09-12 09:41:28 -07:00 committed by GitHub
parent 23467bd7e4
commit ff3b86b5ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 70 additions and 10 deletions

View File

@ -294,21 +294,25 @@ export function requestQueryResults(query) {
return { type: REQUEST_QUERY_RESULTS, query }; return { type: REQUEST_QUERY_RESULTS, query };
} }
export function fetchQueryResults(query, displayLimit) { export function fetchQueryResults(query, displayLimit, timeoutInMs) {
return function (dispatch) { return function (dispatch, getState) {
const { SQLLAB_QUERY_RESULT_TIMEOUT } = getState().common?.conf ?? {};
dispatch(requestQueryResults(query)); dispatch(requestQueryResults(query));
const queryParams = rison.encode({ const queryParams = rison.encode({
key: query.resultsKey, key: query.resultsKey,
rows: displayLimit || null, rows: displayLimit || null,
}); });
const timeout = timeoutInMs ?? SQLLAB_QUERY_RESULT_TIMEOUT;
const controller = new AbortController();
return SupersetClient.get({ return SupersetClient.get({
endpoint: `/api/v1/sqllab/results/?q=${queryParams}`, endpoint: `/api/v1/sqllab/results/?q=${queryParams}`,
parseMethod: 'json-bigint', parseMethod: 'json-bigint',
...(timeout && { timeout, signal: controller.signal }),
}) })
.then(({ json }) => dispatch(querySuccess(query, json))) .then(({ json }) => dispatch(querySuccess(query, json)))
.catch(response => .catch(response => {
controller.abort();
getClientErrorObject(response).then(error => { getClientErrorObject(response).then(error => {
const message = const message =
error.error || error.error ||
@ -318,8 +322,8 @@ export function fetchQueryResults(query, displayLimit) {
return dispatch( return dispatch(
queryFailed(query, message, error.link, error.errors), queryFailed(query, message, error.link, error.errors),
); );
}), });
); });
}; };
} }

View File

@ -174,8 +174,9 @@ describe('async actions', () => {
describe('fetchQueryResults', () => { describe('fetchQueryResults', () => {
const makeRequest = () => { const makeRequest = () => {
const store = mockStore(initialState);
const request = actions.fetchQueryResults(query); const request = actions.fetchQueryResults(query);
return request(dispatch); return request(dispatch, store.getState);
}; };
it('makes the fetch request', () => { it('makes the fetch request', () => {

View File

@ -32,6 +32,7 @@ import {
initialState, initialState,
user, user,
queryWithNoQueryLimit, queryWithNoQueryLimit,
failedQueryWithFrontendTimeoutErrors,
} from 'src/SqlLab/fixtures'; } from 'src/SqlLab/fixtures';
const mockedProps = { const mockedProps = {
@ -104,6 +105,16 @@ const failedQueryWithErrorsState = {
}, },
}, },
}; };
const failedQueryWithTimeoutState = {
...initialState,
sqlLab: {
...initialState.sqlLab,
queries: {
[failedQueryWithFrontendTimeoutErrors.id]:
failedQueryWithFrontendTimeoutErrors,
},
},
};
const newProps = { const newProps = {
displayLimit: 1001, displayLimit: 1001,
@ -319,6 +330,18 @@ describe('ResultSet', () => {
expect(screen.getByText('Database error')).toBeInTheDocument(); expect(screen.getByText('Database error')).toBeInTheDocument();
}); });
test('should render a timeout error with a retrial button', async () => {
await waitFor(() => {
setup(
{ ...mockedProps, queryId: failedQueryWithFrontendTimeoutErrors.id },
mockStore(failedQueryWithTimeoutState),
);
});
expect(
screen.getByRole('button', { name: /Retry fetching results/i }),
).toBeInTheDocument();
});
test('renders if there is no limit in query.results but has queryLimit', async () => { test('renders if there is no limit in query.results but has queryLimit', async () => {
const query = { const query = {
...queries[0], ...queries[0],

View File

@ -42,6 +42,7 @@ import {
css, css,
getNumberFormatter, getNumberFormatter,
getExtensionsRegistry, getExtensionsRegistry,
ErrorTypeEnum,
} from '@superset-ui/core'; } from '@superset-ui/core';
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace'; import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
import { import {
@ -225,8 +226,8 @@ const ResultSet = ({
reRunQueryIfSessionTimeoutErrorOnMount(); reRunQueryIfSessionTimeoutErrorOnMount();
}, [reRunQueryIfSessionTimeoutErrorOnMount]); }, [reRunQueryIfSessionTimeoutErrorOnMount]);
const fetchResults = (q: typeof query) => { const fetchResults = (q: typeof query, timeout?: number) => {
dispatch(fetchQueryResults(q, displayLimit)); dispatch(fetchQueryResults(q, displayLimit, timeout));
}; };
const prevQuery = usePrevious(query); const prevQuery = usePrevious(query);
@ -549,7 +550,18 @@ const ResultSet = ({
link={query.link} link={query.link}
source="sqllab" source="sqllab"
/> />
{trackingUrl} {(query?.extra?.errors?.[0] || query?.errors?.[0])?.error_type ===
ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR ? (
<Button
className="sql-result-track-job"
buttonSize="small"
onClick={() => fetchResults(query, 0)}
>
{t('Retry fetching results')}
</Button>
) : (
trackingUrl
)}
</ResultlessStyles> </ResultlessStyles>
); );
} }

View File

@ -22,6 +22,7 @@ import { ColumnKeyTypeType } from 'src/SqlLab/components/ColumnElement';
import { import {
DatasourceType, DatasourceType,
denormalizeTimestamp, denormalizeTimestamp,
ErrorTypeEnum,
GenericDataType, GenericDataType,
QueryResponse, QueryResponse,
QueryState, QueryState,
@ -546,6 +547,20 @@ export const failedQueryWithErrors = {
tempTable: '', tempTable: '',
}; };
export const failedQueryWithFrontendTimeoutErrors = {
...failedQueryWithErrorMessage,
errors: [
{
error_type: ErrorTypeEnum.FRONTEND_TIMEOUT_ERROR,
message: 'Request timed out',
level: 'error',
extra: {
timeout: 10,
},
},
],
};
const baseQuery: QueryResponse = { const baseQuery: QueryResponse = {
queryId: 567, queryId: 567,
dbId: 1, dbId: 1,

View File

@ -1047,6 +1047,10 @@ SQLLAB_ASYNC_TIME_LIMIT_SEC = int(timedelta(hours=6).total_seconds())
# timeout. # timeout.
SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = int(timedelta(seconds=10).total_seconds()) SQLLAB_QUERY_COST_ESTIMATE_TIMEOUT = int(timedelta(seconds=10).total_seconds())
# Timeout duration for SQL Lab fetching query results by the resultsKey.
# 0 means no timeout.
SQLLAB_QUERY_RESULT_TIMEOUT = 0
# The cost returned by the databases is a relative value; in order to map the cost to # The cost returned by the databases is a relative value; in order to map the cost to
# a tangible value you need to define a custom formatter that takes into consideration # a tangible value you need to define a custom formatter that takes into consideration
# your specific infrastructure. For example, you could analyze queries a posteriori by # your specific infrastructure. For example, you could analyze queries a posteriori by

View File

@ -115,6 +115,7 @@ FRONTEND_CONF_KEYS = (
"PREVENT_UNSAFE_DEFAULT_URLS_ON_DATASET", "PREVENT_UNSAFE_DEFAULT_URLS_ON_DATASET",
"JWT_ACCESS_CSRF_COOKIE_NAME", "JWT_ACCESS_CSRF_COOKIE_NAME",
"SLACK_ENABLE_AVATARS", "SLACK_ENABLE_AVATARS",
"SQLLAB_QUERY_RESULT_TIMEOUT",
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)