Skip to content

Fixes #30399: Fix connector teardown regressions from the source-owns-BaseConnection refactor#30402

Open
IceS2 wants to merge 8 commits into
mainfrom
fix/source-owns-regression-followups
Open

Fixes #30399: Fix connector teardown regressions from the source-owns-BaseConnection refactor#30402
IceS2 wants to merge 8 commits into
mainfrom
fix/source-owns-regression-followups

Conversation

@IceS2

@IceS2 IceS2 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Fixes #30399

Four follow-up regressions from the source-owns-BaseConnection refactor (#29870 / #30094 / #30148 / #30133 / #30174).

Key finding: close_on_failure(self._connection) in each base __init__ disposes only the owner (it unwinds the owner's _on_close teardowns); it never runs a source close() override. So any connector teardown that lived in a close() override was skipped when test_connection raised in __init__. The fix pattern is to move connector teardown onto the owner — register it with _on_close inside _get_client — so it fires on both the normal super().close() and the close_on_failure path, then drop the redundant override.

# Regression Fix
1 Init-failure cleanup skipped: MicroStrategy leaked its server-side API session; Elasticsearch/OpenSearch left the client private key on disk in the SSL staging dir register close_api_session / staging-dir removal on the owner in _get_client; delete the source close() overrides
2 tableau/looker/powerbi close() overrides never chained super().close() → the owned BaseConnection was never disposed on the normal path chain super().close(); tableau drops its direct sign_out() (the owner already registers it via _on_close, so keeping both signs out twice); powerbi moves file_client.delete_tmp_files onto the owner
3 Airflow 2.x disposed airflow.settings.engine — the borrowed process-global engine returned by BackendConnection — on close and on every Automation test-connection, tearing down the host Airflow pool only register dispose for engines this connection built (isinstance(client, Engine) and client is not settings.engine)
4 The dashboard base was the only service base that stopped setting self.connection_obj, breaking the kept, deprecated seam for out-of-tree custom dashboard connectors set connection_obj and read it in the legacy test_connection fallback (same object, behavior unchanged)

One commit per fix (splittable).

Greptile Summary

Fixes connector teardown ownership regressions.

  • Moves connector-specific cleanup callbacks onto their owning BaseConnection.
  • Restores dashboard owner disposal and the deprecated connection_obj seam.
  • Avoids disposing Airflow's process-global engine.
  • Adds lifecycle regression tests for dashboard, Airflow, Elasticsearch, and OpenSearch connectors.

Confidence Score: 4/5

The PR is not yet safe to merge because Elasticsearch cleanup can delete TLS files belonging to another active connector using the shared default staging directory.

The newly registered owner callback recursively removes a shared staging directory rather than only the files owned by the closing connection.

ingestion/src/metadata/ingestion/source/search/elasticsearch/connection.py

Important Files Changed

Filename Overview
ingestion/src/metadata/ingestion/source/search/elasticsearch/connection.py Moves TLS staging cleanup onto the connection owner, but removes the entire shared default staging directory.
ingestion/src/metadata/ingestion/source/search/opensearch/connection.py Moves by-value TLS staging cleanup into the OpenSearch connection lifecycle.
ingestion/src/metadata/ingestion/source/dashboard/dashboard_service.py Restores the legacy dashboard connection_obj alias while preserving owned-connection test behavior.
ingestion/src/metadata/ingestion/source/pipeline/airflow/connection.py Restricts engine disposal to connection-owned SQLAlchemy engines.
ingestion/tests/unit/topology/search/test_search_owned_connection.py Adds coverage for normal and initialization-failure TLS staging cleanup.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Build Elasticsearch connection] --> B[Stage by-value TLS files]
  B --> C[Register owner cleanup]
  C --> D{Close or initialization failure}
  D --> E[Unwind BaseConnection callbacks]
  E --> F[Remove configured staging directory]
Loading

Reviews (4): Last reviewed commit: "Merge branch 'main' into fix/source-owns..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

IceS2 added 4 commits July 23, 2026 13:49
…asticsearch/OpenSearch

close_on_failure closes only the BaseConnection owner, so connector-specific
teardown that lived in a close() override was skipped when test_connection
raised in __init__. Move the teardown onto the owner by registering it with
_on_close inside _get_client, so it runs on both the normal close and the
close_on_failure path:

- MicroStrategy: register close_api_session (server-side session was leaked).
- Elasticsearch/OpenSearch: register removal of the SSL staging dir that holds
  the client private key (the key was left on disk).

The source close() overrides are dropped (base close() now covers owner dispose
plus metadata.close()), along with their now-unused shutil/Path imports.
… overrides

tableau, looker and powerbi override close() without chaining super().close(),
so the owned BaseConnection is never disposed on the normal path. Chain the base
close() (which disposes the owner and closes the metadata client):

- looker: keep compute_percentile, replace metadata.close() with super().close().
- tableau: drop the direct client.sign_out() (the owner already registers it via
  _on_close, so keeping both would sign out twice) and chain super().close().
- powerbi: move the file-client tmp-file cleanup onto the owner (_on_close in
  _get_client) and drop the override so it inherits the base close().
The Airflow 2.x BackendConnection engine is airflow.settings.engine, reached via
settings.Session().get_bind() -- it is borrowed, not built by OpenMetadata. The
owner registered _on_close(engine.dispose) unconditionally for any Engine
client, so closing the owner (including on every Automation test-connection)
tore down the host Airflow process's connection pool. Only register dispose for
engines this connection built, identified by identity against settings.engine.
The dashboard base was the only service base that stopped setting
self.connection_obj after the source-owns refactor. connection_obj is a kept,
deprecated seam for out-of-tree custom connectors and Collate; the eight other
bases still set it. Set connection_obj to the client and read it in the legacy
test_connection fallback, matching the siblings (same object, behavior
unchanged).
@IceS2
IceS2 requested a review from a team as a code owner July 23, 2026 12:25
Copilot AI review requested due to automatic review settings July 23, 2026 12:25

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…e SSL context

get_ssl_context (Elasticsearch) and _handle_ssl_context_by_value (OpenSearch)
write the cert/key files and can raise while parsing them. Registering the
_on_close cleanup after that call left the staged private key on disk when the
SSL build failed, because _build_client only unwinds teardowns registered before
the failure point. Register the cleanup as the staging material is acquired.
Copilot AI review requested due to automatic review settings July 23, 2026 12:45

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@IceS2

IceS2 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — addressing both findings:

greptile P1 — SSL failure leaves staged secrets: Valid, fixed in 15e7046. The _on_close cleanup was registered after get_ssl_context (Elasticsearch) / _handle_ssl_context_by_value (OpenSearch), which write the cert/key files and can raise while parsing them — so a failed SSL build left the staged private key on disk, since _build_client only unwinds teardowns registered before the failure point. Now registered as the staging material is acquired, in both connectors. Added a regression test (test_elasticsearch_removes_staging_dir_when_ssl_context_build_fails, red before the reorder).

gitar — cleanup may delete a shared cert dir: Real observation, but pre-existing and out of scope for this regression PR. The prior source close() already did shutil.rmtree(...stagingDir) on the whole shared default dir (/tmp/openmetadata-certs); this PR relocates that teardown onto the owner, it doesn't introduce the shared-dir behavior. Two by-value connections on the default dir also already collide on write (identical root.pem/client.pem/client_key.pem filenames), so the shared-dir design is broken independently of deletion. The robust fix — per-connection unique subdirs in the shared SSL-staging helpers — is a separate change; I'll open a follow-up rather than widen this PR.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🔴 Playwright Results — 2 test failure(s)

✅ 4526 passed · ❌ 2 failed · 🟡 31 flaky · ⏭️ 102 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 436 0 4 16
✅ Shard 2 11 0 0 0
🟡 Shard 3 820 0 11 8
🔴 Shard 4 811 2 2 25
🟡 Shard 5 830 0 3 5
🟡 Shard 6 828 0 4 46
🟡 Shard 7 790 0 7 2

Genuine Failures (failed on all attempts)

Features/OntologyExplorerRdf.spec.ts › term Relations Graph requests /rdf/glossary/graph scoped to the selected term (glossaryTermId) when RDF is enabled (shard 4)
�[31mTest timeout of 60000ms exceeded.�[39m
Features/OntologyExplorerRdf.spec.ts › glossary Relations Graph calls /rdf/glossary/graph when RDF is enabled and renders nodes from the response (shard 4)
Error: GET /rdf/glossary/graph must be called on the glossary Relations Graph when RDF is enabled

�[2mexpect(�[22m�[31mreceived�[39m�[2m).�[22mtoBe�[2m(�[22m�[32mexpected�[39m�[2m) // Object.is equality�[22m

Expected: �[32mtrue�[39m
Received: �[31mfalse�[39m
🟡 31 flaky test(s) (passed on retry)
  • Flow/TestConnectionModal.spec.ts › failure state shows Edit Connection button and Retry Test button (shard 1, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: chart (shard 1, 1 retry)
  • Pages/SearchSettings.spec.ts › Latest preview config wins when a superseded request resolves late (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a dashboard-scoped user sees dashboards but never tables (shard 1, 1 retry)
  • Features/BulkEditOperationBadges.spec.ts › Glossary bulk edit search filters rows and clear restores them (shard 3, 1 retry)
  • Features/ContextCenterArchive.spec.ts › full document lifecycle: folder expand icon, upload, delete, restore, and permanent delete (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article listing search filters, clears, and shows empty state (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article edit persistence and unsaved title behavior are correct (shard 3, 2 retries)
  • Features/ContextCenterArticles.spec.ts › Text formatting (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › clearing search restores the unfiltered list (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › adding a linked asset in edit mode shows entity badge on the row (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › shared memory IS visible to a user explicitly listed in sharedWith (shard 3, 1 retry)
  • Features/DataProductDomainMigration.spec.ts › Data product with no assets can change domain without confirmation (shard 3, 1 retry)
  • Features/DataQuality/CertificationFilter.spec.ts › Certification filter narrows both table- and testCase-index queries via the flat field path (shard 3, 1 retry)
  • Features/DomainTierCertificationVoting.spec.ts › DataProduct - Tier assign, update, and remove (shard 3, 1 retry)
  • Features/OntologyExplorerRdf.spec.ts › renders without crashing when /rdf/glossary/graph returns duplicate nodes and dangling edges (shard 4, 1 retry)
  • Features/SearchExport.spec.ts › Export queues a background job and downloads from the jobs tray (shard 4, 1 retry)
  • Flow/IngestionBot.spec.ts › Ingestion bot should be able to access domain specific domain (shard 5, 2 retries)
  • Flow/PersonaFlow.spec.ts › Set default persona for team should work properly (shard 5, 1 retry)
  • Pages/DataContractsSemanticRules.spec.ts › Validate Description Rule Is_Not_Set (shard 5, 1 retry)
  • Pages/DomainUIInteractions.spec.ts › Remove owner from domain via UI (shard 6, 1 retry)
  • Pages/Entity.spec.ts › Certification Add Remove (shard 6, 1 retry)
  • Pages/ExploreBrowse.spec.ts › service type drill-down disables unrelated roots and query-panel Clear resets it (shard 6, 1 retry)
  • Pages/ExplorePageRightPanel.spec.ts › Should perform CRUD and Removal operations for pipeline (shard 6, 1 retry)
  • Features/AutoPilot.spec.ts › Create Service and check the AutoPilot status (shard 7, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify Impact Analysis service filter selection (shard 7, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab is NOT visible for pipelineService in platform lineage (shard 7, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab is NOT visible for apiService in platform lineage (shard 7, 1 retry)
  • Pages/TestSuite.spec.ts › Logical TestSuite (shard 7, 1 retry)
  • Pages/UserDetails.spec.ts › Admin user can get all the roles hierarchy and edit roles (shard 7, 1 retry)
  • ... and 1 more

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

IceS2 added 2 commits July 23, 2026 15:28
…nostic

The test faked airflow via sys.modules and re-imported the connection module to
force the 2.x path, but IS_AIRFLOW_3 is computed at import time -- in CI the real
airflow 3.x module was already cached, so the test hit the env-var backend branch
and raised instead of returning the borrowed engine. Exercise the dispose guard
directly: patch `settings` and `_get_connection` so the result is independent of
the Airflow version, using real airflow when installed and a minimal fake only
when it is absent.
…sal tests

Refines the previous test rewrite so it verifies real behavior instead of a
stubbed _get_connection:
- borrowed case exercises the real Airflow 2.x backend path (patch IS_AIRFLOW_3
  to False and provide a settings whose Session().get_bind() returns the
  process-global engine), asserting it is NOT disposed;
- OM-built case uses the real SQLite backend delegate to build a real engine and
  asserts it IS disposed (spying Engine.dispose).

The only patched surface is airflow itself (IS_AIRFLOW_3 + settings); the
connection module's dispatch, _get_backend_engine_from_session and the dispose
guard all run for real. Real airflow is used when installed; a minimal fake only
enables import when absent.
Copilot AI review requested due to automatic review settings July 23, 2026 15:10

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 24, 2026 06:31

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitar-bot

gitar-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 1 resolved / 1 findings

Fixes ingestion connector teardown regressions by registering cleanup callbacks on the owning BaseConnection and protecting Airflow's global engine from disposal. No issues found.

✅ 1 resolved
Edge Case: Staging-dir cleanup may delete a shared cert directory

📄 ingestion/src/metadata/ingestion/source/search/elasticsearch/connection.py:141-144 📄 ingestion/src/metadata/ingestion/source/search/elasticsearch/connection.py:171-176 📄 ingestion/src/metadata/ingestion/source/search/opensearch/connection.py:103-106 📄 ingestion/src/metadata/ingestion/source/search/opensearch/connection.py:128-132
_cleanup_staging_dir calls shutil.rmtree(staging_dir) on the whole directory, and stagingDir defaults to the shared, non-unique path openmetadata-certs (sslCertValues.json). If two search connections (e.g. an Elasticsearch and an OpenSearch source, or two runs in the same process) use the default staging dir, closing/failing one connection removes the directory and the private-key files the other is still relying on. Consider writing per-connection cert files into a unique subdirectory (e.g. under a uuid/tempdir) and only removing that subtree, or removing individual files this connection wrote rather than the whole dir.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

certificates = connection.sslConfig.certificates
if isinstance(certificates, SslCertificatesByValues):
staging_dir = certificates.stagingDir
self._on_close(lambda: _cleanup_staging_dir(staging_dir))

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.

P1 Cleanup deletes shared TLS files

When two connectors use by-value TLS certificates without overriding the shared /tmp/openmetadata-certs staging directory, closing or failing initialization of the Elasticsearch connection recursively deletes that directory, removing the other active connector's certificate and private-key files and causing later TLS setup or reconnects to fail.

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

Ingestion safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix connector teardown regressions from the source-owns-BaseConnection refactor

2 participants