fix(caching): sort extra_cache_keys before hashing (#34543) - #42597
fix(caching): sort extra_cache_keys before hashing (#34543)#42597rusackas wants to merge 3 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #42597 +/- ##
==========================================
- Coverage 65.57% 65.57% -0.01%
==========================================
Files 2818 2818
Lines 160023 160025 +2
Branches 36556 36557 +1
==========================================
- Hits 104940 104938 -2
- Misses 53038 53040 +2
- Partials 2045 2047 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| same_values_different_order = ["CAR_IDS=1,2,3", "CHASSIS_IDS=100,200"] | ||
| cache_key1 = query_object1.cache_key(extra_cache_keys=same_values_different_order) | ||
| cache_key2 = query_object2.cache_key( | ||
| extra_cache_keys=list(reversed(same_values_different_order)) | ||
| ) |
There was a problem hiding this comment.
Suggestion: The fixture covers only strings, but real datasource output contains heterogeneous hashable values such as strings, integers, and None. A common fix for the ordering bug is sorting the deduplicated values, which raises TypeError for mixed incomparable types; this test would pass while leaving that production path broken. Include a mixed-type set of cache-key components in the regression case. [type error]
Severity Level: Major ⚠️
- ❌ Mixed Jinja cache keys could cause query failures.
- ⚠️ Current regression fixture misses integer/string combinations.
- ⚠️ Deterministic sorting may raise runtime TypeError.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** tests/unit_tests/queries/query_object_test.py
**Line:** 109:113
**Comment:**
*Type Error: The fixture covers only strings, but real datasource output contains heterogeneous hashable values such as strings, integers, and `None`. A common fix for the ordering bug is sorting the deduplicated values, which raises `TypeError` for mixed incomparable types; this test would pass while leaving that production path broken. Include a mixed-type set of cache-key components in the regression case.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fixThere was a problem hiding this comment.
Sorting is keyed off str(), not a bare sorted(), so mixed types don't error out. Tried your ["CAR_IDS=1,2,3", 123, None] case locally and both orders hash identically, no TypeError.
|
The flagged issue is correct. The current regression test uses only strings, which masks potential Here is the updated test case: query_object1 = QueryObject(row_limit=1)
query_object2 = QueryObject(row_limit=1)
# Mixed types to test sorting robustness
mixed_values_different_order = ["CAR_IDS=1,2,3", 123, None]
cache_key1 = query_object1.cache_key(extra_cache_keys=mixed_values_different_order)
cache_key2 = query_object2.cache_key(
extra_cache_keys=list(reversed(mixed_values_different_order))
)
assert cache_key1 == cache_key2Would you like me to check the rest of the comments on this PR and implement fixes for them as well? tests/unit_tests/queries/query_object_test.py |
There was a problem hiding this comment.
Code Review Agent Run #fc1c01
Actionable Suggestions - 1
-
tests/unit_tests/queries/query_object_test.py - 1
- Test reveals unfixed cache key ordering bug · Line 89-114
Review Details
-
Files reviewed - 1 · Commit Range:
a0e2dc6..a0e2dc6- tests/unit_tests/queries/query_object_test.py
-
Files skipped - 0
-
Tools
- MyPy (Static Code Analysis) - ✔︎ Successful
- Astral Ruff (Static Code Analysis) - ✔︎ Successful
- Whispers (Secret Scanner) - ✔︎ Successful
- Detect-secrets (Secret Scanner) - ✔︎ Successful
Bito Usage Guide
Commands
Type the following command in the pull request comment and save the comment.
-
/review- Manually triggers a full AI review. -
/pause- Pauses automatic reviews on this pull request. -
/resume- Resumes automatic reviews. -
/resolve- Marks all Bito-posted review comments as resolved. -
/abort- Cancels all in-progress reviews.
Refer to the documentation for additional commands.
Configuration
This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.
Documentation & Help
| def test_cache_key_stable_regardless_of_extra_cache_keys_order(): | ||
| """ | ||
| Regression for #34543: the cache key must not depend on the order of | ||
| ``extra_cache_keys``. | ||
|
|
||
| ``SqlaTable.get_extra_cache_keys`` (superset/connectors/sqla/models.py) | ||
| returns ``list(set(extra_cache_keys))``. Python's string hashing is | ||
| randomized per-process (``PYTHONHASHSEED``), so the same set of values | ||
| can iterate in a different order in the Celery worker process (which | ||
| writes the query results to cache) than in the web process (which | ||
| re-derives the cache key to read them back). Because ``hash_from_dict`` | ||
| only sorts dict keys and not list values, two ``extra_cache_keys`` lists | ||
| with identical Jinja ``url_param()`` values but different order hash to | ||
| different cache keys, causing async chart-data lookups to 422 with | ||
| "Error loading data from cache" whenever more than one url_param is | ||
| referenced (a single-element list has only one possible order, which is | ||
| why the bug is only visible with multiple parameters). | ||
| """ | ||
| query_object1 = QueryObject(row_limit=1) | ||
| query_object2 = QueryObject(row_limit=1) | ||
| same_values_different_order = ["CAR_IDS=1,2,3", "CHASSIS_IDS=100,200"] | ||
| cache_key1 = query_object1.cache_key(extra_cache_keys=same_values_different_order) | ||
| cache_key2 = query_object2.cache_key( | ||
| extra_cache_keys=list(reversed(same_values_different_order)) | ||
| ) | ||
| assert cache_key1 == cache_key2 |
There was a problem hiding this comment.
Test correctly identifies bug #34543 but is currently failing. hash_from_dict at superset/utils/hashing.py:93-94 calls json.dumps(obj, sort_keys=True) which only sorts dict keys—list values retain their original order. Two identical lists in different orders produce different cache keys, causing async chart-data lookups to fail with "Error loading data from cache" when multiple url_param() values are referenced.
Code Review Run #fc1c01
Should Bito avoid suggestions like this for future reviews? (Manage Rules)
- Yes, avoid them
There was a problem hiding this comment.
This landed with the sort in cache_key(), extra_cache_keys gets sorted before hashing now so the test's green.
There was a problem hiding this comment.
The reviewer's suggestion to include mixed-type cache-key components in the regression test was evaluated. Local testing confirms that sorting by str() representation avoids TypeError for mixed types, and the proposed test case correctly handles heterogeneous values without error. The current implementation is appropriate for verifying the cache key stability.
| query_object1 = QueryObject(row_limit=1) | ||
| query_object2 = QueryObject(row_limit=1) | ||
| same_values_different_order = ["CAR_IDS=1,2,3", "CHASSIS_IDS=100,200"] | ||
| cache_key1 = query_object1.cache_key(extra_cache_keys=same_values_different_order) |
There was a problem hiding this comment.
This assertion requires canonicalizing extra_cache_keys inside QueryObject.cache_key, so fixing the nondeterminism at SqlaTable.get_extra_cache_keys would resolve both this path and the legacy BaseViz consumer but still leave this test red. Could this exercise the producer boundary instead so the regression test does not force the narrower fix location?
There was a problem hiding this comment.
Went with sorting at cache_key() over the producer since there's more than one path feeding extra_cache_keys in, get_extra_cache_keys() and query_context_processor.py both land there. One sort at the boundary covers both instead of chasing each producer.
| query_object2 = QueryObject(row_limit=1) | ||
| same_values_different_order = ["CAR_IDS=1,2,3", "CHASSIS_IDS=100,200"] | ||
| cache_key1 = query_object1.cache_key(extra_cache_keys=same_values_different_order) | ||
| cache_key2 = query_object2.cache_key( |
There was a problem hiding this comment.
The real producer appends only raw url_param() values, so list position is what distinguishes which template call produced each value. Making ["1", "2"] equivalent to ["2", "1"] lets swapped parameters render different SQL while sharing a data-cache key and can return the wrong cached rows. Could this test deterministic ordered deduplication at get_extra_cache_keys() instead of declaring the final list order-insensitive?
There was a problem hiding this comment.
SqlaTable.get_extra_cache_keys already does list(set(extra_cache_keys)) before this ever reaches cache_key(), so any positional signal from url_param() call order is already gone by the time we see it. Nothing to lose by sorting.
| cache_key2 = query_object2.cache_key( | ||
| extra_cache_keys=list(reversed(same_values_different_order)) | ||
| ) | ||
| assert cache_key1 == cache_key2 |
There was a problem hiding this comment.
This assertion would also pass if hash_from_dict canonicalized every list, which would collapse order-significant fields such as columns, orderby, or post_processing and could serve another query's cached rows. Could you add a negative control showing that reordering one of those query fields still changes the cache key?
There was a problem hiding this comment.
Added a negative control for that, orderby order still changes the cache key so we're not over-canonicalizing.
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Code Review Agent Run #c49b89Actionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
Per review feedback on #42597: the extra_cache_keys ordering fix canonicalizes only that field, not list values generically. Add a companion test asserting orderby order still changes the cache key, so a future refactor can't accidentally widen the canonicalization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| cache_dict["extra_cache_keys"] = sorted( | ||
| cache_dict["extra_cache_keys"], key=str | ||
| ) |
There was a problem hiding this comment.
Suggestion: The key=str sort is not a complete canonicalization for the declared Hashable value domain. Distinct values such as integer 1 and string "1" have the same sort key, so Python's stable sort preserves whichever order the producer supplied; because SqlaTable.get_extra_cache_keys() derives its list from a set, that order can still differ between processes and produce different cache keys. Use a type-aware deterministic ordering (or normalize each value with an unambiguous type/value representation) so values with equal string representations are ordered consistently. [cache]
Severity Level: Major ⚠️
- ❌ Mixed user-ID and URL-parameter queries can miss async chart-data caches.
- ⚠️ Affected requests may return “Error loading data from cache.”
- ⚠️ Cache duplication can occur across worker and web processes.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** superset/common/query_object.py
**Line:** 451:453
**Comment:**
*Cache: The `key=str` sort is not a complete canonicalization for the declared `Hashable` value domain. Distinct values such as integer `1` and string `"1"` have the same sort key, so Python's stable sort preserves whichever order the producer supplied; because `SqlaTable.get_extra_cache_keys()` derives its list from a set, that order can still differ between processes and produce different cache keys. Use a type-aware deterministic ordering (or normalize each value with an unambiguous type/value representation) so values with equal string representations are ordered consistently.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fixThis is a test-only PR opened as a TDD-style validation of issue #34543. #34543 (filed 2025-08) reports that embedded dashboards with multiple Jinja url_param() filters fail async cache retrieval with a 422 "Error loading data from cache", while a single url_param works fine. Root cause: SqlaTable.get_extra_cache_keys() (superset/connectors/sqla/models.py) returns list(set(extra_cache_keys)). Python randomizes string hashing per-process, so the same set of url_param values can iterate in a different order in the Celery worker (which writes the query results to cache) than in the web process (which re-derives the cache key to read them back). hash_from_dict() only sorts dict keys, not list values, so two extra_cache_keys lists with identical values but different order hash to different cache keys. A single-element list has only one possible order, which is why the bug only appears with multiple parameters. This PR adds one regression test on QueryObject.cache_key(): 1. test_cache_key_stable_regardless_of_extra_cache_keys_order - asserts the cache key is identical for two otherwise-equal query objects whose extra_cache_keys differ only in order. Closes #34543 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
QueryObject.cache_key() merges extra_cache_keys (produced by SqlaTable.get_extra_cache_keys, ultimately from Jinja url_param() calls) straight into the hashed cache_dict in whatever order the caller passed. hash_from_dict only sorts dict keys via json.dumps(sort_keys=True), never list contents, so two otherwise- identical queries whose extra_cache_keys list the same values in a different order hash to different cache keys. SqlaTable.get_extra_cache_keys itself returns list(set(...)), and Python's per-process string-hash randomization means that set can iterate in a different order in the Celery worker that writes a chart's cached result than in the web process that later re-derives the cache key to read it back, whenever 2+ url_params are involved (a single-element list has only one possible order, matching the reported single-vs-multi-parameter split exactly). Order carries no meaning for this field, it's a set of opaque Jinja-derived values, so sort it once where it enters cache_key() rather than at every producer, making any future extra_cache_keys source safe by construction (this also covers the pre-existing call site in superset/common/query_context_processor.py, which passes datasource.get_extra_cache_keys() straight through the same cache_key(extra_cache_keys=...) path). Closes #34543 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per review feedback on #42597: the extra_cache_keys ordering fix canonicalizes only that field, not list values generically. Add a companion test asserting orderby order still changes the cache key, so a future refactor can't accidentally widen the canonicalization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3d6bc70 to
dd86b72
Compare
Code Review Agent Run #9fedffActionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
SUMMARY
Fixes #34543. Embedded dashboards with multiple Jinja
url_param()filters failed async chart-data cache retrieval with a422 Unprocessable Entity/ "Error loading data from cache", while a singleurl_paramworked fine.Root cause:
SqlaTable.get_extra_cache_keys()(superset/connectors/sqla/models.py) returnslist(set(extra_cache_keys)). Python randomizes string hashing per-process (PYTHONHASHSEED), so the same set ofurl_param()values can iterate in a different order in the Celery worker process (which writes the query results to cache) than in the web process (which later re-derives the cache key to read them back).QueryObject.cache_key()merges that list straight into the dict it hashes, in whatever order it arrives;hash_from_dict()only sorts dict keys, never list values. A single-element list has only one possible order, which is exactly why the bug is only visible with 2+ url_params, matching the reported single-vs-multi-parameter split precisely.This was originally opened as a test-only TDD PR pinning the gap down; this update adds the actual fix.
THE FIX
QueryObject.cache_key()(superset/common/query_object.py): sortextra_cache_keys(bystr, since entries are justHashable, not guaranteed mutually orderable) right where it enters the dict that gets hashed, before callinghash_from_dict(). Order carries no meaning for this field, it's an unordered set of opaque Jinja-derived values, so normalizing it here makes any current or future producer ofextra_cache_keyssafe by construction, rather than patching each producer individually.TESTING INSTRUCTIONS
test_cache_key_stable_regardless_of_extra_cache_keys_orderwas expected/confirmed red before the fix (asserts two otherwise-identical query objects withextra_cache_keysdiffering only in order produce the same cache key); now green. Confirmed the rest oftests/unit_tests/queries/,tests/unit_tests/common/, andtests/unit_tests/charts/(344 tests) are unaffected.ADDITIONAL INFORMATION
GLOBAL_ASYNC_QUERIES(only needed to reproduce the end-to-end symptom; the fix and test operate on the cache-key logic directly)🤖 Generated with Claude Code