geido commented on code in PR #44144:
URL: https://github.com/apache/superset/pull/44144#discussion_r3988569172


##########
superset/dashboards/api.py:
##########
@@ -1923,6 +1923,8 @@ def cache_dashboard_screenshot(self, pk: int, **kwargs: 
Any) -> WerkzeugResponse
               $ref: '#/components/responses/404'
             500:
               $ref: '#/components/responses/500'
+            503:

Review Comment:
   Addressed in the latest push. `superset update-api-docs` on the exact 
rebased source now produces no diff; the committed spec includes request 
`permalinkKey`, response `permalink_key`, the existing-task 200 response, and 
the cache-failure 503 response.



##########
superset/utils/screenshot_utils.py:
##########
@@ -382,11 +387,13 @@ def _unready_chart_holders_js_body(*, viewport_only: 
bool) -> str:
         }});
         const hasUnpaintedAgGrid = unpaintedAgGrids.length > 0;
         // Ready = a settled error/empty/missing state, or a slice container
-        // whose renderer has painted. ECharts and AG Grid expose explicit
-        // completion signals; keep either host unready until its signal fires.
+        // whose plugin loaded and rendered. Canvas/grid/map renderers expose
+        // additional paint signals; keep their hosts unready until those fire.
         const isReady = !stillLoading && (
             hasErrorOrEmpty || (
-                hasSliceContainer && !hasUnpaintedEchart && !hasUnpaintedAgGrid
+                hasSliceContainer && hasRenderedChart
+                && !hasUnpaintedEchart && !hasUnpaintedAgGrid
+                && !hasUnpaintedDeckGl

Review Comment:
   Addressed. The rendered/async/stable-ready and blank-image gates are enabled 
only when `require_complete_capture=True`; only the dashboard API screenshot 
worker opts into that mode. Scheduled reports and ordinary thumbnail captures 
retain their prior readiness predicates, with regression coverage for the 
default path.



##########
superset-frontend/plugins/preset-chart-deckgl/src/DeckGLContainer.tsx:
##########
@@ -179,6 +194,7 @@ export const DeckGLContainer = memo(
             <MapLibreMap
               {...viewState}
               onMove={onMove}
+              onIdle={onMapIdle}

Review Comment:
   Addressed and tested fail-closed. Deck readiness is scoped to the current 
source generation and requires MapLibre idle plus a Deck `onAfterRender`; 
MapLibre/style/source and Deck errors are tracked and cannot mark the host 
painted. A Docker capture with explicit OSM tiles reached Updated with painted 
map/layers, while an unreachable tile source reached terminal Error and GET 
returned 404, so a 401/no-egress case cannot cache a blank map.



##########
superset/dashboards/api.py:
##########
@@ -1983,26 +1988,51 @@ def build_response(status_code: int) -> 
WerkzeugResponse:
                 task_status=cache_payload.get_status(),
             )
 
-        if cache_payload.should_trigger_task(
-            force, expected_scope=f"dashboard:{dashboard.id}"
+        if cached_payload is None or cache_payload.should_enqueue_task(
+            force, expected_scope=cache_scope
         ):
             logger.info("Triggering screenshot ASYNC")
-            cache_dashboard_screenshot.delay(
-                username=get_current_user(),
-                guest_token=(
-                    g.user.guest_token
-                    if get_current_user() and isinstance(g.user, GuestUser)
-                    else None
-                ),
-                dashboard_id=dashboard.id,
-                dashboard_url=dashboard_url,
-                thumb_size=thumb_size,
-                window_size=window_size,
-                cache_key=cache_key,
-                force=force,
-            )
+            cache_payload.pending()

Review Comment:
   Addressed in the UI flow. It now retains the returned permalink, polls the 
same POST with `permalinkKey`, stops immediately on terminal Error, and calls 
GET only after Updated. A missing artifact after Updated is treated as cache 
eviction and re-enters status polling with that same permalink; the old generic 
GET-only retry loop is gone.



##########
superset/dashboards/api.py:
##########
@@ -1983,26 +1988,51 @@ def build_response(status_code: int) -> 
WerkzeugResponse:
                 task_status=cache_payload.get_status(),
             )
 
-        if cache_payload.should_trigger_task(
-            force, expected_scope=f"dashboard:{dashboard.id}"
+        if cached_payload is None or cache_payload.should_enqueue_task(
+            force, expected_scope=cache_scope
         ):
             logger.info("Triggering screenshot ASYNC")
-            cache_dashboard_screenshot.delay(
-                username=get_current_user(),
-                guest_token=(
-                    g.user.guest_token
-                    if get_current_user() and isinstance(g.user, GuestUser)
-                    else None
-                ),
-                dashboard_id=dashboard.id,
-                dashboard_url=dashboard_url,
-                thumb_size=thumb_size,
-                window_size=window_size,
-                cache_key=cache_key,
-                force=force,
-            )
+            cache_payload.pending()
+            cache_payload.set_scope(cache_scope)
+            if not screenshot_obj.store_cache_payload(cache_key, 
cache_payload):
+                logger.error(
+                    "Refusing to enqueue dashboard screenshot because Pending "
+                    "state could not be cached: %s",
+                    cache_key,
+                )
+                return self.response(
+                    503,
+                    message=gettext("Screenshot cache is unavailable"),
+                )
+            try:
+                cache_dashboard_screenshot.delay(
+                    username=get_current_user(),
+                    guest_token=(
+                        g.user.guest_token
+                        if get_current_user() and isinstance(g.user, GuestUser)
+                        else None
+                    ),
+                    dashboard_id=dashboard.id,
+                    dashboard_url=dashboard_url,
+                    thumb_size=thumb_size,
+                    window_size=window_size,
+                    cache_key=cache_key,
+                    # The API has already invalidated the prior artifact by
+                    # persisting PENDING. Avoid making duplicate queued tasks
+                    # recompute after another worker has completed the request.
+                    force=False,
+                )
+            except Exception:  # pylint: disable=broad-except
+                cache_payload.error()
+                if not screenshot_obj.store_cache_payload(cache_key, 
cache_payload):
+                    logger.error(
+                        "Could not persist dashboard screenshot Error state "
+                        "after enqueue failure: %s",
+                        cache_key,
+                    )
+                raise
             return build_response(202)
-        return build_response(200)
+        return build_response(202 if cache_payload.is_in_progress() else 200)

Review Comment:
   Addressed in both the endpoint docstring and generated OpenAPI description: 
non-force requests keep returning the cached Error until 
`THUMBNAIL_ERROR_CACHE_TTL` expires, and clients must send `force=true` to 
retry sooner. The behavior is also covered by sticky-error and forced-retry 
tests.



##########
superset/utils/screenshot_utils.py:
##########
@@ -277,6 +277,7 @@ class PlaywrightTimeout(PlaywrightError):  # type: 
ignore[no-redef]  # noqa: N81
 ALERT_SELECTOR = r'[role="alert"]'
 EMPTY_SELECTOR = r".ant-empty, .ag-overlay-no-rows-wrapper:not(.ag-hidden)"
 MISSING_CHART_SELECTOR = r".missing-chart-container"
+CHART_RENDERED_SELECTOR = r'.chart-container[data-chart-status="rendered"]'

Review Comment:
   Addressed. The generic renderer marker is tied to the current render 
generation and successful committed props: a same-viz refresh becomes ready 
after the refreshed renderer commits, a viz change waits for the replacement 
renderer, and stale callbacks cannot complete a newer generation. Tests cover 
both same-viz refresh and viz-type replacement.



##########
superset/tasks/thumbnails.py:
##########
@@ -130,32 +132,51 @@ def cache_dashboard_screenshot(  # pylint: 
disable=too-many-arguments
     # pylint: disable=import-outside-toplevel
     from superset.models.dashboard import Dashboard
 
-    if not thumbnail_cache:
+    if not is_cache_configured(thumbnail_cache):
         logging.warning("No cache set, refusing to compute")
         return
 
-    dashboard = Dashboard.get(dashboard_id)
-
-    logger.info("Caching dashboard: %s", dashboard_url)
-
-    # Requests from Embedded should always use the Guest user
-    if guest_token:
-        current_user = security_manager.get_guest_user_from_token(guest_token)
-    else:
-        _, exec_username = get_executor(
-            executors=current_app.config["THUMBNAIL_EXECUTORS"],
-            model=dashboard,
-            current_user=username,
-        )
-        current_user = security_manager.find_user(exec_username)
-
-    with override_user(current_user):
-        screenshot = DashboardScreenshot(dashboard_url, dashboard.digest)
-        screenshot.cache_scope = f"dashboard:{dashboard.id}"
-        screenshot.compute_and_cache(
-            user=current_user,
-            window_size=window_size,
-            thumb_size=thumb_size,
-            cache_key=cache_key,
-            force=force,
+    try:
+        dashboard = Dashboard.get(dashboard_id)
+
+        logger.info("Caching dashboard: %s", dashboard_url)
+
+        # Requests from Embedded should always use the Guest user
+        if guest_token:
+            current_user = 
security_manager.get_guest_user_from_token(guest_token)
+        else:
+            _, exec_username = get_executor(
+                executors=current_app.config["THUMBNAIL_EXECUTORS"],
+                model=dashboard,
+                current_user=username,
+            )
+            current_user = security_manager.find_user(exec_username)
+
+        with override_user(current_user):
+            screenshot = DashboardScreenshot(
+                dashboard_url,
+                dashboard.digest,
+                require_complete_capture=True,
+            )
+            screenshot.cache_scope = f"dashboard:{dashboard.id}"
+            screenshot.compute_and_cache(
+                user=current_user,
+                window_size=window_size,
+                thumb_size=thumb_size,
+                cache_key=cache_key,
+                force=force,
+            )
+    except Exception:  # pylint: disable=broad-except
+        logger.exception(
+            "Dashboard screenshot task failed before reaching a terminal 
state: %s",
+            cache_key,
         )
+        if cache_key:
+            error_payload = 
ScreenshotCachePayload(scope=f"dashboard:{dashboard_id}")

Review Comment:
   Addressed with producer and worker locks. The setup-failure fallback may 
persist Error only after acquiring the worker lock; a duplicate that cannot 
acquire it cannot overwrite another workers Computing state, and an already 
completed payload is preserved. Targeted tests cover the active-worker and 
completed-payload cases.



##########
superset/utils/cache.py:
##########
@@ -39,6 +39,56 @@
 logger = logging.getLogger(__name__)
 
 
+def is_cache_configured(cache_instance: Cache | None) -> bool:
+    """Return whether a Flask-Caching instance has a configured backend."""
+
+    if cache_instance is None:
+        return False
+    try:
+        return not isinstance(cache_instance.cache, NullCache)
+    except AttributeError:

Review Comment:
   Addressed. The production helper now inspects the real Flask-Caching 
`.cache` backend without an AttributeError test-double fallback, and the 
affected test doubles expose `.cache` explicitly. Cache configuration behavior 
remains covered by unit tests.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to