Skip to content

Fixes #27060: Fix TestCaseRepository deleteChildren O(N²) nested loop and double-delete in deleteAllTestCaseResults#27061

Open
RajdeepKushwaha5 wants to merge 2 commits intoopen-metadata:mainfrom
RajdeepKushwaha5:fix/testcase-repository-delete-children-n-squared-bug
Open

Fixes #27060: Fix TestCaseRepository deleteChildren O(N²) nested loop and double-delete in deleteAllTestCaseResults#27061
RajdeepKushwaha5 wants to merge 2 commits intoopen-metadata:mainfrom
RajdeepKushwaha5:fix/testcase-repository-delete-children-n-squared-bug

Conversation

@RajdeepKushwaha5
Copy link
Copy Markdown
Contributor

Describe your changes:

Fixes #27060

What changes did I make?

Fixed two bugs in TestCaseRepository.java:

Bug 1 — deleteChildren() O(N²) nested loop (lines 896–914):

The method had a nested loop where both the outer and inner loops iterated over the same children list. For N children, deleteById() was called N×N times instead of N — each child was deleted N times. Additionally:

  • TestCaseResolutionStatusRepository was re-instantiated on every outer iteration instead of once
  • The outer loop variable entityRelationshipRecord was only used for logging, never for deletion
  • The log message used hardDelete ? "hard" : "soft" but the enclosing if (hardDelete) guaranteed it always logged "hard"

Fix: Removed the outer loop entirely. Moved repository instantiation outside the loop. Single pass over children with one deleteById() call per child.

Bug 2 — deleteAllTestCaseResults() double-delete (lines 921–933):

The method called testCaseResultRepository.deleteAllTestCaseResults(fqn) synchronously, then submitted the exact same call asynchronously via asyncExecutor. This caused every test case result to be deleted twice — once immediately and once in a background thread (which would find nothing left to delete).

Fix: Removed the redundant async call. Kept only the synchronous call since this runs inside entitySpecificCleanup()cleanup() which operates within a transaction. Also removed the now-unused asyncExecutor field and its imports (ExecutorService, Executors).

Why did I make them?

  • Bug 1 causes O(N²) database operations when hard-deleting a TestCase with many resolution statuses, degrading performance significantly at scale
  • Bug 2 wastes database queries by executing identical DELETE statements twice, with a potential race condition between the sync and async calls

How did I test my changes?

  • Verified the base class EntityRepository.deleteChildren() uses a single loop (correct pattern)
  • Confirmed EntityTimeSeriesRepository.deleteById() is idempotent (returns early if not found), meaning the bug causes silent performance degradation rather than exceptions
  • Verified asyncExecutor had no other call sites in the file after removing the double-delete
  • Ran mvn spotless:check — passes clean

Type of change:

  • Bug fix
  • Improvement
  • New feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.
  • I have added a test that covers the exact scenario we are fixing. For complex issues, comment the issue number in the test for future reference.
    • The fix is a straightforward loop de-duplication. The deleteById() method is idempotent, so existing integration tests for TestCase hard-delete continue to validate the cleanup path. No new test is needed since the behavior (children get deleted) doesn't change — only the number of redundant DB calls does.

Copilot AI review requested due to automatic review settings April 5, 2026 23:47
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 5, 2026

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes performance and correctness issues in TestCaseRepository cleanup paths by removing redundant loops/calls that caused excessive and duplicated deletions during hard deletes.

Changes:

  • Fix deleteChildren() to delete each TestCaseResolutionStatus child exactly once (removes O(N²) nested-loop behavior and avoids re-instantiating the repository per iteration).
  • Fix deleteAllTestCaseResults() to avoid deleting the same test case results twice by removing the duplicate async submission.
  • Remove the now-unused async executor and related imports.

