Fixes #27060: Fix TestCaseRepository deleteChildren O(N²) nested loop and double-delete in deleteAllTestCaseResults#27061
Conversation
|
Hi there 👋 Thanks for your contribution! The OpenMetadata team will review the PR shortly! Once it has been labeled as Let us know if you need any help! |
There was a problem hiding this comment.
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 eachTestCaseResolutionStatuschild 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( |
There was a problem hiding this comment.
@RajdeepKushwaha5 any reason you removed the async excecutor here?
There was a problem hiding this comment.
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!
🔴 Playwright Results — 1 failure(s), 31 flaky✅ 3592 passed · ❌ 1 failed · 🟡 31 flaky · ⏭️ 207 skipped
Genuine Failures (failed on all attempts)❌
|
…e-delete in deleteAllTestCaseResults
27b5285 to
b16130b
Compare
Code Review ✅ ApprovedEliminates the O(N²) nested loop in TestCaseRepository.deleteChildren and removes the double-delete bug in deleteAllTestCaseResults. No issues found. OptionsDisplay: compact → Showing less information. Comment with these commands to change:
Was this helpful? React with 👍 / 👎 | Gitar |
|



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
childrenlist. For N children,deleteById()was called N×N times instead of N — each child was deleted N times. Additionally:TestCaseResolutionStatusRepositorywas re-instantiated on every outer iteration instead of onceentityRelationshipRecordwas only used for logging, never for deletionhardDelete ? "hard" : "soft"but the enclosingif (hardDelete)guaranteed it always logged"hard"Fix: Removed the outer loop entirely. Moved repository instantiation outside the loop. Single pass over
childrenwith onedeleteById()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 viaasyncExecutor. 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-unusedasyncExecutorfield and its imports (ExecutorService,Executors).Why did I make them?
How did I test my changes?
EntityRepository.deleteChildren()uses a single loop (correct pattern)EntityTimeSeriesRepository.deleteById()is idempotent (returns early if not found), meaning the bug causes silent performance degradation rather than exceptionsasyncExecutorhad no other call sites in the file after removing the double-deletemvn spotless:check— passes cleanType of change:
Checklist:
Fixes <issue-number>: <short explanation>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.