Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -36,6 +37,7 @@
import org.openmetadata.schema.api.data.CreateTable;
import org.openmetadata.schema.api.tests.CreateTestCase;
import org.openmetadata.schema.api.tests.CreateTestCaseResolutionStatus;
import org.openmetadata.schema.api.tests.CreateTestCaseResult;
import org.openmetadata.schema.api.tests.CreateTestSuite;
import org.openmetadata.schema.entity.classification.Classification;
import org.openmetadata.schema.entity.classification.Tag;
Expand Down Expand Up @@ -2686,6 +2688,161 @@ void test_incidentReopensAsNewAfterResolveAndNewFailure(TestNamespace ns) {
});
}

@Test
void test_incidentIdDerivation_followsLatestUnresolvedTcrs(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();
Table table = createTable(ns);

TestCase testCase =
TestCaseBuilder.create(client)
.name(ns.prefix("incident_derivation"))
.forTable(table)
.testDefinition("tableRowCountToEqual")
.parameter("value", "100")
.create();

TestCase beforeIncident = client.testCases().get(testCase.getId().toString(), "incidentId");
assertNull(beforeIncident.getIncidentId(), "no incident yet -> incidentId must be null");

client
.testCaseResults()
.create(
testCase.getFullyQualifiedName(),
new CreateTestCaseResult()
.withTimestamp(System.currentTimeMillis())
.withTestCaseStatus(org.openmetadata.schema.tests.type.TestCaseStatus.Failed)
.withResult("Initial failure"));

final UUID stateId =
Awaitility.await()
.atMost(90, TimeUnit.SECONDS)
.pollInterval(Duration.ofSeconds(2))
.until(
() ->
client
.testCases()
.get(testCase.getId().toString(), "incidentId")
.getIncidentId(),
java.util.Objects::nonNull);

client
.testCaseResolutionStatuses()
.create(
new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCase.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Ack));

Awaitility.await("Ack keeps the ongoing incident pointer")
.atMost(90, TimeUnit.SECONDS)
.pollInterval(Duration.ofSeconds(2))
.untilAsserted(
() -> {
org.openmetadata.schema.tests.type.TestCaseResolutionStatus latestStatus =
latestIncidentStatus(client, testCase.getFullyQualifiedName());
assertEquals(
TestCaseResolutionStatusTypes.Ack,
latestStatus.getTestCaseResolutionStatusType());
TestCase fetched = client.testCases().get(testCase.getId().toString(), "incidentId");
assertEquals(stateId, fetched.getIncidentId());
});

client
.testCaseResolutionStatuses()
.create(
new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCase.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Resolved)
.withTestCaseResolutionStatusDetails(
new org.openmetadata.schema.tests.type.Resolved()));

Awaitility.await("Resolved clears the ongoing incident pointer")
.atMost(90, TimeUnit.SECONDS)
.pollInterval(Duration.ofSeconds(2))
.untilAsserted(
() -> {
TestCase fetched = client.testCases().get(testCase.getId().toString(), "incidentId");
assertNull(fetched.getIncidentId());
assertNull(
bulkListedIncidentId(client, table, testCase),
"bulk list derivation must also clear the pointer after resolve");
});

// On this branch a manual Ack after Resolved starts a NEW incident (new stateId); the
// pointer must follow it so the test case page shows the current status.
client
.testCaseResolutionStatuses()
.create(
new CreateTestCaseResolutionStatus()
.withTestCaseReference(testCase.getFullyQualifiedName())
.withTestCaseResolutionStatusType(TestCaseResolutionStatusTypes.Ack));

Awaitility.await("reopen points at the new ongoing incident")
.atMost(90, TimeUnit.SECONDS)
.pollInterval(Duration.ofSeconds(2))
.untilAsserted(
() -> {
org.openmetadata.schema.tests.type.TestCaseResolutionStatus latestStatus =
latestIncidentStatus(client, testCase.getFullyQualifiedName());
assertEquals(
TestCaseResolutionStatusTypes.Ack,
latestStatus.getTestCaseResolutionStatusType());
assertNotEquals(stateId, latestStatus.getStateId());
TestCase fetched = client.testCases().get(testCase.getId().toString(), "incidentId");
assertEquals(latestStatus.getStateId(), fetched.getIncidentId());
assertEquals(
latestStatus.getStateId(),
bulkListedIncidentId(client, table, testCase),
"bulk list derivation must agree with the single read");
});

client
.testCaseResults()
.create(
testCase.getFullyQualifiedName(),
new CreateTestCaseResult()
.withTimestamp(System.currentTimeMillis() + 1)
.withTestCaseStatus(org.openmetadata.schema.tests.type.TestCaseStatus.Success)
.withResult("Passing while incident open"));

