mirror of
https://github.com/apache/superset.git
synced 2026-04-20 00:24:38 +00:00
feat(SIP-39): Async query support for charts (#11499)
* Generate JWT in Flask app * Refactor chart data API query logic, add JWT validation and async worker * Add redis stream implementation, refactoring * Add chart data cache endpoint, refactor QueryContext caching * Typing, linting, refactoring * pytest fixes and openapi schema update * Enforce caching be configured for async query init * Async query processing for explore_json endpoint * Add /api/v1/async_event endpoint * Async frontend for dashboards [WIP] * Chart async error message support, refactoring * Abstract asyncEvent middleware * Async chart loading for Explore * Pylint fixes * asyncEvent middleware -> TypeScript, JS linting * Chart data API: enforce forced_cache, add tests * Add tests for explore_json endpoints * Add test for chart data cache enpoint (no login) * Consolidate set_and_log_cache and add STORE_CACHE_KEYS_IN_METADATA_DB flag * Add tests for tasks/async_queries and address PR comments * Bypass non-JSON result formats for async queries * Add tests for redux middleware * Remove debug statement Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com> * Skip force_cached if no queryObj * SunburstViz: don't modify self.form_data * Fix failing annotation test * Resolve merge/lint issues * Reduce polling delay * Fix new getClientErrorObject reference * Fix flakey unit tests * /api/v1/async_event: increment redis stream ID, add tests * PR feedback: refactoring, configuration * Fixup: remove debugging * Fix typescript errors due to redux upgrade * Update UPDATING.md * Fix failing py tests * asyncEvent_spec.js -> asyncEvent_spec.ts * Refactor flakey Python 3.7 mock assertions * Fix another shared state issue in Py tests * Use 'sub' claim in JWT for user_id * Refactor async middleware config * Fixup: restore FeatureFlag boolean type Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
This commit is contained in:
@@ -52,6 +52,7 @@ from superset import (
|
||||
from superset.connectors.sqla.models import SqlaTable
|
||||
from superset.db_engine_specs.base import BaseEngineSpec
|
||||
from superset.db_engine_specs.mssql import MssqlEngineSpec
|
||||
from superset.extensions import async_query_manager
|
||||
from superset.models import core as models
|
||||
from superset.models.annotations import Annotation, AnnotationLayer
|
||||
from superset.models.dashboard import Dashboard
|
||||
@@ -602,10 +603,13 @@ class TestCore(SupersetTestCase):
|
||||
) == [{"slice_id": slc.id, "viz_error": None, "viz_status": "success"}]
|
||||
|
||||
def test_cache_logging(self):
|
||||
store_cache_keys = app.config["STORE_CACHE_KEYS_IN_METADATA_DB"]
|
||||
app.config["STORE_CACHE_KEYS_IN_METADATA_DB"] = True
|
||||
girls_slice = self.get_slice("Girls", db.session)
|
||||
self.get_json_resp("/superset/warm_up_cache?slice_id={}".format(girls_slice.id))
|
||||
ck = db.session.query(CacheKey).order_by(CacheKey.id.desc()).first()
|
||||
assert ck.datasource_uid == f"{girls_slice.table.id}__table"
|
||||
app.config["STORE_CACHE_KEYS_IN_METADATA_DB"] = store_cache_keys
|
||||
|
||||
def test_shortner(self):
|
||||
self.login(username="admin")
|
||||
@@ -841,6 +845,191 @@ class TestCore(SupersetTestCase):
|
||||
"The datasource associated with this chart no longer exists",
|
||||
)
|
||||
|
||||
def test_explore_json(self):
|
||||
tbl_id = self.table_ids.get("birth_names")
|
||||
form_data = {
|
||||
"queryFields": {
|
||||
"metrics": "metrics",
|
||||
"groupby": "groupby",
|
||||
"columns": "groupby",
|
||||
},
|
||||
"datasource": f"{tbl_id}__table",
|
||||
"viz_type": "dist_bar",
|
||||
"time_range_endpoints": ["inclusive", "exclusive"],
|
||||
"granularity_sqla": "ds",
|
||||
"time_range": "No filter",
|
||||
"metrics": ["count"],
|
||||
"adhoc_filters": [],
|
||||
"groupby": ["gender"],
|
||||
"row_limit": 100,
|
||||
}
|
||||
self.login(username="admin")
|
||||
rv = self.client.post(
|
||||
"/superset/explore_json/", data={"form_data": json.dumps(form_data)},
|
||||
)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
self.assertEqual(data["rowcount"], 2)
|
||||
|
||||
@mock.patch.dict(
|
||||
"superset.extensions.feature_flag_manager._feature_flags",
|
||||
GLOBAL_ASYNC_QUERIES=True,
|
||||
)
|
||||
def test_explore_json_async(self):
|
||||
tbl_id = self.table_ids.get("birth_names")
|
||||
form_data = {
|
||||
"queryFields": {
|
||||
"metrics": "metrics",
|
||||
"groupby": "groupby",
|
||||
"columns": "groupby",
|
||||
},
|
||||
"datasource": f"{tbl_id}__table",
|
||||
"viz_type": "dist_bar",
|
||||
"time_range_endpoints": ["inclusive", "exclusive"],
|
||||
"granularity_sqla": "ds",
|
||||
"time_range": "No filter",
|
||||
"metrics": ["count"],
|
||||
"adhoc_filters": [],
|
||||
"groupby": ["gender"],
|
||||
"row_limit": 100,
|
||||
}
|
||||
async_query_manager.init_app(app)
|
||||
self.login(username="admin")
|
||||
rv = self.client.post(
|
||||
"/superset/explore_json/", data={"form_data": json.dumps(form_data)},
|
||||
)
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
keys = list(data.keys())
|
||||
|
||||
self.assertEqual(rv.status_code, 202)
|
||||
self.assertCountEqual(
|
||||
keys, ["channel_id", "job_id", "user_id", "status", "errors", "result_url"]
|
||||
)
|
||||
|
||||
@mock.patch.dict(
|
||||
"superset.extensions.feature_flag_manager._feature_flags",
|
||||
GLOBAL_ASYNC_QUERIES=True,
|
||||
)
|
||||
def test_explore_json_async_results_format(self):
|
||||
tbl_id = self.table_ids.get("birth_names")
|
||||
form_data = {
|
||||
"queryFields": {
|
||||
"metrics": "metrics",
|
||||
"groupby": "groupby",
|
||||
"columns": "groupby",
|
||||
},
|
||||
"datasource": f"{tbl_id}__table",
|
||||
"viz_type": "dist_bar",
|
||||
"time_range_endpoints": ["inclusive", "exclusive"],
|
||||
"granularity_sqla": "ds",
|
||||
"time_range": "No filter",
|
||||
"metrics": ["count"],
|
||||
"adhoc_filters": [],
|
||||
"groupby": ["gender"],
|
||||
"row_limit": 100,
|
||||
}
|
||||
async_query_manager.init_app(app)
|
||||
self.login(username="admin")
|
||||
rv = self.client.post(
|
||||
"/superset/explore_json/?results=true",
|
||||
data={"form_data": json.dumps(form_data)},
|
||||
)
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
|
||||
@mock.patch(
|
||||
"superset.utils.cache_manager.CacheManager.cache",
|
||||
new_callable=mock.PropertyMock,
|
||||
)
|
||||
@mock.patch("superset.viz.BaseViz.force_cached", new_callable=mock.PropertyMock)
|
||||
def test_explore_json_data(self, mock_force_cached, mock_cache):
|
||||
tbl_id = self.table_ids.get("birth_names")
|
||||
form_data = dict(
|
||||
{
|
||||
"form_data": {
|
||||
"queryFields": {
|
||||
"metrics": "metrics",
|
||||
"groupby": "groupby",
|
||||
"columns": "groupby",
|
||||
},
|
||||
"datasource": f"{tbl_id}__table",
|
||||
"viz_type": "dist_bar",
|
||||
"time_range_endpoints": ["inclusive", "exclusive"],
|
||||
"granularity_sqla": "ds",
|
||||
"time_range": "No filter",
|
||||
"metrics": ["count"],
|
||||
"adhoc_filters": [],
|
||||
"groupby": ["gender"],
|
||||
"row_limit": 100,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
class MockCache:
|
||||
def get(self, key):
|
||||
return form_data
|
||||
|
||||
def set(self):
|
||||
return None
|
||||
|
||||
mock_cache.return_value = MockCache()
|
||||
mock_force_cached.return_value = False
|
||||
|
||||
self.login(username="admin")
|
||||
rv = self.client.get("/superset/explore_json/data/valid-cache-key")
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
self.assertEqual(data["rowcount"], 2)
|
||||
|
||||
@mock.patch(
|
||||
"superset.utils.cache_manager.CacheManager.cache",
|
||||
new_callable=mock.PropertyMock,
|
||||
)
|
||||
def test_explore_json_data_no_login(self, mock_cache):
|
||||
tbl_id = self.table_ids.get("birth_names")
|
||||
form_data = dict(
|
||||
{
|
||||
"form_data": {
|
||||
"queryFields": {
|
||||
"metrics": "metrics",
|
||||
"groupby": "groupby",
|
||||
"columns": "groupby",
|
||||
},
|
||||
"datasource": f"{tbl_id}__table",
|
||||
"viz_type": "dist_bar",
|
||||
"time_range_endpoints": ["inclusive", "exclusive"],
|
||||
"granularity_sqla": "ds",
|
||||
"time_range": "No filter",
|
||||
"metrics": ["count"],
|
||||
"adhoc_filters": [],
|
||||
"groupby": ["gender"],
|
||||
"row_limit": 100,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
class MockCache:
|
||||
def get(self, key):
|
||||
return form_data
|
||||
|
||||
def set(self):
|
||||
return None
|
||||
|
||||
mock_cache.return_value = MockCache()
|
||||
|
||||
rv = self.client.get("/superset/explore_json/data/valid-cache-key")
|
||||
self.assertEqual(rv.status_code, 401)
|
||||
|
||||
def test_explore_json_data_invalid_cache_key(self):
|
||||
self.login(username="admin")
|
||||
cache_key = "invalid-cache-key"
|
||||
rv = self.client.get(f"/superset/explore_json/data/{cache_key}")
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
|
||||
self.assertEqual(rv.status_code, 404)
|
||||
self.assertEqual(data["error"], "Cached data not found")
|
||||
|
||||
@mock.patch(
|
||||
"superset.security.SupersetSecurityManager.get_schemas_accessible_by_user"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user