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

kgabryje 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 1ecff6fe5c1 fix(thumbnails): stabilize digest by sorting datasources 
and charts (#38079)
1ecff6fe5c1 is described below

commit 1ecff6fe5c13ae98dd55cb40ac3baf259f839bef
Author: Amin Ghadersohi <[email protected]>
AuthorDate: Fri Feb 20 03:51:35 2026 -0500

    fix(thumbnails): stabilize digest by sorting datasources and charts (#38079)
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 superset/thumbnails/digest.py              |  4 +-
 tests/unit_tests/thumbnails/test_digest.py | 59 ++++++++++++++++++++++++++++++
 2 files changed, 61 insertions(+), 2 deletions(-)

diff --git a/superset/thumbnails/digest.py b/superset/thumbnails/digest.py
index 2602f2e8a91..9a18dc64fd6 100644
--- a/superset/thumbnails/digest.py
+++ b/superset/thumbnails/digest.py
@@ -79,7 +79,7 @@ def _adjust_string_with_rls(
             if table_ids:
                 security_manager.prefetch_rls_filters(table_ids)
 
-            for datasource in datasources:
+            for datasource in sorted(datasources, key=lambda d: d.id if d else 
-1):
                 if datasource and getattr(datasource, "is_rls_supported", 
False):
                     rls_filters = datasource.get_sqla_row_level_filters()
 
@@ -110,7 +110,7 @@ def get_dashboard_digest(dashboard: Dashboard) -> str | 
None:
         return func(dashboard, executor_type, executor)
 
     unique_string = (
-        f"{dashboard.id}\n{dashboard.charts}\n{dashboard.position_json}\n"
+        
f"{dashboard.id}\n{sorted(dashboard.charts)}\n{dashboard.position_json}\n"
         f"{dashboard.css}\n{dashboard.json_metadata}"
     )
 
diff --git a/tests/unit_tests/thumbnails/test_digest.py 
b/tests/unit_tests/thumbnails/test_digest.py
index 6223bd8bb76..d6d7f7e2c0d 100644
--- a/tests/unit_tests/thumbnails/test_digest.py
+++ b/tests/unit_tests/thumbnails/test_digest.py
@@ -435,6 +435,65 @@ def test_chart_digest(
             assert get_chart_digest(chart=chart) == expected_result
 
 
+def test_dashboard_digest_deterministic_datasource_order(
+    app_context: None,
+) -> None:
+    """
+    Test that different datasource orderings produce the same digest.
+
+    dashboard.datasources returns a set, whose iteration order is
+    non-deterministic across Python processes (due to PYTHONHASHSEED).
+    The digest must sort datasources by ID to ensure stability.
+    """
+    from superset import security_manager
+    from superset.models.dashboard import Dashboard
+    from superset.models.slice import Slice
+    from superset.thumbnails.digest import get_dashboard_digest
+
+    kwargs = {**_DEFAULT_DASHBOARD_KWARGS}
+    slices = [Slice(**slice_kwargs) for slice_kwargs in kwargs.pop("slices")]
+    dashboard = Dashboard(**kwargs, slices=slices)
+
+    def make_datasource(ds_id: int) -> MagicMock:
+        ds = MagicMock(spec=BaseDatasource)
+        ds.id = ds_id
+        ds.type = DatasourceType.TABLE
+        ds.is_rls_supported = True
+        ds.get_sqla_row_level_filters = 
MagicMock(return_value=[f"filter_ds_{ds_id}"])
+        return ds
+
+    ds_a = make_datasource(5)
+    ds_b = make_datasource(3)
+    ds_c = make_datasource(9)
+
+    user = User(id=1, username="1")
+
+    digests = []
+    for ordering in [[ds_a, ds_b, ds_c], [ds_c, ds_a, ds_b], [ds_b, ds_c, 
ds_a]]:
+        with (
+            patch.dict(
+                current_app.config,
+                {
+                    "THUMBNAIL_EXECUTORS": [ExecutorType.CURRENT_USER],
+                    "THUMBNAIL_DASHBOARD_DIGEST_FUNC": None,
+                },
+            ),
+            patch.object(
+                type(dashboard),
+                "datasources",
+                new_callable=PropertyMock,
+                return_value=ordering,
+            ),
+            patch.object(security_manager, "find_user", return_value=user),
+            patch.object(security_manager, "prefetch_rls_filters", 
return_value=None),
+            override_user(user),
+        ):
+            digests.append(get_dashboard_digest(dashboard=dashboard))
+
+    assert digests[0] == digests[1] == digests[2]
+    assert digests[0] is not None
+
+
 def test_dashboard_digest_prefetches_rls_filters(
     app_context: None,
 ) -> None:

Reply via email to