feat(sqllab): Add timeout on fetching query results (#29959)
This commit is contained in:
parent
23467bd7e4
commit
ff3b86b5ff
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}),
|
});
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
||||||
|
|
|
||||||
|
|
@ -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],
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue