[cache warm_up] warm_up slice with dashboard default_filters (#9311)

* [cache warm_up] warm_up slice with dashboard default_filters

* update Celery warmup tasks

* fix code review comments

* add try catch and type checking for parsed dash metadata

* extra code review fix
This commit is contained in:
Grace Guo 2020-03-18 08:21:10 -07:00 committed by GitHub
parent 98ac72074c
commit adebd40d30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 396 additions and 16 deletions

View File

@ -18,6 +18,7 @@
import json
import logging
from typing import Any, Dict, Optional
from urllib import request
from urllib.error import URLError
@ -31,6 +32,7 @@ from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.models.tags import Tag, TaggedObject
from superset.utils.core import parse_human_datetime
from superset.views.utils import build_extra_filters
logger = get_task_logger(__name__)
logger.setLevel(logging.INFO)
@ -54,27 +56,23 @@ def get_form_data(chart_id, dashboard=None):
if not default_filters:
return form_data
# do not apply filters if chart is immune to them
immune_fields = []
filter_scopes = json_metadata.get("filter_scopes", {})
if filter_scopes:
for scopes in filter_scopes.values():
for (field, scope) in scopes.items():
if chart_id in scope.get("immune", []):
immune_fields.append(field)
extra_filters = []
for filters in default_filters.values():
for col, val in filters.items():
if col not in immune_fields:
extra_filters.append({"col": col, "op": "in", "val": val})
layout = json.loads(dashboard.position_json or "{}")
if (
isinstance(layout, dict)
and isinstance(filter_scopes, dict)
and isinstance(default_filters, dict)
):
extra_filters = build_extra_filters(
layout, filter_scopes, default_filters, chart_id
)
if extra_filters:
form_data["extra_filters"] = extra_filters
return form_data
def get_url(chart):
def get_url(chart, extra_filters: Optional[Dict[str, Any]] = None):
"""Return external URL for warming up a given chart/table cache."""
with app.test_request_context():
baseurl = (
@ -82,7 +80,7 @@ def get_url(chart):
"{SUPERSET_WEBSERVER_ADDRESS}:"
"{SUPERSET_WEBSERVER_PORT}".format(**app.config)
)
return f"{baseurl}{chart.url}"
return f"{baseurl}{chart.get_explore_url(overrides=extra_filters)}"
class Strategy:
@ -181,7 +179,8 @@ class TopNDashboardsStrategy(Strategy):
dashboards = session.query(Dashboard).filter(Dashboard.id.in_(dash_ids)).all()
for dashboard in dashboards:
for chart in dashboard.slices:
urls.append(get_url(chart))
form_data_with_filters = get_form_data(chart.id, dashboard)
urls.append(get_url(chart, form_data_with_filters))
return urls

View File

@ -90,6 +90,7 @@ from superset.utils.dashboard_filter_scopes_converter import copy_filter_scopes
from superset.utils.dates import now_as_float
from superset.utils.decorators import etag_cache, stats_timing
from superset.views.database.filters import DatabaseFilter
from superset.views.utils import get_dashboard_extra_filters
from .base import (
api,
@ -1651,6 +1652,7 @@ class Superset(BaseSupersetView):
slices = None
session = db.session()
slice_id = request.args.get("slice_id")
dashboard_id = request.args.get("dashboard_id")
table_name = request.args.get("table_name")
db_name = request.args.get("db_name")
@ -1696,6 +1698,10 @@ class Superset(BaseSupersetView):
for slc in slices:
try:
form_data = get_form_data(slc.id, use_slice_data=True)[0]
if dashboard_id:
form_data["extra_filters"] = get_dashboard_extra_filters(
slc.id, dashboard_id
)
obj = get_viz(
datasource_type=slc.datasource.type,
datasource_id=slc.datasource.id,

View File

@ -27,6 +27,7 @@ from superset import app, db, viz
from superset.connectors.connector_registry import ConnectorRegistry
from superset.exceptions import SupersetException
from superset.legacy import update_time_range
from superset.models.dashboard import Dashboard
from superset.models.slice import Slice
from superset.utils.core import QueryStatus, TimeRangeEndpoint
@ -262,3 +263,90 @@ def get_time_range_endpoints(
return (TimeRangeEndpoint(start), TimeRangeEndpoint(end))
return (TimeRangeEndpoint.INCLUSIVE, TimeRangeEndpoint.EXCLUSIVE)
# see all dashboard components type in
# /superset-frontend/src/dashboard/util/componentTypes.js
CONTAINER_TYPES = ["COLUMN", "GRID", "TABS", "TAB", "ROW"]
def get_dashboard_extra_filters(
slice_id: int, dashboard_id: int
) -> List[Dict[str, Any]]:
session = db.session()
dashboard = session.query(Dashboard).filter_by(id=dashboard_id).one_or_none()
# is chart in this dashboard?
if (
dashboard is None
or not dashboard.json_metadata
or not dashboard.slices
or not any([slc for slc in dashboard.slices if slc.id == slice_id])
):
return []
try:
# does this dashboard have default filters?
json_metadata = json.loads(dashboard.json_metadata)
default_filters = json.loads(json_metadata.get("default_filters", "null"))
if not default_filters:
return []
# are default filters applicable to the given slice?
filter_scopes = json_metadata.get("filter_scopes", {})
layout = json.loads(dashboard.position_json or "{}")
if (
isinstance(layout, dict)
and isinstance(filter_scopes, dict)
and isinstance(default_filters, dict)
):
return build_extra_filters(layout, filter_scopes, default_filters, slice_id)
except json.JSONDecodeError:
pass
return []
def build_extra_filters(
layout: Dict,
filter_scopes: Dict,
default_filters: Dict[str, Dict[str, List]],
slice_id: int,
) -> List[Dict[str, Any]]:
extra_filters = []
# do not apply filters if chart is not in filter's scope or
# chart is immune to the filter
for filter_id, columns in default_filters.items():
scopes_by_filter_field = filter_scopes.get(filter_id, {})
for col, val in columns.items():
current_field_scopes = scopes_by_filter_field.get(col, {})
scoped_container_ids = current_field_scopes.get("scope", ["ROOT_ID"])
immune_slice_ids = current_field_scopes.get("immune", [])
for container_id in scoped_container_ids:
if slice_id not in immune_slice_ids and is_slice_in_container(
layout, container_id, slice_id
):
extra_filters.append({"col": col, "op": "in", "val": val})
return extra_filters
def is_slice_in_container(layout: Dict, container_id: str, slice_id: int) -> bool:
if container_id == "ROOT_ID":
return True
node = layout[container_id]
node_type = node.get("type")
if node_type == "CHART" and node.get("meta", {}).get("chartId") == slice_id:
return True
if node_type in CONTAINER_TYPES:
children = node.get("children", [])
return any(
is_slice_in_container(layout, child_id, slice_id) for child_id in children
)
return False

View File

@ -33,6 +33,22 @@ from .base_tests import SupersetTestCase
URL_PREFIX = "http://0.0.0.0:8081"
mock_positions = {
"DASHBOARD_VERSION_KEY": "v2",
"DASHBOARD_CHART_TYPE-1": {
"type": "CHART",
"id": "DASHBOARD_CHART_TYPE-1",
"children": [],
"meta": {"width": 4, "height": 50, "chartId": 1},
},
"DASHBOARD_CHART_TYPE-2": {
"type": "CHART",
"id": "DASHBOARD_CHART_TYPE-2",
"children": [],
"meta": {"width": 4, "height": 50, "chartId": 2},
},
}
class CacheWarmUpTests(SupersetTestCase):
def __init__(self, *args, **kwargs):
@ -48,6 +64,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
dashboard = MagicMock()
dashboard.json_metadata = None
dashboard.position_json = json.dumps(mock_positions)
result = get_form_data(chart_id, dashboard)
expected = {"slice_id": chart_id}
self.assertEqual(result, expected)
@ -56,6 +73,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
filter_box_id = 2
dashboard = MagicMock()
dashboard.position_json = json.dumps(mock_positions)
dashboard.json_metadata = json.dumps(
{
"filter_scopes": {
@ -76,6 +94,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
dashboard = MagicMock()
dashboard.json_metadata = json.dumps({})
dashboard.position_json = json.dumps(mock_positions)
result = get_form_data(chart_id, dashboard)
expected = {"slice_id": chart_id}
self.assertEqual(result, expected)
@ -84,6 +103,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
filter_box_id = 2
dashboard = MagicMock()
dashboard.position_json = json.dumps(mock_positions)
dashboard.json_metadata = json.dumps(
{
"default_filters": json.dumps(
@ -112,6 +132,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
filter_box_id = 2
dashboard = MagicMock()
dashboard.position_json = json.dumps(mock_positions)
dashboard.json_metadata = json.dumps(
{
"default_filters": json.dumps(
@ -132,6 +153,7 @@ class CacheWarmUpTests(SupersetTestCase):
chart_id = 1
filter_box_id = 2
dashboard = MagicMock()
dashboard.position_json = json.dumps(mock_positions)
dashboard.json_metadata = json.dumps(
{
"default_filters": json.dumps(

View File

@ -56,6 +56,7 @@ from superset.utils.core import (
zlib_decompress,
)
from superset.views.utils import get_time_range_endpoints
from superset.views.utils import build_extra_filters
from tests.base_tests import SupersetTestCase
@ -956,3 +957,267 @@ class UtilsTestCase(SupersetTestCase):
self.assertListEqual(get_iterable(123), [123])
self.assertListEqual(get_iterable([123]), [123])
self.assertListEqual(get_iterable("foo"), ["foo"])
def test_build_extra_filters(self):
layout = {
"CHART-2ee52f30": {
"children": [],
"id": "CHART-2ee52f30",
"meta": {
"chartId": 1020,
"height": 38,
"sliceName": "Chart 927",
"width": 6,
},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-VrhTX2WUlO",
"TABS-N1zN4CIZP0",
"TAB-asWdJzKmTN",
"ROW-i_sG4ccXE",
],
"type": "CHART",
},
"CHART-36bfc934": {
"children": [],
"id": "CHART-36bfc934",
"meta": {
"chartId": 1018,
"height": 26,
"sliceName": "Region Filter",
"width": 2,
},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-W62P60D88",
"ROW-1e064e3c",
"COLUMN-fe3914b8",
],
"type": "CHART",
},
"CHART-E_y2cuNHTv": {
"children": [],
"id": "CHART-E_y2cuNHTv",
"meta": {"chartId": 998, "height": 55, "sliceName": "MAP", "width": 6},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-W62P60D88",
"ROW-1e064e3c",
],
"type": "CHART",
},
"CHART-JNxDOsAfEb": {
"children": [],
"id": "CHART-JNxDOsAfEb",
"meta": {
"chartId": 1015,
"height": 27,
"sliceName": "Population",
"width": 4,
},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-W62P60D88",
"ROW-1e064e3c",
"COLUMN-fe3914b8",
],
"type": "CHART",
},
"CHART-KoOwqalV80": {
"children": [],
"id": "CHART-KoOwqalV80",
"meta": {
"chartId": 927,
"height": 20,
"sliceName": "Chart 927",
"width": 4,
},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-VrhTX2WUlO",
"TABS-N1zN4CIZP0",
"TAB-cHNWcBZC9",
"ROW-9b9vrWKPY",
],
"type": "CHART",
},
"CHART-YCQAPVK7mQ": {
"children": [],
"id": "CHART-YCQAPVK7mQ",
"meta": {
"chartId": 1023,
"height": 38,
"sliceName": "World's Population",
"width": 4,
},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-VrhTX2WUlO",
"ROW-UfxFT36oV5",
],
"type": "CHART",
},
"COLUMN-fe3914b8": {
"children": ["CHART-36bfc934", "CHART-JNxDOsAfEb"],
"id": "COLUMN-fe3914b8",
"meta": {"background": "BACKGROUND_TRANSPARENT", "width": 6},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-W62P60D88",
"ROW-1e064e3c",
],
"type": "COLUMN",
},
"DASHBOARD_VERSION_KEY": "v2",
"GRID_ID": {
"children": [],
"id": "GRID_ID",
"parents": ["ROOT_ID"],
"type": "GRID",
},
"HEADER_ID": {
"id": "HEADER_ID",
"meta": {"text": "Test warmup 1023"},
"type": "HEADER",
},
"ROOT_ID": {
"children": ["TABS-Qq4sdkANSY"],
"id": "ROOT_ID",
"type": "ROOT",
},
"ROW-1e064e3c": {
"children": ["COLUMN-fe3914b8", "CHART-E_y2cuNHTv"],
"id": "ROW-1e064e3c",
"meta": {"background": "BACKGROUND_TRANSPARENT"},
"parents": ["ROOT_ID", "TABS-Qq4sdkANSY", "TAB-W62P60D88"],
"type": "ROW",
},
"ROW-9b9vrWKPY": {
"children": ["CHART-KoOwqalV80"],
"id": "ROW-9b9vrWKPY",
"meta": {"background": "BACKGROUND_TRANSPARENT"},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-VrhTX2WUlO",
"TABS-N1zN4CIZP0",
"TAB-cHNWcBZC9",
],
"type": "ROW",
},
"ROW-UfxFT36oV5": {
"children": ["CHART-YCQAPVK7mQ"],
"id": "ROW-UfxFT36oV5",
"meta": {"background": "BACKGROUND_TRANSPARENT"},
"parents": ["ROOT_ID", "TABS-Qq4sdkANSY", "TAB-VrhTX2WUlO"],
"type": "ROW",
},
"ROW-i_sG4ccXE": {
"children": ["CHART-2ee52f30"],
"id": "ROW-i_sG4ccXE",
"meta": {"background": "BACKGROUND_TRANSPARENT"},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-VrhTX2WUlO",
"TABS-N1zN4CIZP0",
"TAB-asWdJzKmTN",
],
"type": "ROW",
},
"TAB-VrhTX2WUlO": {
"children": ["ROW-UfxFT36oV5", "TABS-N1zN4CIZP0"],
"id": "TAB-VrhTX2WUlO",
"meta": {"text": "New Tab"},
"parents": ["ROOT_ID", "TABS-Qq4sdkANSY"],
"type": "TAB",
},
"TAB-W62P60D88": {
"children": ["ROW-1e064e3c"],
"id": "TAB-W62P60D88",
"meta": {"text": "Tab 2"},
"parents": ["ROOT_ID", "TABS-Qq4sdkANSY"],
"type": "TAB",
},
"TAB-asWdJzKmTN": {
"children": ["ROW-i_sG4ccXE"],
"id": "TAB-asWdJzKmTN",
"meta": {"text": "nested tab 1"},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-VrhTX2WUlO",
"TABS-N1zN4CIZP0",
],
"type": "TAB",
},
"TAB-cHNWcBZC9": {
"children": ["ROW-9b9vrWKPY"],
"id": "TAB-cHNWcBZC9",
"meta": {"text": "test2d tab 2"},
"parents": [
"ROOT_ID",
"TABS-Qq4sdkANSY",
"TAB-VrhTX2WUlO",
"TABS-N1zN4CIZP0",
],
"type": "TAB",
},
"TABS-N1zN4CIZP0": {
"children": ["TAB-asWdJzKmTN", "TAB-cHNWcBZC9"],
"id": "TABS-N1zN4CIZP0",
"meta": {},
"parents": ["ROOT_ID", "TABS-Qq4sdkANSY", "TAB-VrhTX2WUlO"],
"type": "TABS",
},
"TABS-Qq4sdkANSY": {
"children": ["TAB-VrhTX2WUlO", "TAB-W62P60D88"],
"id": "TABS-Qq4sdkANSY",
"meta": {},
"parents": ["ROOT_ID"],
"type": "TABS",
},
}
filter_scopes = {
"1018": {
"region": {"scope": ["TAB-W62P60D88"], "immune": [998]},
"country_name": {"scope": ["ROOT_ID"], "immune": [927, 998]},
}
}
default_filters = {
"1018": {"region": ["North America"], "country_name": ["United States"]}
}
# immune to all filters
slice_id = 998
extra_filters = build_extra_filters(
layout, filter_scopes, default_filters, slice_id
)
expected = []
self.assertEqual(extra_filters, expected)
# in scope
slice_id = 1015
extra_filters = build_extra_filters(
layout, filter_scopes, default_filters, slice_id
)
expected = [
{"col": "region", "op": "in", "val": ["North America"]},
{"col": "country_name", "op": "in", "val": ["United States"]},
]
self.assertEqual(extra_filters, expected)
# not in scope
slice_id = 927
extra_filters = build_extra_filters(
layout, filter_scopes, default_filters, slice_id
)
expected = []
self.assertEqual(extra_filters, expected)