Compare commits

...

4 Commits

Author SHA1 Message Date
Steven Uray
d96b7ad0c3 Revert "fix: Ensure alerts & reports aren't schduled when flag is off (#16639)"
This reverts commit 4dc859f89e.
2021-09-22 16:48:27 -07:00
Elizabeth Thompson
4f7f5f3f5c fix shared query (#16753)
(cherry picked from commit f032cc254c)
2021-09-22 16:48:11 -07:00
Elizabeth Thompson
8ac598dfff only fetch db function when db exists in sql lab (#16754)
(cherry picked from commit d375538671)
2021-09-22 16:47:54 -07:00
Elizabeth Thompson
054d294a85 update execution logs and states for alerts (#16736)
(cherry picked from commit 2a25e2d7ca)
2021-09-22 16:47:30 -07:00
9 changed files with 119 additions and 97 deletions

View File

@@ -612,44 +612,44 @@ describe('async actions', () => {
}); });
describe('queryEditorSetSql', () => { describe('queryEditorSetSql', () => {
const sql = 'SELECT * ';
const expectedActions = [
{
type: actions.QUERY_EDITOR_SET_SQL,
queryEditor,
sql,
},
];
describe('with backend persistence flag on', () => { describe('with backend persistence flag on', () => {
it('updates the tab state in the backend', () => { it('updates the tab state in the backend', () => {
expect.assertions(2); expect.assertions(2);
const sql = 'SELECT * ';
const store = mockStore({}); const store = mockStore({});
return store return store
.dispatch(actions.queryEditorSetSql(queryEditor, sql)) .dispatch(actions.queryEditorSetSql(queryEditor, sql))
.then(() => { .then(() => {
expect(store.getActions()).toHaveLength(0); expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1); expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
}); });
}); });
}); });
}); describe('with backend persistence flag off', () => {
describe('with backend persistence flag off', () => { it('does not update the tab state in the backend', () => {
it('does not update the tab state in the backend', () => { const backendPersistenceOffMock = jest
const backendPersistenceOffMock = jest .spyOn(featureFlags, 'isFeatureEnabled')
.spyOn(featureFlags, 'isFeatureEnabled') .mockImplementation(
.mockImplementation( feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), );
);
const sql = 'SELECT * ';
const store = mockStore({});
const expectedActions = [
{
type: actions.QUERY_EDITOR_SET_SQL,
queryEditor,
sql,
},
];
store.dispatch(actions.queryEditorSetSql(queryEditor, sql)); const store = mockStore({});
expect(store.getActions()).toEqual(expectedActions); store.dispatch(actions.queryEditorSetSql(queryEditor, sql));
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
backendPersistenceOffMock.mockRestore(); expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
backendPersistenceOffMock.mockRestore();
});
}); });
}); });

View File

