Skip to content

Commit

Permalink
enabling force parameter on screenshot endpoints
Browse files Browse the repository at this point in the history
adding CacheScreenshotPayload state machine
  • Loading branch information
fisjac committed Jan 8, 2025
1 parent 4f1a837 commit 5ac969c
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 154 deletions.
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 @@ def cache_screenshot(self, pk: int, **kwargs: Any) -> WerkzeugResponse:
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 @@ def cache_screenshot(self, pk: int, **kwargs: Any) -> WerkzeugResponse:
$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 @@ def cache_screenshot(self, pk: int, **kwargs: Any) -> WerkzeugResponse:

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 @@ def screenshot(self, pk: int, digest: str) -> WerkzeugResponse:
name: digest
responses:
200:
description: Chart thumbnail image
description: Chart screenshot image
content:
image/*:
schema:
Expand All @@ -652,16 +677,16 @@ def screenshot(self, pk: int, digest: str) -> WerkzeugResponse:
"""
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 @@ def thumbnail(self, pk: int, digest: str, **kwargs: Any) -> WerkzeugResponse:

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 @@ def thumbnail(self, pk: int, digest: str, **kwargs: Any) -> WerkzeugResponse:
)
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 @@ def thumbnail(self, pk: int, digest: str, **kwargs: Any) -> WerkzeugResponse:
)
# 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 @@ def thumbnail(self, pk: int, digest: str, **kwargs: Any) -> WerkzeugResponse:
)
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 @@ def thumbnail(self, pk: int, digest: str, **kwargs: Any) -> WerkzeugResponse:
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 @@ def cache_dashboard_screenshot(self, pk: int, **kwargs: Any) -> WerkzeugResponse
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 @@ def cache_dashboard_screenshot(self, pk: int, **kwargs: Any) -> WerkzeugResponse

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 @@ def trigger_celery() -> WerkzeugResponse:
),
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 @@ def trigger_celery() -> WerkzeugResponse:
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 @@ def screenshot(self, pk: int, digest: str) -> WerkzeugResponse:

# 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 @@ def screenshot(self, pk: int, digest: str) -> WerkzeugResponse:
)
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

0 comments on commit 5ac969c

Please sign in to comment.