TestCaseResultRepository testCaseResultRepository =
(TestCaseResultRepository) Entity.getEntityTimeSeriesRepository(TEST_CASE_RESULT);
testCaseResultRepository.deleteAllTestCaseResults(fqn);
asyncExecutor.submit(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@RajdeepKushwaha5 any reason you removed the async excecutor here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@harshach

The current code calls deleteAllTestCaseResults(fqn) twice — once synchronously and then the exact same call again via asyncExecutor.submit():

testCaseResultRepository.deleteAllTestCaseResults(fqn);     // deletes everything right here
asyncExecutor.submit(() -> {
    testCaseResultRepository.deleteAllTestCaseResults(fqn);  // runs later, finds nothing to delete
});

The sync call finishes and wipes all the results before the async task even starts, so the async one always ends up being a no-op — it just executes the same DELETEs against rows that no longer exist.

I kept the sync call and removed the async one (rather than the other way around) because deleteAllTestCaseResults() is called from entitySpecificCleanup(), which runs inside the JDBI transaction in cleanup(). The sync call participates in that transaction — so if anything downstream fails and the transaction rolls back, the results stay intact (consistent with the test case not being deleted). If we kept only the async call, it would run on a separate thread outside the transaction, meaning it could delete results even when the parent entity deletion rolled back.

So basically: the async call was redundant (double-delete), and the sync one is the correct one to keep for transactional safety.

Happy to restructure if you'd prefer a different approach — just let me know!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

🔴 Playwright Results — 1 failure(s), 31 flaky

✅ 3592 passed · ❌ 1 failed · 🟡 31 flaky · ⏭️ 207 skipped

Shard Passed Failed Flaky Skipped
🔴 Shard 1 449 1 7 2
🟡 Shard 2 639 0 4 32
🟡 Shard 3 643 0 6 26
🟡 Shard 4 623 0 4 47
🟡 Shard 5 605 0 4 67
🟡 Shard 6 633 0 6 33

Genuine Failures (failed on all attempts)

Features/Pagination.spec.ts › should test pagination on Table columns (shard 1)
�[31mTest timeout of 60000ms exceeded.�[39m
🟡 31 flaky test(s) (passed on retry)
  • Features/CustomizeDetailPage.spec.ts › Search Index - customization should work (shard 1, 1 retry)
  • Features/CustomizeDetailPage.spec.ts › Database - customization should work (shard 1, 1 retry)
  • Features/Pagination.spec.ts › should test API Collection normal pagination (shard 1, 1 retry)
  • Features/Pagination.spec.ts › should test Stored Procedures normal pagination (shard 1, 1 retry)
  • Features/Pagination.spec.ts › should test Files normal pagination (shard 1, 1 retry)
  • Features/Pagination.spec.ts › should test Spreadsheets complete flow with search (shard 1, 1 retry)
  • Pages/UserCreationWithPersona.spec.ts › Create user with persona and verify on profile (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary (shard 2, 1 retry)
  • Features/ChangeSummaryBadge.spec.ts › Automated badge should appear on entity description with Automated source (shard 2, 1 retry)
  • Features/DataProductRename.spec.ts › should handle multiple consecutive renames and preserve assets (shard 2, 1 retry)
  • Features/Glossary/GlossaryWorkflow.spec.ts › should display correct status badge color and icon (shard 2, 1 retry)
  • Features/Permissions/EntityPermissions.spec.ts › MlModel allow common operations permissions (shard 3, 1 retry)
  • Features/Permissions/EntityPermissions.spec.ts › Metric deny common operations permissions (shard 3, 1 retry)
  • Features/Permissions/GlossaryPermissions.spec.ts › Team-based permissions work correctly (shard 3, 1 retry)
  • Features/UserProfileOnlineStatus.spec.ts › Should not show online status for inactive users (shard 3, 1 retry)
  • Flow/ExploreDiscovery.spec.ts › Should display deleted assets when showDeleted is checked and deleted is not present in queryFilter (shard 3, 1 retry)
  • Flow/PersonaDeletionUserProfile.spec.ts › User profile loads correctly before and after persona deletion (shard 3, 1 retry)
  • Pages/Customproperties-part2.spec.ts › entityReferenceList shows item count, scrollable list, no expand toggle (shard 4, 1 retry)
  • Pages/DataContracts.spec.ts › Create Data Contract and validate for Api Collection (shard 4, 1 retry)
  • Pages/DataContractsSemanticRules.spec.ts › Validate Owner Rule Is_Not (shard 4, 1 retry)
  • Pages/Domains.spec.ts › Multiple consecutive domain renames preserve all associations (shard 4, 1 retry)
  • Pages/EntityDataConsumer.spec.ts › Tag Add, Update and Remove (shard 5, 1 retry)
  • Pages/EntityDataSteward.spec.ts › Follow & Un-follow entity (shard 5, 1 retry)
  • Pages/ExploreTree.spec.ts › Verify Database and Database Schema available in explore tree (shard 5, 1 retry)
  • Pages/Glossary.spec.ts › Add and Remove Assets (shard 5, 2 retries)
  • Pages/Lineage/DataAssetLineage.spec.ts › verify create lineage for entity - Search Index (shard 6, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: searchIndex (shard 6, 1 retry)
  • Pages/ProfilerConfigurationPage.spec.ts › Non admin user (shard 6, 1 retry)
  • Pages/UserDetails.spec.ts › Admin user can edit teams from the user profile (shard 6, 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

Copilot AI review requested due to automatic review settings April 12, 2026 04:37
@gitar-bot
Copy link
Copy Markdown

gitar-bot bot commented Apr 12, 2026

Code Review ✅ Approved

Eliminates the O(N²) nested loop in TestCaseRepository.deleteChildren and removes the double-delete bug in deleteAllTestCaseResults. No issues found.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@sonarqubecloud
Copy link
Copy Markdown

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

Labels

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.

[Bug] TestCaseRepository.deleteChildren() has O(N²) nested loop bug causing redundant deletes

3 participants