Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions superset-frontend/spec/fixtures/mockDatabases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export default {
1: {
allow_ctas: false,
allow_cvas: false,
allow_dml: false,
allow_file_upload: false,
allow_run_async: true,
backend: 'postgresql',
database_name: 'examples',
expose_in_sqllab: true,
force_ctas_schema: null,
id: 1,
},
};

export const disabledAsyncDb = {
21: {
allow_ctas: false,
allow_cvas: false,
allow_dml: false,
allow_file_upload: false,
allow_run_async: false,
backend: 'postgresql',
database_name: 'examples',
expose_in_sqllab: true,
force_ctas_schema: null,
id: 21,
},
};
30 changes: 16 additions & 14 deletions superset-frontend/src/SqlLab/actions/sqlLab.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,28 +252,30 @@ export function querySuccess(query, results) {
return { type: QUERY_SUCCESS, query, results };
}

export function queryFailed(query, msg, link, errors) {
export function logFailedQuery(query, errors) {
return function (dispatch) {
const eventData = {
has_err: true,
start_offset: query.startDttm,
ts: new Date().getTime(),
};
errors?.forEach(({ error_type: errorType, extra }) => {
const messages = extra?.issue_codes?.map(({ message }) => message) || [
errorType,
];
messages.forEach(message => {
dispatch(
logEvent(LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY, {
...eventData,
error_type: errorType,
error_details: message,
}),
);
});
errors?.forEach(({ error_type: errorType, message, extra }) => {
const issueCodes = extra?.issue_codes?.map(({ code }) => code) || [-1];
Comment on lines +262 to +263

This comment was marked as resolved.

dispatch(
logEvent(LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY, {
...eventData,
error_type: errorType,
issue_codes: issueCodes,
error_details: message,
}),
);
});
};
}

export function queryFailed(query, msg, link, errors) {
return function (dispatch) {
dispatch(logFailedQuery(query, errors));
dispatch({ type: QUERY_FAILED, query, msg, link, errors });
};
}
Expand Down
10 changes: 2 additions & 8 deletions superset-frontend/src/SqlLab/actions/sqlLab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ describe('async actions', () => {
});

it('calls queryFailed on fetch error and logs the error details', () => {
expect.assertions(3);
expect.assertions(2);

fetchMock.post(
runQueryEndpoint,
Expand All @@ -312,20 +312,14 @@ describe('async actions', () => {
const expectedActionTypes = [
actions.START_QUERY,
LOG_EVENT,
LOG_EVENT,
actions.QUERY_FAILED,
];
const { dispatch } = store;
const request = actions.runQuery(query);
return request(dispatch, () => initialState).then(() => {
const actions = store.getActions();
expect(actions.map(a => a.type)).toEqual(expectedActionTypes);
expect(actions[1].payload.eventData.error_details).toContain(
'Issue 1000',
);
expect(actions[2].payload.eventData.error_details).toContain(
'Issue 1001',
);
expect(actions[1].payload.eventData.issue_codes).toEqual([1000, 1001]);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryState } from '@superset-ui/core';
import fetchMock from 'fetch-mock';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { render, waitFor } from 'spec/helpers/testing-library';
import { LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY } from 'src/logger/LogUtils';
import {
CLEAR_INACTIVE_QUERIES,
REFRESH_QUERIES,
Expand All @@ -31,9 +33,13 @@ import QueryAutoRefresh, {
} from 'src/SqlLab/components/QueryAutoRefresh';
import { successfulQuery, runningQuery } from 'src/SqlLab/fixtures';
import { QueryDictionary } from 'src/SqlLab/types';
import mockDatabases from 'spec/fixtures/mockDatabases';

const middlewares = [thunk];
const mockStore = configureStore(middlewares);
const mockState = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: could be good to put in a reusable fixture file if there's a good place for it.

databases: mockDatabases,
};

// NOTE: The uses of @ts-ignore in this file is to enable testing of bad inputs to verify the
// function / component handles bad data elegantly
Expand Down Expand Up @@ -106,7 +112,9 @@ describe('QueryAutoRefresh', () => {
});

it('Attempts to refresh when given pending query', async () => {
const store = mockStore();
const store = mockStore({
sqlLab: { ...mockState },
});
fetchMock.get(refreshApi, {
result: [
{
Expand Down Expand Up @@ -135,7 +143,7 @@ describe('QueryAutoRefresh', () => {
});

it('Attempts to clear inactive queries when updated queries are empty', async () => {
const store = mockStore();
const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, {
result: [],
});
Expand Down Expand Up @@ -163,7 +171,7 @@ describe('QueryAutoRefresh', () => {
});

it('Does not fail and attempts to refresh when given pending query and invalid query', async () => {
const store = mockStore();
const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, {
result: [
{
Expand Down Expand Up @@ -193,7 +201,7 @@ describe('QueryAutoRefresh', () => {
});

it('Does NOT Attempt to refresh when given only completed queries', async () => {
const store = mockStore();
const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, {
result: [
{
Expand All @@ -220,4 +228,57 @@ describe('QueryAutoRefresh', () => {
);
expect(fetchMock.calls(refreshApi)).toHaveLength(0);
});

it('logs the failed error for async queries', async () => {
const store = mockStore({ sqlLab: { ...mockState } });
fetchMock.get(refreshApi, {
result: [
{
id: runningQuery.id,
dbId: 1,
state: QueryState.Failed,
extra: {
errors: [
{
error_type: 'TEST_ERROR',
level: 'error',
message: 'Syntax invalid',
extra: {
issue_codes: [
{
code: 102,
message: 'DB failed',
},
],
},
},
],
},
},
],
});
render(
<QueryAutoRefresh
queries={runningQueries}
queriesLastUpdate={queriesLastUpdate}
/>,
{ useRedux: true, store },
);
await waitFor(
() =>
expect(store.getActions()).toContainEqual(
expect.objectContaining({
payload: expect.objectContaining({
eventName: LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY,
eventData: expect.objectContaining({
error_type: 'TEST_ERROR',
error_details: 'Syntax invalid',
issue_codes: [102],
}),
}),
}),
),
{ timeout: QUERY_UPDATE_FREQ + 100 },
);
});
});
30 changes: 28 additions & 2 deletions superset-frontend/src/SqlLab/components/QueryAutoRefresh/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,25 @@
* under the License.
*/
import { useRef } from 'react';
import { useDispatch } from 'react-redux';
import { useSelector, useDispatch } from 'react-redux';
import { isObject } from 'lodash';
import rison from 'rison';
import {
SupersetClient,
Query,
runningQueryStateList,
QueryResponse,
QueryState,
lruCache,
} from '@superset-ui/core';
import { QueryDictionary } from 'src/SqlLab/types';
import { QueryDictionary, SqlLabRootState } from 'src/SqlLab/types';
import useInterval from 'src/SqlLab/utils/useInterval';
import {
refreshQueries,
clearInactiveQueries,
logFailedQuery,
} from 'src/SqlLab/actions/sqlLab';
import type { DatabaseObject } from 'src/features/databases/types';

export const QUERY_UPDATE_FREQ = 2000;
const QUERY_UPDATE_BUFFER_MS = 5000;
Expand Down Expand Up @@ -67,6 +71,17 @@ function QueryAutoRefresh({
// pendingRequest check ensures we only have one active http call to check for query statuses
const pendingRequestRef = useRef(false);
const cleanInactiveRequestRef = useRef(false);
const failedQueries = useRef(lruCache(1000));
const databases = useSelector<SqlLabRootState>(
({ sqlLab }) => sqlLab.databases,
) as Record<string, DatabaseObject>;
Comment on lines +75 to +77

This comment was marked as resolved.

const asyncFetchDbs = useRef(
new Set(
Object.values(databases)
.filter(({ allow_run_async }) => Boolean(allow_run_async))
.map(({ id }) => id),
),
);
const dispatch = useDispatch();

const checkForRefresh = () => {
Expand Down Expand Up @@ -97,6 +112,17 @@ function QueryAutoRefresh({
{},
) ?? {};
dispatch(refreshQueries(queries));
jsonPayload.result.forEach(query => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok with disregarding @korbit-ai's comment given it's probably rare that we have multiple queries in a failed state, can probably afford the multi-rerender in those rare cases.

const { id, dbId, state } = query;
if (
asyncFetchDbs.current.has(dbId) &&
!failedQueries.current.has(id) &&
state === QueryState.Failed
) {
dispatch(logFailedQuery(query, query.extra?.errors));
failedQueries.current.set(id, true);
}
});
Comment on lines +115 to +125

This comment was marked as resolved.

} else {
dispatch(clearInactiveQueries(QUERY_UPDATE_FREQ));
}
Expand Down