Skip to content

fix(dataset): lock the If-Match validator's transaction-id read - #44091

Merged
aminghadersohi merged 11 commits into
apache:masterfrom
mikebridge:sc-120050-ifmatch-locking-reads
Sep 17, 2026
Merged

aminghadersohi merged 11 commits into
apache:masterfrom
mikebridge:sc-120050-ifmatch-locking-reads

Conversation

@mikebridge

@mikebridge mikebridge commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

SUMMARY

The dataset PUT's conditional-write (If-Match) guard compared the client's token against a version-info lookup made with plain consistent reads. On MySQL/InnoDB default REPEATABLE READ, those reads are served from the snapshot pinned by the request's first read (the auth queries), so a version row committed by a concurrent writer between that first read and this request's row lock stayed invisible: the stale token matched, raise_for_stale_write passed, and the second writer overwrote the first — exactly the lost update the lock + If-Match pair exists to prevent, surviving on MySQL only (R1 and R2 both open with token v1; R1 locks and commits v2; R2 acquires the lock but its validator read still sees v1). This is the validator-layer sibling of #44027, which fixed the entity read one layer down. Postgres (READ COMMITTED, fresh snapshot per statement) and SQLite are unaffected.

Fix:

  • current_live_transaction_id_locked (versioning/queries.py) — the live version row's transaction_id (the token input) read under an exclusive row lock (with_for_update()): a locking read is exempt from the REPEATABLE READ snapshot and returns committed data. Exclusive rather than FOR SHARE because this transaction later closes the very row it reads (Continuum sets end_transaction_id at commit), and a shared lock first invites InnoDB's shared→exclusive upgrade deadlock; plain MVCC readers are blocked by neither strength. Deliberately a plain row query, not the existing aggregate — locking clauses and aggregates don't combine (Postgres rejects the combination) — and ordered-and-limited so a defensively tolerated multi-open-row state degrades like the aggregate's max instead of raising MultipleResultsFound.
  • Lock-contention mapping (three surfacing points → retryable 409): the gap lock this locking read plants on a zero-live-row range is what lets two concurrent conditional writers deadlock — on master the version tables carry no locks at all, so this deadlock class is introduced by this PR and is handled by it. Contention at the entity-row acquisition, locked validator read, or command insert is mapped to 409 "retry the same request" (a deadlock loser's If-Match token is never proven stale, so 412's refetch-the-token guidance would be wrong): the two direct OperationalError handlers (entity-row acquisition and validator read), and the insert-point (Continuum's version-row INSERT inside the command) classified narrowly in the DatasetUpdateFailedError branch via ex.__cause__ — the post-commit override_columns refresh raises its own exception type and structurally cannot reach that branch. is_lock_contention_error lives in versioning/db_errors.py beside is_missing_table_error, hardened (guarded empty driver args, pgcode or sqlstate, PG 55P03, accepts None) with a parameterized test set. The brief retention-prune blocking residual is documented on the query helper.
  • current_entity_version_info grows lock_for_stale_check; when set, the locked transaction id replaces the aggregate's. The displayed version number stays a plain aggregate read: a concurrent commit can leave response metadata one behind, but the guard never consults it (documented in the docstring).
  • The dataset PUT passes lock_for_stale_check=is_conditional_write() — only conditional saves pay for the lock; every GET path and unconditional PUT keeps lock-free reads (pinned by test).

Integration note: #44027 merged on September 14 and is included in this branch. Its entity-read refresh complements this PR’s validator-read fix. The _locked_entity = lock_entity_for_update(...) strong reference is preserved because the identity map is weak. Residual lazy child-collection staleness remains separate.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — concurrency fix. Before (MySQL only): a stale If-Match token could pass the 412 guard after a concurrent commit. After: the guard's input reflects committed state.

TESTING INSTRUCTIONS

  • pytest tests/unit_tests/versioning/test_db_errors.py — parameterized classifier coverage: MySQL 1213/1205, PG pgcode/sqlstate 40001/40P01/55P03, text fallback, unrelated codes, empty driver args, and None (no IndexError-to-500).
  • tests/unit_tests/datasets/api_tests.py — the 409 mapping pinned (status + message, so it cannot drift back to 412), the 412 stale-token path unchanged, the endpoint wiring pinned, and an unconditional-PUT test pinning that no lock and no locking read happen without If-Match (the perf contract both ways — hard-coding the flag fails it).
  • pytest tests/unit_tests/versioning/test_version_info_locking.py — 3 dialect-independent pins: the conditional-path read chains an exclusive with_for_update() with order_by desc + limit 1 (weakening or dropping the clause fails on every backend), current_entity_version_info threads the flag AND derives the token from the locked id (derive_version_uuid pinned — the guard consumes version_uuid, not transaction_id), and the default path never invokes the locking read (GETs stay lock-free). The endpoint wiring itself is pinned in tests/unit_tests/datasets/api_tests.py::test_put_dataset_rejects_stale_if_match (lock_for_stale_check=True must reach the helper).
  • pytest tests/integration_tests/versioning/conditional_token_lock_tests.py — real-backend proof: the locking read executes on the actual dialect, agrees with the plain read on the quiet path, and the zero-live-row branch (lazy baselines — the gap-lock branch) resolves to None/unversioned-token exactly like the plain path. The MySQL RR staleness itself cannot flip on Postgres and needs an interleaved read view on MySQL, so — per the honest-scope precedent of fix(versioning): row-lock the live entity during version restore #44015/fix(dataset): refresh the entity under the conditional-write row lock #44027 — the statement-shape pin is the load-bearing regression guard.
  • Manual (MySQL): two sessions PUT the same dataset with the same If-Match; before the fix the second could pass the guard and clobber the first; after, it gets 412.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

🤖 Generated with Claude Code

https://claude.ai/code/session_01JRLEJS4mUqKBoPjSjviKUW

@github-actions github-actions Bot added the api Related to the REST API label Sep 9, 2026
@codecov

codecov Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.40%. Comparing base (4c77b0b) to head (181b79e).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #44091   +/-   ##
=======================================
  Coverage   80.40%   80.40%           
=======================================
  Files        2932     2932           
  Lines      174525   174571   +46     
  Branches    40548    40557    +9     
=======================================
+ Hits       140319   140363   +44     
- Misses      31537    31539    +2     
  Partials     2669     2669           
Flag Coverage Δ
hive 37.15% <23.52%> (-0.01%) ⬇️
mysql 56.60% <39.21%> (-0.01%) ⬇️
postgres 56.61% <39.21%> (-0.02%) ⬇️
presto 39.22% <23.52%> (-0.01%) ⬇️
python 84.76% <100.00%> (+<0.01%) ⬆️
sqlite 56.31% <39.21%> (-0.02%) ⬇️
unit 76.38% <100.00%> (+0.02%) ⬆️

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.

@mikebridge mikebridge changed the title fix(dataset): read the If-Match validator's transaction id under FOR SHARE fix(dataset): lock the If-Match validator's transaction-id read Sep 9, 2026
@pull-request-size pull-request-size Bot added size/XL and removed size/L labels Sep 9, 2026
@github-actions github-actions Bot added the i18n Namespace | Anything related to localization label Sep 9, 2026
@netlify

netlify Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 181b79e
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6aab5bd79a3bca0008831be7
😎 Deploy Preview https://deploy-preview-44091--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@mikebridge
mikebridge marked this pull request as ready for review September 11, 2026 19:16
@bito-code-review

bito-code-review Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #df79b1

Actionable Suggestions - 0
Additional Suggestions - 4
  • superset/versioning/db_errors.py - 1
    • Over-broad text fallback · Line 65-66
      The `str(exc)` substring fallback can return True for a non-contention driver error whose message merely contains "deadlock" or "lock wait timeout". In `superset/datasets/api.py` (lines 730, 824) that maps a genuine failure to a retryable 409 instead of surfacing the real error. Consider gating the text fallback on `orig` carrying no code.
  • tests/integration_tests/versioning/conditional_token_lock_tests.py - 1
    • Fixture batching undermines test premise · Line 50-51
      `_versioned_chart` commits the description change while the birth_names fixture's INSERTs are still pending, so the first commit batches INSERT+UPDATE into one Continuum transaction and the live version row is the INSERT row updated in place — not the "real committed save" the docstring claims. `version_restore_tests.py` added `_persist_fixture_state()` for exactly this. Add a `db.session.commit()` after the query.
  • superset/versioning/queries.py - 1
    • Duplicated live-row predicate · Line 277-278
      The live-row predicate (`end_transaction_id.is_(None)` AND `operation_type != OPERATION_DELETE`) is duplicated between `current_version_info` (191-192) and this new function (277-278). If the live-row definition changes, the stale-check guard in `current_entity_version_info` could compare the If-Match token against a different row than the version display surfaces. Consider sharing one helper.
  • superset/datasets/api.py - 1
    • Redundant rollback · Line 825-825
      `UpdateDatasetCommand.run()` is wrapped in `@transaction`, which already calls `db.session.rollback()` (decorators.py:275) before `on_error` re-raises `DatasetUpdateFailedError`. In the normal (non-nested) path this rollback is a no-op. It only matters if the endpoint ever runs inside an outer transaction (`g.in_transaction` True). Consider removing it or documenting the defensive intent.
Filtered by Review Rules

Bito filtered these suggestions based on rules created automatically for your feedback. Manage rules.

  • tests/unit_tests/versioning/test_version_info_locking.py - 1
Review Details
  • Files reviewed - 10 · Commit Range: f7ab9ae..cb9e727
    • superset/daos/version.py
    • superset/datasets/api.py
    • superset/translations/messages.pot
    • superset/versioning/api_helpers.py
    • superset/versioning/db_errors.py
    • superset/versioning/queries.py
    • tests/integration_tests/versioning/conditional_token_lock_tests.py
    • tests/unit_tests/datasets/api_tests.py
    • tests/unit_tests/versioning/test_db_errors.py
    • tests/unit_tests/versioning/test_version_info_locking.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 an incremental AI Review.

  • /review full - 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

@mikebridge mikebridge closed this Sep 12, 2026
@mikebridge mikebridge reopened this Sep 12, 2026

@fitzee fitzee left a comment

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.

Adversarial review of the If-Match transaction-id locking change. The core idea — take the transaction-id read under an exclusive row lock so a stale token can't slip past REPEATABLE READ, and map genuine lock contention to a retryable 409 — is sound and well-documented. Five findings below; the first is a correctness gap in the 409-mapping goal, the rest are a doc/code contradiction, a misleading comment, a classifier gap, and a cleanup. (Automated review; posted by a human reviewer.)

Comment thread superset/datasets/api.py Outdated
Comment thread superset/versioning/queries.py Outdated
Comment thread superset/datasets/api.py Outdated
Comment thread superset/versioning/db_errors.py Outdated
Comment thread superset/datasets/api.py
@rusackas

Copy link
Copy Markdown
Member

Needs a rebase, then I can review when needed.

mikebridge and others added 5 commits September 14, 2026 11:02
…SHARE

The conditional-write guard compared the client's If-Match token against
a version-info lookup made with plain consistent reads. On MySQL/InnoDB
REPEATABLE READ those are served from the snapshot pinned by the
request's first read, so a version row committed by a concurrent writer
between that first read and this request's row lock stayed invisible —
the stale token matched, raise_for_stale_write passed, and the second
writer overwrote the first: the lost update the lock + If-Match pair
exists to prevent, surviving on MySQL only. Validator-layer sibling of
the entity refresh in apache#44027.

Add current_live_transaction_id_for_share — a plain live-row query (not
the aggregate: locking clauses and aggregates don't combine reliably
across dialects) under with_for_update(read=True), exempt from the RR
snapshot without blocking readers — and thread lock_for_stale_check
through current_entity_version_info; the dataset PUT passes it only for
conditional writes. The displayed version number stays a plain aggregate
read: a concurrent commit can leave response metadata one behind, but
the guard never consults it.

sc-120050

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRLEJS4mUqKBoPjSjviKUW
Three-lens findings on the FOR SHARE design, all addressed:

- The lock is now EXCLUSIVE (with_for_update()): this transaction later
  closes the very row it reads (Continuum sets end_transaction_id at
  commit), so holding a shared lock first invited InnoDB's
  shared-to-exclusive upgrade deadlock whenever anything queued for the
  row in between (e.g. retention pruning). Plain MVCC readers are not
  blocked by either strength, and same-entity writers are already
  serialised by the entity lock, so exclusivity costs nothing. Renamed
  to current_live_transaction_id_locked accordingly.
- The plain-row read is ordered-and-limited so a defensively tolerated
  multi-open-row state (which the aggregate's max absorbs) degrades the
  same way instead of raising MultipleResultsFound on every PUT.
- MySQL's gap lock on the zero-live-row range leaves a rare cross-entity
  deadlock at Continuum's insert; a lock race lost there IS a concurrent
  interleave, so the PUT path maps lock-contention errors
  (is_lock_contention_error) to the same 412 retry semantics as a stale
  token instead of a 500. Residuals documented on the query helper.
- Test holes closed: the endpoint wiring is pinned (the 412 unit test
  asserts lock_for_stale_check=True reaches the helper — previously
  deleting the kwarg at the call site survived every test), the guard's
  real input is pinned (derive_version_uuid must consume the LOCKED id),
  and the zero-live-row branch has an integration test. lock flag is
  keyword-only; is_conditional_write() hoisted; open-PR citations
  reworded intent-shaped; cleanup restores the captured original via
  session.get.

sc-120050

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRLEJS4mUqKBoPjSjviKUW
Cross-model round (author-verified): the documented cross-entity
gap-lock deadlock surfaces at Continuum's version-row INSERT inside the
update command — where the @transaction error mapping already yields the
pre-existing 422 — while the OperationalError handler at the call site
covers only a deadlock at the locking read itself. The residual doc
implied the handler covered the insert case, and its 412 told the loser
to refetch a token that was never proven stale.

Re-map the read-point handler to 409 with retry-the-same-request
guidance, rewrite the residual paragraph to name both surfacing points
and their true outcomes, align is_lock_contention_error's docstring, and
pin the 409 mapping (status + message) so it cannot drift back to 412.

sc-120050

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRLEJS4mUqKBoPjSjviKUW
… handler

Review round (fitzee):

- The entity-row lock acquisition sat OUTSIDE the contention mapping:
  writer B blocking on writer A's row lock and timing out (1205) — the
  most common contention outcome of all three lock points — propagated
  as an uncaught 500 instead of the retryable 409 this PR's contract
  advertises. Now wrapped with the same classification.
- The three lock points share _lock_contention_response(): one rollback
  + one i18n string instead of two hand-synced copies. Its docstring
  states the rollback is LOAD-BEARING for lock-wait-timeout (with
  innodb_rollback_on_timeout OFF a 1205 rolls back only the failing
  statement; the transaction still holds the entity row lock and the
  rollback is what releases it) — replacing the misleading
  "transaction is already dead" cleanup framing.
- Stale comments fixed: queries.py's residual note and the read-point
  parenthetical claimed the insert-point deadlock surfaces as the
  pre-existing 422; the handler added in this same PR classifies it to
  the same 409.
- is_lock_contention_error's text fallback now recognizes SQLite's
  "database (table) is locked" (no error code exists to match), so
  contention on the SQLite-backed integration suite classifies
  correctly; parametrized tests added.
- put() keeps # noqa: C901 (15 > 10 even after the extraction).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRLEJS4mUqKBoPjSjviKUW
@mikebridge
mikebridge force-pushed the sc-120050-ifmatch-locking-reads branch from 9aa4d73 to 0970543 Compare September 14, 2026 17:04
@mikebridge

Copy link
Copy Markdown
Contributor Author

Rebased onto master at 0970543 (post-#44027), CI green — ready for review.

@bito-code-review

bito-code-review Bot commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #a9b289

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset/versioning/db_errors.py - 1
    • False-positive lock contention · Line 65-65
      `str(exc)` on a SQLAlchemy `DBAPIError` (2.0.52) calls `_sql_message()`, which appends `[SQL: ...]` and `[parameters: ...]` to the driver message. A statement or bound parameter containing "deadlock", "lock wait timeout", or "database is locked" would be misclassified as lock contention and routed to `_lock_contention_response()` (409) instead of surfacing the real error. Match on `str(orig)` (the driver message) instead.
  • superset/datasets/api.py - 1
    • Docstring overstates rollback role · Line 576-593
      The docstring states the rollback is "LOAD-BEARING" for lock-wait-timeout, but on the `DatasetUpdateFailedError` path (line 856) the `transaction` decorator already called `db.session.rollback()` before `on_error` raised it, so this rollback is a redundant no-op there. It is load-bearing only for the two direct-catch paths (`lock_entity_for_update`, `current_entity_version_info`). Consider noting that distinction to avoid misleading future maintainers.
Review Details
  • Files reviewed - 10 · Commit Range: a53f3fe..0970543
    • superset/daos/version.py
    • superset/datasets/api.py
    • superset/translations/messages.pot
    • superset/versioning/api_helpers.py
    • superset/versioning/db_errors.py
    • superset/versioning/queries.py
    • tests/integration_tests/versioning/conditional_token_lock_tests.py
    • tests/unit_tests/datasets/api_tests.py
    • tests/unit_tests/versioning/test_db_errors.py
    • tests/unit_tests/versioning/test_version_info_locking.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 an incremental AI Review.

  • /review full - 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

@richardfogaca richardfogaca left a comment

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.

Richard's agent here:

Reviewed 0970543ced89.

Following up on the existing error-classification concern: P2 — unrelated failures can become retryable 409s. db_errors.py:65–76 searches str(exc), including SQL and bound parameters. An isolated probe with a non-locking DataError and deadlock only in its parameters matched as contention. Could we inspect the underlying driver error instead and add negative tests for lock-related words in SQL/parameters? Retrying the same invalid request cannot fix this failure.

…ot the wrapper's rendered SQL (sc-120050)

Richard Fogaça's review: is_lock_contention_error's fallback searched
str(exc), and str(DBAPIError) appends the failing statement and its
bound parameters — so an unrelated failure whose SQL or values merely
contain a lock word (a chart named "deadlock analysis", a column called
lock_wait_timeout) was answered 409 "Retry the same request", advice
that can never succeed because nothing is contending. Reachable at the
response boundary: the command wraps SQLAlchemy errors as
DatasetUpdateFailedError and the endpoint classifies its __cause__.

Classification now reads the DRIVER DIAGNOSTIC only. A structured code
is authoritative: a MySQL errno or PostgreSQL SQLSTATE outside the
contention sets ends classification at False instead of falling through
to a keyword match. Only code-less drivers (SQLite) reach the phrase
check, and then against their own message. A wrapper whose driver error
was not preserved is unclassifiable, not retryable; a raw driver
exception is still its own diagnostic.

Tests: the fallback positives now put their message in the driver error
rather than the statement argument; new negatives cover each phrase
appearing only in SQL or parameters on an unrelated DataError and
OperationalError, a non-contention code whose diagnostic mentions a
deadlock (pinning code precedence), and a wrapper with no orig. At the
response boundary, a contaminated non-lock DataError keeps its 422
while genuine contention stays 409. 11 of these fail without the fix.

Also, the interleaved MySQL regression the lens pass found missing: two
sessions, deterministic, no sleeps. Writer A commits a competing
version between this request's read view opening and its validator
read; the plain read is served from the pre-commit snapshot and still
names v1 — so the client's stale If-Match token MATCHES and the update
would clobber A — while the locking read derives v2's token and the
same token is rejected. Zero-history-to-first-version covered too (the
MySQL gap-lock branch). Both force REPEATABLE READ per-session, since
CI pins the MySQL server to READ COMMITTED and the staleness cannot
flip under it. Verified against a real MySQL 8: 4/4 pass, and with the
locking clause removed both new tests fail while both pre-existing ones
still pass — the verification gap, closed.

Plus: the PUT contract documents the retryable 409 and contrasts it
with 412's refetch guidance, and the new-code annotations the lens pass
listed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JRLEJS4mUqKBoPjSjviKUW
@bito-code-review

bito-code-review Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #fcbd3e

Actionable Suggestions - 0
Review Details
  • Files reviewed - 7 · Commit Range: 0970543..828a7ed
    • superset/datasets/api.py
    • superset/versioning/db_errors.py
    • superset/versioning/queries.py
    • tests/integration_tests/versioning/conditional_token_lock_tests.py
    • tests/unit_tests/datasets/api_tests.py
    • tests/unit_tests/versioning/test_db_errors.py
    • tests/unit_tests/versioning/test_version_info_locking.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 an incremental AI Review.

  • /review full - 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

Mike Bridge and others added 2 commits September 15, 2026 16:45
…ponses

check-openapi-spec-drift went red: the new retryable-409 response and the
reworded 412 were added to the docstrings but the published spec was never
regenerated, so the two disagreed.

Generated in an isolated clone with the CLI's resolved module path asserted
to lie inside it first -- the entry point builds its output path from the
imported module's __file__, so a bare invocation can write into whichever
editable install answers the import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EA4MGEkUcDpyLReqjQBhPy
@github-actions github-actions Bot added the doc Namespace | Anything related to documentation label Sep 15, 2026
@aminghadersohi
aminghadersohi self-requested a review September 16, 2026 06:26

@aminghadersohi aminghadersohi left a comment

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.

No production defect found — the locking read behaves as documented on both MySQL and Postgres. Two inline notes, both test-coverage gaps on paths this PR adds.

Comment thread superset/datasets/api.py
# refresh raises its own exception type and cannot reach this
# branch.) Same retryable classification as the read-point
# handler above: the token is not proven stale.
if conditional and is_lock_contention_error(ex.__cause__):

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.

Only negative coverage here: the contaminated-DataError test asserts this does NOT fire. Replacing it with if False: leaves all 52 unit tests green. A positive twin — args=(1213, ...) as __cause__ — passes as written and fails once removed. Credits fitzee's 422-vs-409 thread.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in c6767b2: a positive command failure whose cause is OperationalError with driver args (1213, ...), plus an unconditional control that must remain 422 without retry text. Replacing the conditional cause handler with if False in a process-local mutation made the positive fail (422 vs 409) while the unconditional control passed. Final candidate: 59 focused tests and 221 versioning unit tests pass, complete changed-file pre-commit passes, and independent final-snapshot review approves. This is unit verification; fresh live MySQL REPEATABLE READ integration remains a CI gate.

Comment thread superset/datasets/api.py
already rolled the transaction back and this clears the aborted
session before responding.
"""
db.session.rollback() # pylint: disable=consider-using-transaction

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.

db.session.rollback() → pass also survives all 52 tests. fitzee's thread warned a future cleanup trusting the comment would leak the entity lock on a 1205; the docstring now says LOAD-BEARING, but nothing pins it. Asserting it on the 1205 path closes that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in c6767b2: the 1205 test covers both direct lock points and asserts rollback exactly once before request teardown. Replacing the helper rollback with pass in-process made both cases fail with zero rollback calls. The docstring also distinguishes the required direct-catch rollback from the decorated command path where rollback already occurred. Final focused suite (59) and full versioning unit suite (221) pass; changed-file hooks and independent final-snapshot review pass. No claim of a fresh live-server lock-release test.

@aminghadersohi aminghadersohi left a comment

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.

Follow-up on the classifier's branch ORDER, after a sibling PR shipped the inverse idiom. db_errors.py is correct here — but the ordering that makes it correct is unpinned. One inline.

Comment on lines +182 to +183
_FakeLockDriverError(args=(1213, "Deadlock found when trying to get lock")),
_FakeLockDriverError(args=(1205, "Lock wait timeout exceeded")),

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.

These fakes set args XOR sqlstate; real PyMySQL (dist 1.2.0) sets both — 1213 carries 40001, 1205 carries HY000. So nothing pins errno-before-sqlstate: swapping that order keeps all 52 green while real 1205 flips to False. 1213 survives the swap by coincidence, so the case has to be 1205.

Suggested change
_FakeLockDriverError(args=(1213, "Deadlock found when trying to get lock")),
_FakeLockDriverError(args=(1205, "Lock wait timeout exceeded")),
_FakeLockDriverError(
args=(1213, "Deadlock found when trying to get lock"), sqlstate="40001"
),
_FakeLockDriverError(
args=(1205, "Lock wait timeout exceeded"), sqlstate="HY000"
),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added both combined driver shapes in c6767b2: 1213/40001 and 1205/HY000. A process-local SQLSTATE-first mutant fails specifically on 1205/HY000 (1 failed, 7 passed in the positive cases), so this pins the decisive errno-first order rather than relying on 1213 agreeing by coincidence. The production classification order is unchanged. Final 59 focused tests, 221 versioning unit tests, complete changed-file hooks and independent final-snapshot review pass; current-head CI is separate.

@bito-code-review

bito-code-review Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #398987

Actionable Suggestions - 0
Review Details
  • Files reviewed - 7 · Commit Range: 828a7ed..c6767b2
    • superset/datasets/api.py
    • superset/versioning/api_helpers.py
    • superset/versioning/db_errors.py
    • superset/versioning/queries.py
    • tests/integration_tests/versioning/conditional_token_lock_tests.py
    • tests/unit_tests/datasets/api_tests.py
    • tests/unit_tests/versioning/test_db_errors.py
  • Files skipped - 1
    • docs/static/resources/openapi.json - Reason: Filter setting
  • 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 an incremental AI Review.

  • /review full - 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

@mikebridge

Copy link
Copy Markdown
Contributor Author

CI refresh: merged Apache master containing the already-merged migration repair #44364 into this branch, published at 181b79e. No duplicate migration was added. The fresh PR merge ref includes repair commit 4c77b0b; its migration directory matches that repaired baseline.

Local verification: 60 focused tests passed (including the single-migration-head regression), all applicable pre-commit hooks passed against the complete branch-changed file list, and the final source snapshot received independent Python review. The only additional changes correct test driver-exception fixtures and their annotations; production behavior is unchanged.

Fresh CI has been triggered; this is not an all-green claim. Live database integration remains a remote-CI gate. The older migration failures tested pre-repair merge snapshots and are not evidence about this new head.

@mikebridge

Copy link
Copy Markdown
Contributor Author

Readiness follow-through at 181b79e: the fresh checks have completed with no failing or pending checks (64 success, 10 skipped, 2 neutral). The PostgreSQL job tested a084632, which includes merged migration repair #44364, and passed 2,792 tests. The MySQL job passed 2,780 tests and ran all four conditional_token_lock_tests.py cases successfully: https://github.com/apache/superset/actions/runs/35177627962/job/105062802120.

The positive chained-cause, rollback and combined errno/SQLSTATE regressions described in the three inline replies remain present. Author remediation is complete; those open threads still need reviewer confirmation. This is not a new approval or automatic thread resolution.

@aminghadersohi aminghadersohi left a comment

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.

Verified at 181b79e: the rollback, chained-__cause__ and errno-before-sqlstate mutants are all killed now — the order mutant fails on 1205/HY000 specifically. No new findings; the production delta is annotations and comment fixes only.

@richardfogaca richardfogaca left a comment

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.

Richard's agent here: re-reviewed at 181b79e. My earlier concern about the classifier matching on rendered SQL/parameters is resolved. It now reads only the driver diagnostic and treats a wrapper without orig as unclassifiable. I also confirmed the locking read is index-backed on each dialect and that dataset PUT is the only If-Match call site. No blocking findings.

Small suggestion, totally optional and fine as a follow-up: the dataset editor special-cases 412 but lets the new 409 fall through to the generic error dialog, so users see "Retry the same request." Could we give 409 a friendlier message along the lines of "another save is in progress, try saving again"? Happy to keep as-is if this is intentional.

@aminghadersohi
aminghadersohi merged commit fc71d0f into apache:master Sep 17, 2026
76 checks passed
villebro pushed a commit that referenced this pull request Sep 20, 2026
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
(cherry picked from commit fc71d0f)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API doc Namespace | Anything related to documentation i18n Namespace | Anything related to localization size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants