fix(dq): stale incident status on the Test Case page after reopening a resolved incident#30182
fix(dq): stale incident status on the Test Case page after reopening a resolved incident#30182manerow wants to merge 7 commits into
Conversation
✅ PR checks passedThe linked issue has a description and all required Shipping project fields set. Thanks! |
… the index-field constant
Code Review 👍 Approved with suggestions 3 resolved / 4 findingsSynchronizes incident status across Test Case pages by deriving the incident pointer from the resolution timeline rather than result stamps. Reopening resolved incidents now correctly reuses the original task, though the new restart detection logic requires monitoring for false-positive workflow failures. 💡 Edge Case: Reopen restart detection may false-positive on immediate workflow completion📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TaskRepository.java:887-901 reopenTaskWithWorkflow now treats a null workflowInstanceId on the refreshed task as a restart failure and rolls the task back to its terminal state, throwing IllegalStateException. This is correct when the trigger fails, but if a reopened governance workflow starts and then immediately reaches its end event (e.g. an auto-transition path), the running Flowable instance ends and workflowInstanceId can be cleared to null even though the restart succeeded — which would wrongly roll the reopened incident back to Resolved. If success is not guaranteed to leave a persisted, non-null workflowInstanceId, prefer keying failure detection off the explicit failure marker (workflow-start-failed stage) or an affirmative success signal rather than the absence of an instance id. ✅ 3 resolved✅ Edge Case: Fallback reopen leaves denormalized row incidentId stale
✅ Quality: Duplicated search-index field list risks silent drift
✅ Performance: Extra TCRS query on every non-failed result ingestion
🤖 Prompt for agentsOptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
…ource the workflow-start stage constant, and trim comments to essentials
Code Review ✅ Approved 4 resolved / 4 findingsConsistent incident state tracking is now enforced by deriving status from the latest unresolved resolution record rather than ingestion-time snapshots. This change correctly synchronizes incident lifecycles, resolves stale status displays after reopening, and ensures workflow consistency across task transitions. ✅ 4 resolved✅ Edge Case: Fallback reopen leaves denormalized row incidentId stale
✅ Quality: Duplicated search-index field list risks silent drift
✅ Performance: Extra TCRS query on every non-failed result ingestion
✅ Edge Case: Reopen restart detection may false-positive on immediate workflow completion
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
1 similar comment
Code Review ✅ Approved 4 resolved / 4 findingsConsistent incident state tracking is now enforced by deriving status from the latest unresolved resolution record rather than ingestion-time snapshots. This change correctly synchronizes incident lifecycles, resolves stale status displays after reopening, and ensures workflow consistency across task transitions. ✅ 4 resolved✅ Edge Case: Fallback reopen leaves denormalized row incidentId stale
✅ Quality: Duplicated search-index field list risks silent drift
✅ Performance: Extra TCRS query on every non-failed result ingestion
✅ Edge Case: Reopen restart detection may false-positive on immediate workflow completion
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |
| updated.setTestCaseStatus( | ||
| testCaseResult != null ? testCaseResult.getTestCaseStatus() : original.getTestCaseStatus()); | ||
| updated.setIncidentId(testCaseResult != null ? testCaseResult.getIncidentId() : null); | ||
| updated.setIncidentId(resolveOngoingIncidentId(original)); |
There was a problem hiding this comment.
Stale write remains possible.
resolveOngoingIncidentId reads the TCRS timeline before the row update, with no lock or version check joining those operations. A result update can read incident X while it is open, then a concurrent resolution can append Resolved and clear the test-case row before this updater writes the previously read X back. No-fields reads then show the resolved incident as open. The timeline state must be validated atomically with the row write, or an older observation must be prevented from overwriting a newer transition.
| storeEntity(reopened, true); | ||
| postUpdate(openSnapshot, reopened); | ||
|
|
||
| boolean started = triggerWorkflowManagedTask(reopened); |
There was a problem hiding this comment.
Pending reopen lacks recovery. This stores the task as Open with
workflowInstanceId = null before starting Flowable. The rollback only runs after triggerWorkflowManagedTask returns false. If the process stops between the database write and the workflow call, the task remains Open in pending-workflow-start with no live workflow, and no recovery path shown here retries it. Later incident operations can reuse a task whose transitions, assignment, and notifications cannot run. The pending state needs restart reconciliation or another durable coordination mechanism.
|
|
🔴 Playwright Results — 2 failure(s), 29 flaky✅ 4539 passed · ❌ 2 failed · 🟡 29 flaky · ⏭️ 96 skipped
Genuine Failures (failed on all attempts)❌
|



