This is an automated email from the ASF dual-hosted git repository.

robdiciuccio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new ab153e6  feat: Synchronously return cached charts (#15157)
ab153e6 is described below

commit ab153e66cc6a3ad6a55d9d2b19aef3ef2f9f2625
Author: Ben Reinhart <[email protected]>
AuthorDate: Tue Jun 22 10:00:57 2021 -0700

    feat: Synchronously return cached charts (#15157)
    
    * feat: Synchronously return cached charts
    
    * Fix lint issue
    
    * Fix python lint error
    
    * Change getChartDataRequest to return response
    
    * Fix lint errors
    
    * Add test
    
    * explore_json: skip cached data check for forced refresh
    
    Co-authored-by: Rob DiCiuccio <[email protected]>
---
 superset-frontend/src/chart/chartAction.js         | 33 +++++++---
 .../FilterBar/FilterControls/FilterValue.tsx       | 43 ++++++++-----
 .../FiltersConfigForm/FiltersConfigForm.tsx        | 37 +++++++----
 .../explore/components/DataTablesPane/index.tsx    |  4 +-
 .../explore/components/controls/ViewQueryModal.tsx |  4 +-
 superset/charts/api.py                             | 39 ++++++++---
 superset/views/core.py                             | 29 ++++++++-
 tests/charts/api_tests.py                          | 75 +++++++++++++---------
 8 files changed, 181 insertions(+), 83 deletions(-)

diff --git a/superset-frontend/src/chart/chartAction.js 
b/superset-frontend/src/chart/chartAction.js
index 9be1b6a..5052416 100644
--- a/superset-frontend/src/chart/chartAction.js
+++ b/superset-frontend/src/chart/chartAction.js
@@ -145,11 +145,12 @@ const legacyChartDataRequest = async (
     'GET' && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)
       ? SupersetClient.get
       : SupersetClient.post;
-  return clientMethod(querySettings).then(({ json }) =>
+  return clientMethod(querySettings).then(({ json, response }) =>
     // Make the legacy endpoint return a payload that corresponds to the
     // V1 chart data endpoint response signature.
     ({
-      result: [json],
+      response,
+      json: { result: [json] },
     }),
   );
 };
@@ -196,7 +197,8 @@ const v1ChartDataRequest = async (
     headers: { 'Content-Type': 'application/json' },
     body: JSON.stringify(payload),
   };
-  return SupersetClient.post(querySettings).then(({ json }) => json);
+
+  return SupersetClient.post(querySettings);
 };
 
 export async function getChartDataRequest({
@@ -390,14 +392,25 @@ export function exploreJSON(
     dispatch(chartUpdateStarted(controller, formData, key));
 
     const chartDataRequestCaught = chartDataRequest
-      .then(response => {
-        const queriesResponse = response.result;
+      .then(({ response, json }) => {
         if (isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) {
           // deal with getChartDataRequest transforming the response data
-          const result = 'result' in response ? response.result[0] : response;
-          return waitForAsyncData(result);
+          const result = 'result' in json ? json.result[0] : json;
+          switch (response.status) {
+            case 200:
+              // Query results returned synchronously, meaning query was 
already cached.
+              return Promise.resolve([result]);
+            case 202:
+              // Query is running asynchronously and we must await the results
+              return waitForAsyncData(result);
+            default:
+              throw new Error(
+                `Received unexpected response status (${response.status}) 
while fetching chart data`,
+              );
+          }
         }
-        return queriesResponse;
+
+        return json.result;
       })
       .then(queriesResponse => {
         queriesResponse.forEach(resultItem =>
@@ -541,11 +554,11 @@ export function postChartFormData(
 export function redirectSQLLab(formData) {
   return dispatch => {
     getChartDataRequest({ formData, resultFormat: 'json', resultType: 'query' 
})
-      .then(({ result }) => {
+      .then(({ json }) => {
         const redirectUrl = '/superset/sqllab/';
         const payload = {
           datasourceKey: formData.datasource,
-          sql: result[0].query,
+          sql: json.result[0].query,
         };
         postForm(redirectUrl, payload);
       })
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
index da66478..59ad936 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterControls/FilterValue.tsx
@@ -118,25 +118,36 @@ const FilterValue: React.FC<FilterProps> = ({
         requestParams: { dashboardId: 0 },
         ownState: filterOwnState,
       })
-        .then(response => {
+        .then(({ response, json }) => {
           if (isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) {
             // deal with getChartDataRequest transforming the response data
-            const result = 'result' in response ? response.result[0] : 
response;
-            waitForAsyncData(result)
-              .then((asyncResult: ChartDataResponseResult[]) => {
-                setIsRefreshing(false);
-                setIsLoading(false);
-                setState(asyncResult);
-              })
-              .catch((error: ClientErrorObject) => {
-                setError(
-                  error.message || error.error || t('Check configuration'),
-                );
-                setIsRefreshing(false);
-                setIsLoading(false);
-              });
+            const result = 'result' in json ? json.result[0] : json;
+
+            if (response.status === 200) {
+              setIsRefreshing(false);
+              setIsLoading(false);
+              setState([result]);
+            } else if (response.status === 202) {
+              waitForAsyncData(result)
+                .then((asyncResult: ChartDataResponseResult[]) => {
+                  setIsRefreshing(false);
+                  setIsLoading(false);
+                  setState(asyncResult);
+                })
+                .catch((error: ClientErrorObject) => {
+                  setError(
+                    error.message || error.error || t('Check configuration'),
+                  );
+                  setIsRefreshing(false);
+                  setIsLoading(false);
+                });
+            } else {
+              throw new Error(
+                `Received unexpected response status (${response.status}) 
while fetching chart data`,
+              );
+            }
           } else {
-            setState(response.result);
+            setState(json.result);
             setError('');
             setIsRefreshing(false);
             setIsLoading(false);
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index 449b5949..65a4ec4 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -421,24 +421,35 @@ const FiltersConfigForm = (
         force,
         requestParams: { dashboardId: 0 },
       })
-        .then(response => {
+        .then(({ response, json }) => {
           if (isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) {
             // deal with getChartDataRequest transforming the response data
-            const result = 'result' in response ? response.result[0] : 
response;
-            waitForAsyncData(result)
-              .then((asyncResult: ChartDataResponseResult[]) => {
-                setNativeFilterFieldValuesWrapper({
-                  defaultValueQueriesData: asyncResult,
-                });
-              })
-              .catch((error: ClientErrorObject) => {
-                setError(
-                  error.message || error.error || t('Check configuration'),
-                );
+            const result = 'result' in json ? json.result[0] : json;
+
+            if (response.status === 200) {
+              setNativeFilterFieldValuesWrapper({
+                defaultValueQueriesData: [result],
               });
+            } else if (response.status === 202) {
+              waitForAsyncData(result)
+                .then((asyncResult: ChartDataResponseResult[]) => {
+                  setNativeFilterFieldValuesWrapper({
+                    defaultValueQueriesData: asyncResult,
+                  });
+                })
+                .catch((error: ClientErrorObject) => {
+                  setError(
+                    error.message || error.error || t('Check configuration'),
+                  );
+                });
+            } else {
+              throw new Error(
+                `Received unexpected response status (${response.status}) 
while fetching chart data`,
+              );
+            }
           } else {
             setNativeFilterFieldValuesWrapper({
-              defaultValueQueriesData: response.result,
+              defaultValueQueriesData: json.result,
             });
           }
         })
diff --git a/superset-frontend/src/explore/components/DataTablesPane/index.tsx 
b/superset-frontend/src/explore/components/DataTablesPane/index.tsx
index f2b33f2..810433a 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/index.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/index.tsx
@@ -149,9 +149,9 @@ export const DataTablesPane = ({
         resultType,
         ownState,
       })
-        .then(response => {
+        .then(({ json }) => {
           // Only displaying the first query is currently supported
-          const result = response.result[0];
+          const result = json.result[0];
           setData(prevData => ({ ...prevData, [resultType]: result.data }));
           setIsLoading(prevIsLoading => ({
             ...prevIsLoading,
diff --git 
a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx 
b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx
index dadaace..70308ed 100644
--- a/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx
+++ b/superset-frontend/src/explore/components/controls/ViewQueryModal.tsx
@@ -58,9 +58,9 @@ const ViewQueryModal: React.FC<Props> = props => {
       resultFormat: 'json',
       resultType,
     })
-      .then(response => {
+      .then(({ json }) => {
         // Only displaying the first query is currently supported
-        const result = response.result[0];
+        const result = json.result[0];
         setLanguage(result.language);
         setQuery(result.query);
         setIsLoading(false);
diff --git a/superset/charts/api.py b/superset/charts/api.py
index f248f16..c3658b9 100644
--- a/superset/charts/api.py
+++ b/superset/charts/api.py
@@ -480,17 +480,9 @@ class ChartRestApi(BaseSupersetModelRestApi):
         except ChartBulkDeleteFailedError as ex:
             return self.response_422(message=str(ex))
 
-    def get_data_response(
-        self, command: ChartDataCommand, force_cached: bool = False
-    ) -> Response:
-        try:
-            result = command.run(force_cached=force_cached)
-        except ChartDataCacheLoadError as exc:
-            return self.response_422(message=exc.message)
-        except ChartDataQueryFailedError as exc:
-            return self.response_400(message=exc.message)
-
+    def send_chart_response(self, result: Dict[Any, Any]) -> Response:
         result_format = result["query_context"].result_format
+
         if result_format == ChartDataResultFormat.CSV:
             # Verify user has permission to export CSV file
             if not security_manager.can_access("can_csv", "Superset"):
@@ -512,6 +504,18 @@ class ChartRestApi(BaseSupersetModelRestApi):
 
         return self.response_400(message=f"Unsupported result_format: 
{result_format}")
 
+    def get_data_response(
+        self, command: ChartDataCommand, force_cached: bool = False
+    ) -> Response:
+        try:
+            result = command.run(force_cached=force_cached)
+        except ChartDataCacheLoadError as exc:
+            return self.response_422(message=exc.message)
+        except ChartDataQueryFailedError as exc:
+            return self.response_400(message=exc.message)
+
+        return self.send_chart_response(result)
+
     @expose("/data", methods=["POST"])
     @protect()
     @statsd_metrics
@@ -589,7 +593,22 @@ class ChartRestApi(BaseSupersetModelRestApi):
             and query_context.result_format == ChartDataResultFormat.JSON
             and query_context.result_type == ChartDataResultType.FULL
         ):
+            # First, look for the chart query results in the cache.
+            try:
+                result = command.run(force_cached=True)
+            except ChartDataCacheLoadError:
+                result = None  # type: ignore
+
+            already_cached_result = result is not None
+
+            # If the chart query has already been cached, return it 
immediately.
+            if already_cached_result:
+                return self.send_chart_response(result)
 
+            # Otherwise, kick off a background job to run the chart query.
+            # Clients will either poll or be notified of query completion,
+            # at which point they will call the /data/<cache_key> endpoint
+            # to retrieve the results.
             try:
                 command.validate_async_request(request)
             except AsyncQueryTokenException:
diff --git a/superset/views/core.py b/superset/views/core.py
index f81505c..31e9116 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -434,6 +434,10 @@ class Superset(BaseSupersetView):  # pylint: 
disable=too-many-public-methods
     def get_samples(self, viz_obj: BaseViz) -> FlaskResponse:
         return self.json_response({"data": viz_obj.get_samples()})
 
+    @staticmethod
+    def send_data_payload_response(viz_obj: BaseViz, payload: Any) -> 
FlaskResponse:
+        return 
data_payload_response(*viz_obj.payload_json_and_has_error(payload))
+
     def generate_json(
         self, viz_obj: BaseViz, response_type: Optional[str] = None
     ) -> FlaskResponse:
@@ -452,7 +456,7 @@ class Superset(BaseSupersetView):  # pylint: 
disable=too-many-public-methods
             return self.get_samples(viz_obj)
 
         payload = viz_obj.get_payload()
-        return 
data_payload_response(*viz_obj.payload_json_and_has_error(payload))
+        return self.send_data_payload_response(viz_obj, payload)
 
     @event_logger.log_this
     @api
@@ -609,6 +613,29 @@ class Superset(BaseSupersetView):  # pylint: 
disable=too-many-public-methods
                 is_feature_enabled("GLOBAL_ASYNC_QUERIES")
                 and response_type == utils.ChartDataResultFormat.JSON
             ):
+                # First, look for the chart query results in the cache.
+                try:
+                    viz_obj = get_viz(
+                        datasource_type=cast(str, datasource_type),
+                        datasource_id=datasource_id,
+                        form_data=form_data,
+                        force_cached=True,
+                        force=force,
+                    )
+                    payload = viz_obj.get_payload()
+                except CacheLoadError:
+                    payload = None  # type: ignore
+
+                already_cached_result = payload is not None
+
+                # If the chart query has already been cached, return it 
immediately.
+                if already_cached_result:
+                    return self.send_data_payload_response(viz_obj, payload)
+
+                # Otherwise, kick off a background job to run the chart query.
+                # Clients will either poll or be notified of query completion,
+                # at which point they will call the 
/explore_json/data/<cache_key>
+                # endpoint to retrieve the results.
                 try:
                     async_channel_id = 
async_query_manager.parse_jwt_from_request(
                         request
diff --git a/tests/charts/api_tests.py b/tests/charts/api_tests.py
index b43abfb..322e070 100644
--- a/tests/charts/api_tests.py
+++ b/tests/charts/api_tests.py
@@ -23,6 +23,7 @@ from typing import Optional
 from unittest import mock
 from zipfile import is_zipfile, ZipFile
 
+from tests.conftest import with_feature_flags
 from superset.models.sql_lab import Query
 from tests.insert_chart_mixin import InsertChartMixin
 from tests.fixtures.birth_names_dashboard import 
load_birth_names_dashboard_with_slices
@@ -46,7 +47,12 @@ from superset.models.dashboard import Dashboard
 from superset.models.reports import ReportSchedule, ReportScheduleType
 from superset.models.slice import Slice
 from superset.utils import core as utils
-from superset.utils.core import AnnotationType, get_example_database, 
get_main_database
+from superset.utils.core import (
+    AnnotationType,
+    ChartDataResultFormat,
+    get_example_database,
+    get_main_database,
+)
 
 
 from tests.base_api_tests import ApiOwnersTestCaseMixin
@@ -1386,10 +1392,8 @@ class TestChartApi(SupersetTestCase, 
ApiOwnersTestCaseMixin, InsertChartMixin):
         if get_example_database().backend != "presto":
             assert "('boy' = 'boy')" in result
 
-    @mock.patch.dict(
-        "superset.extensions.feature_flag_manager._feature_flags",
-        GLOBAL_ASYNC_QUERIES=True,
-    )
+    @with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
     def test_chart_data_async(self):
         """
         Chart data API: Test chart data query (async)
@@ -1405,10 +1409,35 @@ class TestChartApi(SupersetTestCase, 
ApiOwnersTestCaseMixin, InsertChartMixin):
             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,
-    )
+    @with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
+    def test_chart_data_async_cached_sync_response(self):
+        """
+        Chart data API: Test chart data query returns results synchronously
+        when results are already cached.
+        """
+        async_query_manager.init_app(app)
+        self.login(username="admin")
+
+        class QueryContext:
+            result_format = ChartDataResultFormat.JSON
+
+        cmd_run_val = {
+            "query_context": QueryContext(),
+            "queries": [{"query": "select * from foo"}],
+        }
+
+        with mock.patch.object(
+            ChartDataCommand, "run", return_value=cmd_run_val
+        ) as patched_run:
+            request_payload = get_query_context("birth_names")
+            rv = self.post_assert_metric(CHART_DATA_URI, request_payload, 
"data")
+            self.assertEqual(rv.status_code, 200)
+            data = json.loads(rv.data.decode("utf-8"))
+            patched_run.assert_called_once_with(force_cached=True)
+            self.assertEqual(data, {"result": [{"query": "select * from 
foo"}]})
+
+    @with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
     @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
     def test_chart_data_async_results_type(self):
         """
@@ -1421,10 +1450,8 @@ class TestChartApi(SupersetTestCase, 
ApiOwnersTestCaseMixin, InsertChartMixin):
         rv = self.post_assert_metric(CHART_DATA_URI, request_payload, "data")
         self.assertEqual(rv.status_code, 200)
 
-    @mock.patch.dict(
-        "superset.extensions.feature_flag_manager._feature_flags",
-        GLOBAL_ASYNC_QUERIES=True,
-    )
+    @with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
     def test_chart_data_async_invalid_token(self):
         """
         Chart data API: Test chart data query (async)
@@ -1439,10 +1466,7 @@ class TestChartApi(SupersetTestCase, 
ApiOwnersTestCaseMixin, InsertChartMixin):
         self.assertEqual(rv.status_code, 401)
 
     @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
-    @mock.patch.dict(
-        "superset.extensions.feature_flag_manager._feature_flags",
-        GLOBAL_ASYNC_QUERIES=True,
-    )
+    @with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
     @mock.patch.object(ChartDataCommand, "load_query_context_from_cache")
     def test_chart_data_cache(self, load_qc_mock):
         """
@@ -1469,11 +1493,9 @@ class TestChartApi(SupersetTestCase, 
ApiOwnersTestCaseMixin, InsertChartMixin):
         self.assertEqual(rv.status_code, 200)
         self.assertEqual(data["result"][0]["rowcount"], expected_row_count)
 
-    @mock.patch.dict(
-        "superset.extensions.feature_flag_manager._feature_flags",
-        GLOBAL_ASYNC_QUERIES=True,
-    )
+    @with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
     @mock.patch.object(ChartDataCommand, "load_query_context_from_cache")
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
     def test_chart_data_cache_run_failed(self, load_qc_mock):
         """
         Chart data cache API: Test chart data async cache request with run 
failure
@@ -1490,11 +1512,9 @@ class TestChartApi(SupersetTestCase, 
ApiOwnersTestCaseMixin, InsertChartMixin):
         self.assertEqual(rv.status_code, 422)
         self.assertEqual(data["message"], "Error loading data from cache")
 
-    @mock.patch.dict(
-        "superset.extensions.feature_flag_manager._feature_flags",
-        GLOBAL_ASYNC_QUERIES=True,
-    )
+    @with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
     @mock.patch.object(ChartDataCommand, "load_query_context_from_cache")
+    @pytest.mark.usefixtures("load_birth_names_dashboard_with_slices")
     def test_chart_data_cache_no_login(self, load_qc_mock):
         """
         Chart data cache API: Test chart data async cache request (no login)
@@ -1514,10 +1534,7 @@ class TestChartApi(SupersetTestCase, 
ApiOwnersTestCaseMixin, InsertChartMixin):
 
         self.assertEqual(rv.status_code, 401)
 
-    @mock.patch.dict(
-        "superset.extensions.feature_flag_manager._feature_flags",
-        GLOBAL_ASYNC_QUERIES=True,
-    )
+    @with_feature_flags(GLOBAL_ASYNC_QUERIES=True)
     def test_chart_data_cache_key_error(self):
         """
         Chart data cache API: Test chart data async cache request with invalid 
cache key

Reply via email to