Skip to content

Fixes #30173: fix(ingestion): security/metadata/mcp sources own their BaseConnection; dedupe close-on-failure#30174

Open
IceS2 wants to merge 7 commits into
mainfrom
fix/security-and-close-on-failure
Open

Fixes #30173: fix(ingestion): security/metadata/mcp sources own their BaseConnection; dedupe close-on-failure#30174
IceS2 wants to merge 7 commits into
mainfrom
fix/security-and-close-on-failure

Conversation

@IceS2

@IceS2 IceS2 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #30173

Closes out the OSS side of source-owns-BaseConnection. Two commits, splittable.

1. Security, metadata and mcp sources own their BaseConnection (62577c2381)

SecurityServiceSource built two clients — one through the owner-discarding get_connection seam, a second through its get_client hook — and then discarded the test-connection result, so a failing connection never raised. The base now owns one BaseConnection, takes its client from the owner, tests through run_test_connection (which raises), and disposes it on close(). get_client is demoted to the legacy path for connectors without a connection_class, and is now declared on the base instead of being an implicit contract.

Key finding: for the only consumer of this base, the client's constructor performs a real login. Routing just the unused self.connection attribute through the owner would have left the second login in place — the fix only works because self.client comes from the owner too.

The metadata (atlas, amundsen, alationsink) and mcp sources have no service base but carry the same pattern in their own __init__. Each now owns its connection and tests through it. drives/ (plural) is a dead package containing only __init__.py — nothing to migrate.

2. Dedupe close-on-failure behind a context manager (c4fcc7319b)

close_on_failure in source/connections.py replaces the drifted try: self.test_connection() except: <dispose>; raise in 24 constructors.

self.close() was the wrong teardown for a constructor failure: it is successful-run teardown, and it is polymorphic, so it dispatched to subclass overrides touching attributes set after super().__init__() returned — airflow's self.session, databrickspipeline's self._table_lookup_cache — masking the real connection error behind an AttributeError. saperp's override closes the OpenMetadata client outright. The context manager releases only the owner, which is the only resource in flight when the test step runs.

CommonDbSourceService keeps an explicit failure branch: its SSL temp files are owned by the source, not the connection, until they move into a BaseConnection hook.

Pre-existing bugs fixed while here

airflow, openlineage close() never called super().close(), so the owner was never disposed. airflow closes its session before the engine dispose super().close() triggers.
OpenLineageConnection never registered the Kafka consumer teardown
SftpConnection never registered the SFTP transport teardown
AmundsenConnection never registered the Neo4j driver teardown

The last three leaked one client per temporary owner — i.e. on every Automation test-connection.

Compatibility

Additive. connection_obj is still set, test_connection_common keeps its 3-arg signature, and the get_connection / import_connection_fn seam is untouched — Collate and custom connectors are unaffected.

Greptile Summary

This PR moves ingestion sources to owned connections and centralizes failure cleanup. The main changes are:

  • Reuses one owned client for security, metadata, and MCP sources.
  • Adds close_on_failure for constructor verification failures.
  • Registers missing SFTP, Amundsen, Kafka, and Kinesis cleanup callbacks.
  • Routes Airflow and OpenLineage cleanup through their base source.

Confidence Score: 5/5

This looks safe to merge.

  • Verification failures remain visible when owned connection cleanup raises.
  • The Kinesis client is now registered with the connection owner.
  • No blocking incomplete or unsafe fix remains in the reviewed lifecycle paths.

Important Files Changed

Filename Overview
ingestion/src/metadata/ingestion/source/connections.py Adds shared failure cleanup that preserves the connection verification error when normal teardown fails.
ingestion/src/metadata/ingestion/source/pipeline/openlineage/connection.py Registers both Kafka and Kinesis clients with the owning connection for cleanup.
ingestion/src/metadata/ingestion/source/security/security_service.py Uses one owned connection and client for login, verification, ingestion, and cleanup.
ingestion/src/metadata/ingestion/source/database/common_db_source.py Keeps explicit legacy engine and SSL cleanup while using owner cleanup for migrated connectors.

Reviews (6): Last reviewed commit: "Merge branch 'main' into fix/security-an..." | Re-trigger Greptile

Context used (3)

  • Context used - CLAUDE.md (source)
  • Context used - openmetadata-ui-core-components/CLAUDE.md (source)
  • Context used - AGENTS.md (source)

IceS2 added 2 commits July 17, 2026 09:59
…ection

SecurityServiceSource built two clients on start-up: one through the
owner-discarding `get_connection` seam and a second one through the
`get_client` hook. It also discarded the test-connection result, so a
failing connection never raised and ingestion carried on against a broken
client. The base now owns a single `BaseConnection`, reuses it for the test
step through `run_test_connection` (which raises), and disposes it on
`close()`.

The metadata (atlas, amundsen, alationsink) and mcp sources have no service
base but carry the same double-connection pattern in their own `__init__`:
they built a client through the seam and then had `test_connection_common`
build a second one. Each now owns its connection and tests through it.

Pre-existing bugs fixed:
- AmundsenConnection never registered the Neo4j driver teardown, so every
  temporary owner (test connection, automations) leaked a driver.
- SecurityServiceSource.get_client was undeclared on the base.
Every service base re-derived `try: self.test_connection() except: <dispose>`
in its constructor, and the shape had drifted: most called `self.close()`,
pipeline disposed `self._connection` directly.

`self.close()` is the wrong teardown for a constructor failure. It is
successful-run teardown (it runs `compute_percentile`, `self.metadata.close()`)
and it is polymorphic, so it dispatches to subclass overrides that touch
attributes set *after* `super().__init__()` returns - airflow's `self.session`,
databrickspipeline's `self._table_lookup_cache` - and the resulting
AttributeError masks the connection error the user needs to see. saperp's
override goes further and closes the OpenMetadata client.

`close_on_failure` releases only the owner, which is the only resource in
flight when the test step runs. Applied to the 10 service bases and to the
leaf sources that own a connection directly.

CommonDbSourceService keeps an explicit failure branch: its SSL temp files
belong to the source, not the connection owner, until they move into a
BaseConnection hook.

Also fixed while here:
- airflow and openlineage `close()` never called `super().close()`, so the
  owner was never disposed. airflow closes its session before the engine
  dispose that `super().close()` triggers.
- OpenLineageConnection never registered the Kafka consumer teardown and
  SftpConnection never registered the SFTP transport teardown, so every
  temporary owner (test connection, automations) leaked one.
Copilot AI review requested due to automatic review settings July 17, 2026 09:20
@IceS2
IceS2 requested a review from a team as a code owner July 17, 2026 09:20

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.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

✅ PR checks passed

The linked issue has a description and all required Shipping project fields set. Thanks!

Comment thread ingestion/src/metadata/ingestion/source/connections.py Outdated
Comment thread ingestion/src/metadata/ingestion/source/pipeline/openlineage/connection.py Outdated
…ailure

- CommonDbSourceService: a connector with no connection_class gets its engine
  from the legacy seam, so nobody owns it. close_on_failure only releases the
  owner, which left that engine and its session leaked on a failing test step.
  The failure branch now releases it (gitar-bot).
- close_on_failure: a teardown that raises replaced the verification error that
  triggered it. The teardown error is logged instead; the caller gets the error
  that matters (greptile).
- OpenLineageConnection: register the Kinesis client teardown too. Only the
  Kafka branch had one, so Kinesis runs dropped the client without releasing it
  (greptile).
- SecurityServiceSource: drop the `get_client` hook. It duplicated the seam --
  for the only implementation, `get_client()` and `get_connection()` build the
  same client the same way -- and the seam already covers connectors without a
  connection_class.
- atlas, amundsen, alationsink, mcp: these ship a connection_class, so
  create_connection never returns None. Drop the unreachable legacy fallback and
  take the client from the owner, as the other migrated leaf sources do.
Copilot AI review requested due to automatic review settings July 17, 2026 10:50

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 17, 2026

Copy link
Copy Markdown
Contributor Author

Review addressed in 69330991b0. All three findings were valid.

gitar: legacy-path engine leaked on test-connection failure — correct, and it was a regression I introduced. close_on_failure releases only the owner, but a connector without a connection_class gets its engine from the legacy seam, where nobody owns it; the previous self.close() disposed it via _release_engine(). This is live for the connectors that resolve through the seam rather than a connection_class. The failure branch now calls _release_engine() when self._connection is None. Locked by test_legacy_engine_disposed_when_test_connection_fails, verified failing without the fix.