Fixes #30168
The mental model you need first
When a Data Quality test fails, the system opens an incident: a tracked problem someone has to work. An incident has a lifecycle (New -> Acknowledged -> Assigned -> Resolved) and is identified by a
stateId. On main, each incident is also a Task entity (a real work item with an assignee, driven by a workflow engine); the Task's id is the incident'sstateId.Two separate things get written as a test lives its life:
result.incidentId). This stamp is written once, at the moment the result is ingested, and never touched again.stateId.The Test Case page needs to answer one question: "Does this test case have an open incident right now, and what's its status?" It reads a field called
TestCase.incidentIdto do that. That field is the whole story.The problem (what the customer saw)
Someone resolved an incident by mistake, then set it back to Acknowledged. The incident itself showed Acknowledged correctly, but the Test Case page kept showing Resolved, forever, until the test failed again.
Why it happened (how it worked before)
TestCase.incidentIdwas computed from the wrong source, and inconsistently:result.incidentId). But that stamp is frozen at ingestion time. It answers "which incident did the last run belong to," which is not the same question as "what is the current incident."So the field didn't reliably mean "the current open incident." It meant "whatever incident the last failing run was stamped with" (list) or "the latest incident, resolved or not" (detail). These two disagreed with each other, and both drifted from reality.
The reopen scenario is where it breaks visibly:
incidentId = S1.Now
TestCase.incidentId(from the frozen result stamp) still says S1, and S1's latest status is Resolved. So the page shows Resolved, even though the live incident is S2/Ack. It only "self-heals" when the test fails again, because that writes a new result stamped with the current incident.On main there was an extra layer of broken: because of a latent bug (the code that looked up the incident's Task by id never actually loaded the field it filtered on), reopening didn't just mis-point the field, it created a phantom incident with no Task at all, outside the workflow (no assignee flow, no notifications).
What the PR changes
TestCase.incidentIdnow always means: thestateIdof the latest unresolved incident record, or null if there's no open incident. Both the single-read and the bulk (list) paths now compute it the same way, from the incident timeline. The frozen result stamp is no longer consulted for this. This is the core fix.stateIdand reopens the same Task (the workflow is restarted on it), instead of minting a phantom. A genuinely new failure after a resolution still starts a fresh incident; that path is untouched. The distinction is deliberate: a human un-resolving is correcting the incident; a new failure is a new problem.incidentIdon the test case row (for reads that don't request the field). The PR updates it on every incident status change (set to the incident id while active, cleared to null on resolve) so it stays aligned with the current incident.How it works now (same scenario)
TestCase.incidentIdbecomes null -> page shows No Incident (correct: nothing is open).TestCase.incidentId= S1 -> page shows Ack.Verified on a live deployment: after step 4 the incident id is the same S1, the Task is back to
InProgressat theackstage, and the timeline reads New -> Ack -> Resolved -> New -> Ack under one id.Why this is the right way
Two intended, correct side effects fall out of this: after a resolve the page shows "No Incident" instead of a stale Resolved chip, and a currently-passing test that still has an unresolved incident now correctly shows it (because the incident is genuinely still open until someone resolves it).
Testing
IncidentTaskIntegrationITtests: reopen keeps the stateId and returns the same Task to Open (Ack and Assigned variants), re-resolving completes that same Task, a new failure on a reopened incident reuses it, and there is never a second Task per incident.TestCaseResourceIT#test_incidentIdDerivation_followsLatestUnresolvedTcrs: derivation matrix over the single GET and the bulk list (no incident -> null, New/Ack -> stateId, Resolved -> null, reopen -> same stateId, passing run while open -> stateId).IncidentTaskIntegrationITandTestCaseResourceITsuites pass.Resolved -> Ackthe test case page now shows Ack.