Skip to content

fix(mcp): report the committed chart when its instance is detached - #42621

Merged
rusackas merged 2 commits into
apache:masterfrom
verdier:fix/mcp-generate-chart-detached-instance
Aug 2, 2026
Merged

fix(mcp): report the committed chart when its instance is detached#42621
rusackas merged 2 commits into
apache:masterfrom
verdier:fix/mcp-generate-chart-detached-instance

Conversation

@verdier

@verdier verdier commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

Fixes #42567.

Above two concurrent generate_chart calls, the chart is committed to the database and the tool still returns an error. The MCP service shares one scoped session across in-flight requests, so a session teardown in one request detaches the Slice another request has just committed; every later read of that instance then raises DetachedInstanceError and the whole tool call is turned into a CHART_GENERATION_FAILED response. The row is there, the client is told it failed, and agents retry — which is how you end up with duplicate charts.

The guarded re-fetch added in #38767 / #38859 hardened serialize_chart_object, but the instance is read in several other places after the commit, and none of them are covered:

  • the explore URL (slice_id={chart.id}) and the form-data cache parameters,
  • analyze_chart_capabilities / analyze_chart_semantics, which read chart.viz_type, and validate_chart_dataset, which reads chart.datasource_id — both through a 3-argument getattr, which swallows AttributeError, not DetachedInstanceError,
  • the api_endpoints block and the completion log in the result payload,
  • and the minimal fallback response itself, which is built by reading chart.id, chart.slice_name, chart.viz_type and chart.uuid off the very instance whose re-fetch just failed.

This PR captures the chart's scalar fields once, immediately after command.run() while the instance is known to be attached, and builds everything downstream from those values. chart_id was already captured there and used for previews; it is now used consistently.

The helpers involved never needed an ORM instance, only one field each, so they now take that field: analyze_chart_capabilities / analyze_chart_semantics take viz_type: str | None, and validate_chart_dataset takes datasource_id: int | None. Each caller therefore reads it while its own instance is attached, instead of inheriting a read it cannot see. The read-path tools (get_chart_data, get_chart_preview, get_chart_info, get_chart_sql, auth.validate_chart_dataset_access) pass chart.datasource_id at the call site — no behaviour change for them, the same read simply moves into view.

Not addressed here: the underlying design, where one scoped session is shared by concurrent MCP tool calls. That is what makes instances detach in the first place, and it can still surface elsewhere — for example get_user_roles lazy-loading User.roles during dataset lookup, which I also hit at high concurrency on a long-running process. This PR removes the generate_chart failure mode; it does not make the session model concurrency-safe. I traced that mechanism and opened #42622 for it, with the evidence and three possible directions — happy to implement whichever one maintainers prefer.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — server-side behaviour.

TESTING INSTRUCTIONS

Unit tests

pytest tests/unit_tests/mcp_service/chart/tool/test_generate_chart.py

Two new tests in TestGenerateChartDetachedInstance drive generate_chart(save_chart=True) with a Slice stand-in that is detached right after the commit — one for the normal path, one for the fallback path where the DAO re-fetch raises SQLAlchemyError. Both fail on master with the production symptom (success=False, DetachedInstanceError) and pass with this change. The full tests/unit_tests/mcp_service suite passes (3129 tests).

End-to-end, against the harness from the issue

apache/superset:6.1.0-py311, superset mcp run, stateless_http=True, stock SQLAlchemy pool, N concurrent generate_chart calls with save_chart=true over two bearer identities, asserted against rows in the metadata database:

concurrency stock 6.1.0 with this change
5 5/5 5/5
10 2/10, 8 DetachedInstanceError, 10 rows written 10/10
20 5/20, 15 DetachedInstanceError, 20 rows written 20/20
40 5/40, 35 DetachedInstanceError, 40 rows written 40/40, 0 errors, 40 rows, 0 cross-attribution

Every row was written in both columns — the failures were purely post-commit reads, which is what makes this reproducible and what the change removes. Same container, same harness, process restarted before each run.

ADDITIONAL INFORMATION

Above two concurrent `generate_chart` calls the chart is written to the
database and the tool still returns an error: the MCP service shares one
scoped session across in-flight requests, so a teardown in one request
detaches the `Slice` another request just committed, and every later read
of `chart.id` raises `DetachedInstanceError`.

The guarded re-fetch added in apache#38767/apache#38859 only covers
`serialize_chart_object`; the identity reads after it (explore URL,
form-data cache, result payload, completion log) and the viz type read by
`analyze_chart_capabilities`/`analyze_chart_semantics` are not covered,
and the minimal fallback response itself reads the detached instance.

Capture the chart's scalar fields once, while the instance is known to be
attached, and build everything downstream from those values. The analysis
helpers only ever needed the viz type, so they now take it directly
instead of an ORM instance.

Fixes apache#42567
@dosubot dosubot Bot added the change:backend Requires changing the backend label Jul 30, 2026
@bito-code-review

bito-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #c7d861

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/mcp_service/chart/chart_utils.py - 1
    • Semantic duplication of fallback logic · Line 1511-1512
      The `if not viz_type: viz_type = _resolve_viz_type(config)` block is copy-pasted identically in both `analyze_chart_capabilities` (line 1511) and `analyze_chart_semantics` (line 1559). If the fallback logic needs to change (e.g., additional validation, different default), both copies must be updated in sync — a divergence risk. Extract into a shared helper function to eliminate duplication.
