Skip to content

fix(sqllab): roll back session before retrying get_query after a broken transaction - #42675

Merged
eschutho merged 2 commits into
masterfrom
fix-sql-lab-get-query-session-rollback
Aug 3, 2026
Merged

fix(sqllab): roll back session before retrying get_query after a broken transaction#42675
eschutho merged 2 commits into
masterfrom
fix-sql-lab-get-query-session-rollback

Conversation

@eschutho

@eschutho eschutho commented Aug 1, 2026

Copy link
Copy Markdown
Member

Sentry issue

SUPERSET-PYTHON-WDZSqlLabException: Failed at getting query30,500 occurrences, 24 users impacted, first seen 2025-04-05, still firing daily in production.

Root cause

superset/sql_lab.py::get_query is wrapped in a backoff.on_exception(SqlLabException, interval=1, max_tries=5) retry loop:

def get_query(query_id: int) -> Query:
    """attempts to get the query and retry if it cannot"""
    try:
        return db.session.query(Query).filter_by(id=query_id).one()
    except Exception as ex:
        raise SqlLabException("Failed at getting query") from ex

The Sentry event's exception chain shows a SQLAlchemy PendingRollbackError ("This Session's transaction has been rolled back due to a previous exception during flush... first issue Session.rollback()"), itself chained under an OperationalError/QueryCanceled from a dropped connection or statement timeout. Once a session enters this state, SQLAlchemy refuses any further operation on it until .rollback() is called explicitly.

Because get_query's except block never rolls back, every one of the 5 backoff retries reuses the same poisoned session and fails identically with the same PendingRollbackError — the retry loop is guaranteed to burn all 5 attempts and give up, with zero chance to recover from what may have been a transient connection blip.

Verified with a standalone repro (pinned sqlalchemy==1.4.54, in-memory sqlite): poisoning a session via a failed flush, then calling a get_query-shaped function under the same backoff.on_exception decorator — without a rollback, all 5 retries fail identically; adding session.rollback() in the except block lets the very next retry succeed.

Fix

Add db.session.rollback() in get_query's except block before re-raising SqlLabException, so each backoff retry starts from a clean session and has a real chance of succeeding instead of a guaranteed-failure loop.

Tradeoffs

None — this is additive-only. The exception raised (SqlLabException) and logged (via get_query_backoff_handler/get_query_giveup_handler) is unchanged; only the session state between retries changes, giving the existing retry mechanism a real chance to work as originally designed. No change to execute_query/execute_sql_statements/handle_query_error, and no change to the backoff decorator's parameters.

Testing

  • Added test_get_query_rolls_back_session_before_retrying to tests/unit_tests/sql_lab_test.py, asserting get_query recovers via retry after a first failure, and that db.session.rollback() is called before the successful retry.
  • tests/unit_tests/sql_lab_test.py — 12 passed.
  • ruff check / ruff format --check (pinned 0.9.7) — clean on both changed files.

Shortcut

sc-115684

Fixes SUPERSET-PYTHON-WDZ

🤖 Generated with Claude Code

…en transaction

`get_query` catches any exception from the ORM lookup and relies on the
`backoff` decorator to retry up to 5 times. When the underlying failure is
(or causes) a SQLAlchemy `PendingRollbackError` - e.g. a `PendingRollbackError`
chained under an `OperationalError`/`QueryCanceled` from a dropped connection
or statement timeout - the session is left in a broken state that SQLAlchemy
refuses to use again until `.rollback()` is called explicitly. Since the
session was never rolled back, every one of the 5 retries reused the same
poisoned session and failed identically, so the retry loop never had a
chance to recover from what may be a transient connection blip.

Roll back the session in the except block before raising `SqlLabException`
so each `backoff` retry starts from a clean session. The exception raised
and logged is unchanged.

Fixes SUPERSET-PYTHON-WDZ

Co-Authored-By: Claude <noreply@anthropic.com>
@dosubot dosubot Bot added the sqllab Namespace | Anything related to the SQL Lab label Aug 1, 2026
@bito-code-review

bito-code-review Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #272215

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 7d5e8ad..7d5e8ad
    • superset/sql_lab.py
    • tests/unit_tests/sql_lab_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

AI Code Review powered by Bito Logo

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.43%. Comparing base (22c305f) to head (09c9410).
⚠️ Report is 16 commits behind head on master.

Files with missing lines Patch % Lines
superset/sql_lab.py 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42675      +/-   ##
==========================================
- Coverage   65.43%   65.43%   -0.01%     
==========================================
  Files        2810     2810              
  Lines      159460   159475      +15     
  Branches    36396    36398       +2     
==========================================
+ Hits       104350   104354       +4     
- Misses      53068    53076       +8     
- Partials     2042     2045       +3     
Flag Coverage Δ
hive 38.07% <0.00%> (+<0.01%) ⬆️
mysql 57.79% <0.00%> (-0.01%) ⬇️
postgres 57.84% <0.00%> (-0.01%) ⬇️
presto 39.96% <0.00%> (ø)
python 59.22% <0.00%> (-0.01%) ⬇️
sqlite 57.46% <0.00%> (-0.01%) ⬇️
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread superset/sql_lab.py Outdated
except Exception as ex:
# roll back so a poisoned session (e.g. PendingRollbackError after a
# failed flush) doesn't fail every subsequent backoff retry identically
db.session.rollback()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: A failed database rollback can itself raise an exception, which would replace the original error and escape as a non-SqlLabException. Because the decorator retries only SqlLabException, this prevents retries precisely when the session or connection is too broken to roll back normally. Preserve the original wrapped exception and ensure rollback failures do not bypass the retry contract. [possible bug]

Severity Level: Major ⚠️
- ❌ Broken database connections can bypass `get_query()` retries.
- ⚠️ SQL Lab error handling receives an unexpected rollback exception.
- ⚠️ Transient query failures may fail instead of recovering.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/sql_lab.py
**Line:** 166:166
**Comment:**
	*Possible Bug: A failed database rollback can itself raise an exception, which would replace the original error and escape as a non-`SqlLabException`. Because the decorator retries only `SqlLabException`, this prevents retries precisely when the session or connection is too broken to roll back normally. Preserve the original wrapped exception and ensure rollback failures do not bypass the retry contract.

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 fix
👍 | 👎

@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged issue is correct. If db.session.rollback() fails, it raises an exception that escapes the try...except block, bypassing the retry logic because the decorator only catches SqlLabException. To resolve this, wrap the rollback in its own try...except block to ensure the original exception is preserved and re-raised as a SqlLabException.

    except Exception as ex:
        try:
            db.session.rollback()
        except Exception:
            # Log rollback failure if necessary
            pass
        raise SqlLabException("Failed at getting query") from ex

superset/sql_lab.py

except Exception as ex:
        try:
            db.session.rollback()
        except Exception:
            # Log rollback failure if necessary
            pass
        raise SqlLabException("Failed at getting query") from ex

@eschutho
eschutho requested a review from rebenitez1802 August 1, 2026 15:22
…ntract

get_query's backoff decorator only retries on SqlLabException. If
db.session.rollback() itself raises (e.g. the connection is fully
dead), that new exception would replace the intended SqlLabException
and bypass the retry contract. Swallow rollback failures so the
original lookup error is always what gets raised.

Co-Authored-By: Claude <noreply@anthropic.com>
@bito-code-review

bito-code-review Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #974632

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 7d5e8ad..09c9410
    • superset/sql_lab.py
    • tests/unit_tests/sql_lab_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

AI Code Review powered by Bito Logo

@eschutho
eschutho merged commit 25ab961 into master Aug 3, 2026
60 checks passed
@eschutho
eschutho deleted the fix-sql-lab-get-query-session-rollback branch August 3, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preset-io size/M sqllab Namespace | Anything related to the SQL Lab

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants