Skip to content

Backport to 1.13: reclaim orphaned ingestion pipelines (#29836) + Data Retention build fix (#29984), realign DataRetention with main#30030

Merged
mohityadav766 merged 4 commits into
1.13from
cherry-pick-dataretention-1.13
Jul 14, 2026
Merged

Backport to 1.13: reclaim orphaned ingestion pipelines (#29836) + Data Retention build fix (#29984), realign DataRetention with main#30030
mohityadav766 merged 4 commits into
1.13from
cherry-pick-dataretention-1.13

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Jul 14, 2026

Copy link
Copy Markdown
Member

Backports the orphaned-ingestion-pipeline reindex fix to 1.13, and realigns DataRetention.java so it is byte-identical to main.

Commits (cherry-picked with -x, in main's order)

PR Why it's here
#28384 feat(dataRetention): sweep orphan time-series rows for per-type tables Prerequisite. Adds deleteOrphanedRecords to CollectionDAO; main's DataRetention calls it and 1.13 didn't have it.
#29836 fix(search-indexing): reclaim orphaned ingestion pipelines and guard null testDefinition during reindex The requested fix.
#29965 Fixes #20753: replace non-schema jobStackTrace with IndexingError.stackTrace Prerequisite. Changes failureDetails from raw Map to the schema-typed IndexingError.
#29984 Fix Data Retention Build Issue The requested build fixup — it is the fixup for #29965, so it can't land without it.

#29836 and #29984 cannot compile on 1.13 without #28384 and #29965, so all four are picked together.

Note on the executeOrphanCleanup conflict

1.13 previously received a partial backport of #28159 (the test-case cleanup parts) which silently dropped its MAX_ORPHAN_CLEANUP_ITERATIONS guard — that guard protects executeOrphanCleanup, which only exists once #28384 lands.

Without the cap, executeOrphanCleanup is a bare while (true) that spins forever if a delete query keeps reporting non-zero deletions (e.g. rows blocked by an FK constraint), wedging the entire DataRetention job. Resolving that method to main's shape folds the cap back in, so 1.13 does not ship the uncapped loop.

Result

DataRetention.java, OrphanIngestionPipelineCleanup.java, TestCaseIndex.java, OmAppJobListener.java and DataContractValidationApp.java are now identical to main.

Verification

  • openmetadata-service compiles (main + test sources)
  • openmetadata-integration-tests compiles, including the two new ITs (OrphanIngestionPipelineCleanupIT, OrphanedTimeSeriesCleanupIT)
  • mvn spotless:check clean
  • OmAppJobListenerTest — 13/13 pass
  • No jobStackTrace / HashMap failure-context residue left in DataRetention

🤖 Generated with Claude Code

Greptile Summary

This PR backports DataRetention cleanup and app failure-context fixes. The main changes are:

  • Orphan ingestion pipelines are reclaimed during DataRetention.
  • Orphan time-series and execution rows get per-table delete queries.
  • Test-case indexing skips missing test-definition metadata.
  • App failure details now use schema-typed IndexingError records.
  • Integration and listener tests cover the new cleanup and failure shapes.

Confidence Score: 5/5

This looks safe to merge after checking the query-cost cleanup edge case.

  • The main cleanup paths are scoped to their expected parent rows or relationships.
  • The remaining concern is limited to query-cost rows that already have a null parent hash.
  • No blocking issues were found in the changed code.

openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/dataRetention/DataRetention.java Adds orphan ingestion-pipeline and orphan time-series cleanup to the scheduled DataRetention flow.
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java Adds bounded orphan-delete queries for profiler data, query cost records, resolution statuses, agent executions, and MCP executions.
openmetadata-service/src/main/java/org/openmetadata/service/util/OrphanIngestionPipelineCleanup.java Introduces hard cleanup for ingestion pipelines that no longer have a container relationship.
openmetadata-service/src/main/java/org/openmetadata/service/apps/bundles/dataContracts/DataContractValidationApp.java Moves failure details from a raw map into IndexingError with per-entity failures.
openmetadata-service/src/main/java/org/openmetadata/service/apps/scheduler/OmAppJobListener.java Stores Quartz job exceptions in FailureContext.failure as IndexingError.
openmetadata-service/src/main/java/org/openmetadata/service/search/indexes/TestCaseIndex.java Adds a null guard before loading test-definition fields for search indexing.

Reviews (1): Last reviewed commit: "Fix Data Retention Build Issue (#29984)" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

  • Context used - CLAUDE.md (source)

harshach and others added 4 commits July 14, 2026 13:13
#28384)

* feat(dataRetention): sweep orphan time-series rows for per-type tables

Adds `cleanOrphanedTimeSeriesRows()` to `DataRetention` alongside the existing
orphan-relationship and orphan-tag sweeps. Each of the five affected DAOs gets a
`deleteOrphanedRecords(int limit)` query (MySQL + PostgreSQL) that left-joins
to its parent and deletes rows the parent no longer covers:

- `testCaseResolutionStatus`: parent link via `entity_relationship` PARENT_OF
- `agentExecution`: `agentId` → `ai_application_entity.id`
- `mcpExecution`: `serverId` → `mcp_server_entity.id`
- `profile_data`: `entityFQNHash` → `table_entity.fqnHash`
- `query_cost_time_series`: `entityFQNHash` → `query_entity.fqnHash`

The sweep runs after `cleanOrphanedRelationshipsAndHierarchies()` so the
PARENT_OF check sees the post-cleanup `entity_relationship` state. Pairs with
PR #28367, where the bulk hard-delete cascade now skips time-series children
and relies on `DataRetention` to reclaim them out-of-band.

Adds `OrphanedTimeSeriesCleanupIT` covering all five per-type queries:
inserts a real-parent row and a bogus-parent row through the existing DAO
`insert(...)` paths, runs `deleteOrphanedRecords(BATCH)`, asserts the orphan
is gone and the valid row is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(dataRetention): fix orphan-cleanup IT name lengths and McpExecution table arg

- McpExecutionDAO.insertWithoutExtension uses `<table>` placeholder; the test
  was passing `null`, which made Jdbi fail to render the SQL template. Pass
  the literal table name `mcp_execution_entity`.
- `ns.prefix(...)` embeds class + method names, so chaining it through
  database -> schema -> table -> auto-created test_suite pushed the test_suite
  `name` column past its VARCHAR(256) bound. Use `ns.uniqueShortId()` for the
  hierarchy components and shorten the test method names so the resulting
  FQN stays well under the column limit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* review(dataRetention): bound profile-data orphan delete by rows + clarify PARENT_OF=9

Addresses two PR review findings:

- `profiler_data_time_series.deleteOrphanedRecords` previously LIMITed distinct
  `entityFQNHash` values, then deleted every row for each hash — a batch could
  delete tens of millions of rows if many orphan hashes each had thousands of
  rows. Switch to row-level limiting: single-table `DELETE ... WHERE NOT EXISTS
  (...) LIMIT N` on MySQL, and `ctid IN (SELECT ... LIMIT N)` on PostgreSQL
  (the table has no `id` column, so we use Postgres ctid for the inner subquery).
  This matches the row-count cap used by the other four orphan-cleanup queries.

- Annotate `er.relation = 9` in the testCaseResolutionStatus query with a
  `// 9 = Relationship.PARENT_OF` inline comment plus a leading block comment
  noting the ordinal is stable because the enum appends new values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 5f5a123)
…null testDefinition during reindex (#29836)

Two reindex failures observed in SearchIndexingApplication that DataRetention did
not cover:

1. Orphaned ingestion pipelines. When a service (or test suite) is hard-deleted
   without the cascade reaching its ingestion pipeline, the pipeline row survives
   with no incoming CONTAINS relationship. Reindex then throws "Entity type
   ingestionPipeline <id> does not have expected relationship contains to/from
   entity type null" because IngestionPipelineRepository.setFields resolves the
   container via getContainer (mustHaveRelationship=true). Nothing swept these.

   Adds OrphanIngestionPipelineCleanup (mirrors OrphanTestCaseRelationshipCleanup)
   wired into DataRetention: pages pipelines, finds ones with no container, and
   hard-deletes them via the standard delete with a force-delete fallback (search
   doc, pipeline-status time series, relationship rows, entity row) since the
   standard delete itself resolves the missing container and throws.

2. NullPointerException building the TestCase search doc. TestCaseIndex called
   testCase.getTestDefinition().getId() with no null check, so a test case whose
   testDefinition relationship is gone NPE'd. Unlike the "expected relationship"
   errors, this NPE is not classified as a stale-reference warning, so it failed
   the reindex outright. Guards the lookup exactly like TestCaseResultIndex does.

Adds OrphanIngestionPipelineCleanupIT covering selective deletion (orphan removed,
healthy pipeline preserved).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit b8d9f3e)
…ckTrace (#29965)

* Fixes #20753: replace non-schema jobStackTrace with IndexingError.stackTrace

App failure contexts wrote an ad-hoc "jobStackTrace" map key that is not part
of the schema. The failureContext.failure field is already typed as
IndexingError, which has a schema-defined stackTrace field. Use the typed
IndexingError (matching the canonical pattern used across the other apps)
instead of raw maps, deduplicating DataRetention's repeated failure blocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Convert DataContractValidationApp failure to typed IndexingError

Replace the generic Map<String, Object> failure collector with the typed
IndexingError: the top-level exception populates message/stackTrace, and the
per-entity errors go into the schema's failedEntities (EntityError) list
instead of being keyed by FQN in a raw map. Makes all three apps consistent
with the schema-defined failureContext.failure shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit a33d41c)
* Fix Data Retention Build Issue

* Checkstyle

(cherry picked from commit ee2a438)
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Jul 14, 2026
+ " WHERE qe.fqnHash IS NULL "
+ " LIMIT :limit"
+ " ) sub"
+ ")",

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.

P2 Null Query Hashes Are Deleted

When query_cost_time_series.entityFQNHash is NULL, the left join cannot match any query_entity row, so this cleanup treats the record as orphaned and DataRetention can hard-delete it. If existing query-cost rows were written without a query FQN hash, the scheduled cleanup removes live cost history instead of only rows whose parent query was deleted.

@mohityadav766 mohityadav766 self-assigned this Jul 14, 2026
@mohityadav766 mohityadav766 merged commit 47c2dbb into 1.13 Jul 14, 2026
33 of 50 checks passed
@mohityadav766 mohityadav766 deleted the cherry-pick-dataretention-1.13 branch July 14, 2026 09:02
Comment on lines +8672 to +8686
@ConnectionAwareSqlUpdate(
value =
"DELETE FROM profiler_data_time_series "
+ "WHERE NOT EXISTS ("
+ " SELECT 1 FROM table_entity te "
+ " WHERE te.fqnHash = profiler_data_time_series.entityFQNHash"
+ ") "
+ "LIMIT :limit",
connectionType = MYSQL)
@ConnectionAwareSqlUpdate(
value =
"DELETE FROM profiler_data_time_series "
+ "WHERE ctid IN ("
+ " SELECT pdts.ctid FROM profiler_data_time_series pdts "
+ " WHERE NOT EXISTS ("

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 Bug: Orphan profiler cleanup deletes all column-profile rows

ProfilerDataTimeSeriesDAO.deleteOrphanedRecords deletes any profiler_data_time_series row whose entityFQNHash is absent from table_entity.fqnHash. But column profiles are stored under the COLUMN's FQN (TableRepository.java:1082 passes column.getFullyQualifiedName()), e.g. service.db.schema.table.columnName, whose hash never matches a table_entity.fqnHash (which only holds table FQN hashes). As a result every valid table.columnProfile row is treated as an orphan and hard-deleted on each DataRetention run — silent, permanent loss of all column profiling history. The IT (OrphanedTimeSeriesCleanupIT.profilerDataOrphans) only exercises a table-level table.tableProfile row, so it passes while masking the bug. Fix: the orphan test must resolve the parent TABLE, not require the row's own FQN to be a table. Since the stored value is a hash, prefix matching isn't possible; scope the delete to table-keyed extensions only (e.g. AND extension IN ('table.tableProfile','table.systemProfile')) or add a table-existence check that works for column FQNs before shipping this query.

Restrict the delete to table-keyed extensions so column-profile rows (keyed by column FQN) are never matched. Apply the same extension filter to the Postgres variant. Note: this leaves genuinely orphaned column profiles behind — a complete fix needs a table-existence check that handles column FQNs.:

// MySQL — only sweep table-keyed profiler rows; column profiles are keyed by the
// COLUMN FQN and would otherwise all be flagged as orphans.
"DELETE FROM profiler_data_time_series "
    + "WHERE extension IN ('table.tableProfile','table.systemProfile') "
    + "AND NOT EXISTS ("
    + "  SELECT 1 FROM table_entity te "
    + "  WHERE te.fqnHash = profiler_data_time_series.entityFQNHash"
    + ") "
    + "LIMIT :limit"
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown
Code Review 🚫 Blocked 0 resolved / 1 findings

Backports ingestion pipeline fixes and aligns DataRetention with main, but contains a critical flaw in ProfilerDataTimeSeriesDAO where orphan cleanup deletes all column-profile rows.

🚨 Bug: Orphan profiler cleanup deletes all column-profile rows

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java:8672-8686 📄 openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/OrphanedTimeSeriesCleanupIT.java:184-198

ProfilerDataTimeSeriesDAO.deleteOrphanedRecords deletes any profiler_data_time_series row whose entityFQNHash is absent from table_entity.fqnHash. But column profiles are stored under the COLUMN's FQN (TableRepository.java:1082 passes column.getFullyQualifiedName()), e.g. service.db.schema.table.columnName, whose hash never matches a table_entity.fqnHash (which only holds table FQN hashes). As a result every valid table.columnProfile row is treated as an orphan and hard-deleted on each DataRetention run — silent, permanent loss of all column profiling history. The IT (OrphanedTimeSeriesCleanupIT.profilerDataOrphans) only exercises a table-level table.tableProfile row, so it passes while masking the bug. Fix: the orphan test must resolve the parent TABLE, not require the row's own FQN to be a table. Since the stored value is a hash, prefix matching isn't possible; scope the delete to table-keyed extensions only (e.g. AND extension IN ('table.tableProfile','table.systemProfile')) or add a table-existence check that works for column FQNs before shipping this query.

Restrict the delete to table-keyed extensions so column-profile rows (keyed by column FQN) are never matched. Apply the same extension filter to the Postgres variant. Note: this leaves genuinely orphaned column profiles behind — a complete fix needs a table-existence check that handles column FQNs.
// MySQL — only sweep table-keyed profiler rows; column profiles are keyed by the
// COLUMN FQN and would otherwise all be flagged as orphans.
"DELETE FROM profiler_data_time_series "
    + "WHERE extension IN ('table.tableProfile','table.systemProfile') "
    + "AND NOT EXISTS ("
    + "  SELECT 1 FROM table_entity te "
    + "  WHERE te.fqnHash = profiler_data_time_series.entityFQNHash"
    + ") "
    + "LIMIT :limit"
🤖 Prompt for agents
Code Review: Backports ingestion pipeline fixes and aligns DataRetention with main, but contains a critical flaw in ProfilerDataTimeSeriesDAO where orphan cleanup deletes all column-profile rows.

1. 🚨 Bug: Orphan profiler cleanup deletes all column-profile rows
   Files: openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java:8672-8686, openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/OrphanedTimeSeriesCleanupIT.java:184-198

   ProfilerDataTimeSeriesDAO.deleteOrphanedRecords deletes any profiler_data_time_series row whose entityFQNHash is absent from table_entity.fqnHash. But column profiles are stored under the COLUMN's FQN (TableRepository.java:1082 passes column.getFullyQualifiedName()), e.g. service.db.schema.table.columnName, whose hash never matches a table_entity.fqnHash (which only holds table FQN hashes). As a result every valid `table.columnProfile` row is treated as an orphan and hard-deleted on each DataRetention run — silent, permanent loss of all column profiling history. The IT (OrphanedTimeSeriesCleanupIT.profilerDataOrphans) only exercises a table-level `table.tableProfile` row, so it passes while masking the bug. Fix: the orphan test must resolve the parent TABLE, not require the row's own FQN to be a table. Since the stored value is a hash, prefix matching isn't possible; scope the delete to table-keyed extensions only (e.g. AND extension IN ('table.tableProfile','table.systemProfile')) or add a table-existence check that works for column FQNs before shipping this query.

   Fix (Restrict the delete to table-keyed extensions so column-profile rows (keyed by column FQN) are never matched. Apply the same extension filter to the Postgres variant. Note: this leaves genuinely orphaned column profiles behind — a complete fix needs a table-existence check that handles column FQNs.):
   // MySQL — only sweep table-keyed profiler rows; column profiles are keyed by the
   // COLUMN FQN and would otherwise all be flagged as orphans.
   "DELETE FROM profiler_data_time_series "
       + "WHERE extension IN ('table.tableProfile','table.systemProfile') "
       + "AND NOT EXISTS ("
       + "  SELECT 1 FROM table_entity te "
       + "  WHERE te.fqnHash = profiler_data_time_series.entityFQNHash"
       + ") "
       + "LIMIT :limit"

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

@github-actions

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

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

2 participants