Review Details
  • Files reviewed - 4 · Commit Range: 322f5d8..322f5d8
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/tool/generate_chart.py
    • superset/mcp_service/chart/tool/update_chart.py
    • tests/unit_tests/mcp_service/chart/tool/test_generate_chart.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 Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.44%. Comparing base (1df0cca) to head (5e9469b).
⚠️ Report is 4 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/chart/tool/generate_chart.py 0.00% 14 Missing ⚠️
superset/mcp_service/chart/chart_utils.py 0.00% 3 Missing ⚠️
superset/mcp_service/chart/tool/update_chart.py 0.00% 3 Missing ⚠️
superset/mcp_service/auth.py 0.00% 1 Missing ⚠️
superset/mcp_service/chart/tool/get_chart_data.py 0.00% 1 Missing ⚠️
superset/mcp_service/chart/tool/get_chart_info.py 0.00% 1 Missing ⚠️
...perset/mcp_service/chart/tool/get_chart_preview.py 0.00% 1 Missing ⚠️
superset/mcp_service/chart/tool/get_chart_sql.py 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42621      +/-   ##
==========================================
- Coverage   65.45%   65.44%   -0.01%     
==========================================
  Files        2810     2810              
  Lines      159335   159353      +18     
  Branches    36362    36366       +4     
==========================================
+ Hits       104285   104286       +1     
- Misses      53007    53024      +17     
  Partials     2043     2043              
Flag Coverage Δ
hive 38.08% <0.00%> (-0.01%) ⬇️
mysql 57.82% <0.00%> (-0.02%) ⬇️
postgres 57.86% <0.00%> (-0.02%) ⬇️
presto 39.98% <0.00%> (-0.01%) ⬇️
python 59.25% <0.00%> (-0.02%) ⬇️
sqlite 57.49% <0.00%> (-0.02%) ⬇️
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.

@verdier

verdier commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

For the record: I traced the mechanism behind this failure mode and opened #42622 for it. Short version — mcp_auth_hook pushes one Flask app context per tool call, but db.session is scoped by greenlet.getcurrent (flask-sqlalchemy 2.5.1), so concurrent asyncio tool calls all resolve to the same Session; the first call to finish pops its context and teardown_appcontext removes that shared session under the calls still running.

This PR is still worth having on its own — the reads it removes were re-fetching values the function already held, and it stops a committed chart from being reported as a failure — but it is not a concurrency fix, and I did not want #42567 closing to read as one.

@sadpandajoe sadpandajoe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Found one remaining detached-instance access in the committed-chart path.

Comment thread superset/mcp_service/chart/tool/generate_chart.py
@bito-code-review

Copy link
Copy Markdown
Contributor

The proposed approach of snapshotting the datasource identity (alongside the other chart attributes) is a sound strategy to avoid DetachedInstanceError during concurrent operations. By capturing these values immediately after the chart is committed and before any await points, you ensure that subsequent logic—such as validate_chart_dataset—operates on stable, local data rather than relying on an instance that might have been detached by a concurrent session teardown. Regarding the regression test, exercising this validator directly is indeed preferable to mocking it, as it provides higher confidence that the fix correctly handles the state transition.

superset/mcp_service/chart/tool/generate_chart.py

chart_id = chart.id
                    chart_slice_name = chart.slice_name
                    chart_viz_type = chart.viz_type
                    chart_uuid = str(chart.uuid) if chart.uuid else None
                    # Also snapshot datasource_id here if needed for validation
                    chart_datasource_id = chart.datasource_id

Review follow-up: the validator read `chart.datasource_id` off the
instance itself, through a three-argument `getattr` — which swallows
`AttributeError`, not `DetachedInstanceError`. That read sits after an
await in `generate_chart`, so it could report an already-committed chart
as failed, exactly like the reads this branch already fixed.

Take the id instead of the chart, so every caller reads it while its own
instance is attached. `generate_chart` uses the value it snapshots at
creation time; the read-path tools pass `chart.datasource_id` at the call
site.

The regression test now exercises the real validator instead of mocking
it.
@bito-code-review

bito-code-review Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #991b74

Actionable Suggestions - 0
Review Details
  • Files reviewed - 9 · Commit Range: 322f5d8..5e9469b
    • superset/mcp_service/auth.py
    • superset/mcp_service/chart/chart_utils.py
    • superset/mcp_service/chart/tool/generate_chart.py
    • superset/mcp_service/chart/tool/get_chart_data.py
    • superset/mcp_service/chart/tool/get_chart_info.py
    • superset/mcp_service/chart/tool/get_chart_preview.py
    • superset/mcp_service/chart/tool/get_chart_sql.py
    • tests/unit_tests/mcp_service/chart/test_chart_utils.py
    • tests/unit_tests/mcp_service/chart/tool/test_generate_chart.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

@rusackas
rusackas requested a review from aminghadersohi July 31, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change:backend Requires changing the backend size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP: generate_chart raises DetachedInstanceError above 2 concurrent calls, after the chart is committed

3 participants