Awaitility.await("a passing run hides the incident (rule A)")
.atMost(90, TimeUnit.SECONDS)
.pollInterval(Duration.ofSeconds(2))
.untilAsserted(
() -> {
TestCase fetched =
client.testCases().get(testCase.getId().toString(), "incidentId,testCaseResult");
assertNotNull(fetched.getTestCaseResult());
assertEquals(
org.openmetadata.schema.tests.type.TestCaseStatus.Success,
fetched.getTestCaseResult().getTestCaseStatus());
assertNull(fetched.getIncidentId());
assertNull(bulkListedIncidentId(client, table, testCase));
});
}

private UUID bulkListedIncidentId(OpenMetadataClient client, Table table, TestCase testCase) {
RequestOptions options =
RequestOptions.builder()
.queryParam("fields", "incidentId")
.queryParam("entityLink", "<#E::table::" + table.getFullyQualifiedName() + ">")
.queryParam("includeAllTests", "true")
.queryParam("limit", "100")
.build();

String responseJson =
client
.getHttpClient()
.executeForString(HttpMethod.GET, "/v1/dataQuality/testCases", null, options);
TestCaseResource.TestCaseList result =
JsonUtils.readValue(responseJson, TestCaseResource.TestCaseList.class);

return result.getData().stream()
.filter(tc -> testCase.getId().equals(tc.getId()))
.findFirst()
.map(TestCase::getIncidentId)
.orElse(null);
}

