Skip to content

Commit

Permalink
chore: Migrate /superset/stop_query/ to API v1 (#22624)
Browse files Browse the repository at this point in the history
  • Loading branch information
diegomedina248 committed Jan 16, 2023
1 parent 80b3113 commit 3ed288d
Show file tree
Hide file tree
Showing 11 changed files with 1,177 additions and 434 deletions.
1,258 changes: 833 additions & 425 deletions docs/static/resources/openapi.json

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions superset-frontend/src/SqlLab/actions/sqlLab.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,9 +450,9 @@ export function validateQuery(queryEditor, sql) {
export function postStopQuery(query) {
return function (dispatch) {
return SupersetClient.post({
endpoint: '/superset/stop_query/',
postPayload: { client_id: query.id },
stringify: false,
endpoint: '/api/v1/query/stop',
body: JSON.stringify({ client_id: query.id }),
headers: { 'Content-Type': 'application/json' },
})
.then(() => dispatch(stopQuery(query)))
.then(() => dispatch(addSuccessToast(t('Query was stopped.'))))
Expand Down
11 changes: 8 additions & 3 deletions superset-frontend/src/SqlLab/actions/sqlLab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,15 @@ describe('async actions', () => {
});

describe('postStopQuery', () => {
const stopQueryEndpoint = 'glob:*/superset/stop_query/*';
const stopQueryEndpoint = 'glob:*/api/v1/query/stop';
fetchMock.post(stopQueryEndpoint, {});
const baseQuery = {
...query,
id: 'test_foo',
};

const makeRequest = () => {
const request = actions.postStopQuery(query);
const request = actions.postStopQuery(baseQuery);
return request(dispatch);
};

Expand All @@ -346,7 +350,8 @@ describe('async actions', () => {

return makeRequest().then(() => {
const call = fetchMock.calls(stopQueryEndpoint)[0];
expect(call[1].body.get('client_id')).toBe(query.id);
const body = JSON.parse(call[1].body);
expect(body.client_id).toBe(baseQuery.id);
});
});
});
Expand Down
1 change: 1 addition & 0 deletions superset/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class RouteMethod: # pylint: disable=too-few-public-methods
"get_data": "read",
"samples": "read",
"delete_ssh_tunnel": "write",
"stop_query": "read",
}

EXTRA_FORM_DATA_APPEND_KEYS = {
Expand Down
4 changes: 4 additions & 0 deletions superset/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,7 @@ def __init__(self, error: ValidationError):

class SupersetCancelQueryException(SupersetException):
status = 422


class QueryNotFoundException(SupersetException):
status = 404
78 changes: 76 additions & 2 deletions superset/queries/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,29 @@
# under the License.
import logging

import backoff
from flask_appbuilder.api import expose, protect, request, safe
from flask_appbuilder.models.sqla.interface import SQLAInterface

from superset import db, event_logger
from superset.constants import MODEL_API_RW_METHOD_PERMISSION_MAP, RouteMethod
from superset.databases.filters import DatabaseFilter
from superset.exceptions import SupersetException
from superset.models.sql_lab import Query
from superset.queries.dao import QueryDAO
from superset.queries.filters import QueryFilter
from superset.queries.schemas import openapi_spec_methods_override, QuerySchema
from superset.views.base_api import BaseSupersetModelRestApi, RelatedFieldFilter
from superset.queries.schemas import (
openapi_spec_methods_override,
QuerySchema,
StopQuerySchema,
)
from superset.superset_typing import FlaskResponse
from superset.views.base_api import (
BaseSupersetModelRestApi,
RelatedFieldFilter,
requires_json,
statsd_metrics,
)
from superset.views.filters import BaseFilterRelatedUsers, FilterRelatedOwners

logger = logging.getLogger(__name__)
Expand All @@ -43,6 +58,7 @@ class QueryRestApi(BaseSupersetModelRestApi):
RouteMethod.GET_LIST,
RouteMethod.RELATED,
RouteMethod.DISTINCT,
"stop_query",
}

list_columns = [
Expand Down Expand Up @@ -95,9 +111,11 @@ class QueryRestApi(BaseSupersetModelRestApi):
base_filters = [["id", QueryFilter, lambda: []]]
base_order = ("changed_on", "desc")
list_model_schema = QuerySchema()
stop_query_schema = StopQuerySchema()

openapi_spec_tag = "Queries"
openapi_spec_methods = openapi_spec_methods_override
openapi_spec_component_schemas = (StopQuerySchema,)

order_columns = [
"changed_on",
Expand All @@ -123,3 +141,59 @@ class QueryRestApi(BaseSupersetModelRestApi):
base_related_field_filters = {"database": [["id", DatabaseFilter, lambda: []]]}
allowed_rel_fields = {"database", "user"}
allowed_distinct_fields = {"status"}

@expose("/stop", methods=["POST"])
@protect()
@safe
@statsd_metrics
@event_logger.log_this_with_context(
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
f".stop_query",
log_to_statsd=False,
)
@backoff.on_exception(
backoff.constant,
Exception,
interval=1,
on_backoff=lambda details: db.session.rollback(),
on_giveup=lambda details: db.session.rollback(),
max_tries=5,
)
@requires_json
def stop_query(self) -> FlaskResponse:
"""Manually stop a query with client_id
---
post:
summary: Manually stop a query with client_id
requestBody:
description: Stop query schema
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StopQuerySchema'
responses:
200:
description: Query stopped
content:
application/json:
schema:
type: object
properties:
result:
type: string
400:
$ref: '#/components/responses/400'
401:
$ref: '#/components/responses/401'
404:
$ref: '#/components/responses/404'
500:
$ref: '#/components/responses/500'
"""
try:
body = self.stop_query_schema.load(request.json)
QueryDAO.stop_query(body["client_id"])
return self.response(200, result="OK")
except SupersetException as ex:
return self.response(ex.status, message=ex.message)
27 changes: 27 additions & 0 deletions superset/queries/dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@
from datetime import datetime
from typing import Any, Dict

from superset import sql_lab
from superset.common.db_query_status import QueryStatus
from superset.dao.base import BaseDAO
from superset.exceptions import QueryNotFoundException, SupersetCancelQueryException
from superset.extensions import db
from superset.models.sql_lab import Query, SavedQuery
from superset.queries.filters import QueryFilter
from superset.utils.dates import now_as_float

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -56,3 +60,26 @@ def save_metadata(query: Query, payload: Dict[str, Any]) -> None:
columns = payload.get("columns", {})
db.session.add(query)
query.set_extra_json_key("columns", columns)

@staticmethod
def stop_query(client_id: str) -> None:
query = db.session.query(Query).filter_by(client_id=client_id).one_or_none()
if not query:
raise QueryNotFoundException(f"Query with client_id {client_id} not found")

if query.status in [
QueryStatus.FAILED,
QueryStatus.SUCCESS,
QueryStatus.TIMED_OUT,
]:
logger.warning(
"Query with client_id could not be stopped: query already complete",
)
return

if not sql_lab.cancel_query(query):
raise SupersetCancelQueryException("Could not cancel query")

query.status = QueryStatus.STOPPED
query.end_time = now_as_float()
db.session.commit()
8 changes: 8 additions & 0 deletions superset/queries/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,11 @@ class Meta: # pylint: disable=too-few-public-methods
# pylint: disable=no-self-use
def get_sql_tables(self, obj: Query) -> List[Table]:
return obj.sql_tables


class StopQuerySchema(Schema):
"""
Schema for the stop_query API call.
"""

client_id = fields.String()
1 change: 1 addition & 0 deletions superset/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2298,6 +2298,7 @@ def results_exec(key: str) -> FlaskResponse:
on_giveup=lambda details: db.session.rollback(),
max_tries=5,
)
@deprecated()
def stop_query(self) -> FlaskResponse:
client_id = request.form.get("client_id")
query = db.session.query(Query).filter_by(client_id=client_id).one()
Expand Down
52 changes: 52 additions & 0 deletions tests/integration_tests/queries/api_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
# isort:skip_file
"""Unit tests for Superset"""
from datetime import datetime, timedelta
from unittest import mock
import json
import random
import string
Expand Down Expand Up @@ -392,3 +393,54 @@ def test_get_list_query_no_data_access(self):
# rollback changes
db.session.delete(query)
db.session.commit()

@mock.patch("superset.sql_lab.cancel_query")
@mock.patch("superset.views.core.db.session")
def test_stop_query_not_found(
self, mock_superset_db_session, mock_sql_lab_cancel_query
):
"""
Handles stop query when the DB engine spec does not
have a cancel query method (with invalid client_id).
"""
form_data = {"client_id": "foo2"}
query_mock = mock.Mock()
query_mock.return_value = None
self.login(username="admin")
mock_superset_db_session.query().filter_by().one_or_none = query_mock
mock_sql_lab_cancel_query.return_value = True
rv = self.client.post(
"/api/v1/query/stop",
data=json.dumps(form_data),
content_type="application/json",
)

assert rv.status_code == 404
data = json.loads(rv.data.decode("utf-8"))
assert data["message"] == "Query with client_id foo2 not found"

@mock.patch("superset.sql_lab.cancel_query")
@mock.patch("superset.views.core.db.session")
def test_stop_query(self, mock_superset_db_session, mock_sql_lab_cancel_query):
"""
Handles stop query when the DB engine spec does not
have a cancel query method.
"""
form_data = {"client_id": "foo"}
query_mock = mock.Mock()
query_mock.client_id = "foo"
query_mock.status = QueryStatus.RUNNING
self.login(username="admin")
mock_superset_db_session.query().filter_by().one_or_none().return_value = (
query_mock
)
mock_sql_lab_cancel_query.return_value = True
rv = self.client.post(
"/api/v1/query/stop",
data=json.dumps(form_data),
content_type="application/json",
)

assert rv.status_code == 200
data = json.loads(rv.data.decode("utf-8"))
assert data["result"] == "OK"
Loading

0 comments on commit 3ed288d

Please sign in to comment.