Skip to content

fix(models): close has_view connection and dispose evicted engines - #45861

Open
sadpandajoe wants to merge 2 commits into
masterfrom
fix-duckdb-engine-cache-stale-config
Open

sadpandajoe wants to merge 2 commits into
masterfrom
fix-duckdb-engine-cache-stale-config

Conversation

@sadpandajoe

Copy link
Copy Markdown
Member

SUMMARY

Databases that allow only one configuration per database file per process, such as DuckDB, reject opening a file with different settings while any connection to it is still open in that process (Connection Error: Can't open a connection to same database file with a different configuration than existing connections). After a connection's settings are edited (for example a rotated access token), Superset could keep a connection with the old settings open in two ways, so the edited settings failed to open until the process released it:

  • Database.has_view opened a connection with engine.connect() and never released it. With the default NullPool a connection is only closed once it is released, so the database stayed open with its previous settings until the cyclic garbage collector reclaimed the leaked connection. Disposing or evicting the engine does not help, because the connection is checked out rather than pooled. has_view is reached from physical-table metadata discovery when has_table is false.
  • _evict_engine_cache dropped a database's cached engines on update or delete without disposing them, so an engine whose pool held an idle connection (for example when DB_CONNECTION_MUTATOR swaps in a pooled poolclass) kept that connection open after the edit.

This PR:

  • releases the has_view connection with a context manager (same get_view_names call, same exception fallback, same return value);
  • disposes the engines evicted by _evict_engine_cache, after the cache lock is released. A failure to dispose is logged with a static message and never interrupts the update or delete.

Root Cause

Two independent ways a process leaves a connection with the old configuration open, reproduced against a real file-backed DuckDB database (DuckDB 1.5.6, duckdb-engine 0.17.0, SQLAlchemy 2.0.55):

  1. has_view leaked its connection (default NullPool, no mutator needed).
  2. Eviction on update never disposed pooled engines.

Limits (stated plainly)

This change does not claim to resolve every way these errors can appear:

  • eviction is per process, so engines pooled in other worker processes are not affected;
  • Engine.dispose() closes idle pooled connections; connections checked out at that moment are not released deterministically;
  • validating new settings while a saved pooled engine still holds the old ones (the pre-save validation / Test Connection path) is not addressed;
  • long-running statements that hold an old-configuration connection, and database-side instance lifetime, are not addressed.

A cross-worker retirement of stale engines and disposal of unsaved-database engines are possible follow-ups; they need their own design (a settings generation that is ordered with the commit).

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable (backend only). Regression guards, RED on the parent commit and GREEN with the fix:

  • test_has_view_closes_its_connection
  • test_engine_evicted_on_update_releases_pooled_connection

Both fail on the parent commit with sqlalchemy.exc.OperationalError: (_duckdb.ConnectionException) Connection Error: Can't open a connection to same database file with a different configuration than existing connections and pass with the fix. test_engine_cache_eviction_survives_dispose_failure covers the dispose-failure handling.

TESTING INSTRUCTIONS

pytest tests/unit_tests/models/core_test.py -p no:cacheprovider

The two regression guards alone: pytest tests/unit_tests/models/core_test.py -k "pooled_connection or has_view_closes" -p no:cacheprovider.

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

No new dependency, no migration, no change to the cache key or the nullpool default. For engines using NullPool, disposal is a no-op.

sadpandajoe and others added 2 commits October 29, 2026 21:47
…viction

DuckDB allows one configuration per database file per process, so a
connection that is left open keeps the previous settings alive and opening the
same file with edited settings fails with a conflicting configuration error.

has_view opens a connection and never releases it; with the default NullPool
the connection is only closed on release, so it stays open until the cyclic
garbage collector reclaims it. Evicting a database's cached engines on update
or delete drops them without disposing, so an idle pooled connection stays open
too.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
has_view acquired a connection without releasing it. With the default NullPool
a connection is only closed on release, so the database stayed open with its
previous settings until the cyclic garbage collector reclaimed the leaked
connection. For databases that allow one configuration per file per process,
such as DuckDB, opening the edited settings failed in the meantime. The
connection is now released with a context manager.

Evicting a database's cached engines on update or delete now also disposes
them, outside the cache lock, so idle pooled connections are closed. A failure
to dispose is logged and never interrupts the update or delete. Connections
checked out at that moment stay on the engine's previous pool.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.70%. Comparing base (65db213) to head (58c5567).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #45861   +/-   ##
=======================================
  Coverage   84.70%   84.70%           
=======================================
  Files        3069     3069           
  Lines      205597   205614   +17     
  Branches    48327    48330    +3     
=======================================
+ Hits       174148   174164   +16     
- Misses      28157    28158    +1     
  Partials     3292     3292           
Flag Coverage Δ
hive 34.51% <8.33%> (-0.01%) ⬇️
mysql 52.59% <83.33%> (-0.01%) ⬇️
postgres 52.59% <58.33%> (-0.01%) ⬇️
presto 36.24% <33.33%> (-0.01%) ⬇️
python 87.75% <100.00%> (+<0.01%) ⬆️
sqlite 52.36% <83.33%> (-0.01%) ⬇️
unit 82.48% <75.00%> (+<0.01%) ⬆️

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.
@sadpandajoe
sadpandajoe marked this pull request as ready for review October 29, 2026 22:45

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1 participant