private org.openmetadata.schema.tests.type.TestCaseResolutionStatus latestIncidentStatus(
OpenMetadataClient client, String testCaseFqn) {
ListParams params =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8865,6 +8865,21 @@ List<String> listTestCaseResolutionForEntityFQNHash(
+ "WHERE stateId = :stateId ORDER BY timestamp ASC LIMIT 1")
String listFirstTestCaseResolutionStatusesForStateId(@Bind("stateId") String stateId);

@RegisterRowMapper(LatestRecordWithFQNHashMapper.class)
@SqlQuery(
"SELECT entityFQNHash, json FROM ("
+ "SELECT entityFQNHash, json, "
+ "ROW_NUMBER() OVER (PARTITION BY entityFQNHash ORDER BY timestamp DESC, id DESC) AS rn "
+ "FROM test_case_resolution_status_time_series "
+ "WHERE entityFQNHash IN (<entityFQNHashes>)) ranked "
+ "WHERE rn = 1")
List<LatestRecordWithFQNHash> getLatestRecordBatchInternal(
@BindListFQN("entityFQNHashes") List<String> entityFQNs);
Comment thread
gitar-bot[bot] marked this conversation as resolved.

default List<LatestRecordWithFQNHash> getLatestRecordBatch(List<String> entityFQNs) {
return EntityDAO.queryInChunks(entityFQNs, this::getLatestRecordBatchInternal);
}

@SqlUpdate(
"DELETE FROM test_case_resolution_status_time_series WHERE entityFQNHash = :entityFQNHash")
void delete(@BindFQN("entityFQNHash") String entityFQNHash);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import org.openmetadata.schema.tests.type.TestCaseResolutionStatus;
import org.openmetadata.schema.tests.type.TestCaseResolutionStatusTypes;
import org.openmetadata.schema.tests.type.TestCaseResult;
import org.openmetadata.schema.tests.type.TestCaseStatus;
import org.openmetadata.schema.type.ApiStatus;
import org.openmetadata.schema.type.ChangeDescription;
import org.openmetadata.schema.type.EntityReference;
Expand Down Expand Up @@ -411,27 +412,47 @@ private void fetchAndSetTestCaseResultsAndIncidents(
fqnHashToFqn.put(FullyQualifiedName.buildHash(fqn), fqn);
}

List<CollectionDAO.LatestRecordWithFQNHash> records =
daoCollection.dataQualityDataTimeSeriesDao().getLatestRecordBatch(fqns);

Map<String, TestCaseResult> fqnToResult = new HashMap<>();
for (CollectionDAO.LatestRecordWithFQNHash record : records) {
String fqn = fqnHashToFqn.get(record.getEntityFQNHash());
if (fqn != null && record.getJson() != null) {
TestCaseResult result = JsonUtils.readValue(record.getJson(), TestCaseResult.class);
if (result != null) {
fqnToResult.put(fqn, result);
if (setResults || setIncidents) {
List<CollectionDAO.LatestRecordWithFQNHash> records =
daoCollection.dataQualityDataTimeSeriesDao().getLatestRecordBatch(fqns);
for (CollectionDAO.LatestRecordWithFQNHash record : records) {
String fqn = fqnHashToFqn.get(record.getEntityFQNHash());
if (fqn != null && record.getJson() != null) {
TestCaseResult result = JsonUtils.readValue(record.getJson(), TestCaseResult.class);
if (result != null) {
fqnToResult.put(fqn, result);
}
}
}
}

Map<String, UUID> fqnToOngoingIncident = new HashMap<>();
if (setIncidents) {
List<CollectionDAO.LatestRecordWithFQNHash> incidentRecords =
daoCollection.testCaseResolutionStatusTimeSeriesDao().getLatestRecordBatch(fqns);
for (CollectionDAO.LatestRecordWithFQNHash record : incidentRecords) {
String fqn = fqnHashToFqn.get(record.getEntityFQNHash());
if (fqn != null && record.getJson() != null) {
TestCaseResolutionStatus latest =
JsonUtils.readValue(record.getJson(), TestCaseResolutionStatus.class);
if (latest != null
&& latest.getStateId() != null
&& !TestCaseResolutionStatusTypes.Resolved.equals(
latest.getTestCaseResolutionStatusType())
&& isFailedResult(fqnToResult.get(fqn))) {
fqnToOngoingIncident.put(fqn, latest.getStateId());
}
}
}
}

for (TestCase testCase : testCases) {
TestCaseResult result = fqnToResult.get(testCase.getFullyQualifiedName());
if (setResults) {
testCase.setTestCaseResult(result);
testCase.setTestCaseResult(fqnToResult.get(testCase.getFullyQualifiedName()));
}
if (setIncidents) {
testCase.setIncidentId(result != null ? result.getIncidentId() : null);
testCase.setIncidentId(fqnToOngoingIncident.get(testCase.getFullyQualifiedName()));
}
}
}
Expand Down Expand Up @@ -1053,22 +1074,29 @@ private TestCaseResult getTestCaseResult(TestCase testCase) {
return testCaseResult;
}

/**
* Check all the test case results that have an ongoing incident and get the stateId of the
* incident
*/
/** StateId of the test case's ongoing incident: latest unresolved record, or null if resolved. */
private UUID getIncidentId(TestCase test) {
UUID ongoingIncident = null;

String json =
daoCollection.dataQualityDataTimeSeriesDao().getLatestRecord(test.getFullyQualifiedName());
TestCaseResult latestTestCaseResult = JsonUtils.readValue(json, TestCaseResult.class);
if (!latestResultIsFailed(test.getFullyQualifiedName())) {
return null;
}
TestCaseResolutionStatusRepository tcrsRepo =
(TestCaseResolutionStatusRepository)
Entity.getEntityTimeSeriesRepository(Entity.TEST_CASE_RESOLUTION_STATUS);
TestCaseResolutionStatus latest = tcrsRepo.getLatestRecord(test.getFullyQualifiedName());
return Boolean.TRUE.equals(tcrsRepo.unresolvedIncident(latest)) ? latest.getStateId() : null;
}

if (!nullOrEmpty(latestTestCaseResult)) {
ongoingIncident = latestTestCaseResult.getIncidentId();
private boolean latestResultIsFailed(String testCaseFqn) {
List<CollectionDAO.LatestRecordWithFQNHash> records =
daoCollection.dataQualityDataTimeSeriesDao().getLatestRecordBatch(List.of(testCaseFqn));
if (records.isEmpty() || records.get(0).getJson() == null) {
Comment on lines 1078 to +1092

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Performance: getIncidentId re-queries latest result already fetched in setFields

In setFields, getTestCaseResult(test) populates test.getTestCaseResult() immediately before getIncidentId(test) is called. getIncidentId then invokes latestResultIsFailed(fqn), which issues a fresh getLatestRecordBatch DB query instead of reusing the result already on the entity, adding one extra time-series query per single-read whenever both the result and incident fields are requested. Consider passing the already-fetched TestCaseResult into getIncidentId/latestResultIsFailed and only falling back to the batch query when it is absent.

Was this helpful? React with 👍 / 👎

return false;
}
return isFailedResult(JsonUtils.readValue(records.get(0).getJson(), TestCaseResult.class));
}

return ongoingIncident;
private static boolean isFailedResult(TestCaseResult result) {
return result != null && result.getTestCaseStatus() == TestCaseStatus.Failed;
}

public int getTestCaseCount(List<UUID> testCaseIds) {
Expand Down
Loading