@@ -898,6 +898,8 @@ export function updateSavedQuery(query) {
export function queryEditorSetSql(queryEditor, sql) { export function queryEditorSetSql(queryEditor, sql) {
return function (dispatch) { return function (dispatch) {
// saved query and set tab state use this action
dispatch({ type: QUERY_EDITOR_SET_SQL, queryEditor, sql });
if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) { if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
return SupersetClient.put({ return SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`), endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
@@ -914,7 +916,7 @@ export function queryEditorSetSql(queryEditor, sql) {
), ),
); );
} }
return dispatch({ type: QUERY_EDITOR_SET_SQL, queryEditor, sql }); return Promise.resolve();
}; };
} }

View File

@@ -30,6 +30,7 @@ import {
AceCompleterKeyword, AceCompleterKeyword,
FullSQLEditor as AceEditor, FullSQLEditor as AceEditor,
} from 'src/components/AsyncAceEditor'; } from 'src/components/AsyncAceEditor';
import { QueryEditor } from '../types';
type HotKey = { type HotKey = {
key: string; key: string;
@@ -51,7 +52,7 @@ interface Props {
tables: any[]; tables: any[];
functionNames: string[]; functionNames: string[];
extendedTables: Array<{ name: string; columns: any[] }>; extendedTables: Array<{ name: string; columns: any[] }>;
queryEditor: any; queryEditor: QueryEditor;
height: string; height: string;
hotkeys: HotKey[]; hotkeys: HotKey[];
onChange: (sql: string) => void; onChange: (sql: string) => void;
@@ -86,10 +87,12 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
componentDidMount() { componentDidMount() {
// Making sure no text is selected from previous mount // Making sure no text is selected from previous mount
this.props.actions.queryEditorSetSelectedText(this.props.queryEditor, null); this.props.actions.queryEditorSetSelectedText(this.props.queryEditor, null);
this.props.actions.queryEditorSetFunctionNames( if (this.props.queryEditor.dbId) {
this.props.queryEditor, this.props.actions.queryEditorSetFunctionNames(
this.props.queryEditor.dbId, this.props.queryEditor,
); this.props.queryEditor.dbId,
);
}
this.setAutoCompleter(this.props); this.setAutoCompleter(this.props);
} }
@@ -228,8 +231,8 @@ class AceEditorWrapper extends React.PureComponent<Props, State> {
getAceAnnotations() { getAceAnnotations() {
const { validationResult } = this.props.queryEditor; const { validationResult } = this.props.queryEditor;
const resultIsReady = validationResult && validationResult.completed; const resultIsReady = validationResult?.completed;
if (resultIsReady && validationResult.errors.length > 0) { if (resultIsReady && validationResult?.errors?.length) {
const errors = validationResult.errors.map((err: any) => ({ const errors = validationResult.errors.map((err: any) => ({
type: 'error', type: 'error',
row: err.line_number - 1, row: err.line_number - 1,

View File

@@ -26,16 +26,10 @@ import CopyToClipboard from 'src/components/CopyToClipboard';
import { storeQuery } from 'src/utils/common'; import { storeQuery } from 'src/utils/common';
import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags'; import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
import { QueryEditor } from '../types';
interface ShareSqlLabQueryPropTypes { interface ShareSqlLabQueryPropTypes {
queryEditor: { queryEditor: QueryEditor;
dbId: number;
title: string;
schema: string;
autorun: boolean;
sql: string;
remoteId: number | null;
};
addDangerToast: (msg: string) => void; addDangerToast: (msg: string) => void;
} }

View File

@@ -69,3 +69,16 @@ export type Query = {
queryLimit: number; queryLimit: number;
limitingFactor: string; limitingFactor: string;
}; };
export interface QueryEditor {
dbId?: number;
title: string;
schema: string;
autorun: boolean;
sql: string;
remoteId: number | null;
validationResult?: {
completed: boolean;
errors: SupersetError[];
};
}

View File

@@ -40,7 +40,6 @@ from superset.models.reports import (
) )
from superset.reports.commands.alert import AlertCommand from superset.reports.commands.alert import AlertCommand
from superset.reports.commands.exceptions import ( from superset.reports.commands.exceptions import (
ReportScheduleAlertEndGracePeriodError,
ReportScheduleAlertGracePeriodError, ReportScheduleAlertGracePeriodError,
ReportScheduleCsvFailedError, ReportScheduleCsvFailedError,
ReportScheduleCsvTimeout, ReportScheduleCsvTimeout,
@@ -403,7 +402,7 @@ class BaseReportState:
def is_in_grace_period(self) -> bool: def is_in_grace_period(self) -> bool:
""" """
Checks if an alert is on it's grace period Checks if an alert is in it's grace period
""" """
last_success = ReportScheduleDAO.find_last_success_log( last_success = ReportScheduleDAO.find_last_success_log(
self._report_schedule, session=self._session self._report_schedule, session=self._session
@@ -418,7 +417,7 @@ class BaseReportState:
def is_in_error_grace_period(self) -> bool: def is_in_error_grace_period(self) -> bool:
""" """
Checks if an alert/report on error is on it's notification grace period Checks if an alert/report on error is in it's notification grace period
""" """
last_success = ReportScheduleDAO.find_last_error_notification( last_success = ReportScheduleDAO.find_last_error_notification(
self._report_schedule, session=self._session self._report_schedule, session=self._session
@@ -435,7 +434,7 @@ class BaseReportState:
def is_on_working_timeout(self) -> bool: def is_on_working_timeout(self) -> bool:
""" """
Checks if an alert is on a working timeout Checks if an alert is in a working timeout
""" """
last_working = ReportScheduleDAO.find_last_entered_working_log( last_working = ReportScheduleDAO.find_last_entered_working_log(
self._report_schedule, session=self._session self._report_schedule, session=self._session
@@ -533,7 +532,6 @@ class ReportSuccessState(BaseReportState):
current_states = [ReportState.SUCCESS, ReportState.GRACE] current_states = [ReportState.SUCCESS, ReportState.GRACE]
def next(self) -> None: def next(self) -> None:
self.set_state_and_log(ReportState.WORKING)
if self._report_schedule.type == ReportScheduleType.ALERT: if self._report_schedule.type == ReportScheduleType.ALERT:
if self.is_in_grace_period(): if self.is_in_grace_period():
self.set_state_and_log( self.set_state_and_log(
@@ -541,11 +539,23 @@ class ReportSuccessState(BaseReportState):
error_message=str(ReportScheduleAlertGracePeriodError()), error_message=str(ReportScheduleAlertGracePeriodError()),
) )
return return
self.set_state_and_log( self.set_state_and_log(ReportState.WORKING)
ReportState.NOOP, try:
error_message=str(ReportScheduleAlertEndGracePeriodError()), if not AlertCommand(self._report_schedule).run():
) self.set_state_and_log(ReportState.NOOP)
return return
except CommandException as ex:
self.send_error(
f"Error occurred for {self._report_schedule.type}:"
f" {self._report_schedule.name}",
str(ex),
)
self.set_state_and_log(
ReportState.ERROR,
error_message=REPORT_SCHEDULE_ERROR_NOTIFICATION_MARKER,
)
raise ex
try: try:
self.send() self.send()
self.set_state_and_log(ReportState.SUCCESS) self.set_state_and_log(ReportState.SUCCESS)

View File

@@ -19,7 +19,7 @@ import logging
from celery.exceptions import SoftTimeLimitExceeded from celery.exceptions import SoftTimeLimitExceeded
from dateutil import parser from dateutil import parser
from superset import app, is_feature_enabled from superset import app
from superset.commands.exceptions import CommandException from superset.commands.exceptions import CommandException
from superset.extensions import celery_app from superset.extensions import celery_app
from superset.reports.commands.exceptions import ReportScheduleUnexpectedError from superset.reports.commands.exceptions import ReportScheduleUnexpectedError
@@ -37,8 +37,6 @@ def scheduler() -> None:
""" """
Celery beat main scheduler for reports Celery beat main scheduler for reports
""" """
if not is_feature_enabled("ALERT_REPORTS"):
return
with session_scope(nullpool=True) as session: with session_scope(nullpool=True) as session:
active_schedules = ReportScheduleDAO.find_active(session) active_schedules = ReportScheduleDAO.find_active(session)
for active_schedule in active_schedules: for active_schedule in active_schedules:

View File

@@ -365,30 +365,47 @@ def create_alert_slack_chart_success():
cleanup_report_schedule(report_schedule) cleanup_report_schedule(report_schedule)
@pytest.fixture() @pytest.fixture(
def create_alert_slack_chart_grace(): params=["alert1",]
)
def create_alert_slack_chart_grace(request):
param_config = {
"alert1": {
"sql": "SELECT count(*) from test_table",
"validator_type": ReportScheduleValidatorType.OPERATOR,
"validator_config_json": '{"op": "<", "threshold": 10}',
},
}
with app.app_context(): with app.app_context():
chart = db.session.query(Slice).first() chart = db.session.query(Slice).first()
report_schedule = create_report_notification( example_database = get_example_database()
slack_channel="slack_channel", with create_test_table_context(example_database):
chart=chart, report_schedule = create_report_notification(
report_type=ReportScheduleType.ALERT, slack_channel="slack_channel",
) chart=chart,
report_schedule.last_state = ReportState.GRACE report_type=ReportScheduleType.ALERT,
report_schedule.last_eval_dttm = datetime(2020, 1, 1, 0, 0) database=example_database,
sql=param_config[request.param]["sql"],
validator_type=param_config[request.param]["validator_type"],
validator_config_json=param_config[request.param][
"validator_config_json"
],
)
report_schedule.last_state = ReportState.GRACE
report_schedule.last_eval_dttm = datetime(2020, 1, 1, 0, 0)
log = ReportExecutionLog( log = ReportExecutionLog(
report_schedule=report_schedule, report_schedule=report_schedule,
state=ReportState.SUCCESS, state=ReportState.SUCCESS,
start_dttm=report_schedule.last_eval_dttm, start_dttm=report_schedule.last_eval_dttm,
end_dttm=report_schedule.last_eval_dttm, end_dttm=report_schedule.last_eval_dttm,
scheduled_dttm=report_schedule.last_eval_dttm, scheduled_dttm=report_schedule.last_eval_dttm,
) )
db.session.add(log) db.session.add(log)
db.session.commit() db.session.commit()
yield report_schedule yield report_schedule
cleanup_report_schedule(report_schedule) cleanup_report_schedule(report_schedule)
@pytest.fixture( @pytest.fixture(
@@ -1051,11 +1068,18 @@ def test_report_schedule_success_grace(create_alert_slack_chart_success):
@pytest.mark.usefixtures("create_alert_slack_chart_grace") @pytest.mark.usefixtures("create_alert_slack_chart_grace")
def test_report_schedule_success_grace_end(create_alert_slack_chart_grace): @patch("superset.reports.notifications.slack.WebClient.files_upload")
@patch("superset.utils.screenshots.ChartScreenshot.get_screenshot")
def test_report_schedule_success_grace_end(
screenshot_mock, file_upload_mock, create_alert_slack_chart_grace
):
""" """
ExecuteReport Command: Test report schedule on grace to noop ExecuteReport Command: Test report schedule on grace to noop
""" """
# set current time to within the grace period
screenshot_mock.return_value = SCREENSHOT_FILE
# set current time to after the grace period
current_time = create_alert_slack_chart_grace.last_eval_dttm + timedelta( current_time = create_alert_slack_chart_grace.last_eval_dttm + timedelta(
seconds=create_alert_slack_chart_grace.grace_period + 1 seconds=create_alert_slack_chart_grace.grace_period + 1
) )
@@ -1066,7 +1090,7 @@ def test_report_schedule_success_grace_end(create_alert_slack_chart_grace):
).run() ).run()
db.session.commit() db.session.commit()
assert create_alert_slack_chart_grace.last_state == ReportState.NOOP assert create_alert_slack_chart_grace.last_state == ReportState.SUCCESS
@pytest.mark.usefixtures("create_alert_email_chart") @pytest.mark.usefixtures("create_alert_email_chart")

View File

@@ -115,25 +115,3 @@ def test_scheduler_celery_no_timeout_utc(execute_mock):
db.session.delete(report_schedule) db.session.delete(report_schedule)
db.session.commit() db.session.commit()
app.config["ALERT_REPORTS_WORKING_TIME_OUT_KILL"] = True app.config["ALERT_REPORTS_WORKING_TIME_OUT_KILL"] = True
@patch("superset.tasks.scheduler.is_feature_enabled")
@patch("superset.tasks.scheduler.execute.apply_async")
def test_scheduler_feature_flag_off(execute_mock, is_feature_enabled):
"""
Reports scheduler: Test scheduler with feature flag off
"""
with app.app_context():
is_feature_enabled.return_value = False
report_schedule = insert_report_schedule(
type=ReportScheduleType.ALERT,
name="report",
crontab="0 9 * * *",
timezone="UTC",
)
with freeze_time("2020-01-01T09:00:00Z"):
scheduler()
execute_mock.assert_not_called()
db.session.delete(report_schedule)
db.session.commit()