greptile: cleanup masks connection failure — valid. Not a regression (the old self.close() had the same exposure), but this PR exists to stop teardown from masking the connection error, so leaving it would have been inconsistent. A failing teardown is now logged and the verification error propagates.

greptile: Kinesis client remains unowned — valid. Registering the teardown for the Kafka branch and not its sibling was an inconsistency on my side. Both branches now register it.

Two more changes from self-review:

  • SecurityServiceSource.get_client is gone. It duplicated the seam: for the only implementation, get_client() and the seam's get_connection() build the same client the same way, and the seam already covers connectors without a connection_class.
  • atlas, amundsen, alationsink and mcp all ship a connection_class, so create_connection never returns None for them. The else get_connection(...) fallback was unreachable; they now take the client from the owner with a cast, matching the other migrated leaf sources.

Copilot could not review (quota).

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (24 flaky)

✅ 4547 passed · ❌ 0 failed · 🟡 24 flaky · ⏭️ 95 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 435 0 5 16
✅ Shard 2 11 0 0 0
🟡 Shard 3 826 0 6 8
🟡 Shard 4 822 0 1 18
🟡 Shard 5 839 0 2 5
🟡 Shard 6 785 0 3 46
🟡 Shard 7 829 0 7 2
🟡 24 flaky test(s) (passed on retry)
  • Flow/TestConnectionModal.spec.ts › modal opens with gate card and capability checks sections (shard 1, 1 retry)
  • Flow/TestConnectionModal.spec.ts › raw log toggle shows and hides connection log (shard 1, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: table (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 › the browse tree only shows the asset-type categories a user can access (shard 1, 1 retry)
  • Features/BulkImportWithDotInName.spec.ts › Full import cycle with dot in service name (shard 3, 1 retry)
  • Features/ContextCenterArchive.spec.ts › archive page lazy-loads more rows on scroll within its own scroll container (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 list cards, recently viewed widget, and pagination work (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › clearing search restores the unfiltered list (shard 3, 1 retry)
  • Features/ContextCenterPermission.spec.ts › created-by-me filter on the archive page shows only the current user's archived documents (shard 3, 1 retry)
  • Features/Glossary/GlossaryTermRelationsGraphNested.spec.ts › viewing a child term: parent appears as a 1-hop neighbour via parentOf edge (shard 4, 1 retry)
  • Flow/MetricListSearch.spec.ts › typing in the search box filters the metric list server-side (shard 5, 1 retry)
  • Pages/CustomProperties.spec.ts › Enum (shard 5, 1 retry)
  • Pages/Entity.spec.ts › Announcement create, edit & delete (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_KnowledgeCenter.spec.ts › Should remove user owner for knowledgeCenter (shard 6, 1 retry)
  • Pages/ExplorePageRightPanel.spec.ts › Should verify deleted tag not visible in tag selection for dashboardDataModel (shard 7, 1 retry)
  • Pages/Glossary.spec.ts › Approve and reject glossary term from Glossary Listing (shard 7, 2 retries)
  • Pages/Glossary.spec.ts › Verify Glossary Term Deny Permission (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)

📦 Download artifacts

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

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 17, 2026 19:11

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 18, 2026 15:11

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 18, 2026

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

Centralizes connection ownership and teardown logic within BaseConnection to prevent resource leaks across security, metadata, and MCP sources. This refactor resolves legacy cleanup issues and replaces inconsistent close-on-failure patterns with a robust context manager.

✅ 1 resolved
Bug: Legacy-path engine leaked on test-connection failure in CommonDbSource

📄 ingestion/src/metadata/ingestion/source/database/common_db_source.py:129-143 📄 ingestion/src/metadata/ingestion/source/database/common_db_source.py:169-183
In CommonDbSourceService.init, the failure branch now only calls cleanup_temp_files() and closes self._connection (via close_on_failure). When the connector has no connection_class, self._connection is None and self.engine comes from the legacy get_connection() path; on a failing test_connection that engine is no longer disposed (the previous self.close() ran _release_engine(), which called engine.dispose() and session.remove()). This leaks the SQLAlchemy engine/session for legacy connectors. Consider disposing the legacy engine (or calling _release_engine) in the failure branch when self._connection is None.

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

@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.

Security, metadata and mcp sources open two connections; close-on-failure has drifted across the service bases

2 participants