Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(thumbnail cache): Enabling force parameter on screenshot/thumbnail cache #31757

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 71 additions & 45 deletions superset/charts/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from werkzeug.wrappers import Response as WerkzeugResponse
from werkzeug.wsgi import FileWrapper

from superset import app, is_feature_enabled, thumbnail_cache
from superset import app, is_feature_enabled
from superset.charts.filters import (
ChartAllTextFilter,
ChartCertifiedFilter,
Expand Down Expand Up @@ -84,7 +84,12 @@
from superset.tasks.thumbnails import cache_chart_thumbnail
from superset.tasks.utils import get_current_user
from superset.utils import json
from superset.utils.screenshots import ChartScreenshot, DEFAULT_CHART_WINDOW_SIZE
from superset.utils.screenshots import (
ChartScreenshot,
DEFAULT_CHART_WINDOW_SIZE,
ScreenshotCachePayload,
StatusValues,
)
from superset.utils.urls import get_url_path
from superset.views.base_api import (
BaseSupersetModelRestApi,
Expand Down Expand Up @@ -564,8 +569,14 @@
schema:
$ref: '#/components/schemas/screenshot_query_schema'
responses:
200:
description: Chart async result
content:
application/json:
schema:
$ref: "#/components/schemas/ChartCacheScreenshotResponseSchema"
202:
description: Chart async result
description: Chart async created
content:
application/json:
schema:
Expand All @@ -580,6 +591,7 @@
$ref: '#/components/responses/500'
"""
rison_dict = kwargs["rison"]
force = rison_dict.get("force")

Check warning on line 594 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L594

Added line #L594 was not covered by tests
window_size = rison_dict.get("window_size") or DEFAULT_CHART_WINDOW_SIZE

# Don't shrink the image if thumb_size is not specified
Expand All @@ -591,25 +603,38 @@

chart_url = get_url_path("Superset.slice", slice_id=chart.id)
screenshot_obj = ChartScreenshot(chart_url, chart.digest)
cache_key = screenshot_obj.cache_key(window_size, thumb_size)
cache_key = screenshot_obj.get_cache_key(window_size, thumb_size)

Check warning on line 606 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L606

Added line #L606 was not covered by tests
image_url = get_url_path(
"ChartRestApi.screenshot", pk=chart.id, digest=cache_key
)

def trigger_celery() -> WerkzeugResponse:
logger.info("Triggering screenshot ASYNC")
cache_chart_thumbnail.delay(
current_user=get_current_user(),
chart_id=chart.id,
force=True,
window_size=window_size,
thumb_size=thumb_size,
)
return self.response(
202, cache_key=cache_key, chart_url=chart_url, image_url=image_url
)
if force or not screenshot_obj.get_from_cache_key(cache_key):
payload = ScreenshotCachePayload()
screenshot_obj.cache.set(cache_key, payload)

Check warning on line 613 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L611-L613

Added lines #L611 - L613 were not covered by tests

return trigger_celery()
if (cache_payload := screenshot_obj.get_from_cache_key(cache_key)) is not None:

Check warning on line 615 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L615

Added line #L615 was not covered by tests

def build_response(status_code: int) -> WerkzeugResponse:
return self.response(

Check warning on line 618 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L617-L618

Added lines #L617 - L618 were not covered by tests
status_code,
cache_key=cache_key,
chart_url=chart_url,
image_url=image_url,
updated_at=cache_payload.get_timestamp(),
update_status=cache_payload.get_status(),
)

if cache_payload.status != StatusValues.UPDATED:
logger.info("Triggering screenshot ASYNC")
cache_chart_thumbnail.delay(

Check warning on line 629 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L627-L629

Added lines #L627 - L629 were not covered by tests
current_user=get_current_user(),
chart_id=chart.id,
window_size=window_size,
thumb_size=thumb_size,
)
return build_response(202)
return build_response(200)
return self.response_500()

Check warning on line 637 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L635-L637

Added lines #L635 - L637 were not covered by tests

@expose("/<pk>/screenshot/<digest>/", methods=("GET",))
@protect()
Expand All @@ -635,7 +660,7 @@
name: digest
responses:
200:
description: Chart thumbnail image
description: Chart screenshot image
content:
image/*:
schema:
Expand All @@ -652,16 +677,16 @@
"""
chart = self.datamodel.get(pk, self._base_filters)

# Making sure the chart still exists
if not chart:
return self.response_404()

# fetch the chart screenshot using the current user and cache if set
if img := ChartScreenshot.get_from_cache_key(thumbnail_cache, digest):
return Response(
FileWrapper(img), mimetype="image/png", direct_passthrough=True
)
# TODO: return an empty image
if cache_payload := ChartScreenshot.get_from_cache_key(digest):
if cache_payload.status == StatusValues.UPDATED:
return Response(

Check warning on line 685 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L683-L685

Added lines #L683 - L685 were not covered by tests
FileWrapper(cache_payload.get_image()),
mimetype="image/png",
direct_passthrough=True,
)
return self.response_404()

@expose("/<pk>/thumbnail/<digest>/", methods=("GET",))
Expand Down Expand Up @@ -713,33 +738,32 @@

current_user = get_current_user()
url = get_url_path("Superset.slice", slice_id=chart.id)
if kwargs["rison"].get("force", False):
logger.info(
"Triggering thumbnail compute (chart id: %s) ASYNC", str(chart.id)
)
cache_chart_thumbnail.delay(
current_user=current_user,
chart_id=chart.id,
force=True,
)
return self.response(202, message="OK Async")
# fetch the chart screenshot using the current user and cache if set
screenshot = ChartScreenshot(url, chart.digest).get_from_cache(
cache=thumbnail_cache
)
# If not screenshot then send request to compute thumb to celery
if not screenshot:
screenshot_obj = ChartScreenshot(url, chart.digest)
cache_key = screenshot_obj.get_cache_key()
cache_payload = screenshot_obj.get_from_cache_key(cache_key)

Check warning on line 743 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L741-L743

Added lines #L741 - L743 were not covered by tests

if not cache_payload:
cache_payload = ScreenshotCachePayload()
screenshot_obj.cache.set(cache_key, cache_payload)

Check warning on line 747 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L745-L747

Added lines #L745 - L747 were not covered by tests

if (

Check warning on line 749 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L749

Added line #L749 was not covered by tests
kwargs["rison"].get("force", False)
or cache_payload.status != StatusValues.UPDATED
):
self.incr_stats("async", self.thumbnail.__name__)
logger.info(
"Triggering thumbnail compute (chart id: %s) ASYNC", str(chart.id)
)
cache_chart_thumbnail.delay(
current_user=current_user,
chart_id=chart.id,
force=True,
)
return self.response(202, message="OK Async")
# If digests
return self.response(

Check warning on line 761 in superset/charts/api.py

View check run for this annotation

Codecov / codecov/patch

superset/charts/api.py#L761

Added line #L761 was not covered by tests
202,
updated_at=cache_payload.get_timestamp(),
update_status=cache_payload.get_status(),
)
# TODO remove digest from params... currently does nothing
if chart.digest != digest:
self.incr_stats("redirect", self.thumbnail.__name__)
return redirect(
Expand All @@ -749,7 +773,9 @@
)
self.incr_stats("from_cache", self.thumbnail.__name__)
return Response(
FileWrapper(screenshot), mimetype="image/png", direct_passthrough=True
FileWrapper(cache_payload.get_image()),
mimetype="image/png",
direct_passthrough=True,
)

@expose("/export/", methods=("GET",))
Expand Down
15 changes: 15 additions & 0 deletions superset/charts/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,21 @@ class ChartCacheScreenshotResponseSchema(Schema):
image_url = fields.String(
metadata={"description": "The url to fetch the screenshot"}
)
update_status = fields.String(
metadata={"description": "The status of the async screenshot"}
)
updated_at = fields.String(
metadata={"description": "The timestamp of the last change in status"}
)


class ChartGetCachedScreenshotResponseSchema(Schema):
update_status = fields.String(
metadata={"description": "The status of the async screenshot"}
)
updated_at = fields.String(
metadata={"description": "The timestamp of the last change in status"}
)


class ChartDataColumnSchema(Schema):
Expand Down
1 change: 1 addition & 0 deletions superset/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ class D3TimeFormat(TypedDict, total=False):

THUMBNAIL_CACHE_CONFIG: CacheConfig = {
"CACHE_TYPE": "NullCache",
"CACHE_DEFAULT_TIMEOUT": 3600,
"CACHE_NO_NULL_WARNING": True,
}

Expand Down
64 changes: 38 additions & 26 deletions superset/dashboards/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from werkzeug.wrappers import Response as WerkzeugResponse
from werkzeug.wsgi import FileWrapper

from superset import db, is_feature_enabled, thumbnail_cache
from superset import db, is_feature_enabled
from superset.charts.schemas import ChartEntityResponseSchema
from superset.commands.dashboard.copy import CopyDashboardCommand
from superset.commands.dashboard.create import CreateDashboardCommand
Expand Down Expand Up @@ -116,6 +116,8 @@
from superset.utils.screenshots import (
DashboardScreenshot,
DEFAULT_DASHBOARD_WINDOW_SIZE,
ScreenshotCachePayload,
StatusValues,
)
from superset.utils.urls import get_url_path
from superset.views.base_api import (
Expand Down Expand Up @@ -1105,27 +1107,24 @@
)
# If force, request a screenshot from the workers
current_user = get_current_user()
if kwargs["rison"].get("force", False):
screenshot_obj = DashboardScreenshot(dashboard_url, digest)
cache_key = screenshot_obj.get_cache_key()
cache_payload = screenshot_obj.get_from_cache_key(cache_key)
if not cache_payload or kwargs["rison"].get("force", False):
cache_payload = ScreenshotCachePayload()
screenshot_obj.cache.set(cache_key, cache_payload)

Check warning on line 1115 in superset/dashboards/api.py

View check run for this annotation

Codecov / codecov/patch

superset/dashboards/api.py#L1110-L1115

Added lines #L1110 - L1115 were not covered by tests

if cache_payload.status != StatusValues.UPDATED:

Check warning on line 1117 in superset/dashboards/api.py

View check run for this annotation

Codecov / codecov/patch

superset/dashboards/api.py#L1117

Added line #L1117 was not covered by tests
cache_dashboard_thumbnail.delay(
current_user=current_user,
dashboard_id=dashboard.id,
force=True,
)
return self.response(202, message="OK Async")
# fetch the dashboard screenshot using the current user and cache if set
screenshot = DashboardScreenshot(
dashboard_url, dashboard.digest
).get_from_cache(cache=thumbnail_cache)
# If the screenshot does not exist, request one from the workers
if not screenshot:
self.incr_stats("async", self.thumbnail.__name__)
cache_dashboard_thumbnail.delay(
current_user=current_user,
dashboard_id=dashboard.id,
force=True,
return self.response(

Check warning on line 1122 in superset/dashboards/api.py

View check run for this annotation

Codecov / codecov/patch

superset/dashboards/api.py#L1122

Added line #L1122 was not covered by tests
202,
updated_at=cache_payload.get_timestamp(),
update_status=cache_payload.get_status(),
)
return self.response(202, message="OK Async")
# If digests

if dashboard.digest != digest:
self.incr_stats("redirect", self.thumbnail.__name__)
return redirect(
Expand All @@ -1137,7 +1136,9 @@
)
self.incr_stats("from_cache", self.thumbnail.__name__)
return Response(
FileWrapper(screenshot), mimetype="image/png", direct_passthrough=True
FileWrapper(cache_payload.get_image()),
mimetype="image/png",
direct_passthrough=True,
)

@expose("/<pk>/cache_dashboard_screenshot/", methods=("POST",))
Expand All @@ -1150,7 +1151,9 @@
f".cache_dashboard_screenshot",
log_to_statsd=False,
)
def cache_dashboard_screenshot(self, pk: int, **kwargs: Any) -> WerkzeugResponse:
def cache_dashboard_screenshot(
self, pk: int, force: bool, **kwargs: Any
) -> WerkzeugResponse:
"""Compute and cache a screenshot.
---
post:
Expand Down Expand Up @@ -1185,7 +1188,6 @@
payload = CacheScreenshotSchema().load(request.json)
except ValidationError as error:
return self.response_400(message=error.messages)

dashboard = cast(Dashboard, self.datamodel.get(pk, self._base_filters))
if not dashboard:
return self.response_404()
Expand All @@ -1210,10 +1212,17 @@

dashboard_url = get_url_path("Superset.dashboard_permalink", key=permalink_key)
screenshot_obj = DashboardScreenshot(dashboard_url, dashboard.digest)
cache_key = screenshot_obj.cache_key(window_size, thumb_size, dashboard_state)
cache_key = screenshot_obj.get_cache_key(

Check warning on line 1215 in superset/dashboards/api.py

View check run for this annotation

Codecov / codecov/patch

superset/dashboards/api.py#L1215

Added line #L1215 was not covered by tests
window_size, thumb_size, dashboard_state
)
image_url = get_url_path(
"DashboardRestApi.screenshot", pk=dashboard.id, digest=cache_key
)
force = kwargs["rison"].get("force", False)
cache_payload = screenshot_obj.get_from_cache_key(cache_key)
if force or cache_payload is None:
cache_payload = ScreenshotCachePayload()
screenshot_obj.cache.set(cache_key, cache_payload)

Check warning on line 1225 in superset/dashboards/api.py

View check run for this annotation

Codecov / codecov/patch

superset/dashboards/api.py#L1221-L1225

Added lines #L1221 - L1225 were not covered by tests

def trigger_celery() -> WerkzeugResponse:
logger.info("Triggering screenshot ASYNC")
Expand All @@ -1226,8 +1235,6 @@
),
dashboard_id=dashboard.id,
dashboard_url=dashboard_url,
cache_key=cache_key,
force=False,
thumb_size=thumb_size,
window_size=window_size,
)
Expand All @@ -1236,6 +1243,8 @@
cache_key=cache_key,
dashboard_url=dashboard_url,
image_url=image_url,
updated_at=cache_payload.get_timestamp(),
update_status=cache_payload.get_status(),
)

return trigger_celery()
Expand Down Expand Up @@ -1294,9 +1303,12 @@

# fetch the dashboard screenshot using the current user and cache if set

if img := DashboardScreenshot.get_from_cache_key(thumbnail_cache, digest):
if cache_payload := DashboardScreenshot.get_from_cache_key(digest):
image = cache_payload.get_image()
if not image:
return self.response_404()

Check warning on line 1309 in superset/dashboards/api.py

View check run for this annotation

Codecov / codecov/patch

superset/dashboards/api.py#L1306-L1309

Added lines #L1306 - L1309 were not covered by tests
if download_format == "pdf":
pdf_img = img.getvalue()
pdf_img = image.getvalue()

Check warning on line 1311 in superset/dashboards/api.py

View check run for this annotation

Codecov / codecov/patch

superset/dashboards/api.py#L1311

Added line #L1311 was not covered by tests
# Convert the screenshot to PDF
pdf_data = build_pdf_from_screenshots([pdf_img])

Expand All @@ -1308,7 +1320,7 @@
)
if download_format == "png":
return Response(
FileWrapper(img),
FileWrapper(image),
mimetype="image/png",
direct_passthrough=True,
)
Expand Down
6 changes: 6 additions & 0 deletions superset/dashboards/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,12 @@ class DashboardCacheScreenshotResponseSchema(Schema):
image_url = fields.String(
metadata={"description": "The url to fetch the screenshot"}
)
update_status = fields.String(
metadata={"description": "The status of the async screenshot"}
)
updated_at = fields.String(
metadata={"description": "The timestamp of the last change in status"}
)


class CacheScreenshotSchema(Schema):
Expand Down
1 change: 0 additions & 1 deletion superset/models/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,6 @@ def update_thumbnail(self) -> None:
cache_dashboard_thumbnail.delay(
current_user=get_current_user(),
dashboard_id=self.id,
force=True,
)

@classmethod
Expand Down
Loading
Loading