MINOR: use polling mechanism for bnackend tests (#15962)

* handle unstable tests

* handle retry by polling ES

* remove waitForAsyncOp

* use retry

* format

* poll elastic for 10 seconds

* fixed tag delete test with polling

* poll up to 3 seconds

* fixed tag delete test again

* fixed assertEntityReferenceFromSearch

* fixed assertEntityReferenceFromSearch

* better error messages
This commit is contained in:
Imri Paran 2024-04-22 11:10:59 +02:00 committed by GitHub
parent 3435a9612e
commit 2e1786f7b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 117 additions and 42 deletions

View File

@ -2019,7 +2019,9 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
permissionNotAllowed(TEST_USER_NAME, List.of(MetadataOperation.DELETE))); permissionNotAllowed(TEST_USER_NAME, List.of(MetadataOperation.DELETE)));
} }
/** Soft delete an entity and then use restore request to restore it back */ /**
* Soft delete an entity and then use restore request to restore it back
*/
@Test @Test
@Execution(ExecutionMode.CONCURRENT) @Execution(ExecutionMode.CONCURRENT)
void delete_restore_entity_200(TestInfo test) throws IOException { void delete_restore_entity_200(TestInfo test) throws IOException {
@ -2238,25 +2240,37 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
} }
// check if the added tag if also added in the entity in search // check if the added tag if also added in the entity in search
assertTrue(fqnList.contains(tagLabel.getTagFQN())); assertTrue(fqnList.contains(tagLabel.getTagFQN()));
fqnList.clear();
// delete the tag // delete the tag
tagResourceTest.deleteEntity(tag.getId(), false, true, ADMIN_AUTH_HEADERS); tagResourceTest.deleteEntity(tag.getId(), false, true, ADMIN_AUTH_HEADERS);
waitForEsAsyncOp(500);
response = T finalEntity = entity;
getResponseFormSearch( TestUtils.assertEventually(
indexMapping.getIndexName(Entity.getSearchRepository().getClusterAlias())); test.getDisplayName(),
hits = response.getHits().getHits(); () -> {
for (SearchHit hit : hits) { fqnList.clear();
Map<String, Object> sourceAsMap = hit.getSourceAsMap(); SearchResponse afterDeleteResponse;
if (sourceAsMap.get("id").toString().equals(entity.getId().toString())) { try {
@SuppressWarnings("unchecked") afterDeleteResponse =
List<Map<String, String>> listTags = (List<Map<String, String>>) sourceAsMap.get("tags"); getResponseFormSearch(
listTags.forEach(tempMap -> fqnList.add(tempMap.get("tagFQN"))); indexMapping.getIndexName(Entity.getSearchRepository().getClusterAlias()));
break; } catch (HttpResponseException e) {
} throw new RuntimeException(e);
} }
// check if the relationships of tag are also deleted in search
assertFalse(fqnList.contains(tagLabel.getTagFQN())); SearchHit[] hitsAfterDelete = afterDeleteResponse.getHits().getHits();
for (SearchHit hit : hitsAfterDelete) {
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
if (sourceAsMap.get("id").toString().equals(finalEntity.getId().toString())) {
@SuppressWarnings("unchecked")
List<Map<String, String>> listTags =
(List<Map<String, String>>) sourceAsMap.get("tags");
listTags.forEach(tempMap -> fqnList.add(tempMap.get("tagFQN")));
break;
}
}
// check if the relationships of tag are also deleted in search
assertFalse(fqnList.contains(tagLabel.getTagFQN()));
});
} }
@Test @Test
@ -2587,7 +2601,9 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
return TestUtils.put(target, restore, entityClass, status, authHeaders); return TestUtils.put(target, restore, entityClass, status, authHeaders);
} }
/** Helper function to create an entity, submit POST API request and validate response. */ /**
* Helper function to create an entity, submit POST API request and validate response.
*/
public T createAndCheckEntity(K create, Map<String, String> authHeaders) throws IOException { public T createAndCheckEntity(K create, Map<String, String> authHeaders) throws IOException {
// Validate an entity that is created has all the information set in create request // Validate an entity that is created has all the information set in create request
String updatedBy = SecurityUtil.getPrincipalName(authHeaders); String updatedBy = SecurityUtil.getPrincipalName(authHeaders);
@ -2712,7 +2728,9 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
} }
} }
/** Helper function to generate JSON PATCH, submit PATCH API request and validate response. */ /**
* Helper function to generate JSON PATCH, submit PATCH API request and validate response.
*/
protected final T patchEntityAndCheck( protected final T patchEntityAndCheck(
T updated, T updated,
String originalJson, String originalJson,
@ -3117,7 +3135,9 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
.withFieldsDeleted(new ArrayList<>()); .withFieldsDeleted(new ArrayList<>());
} }
/** Compare fullyQualifiedName in the entityReference */ /**
* Compare fullyQualifiedName in the entityReference
*/
protected static void assertReference(String expected, EntityReference actual) { protected static void assertReference(String expected, EntityReference actual) {
if (expected != null) { if (expected != null) {
assertNotNull(actual); assertNotNull(actual);
@ -3128,7 +3148,9 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
} }
} }
/** Compare entity Id and types in the entityReference */ /**
* Compare entity Id and types in the entityReference
*/
protected static void assertReference(EntityReference expected, EntityReference actual) { protected static void assertReference(EntityReference expected, EntityReference actual) {
// If the actual value is inherited, it will never match the expected // If the actual value is inherited, it will never match the expected
// We just ignore the validation in these cases // We just ignore the validation in these cases
@ -3146,37 +3168,43 @@ public abstract class EntityResourceTest<T extends EntityInterface, K extends Cr
} }
protected void assertEntityReferenceFromSearch(T entity, EntityReference actual) protected void assertEntityReferenceFromSearch(T entity, EntityReference actual)
throws IOException, InterruptedException { throws IOException {
RestClient searchClient = getSearchClient(); RestClient searchClient = getSearchClient();
IndexMapping index = Entity.getSearchRepository().getIndexMapping(entityType); IndexMapping index = Entity.getSearchRepository().getIndexMapping(entityType);
Response response;
Request request = new Request("GET", String.format("%s/_search", index.getIndexName(null))); Request request = new Request("GET", String.format("%s/_search", index.getIndexName(null)));
String query = String query =
String.format( String.format(
"{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"_id\":\"%s\"}}]}}}", entity.getId()); "{\"query\":{\"bool\":{\"filter\":[{\"term\":{\"_id\":\"%s\"}}]}}}", entity.getId());
request.setJsonEntity(query); request.setJsonEntity(query);
try { try {
waitForEsAsyncOp(); assertEventually(
response = searchClient.performRequest(request); "assertEntityReferenceFromSearch_" + entity.getFullyQualifiedName(),
() -> {
Response response = searchClient.performRequest(request);
String jsonString = EntityUtils.toString(response.getEntity());
@SuppressWarnings("unchecked")
HashMap<String, Object> map =
(HashMap<String, Object>) JsonUtils.readOrConvertValue(jsonString, HashMap.class);
@SuppressWarnings("unchecked")
LinkedHashMap<String, Object> hits = (LinkedHashMap<String, Object>) map.get("hits");
@SuppressWarnings("unchecked")
ArrayList<LinkedHashMap<String, Object>> hitsList =
(ArrayList<LinkedHashMap<String, Object>>) hits.get("hits");
assertEquals(1, hitsList.size());
LinkedHashMap<String, Object> doc = hitsList.get(0);
@SuppressWarnings("unchecked")
LinkedHashMap<String, Object> source =
(LinkedHashMap<String, Object>) doc.get("_source");
EntityReference domainReference =
JsonUtils.readOrConvertValue(source.get("domain"), EntityReference.class);
assertEquals(domainReference.getId(), actual.getId());
assertEquals(domainReference.getType(), actual.getType());
});
} finally { } finally {
searchClient.close(); searchClient.close();
} }
String jsonString = EntityUtils.toString(response.getEntity());
HashMap<String, Object> map =
(HashMap<String, Object>) JsonUtils.readOrConvertValue(jsonString, HashMap.class);
LinkedHashMap<String, Object> hits = (LinkedHashMap<String, Object>) map.get("hits");
ArrayList<LinkedHashMap<String, Object>> hitsList =
(ArrayList<LinkedHashMap<String, Object>>) hits.get("hits");
assertEquals(1, hitsList.size());
LinkedHashMap<String, Object> doc = (LinkedHashMap<String, Object>) hitsList.get(0);
LinkedHashMap<String, Object> source = (LinkedHashMap<String, Object>) doc.get("_source");
EntityReference domainReference =
JsonUtils.readOrConvertValue(source.get("domain"), EntityReference.class);
assertEquals(domainReference.getId(), actual.getId());
assertEquals(domainReference.getType(), actual.getType());
} }
protected static void checkOwnerOwns(EntityReference owner, UUID entityId, boolean expectedOwning) protected static void checkOwnerOwns(EntityReference owner, UUID entityId, boolean expectedOwning)

View File

@ -215,6 +215,7 @@ public class GlossaryTermResourceTest extends EntityResourceTest<GlossaryTerm, C
.withName("t1") .withName("t1")
.withGlossary(glossary.getFullyQualifiedName()) .withGlossary(glossary.getFullyQualifiedName())
.withDescription("desc"); .withDescription("desc");
GlossaryTerm t1 = assertDomainInheritance(create, DOMAIN.getEntityReference()); GlossaryTerm t1 = assertDomainInheritance(create, DOMAIN.getEntityReference());
// Create terms t12 under t1 without reviewers and owner // Create terms t12 under t1 without reviewers and owner

View File

@ -0,0 +1,7 @@
package org.openmetadata.service.util;
public class RetryableAssertionError extends Exception {
public RetryableAssertionError(Throwable cause) {
super(cause);
}
}

View File

@ -24,11 +24,16 @@ import static org.openmetadata.service.Entity.ADMIN_USER_NAME;
import static org.openmetadata.service.Entity.SEPARATOR; import static org.openmetadata.service.Entity.SEPARATOR;
import static org.openmetadata.service.security.SecurityUtil.authHeaders; import static org.openmetadata.service.security.SecurityUtil.authHeaders;
import io.github.resilience4j.core.functions.CheckedRunnable;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.retry.RetryRegistry;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.net.URI; import java.net.URI;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
@ -85,6 +90,7 @@ import org.openmetadata.service.resources.glossary.GlossaryTermResourceTest;
import org.openmetadata.service.resources.tags.TagResourceTest; import org.openmetadata.service.resources.tags.TagResourceTest;
import org.openmetadata.service.resources.teams.UserResourceTest; import org.openmetadata.service.resources.teams.UserResourceTest;
import org.openmetadata.service.security.SecurityUtil; import org.openmetadata.service.security.SecurityUtil;
import org.opentest4j.AssertionFailedError;
@Slf4j @Slf4j
public final class TestUtils { public final class TestUtils {
@ -183,6 +189,14 @@ public final class TestUtils {
.withUsername("admin") .withUsername("admin")
.withPassword("admin")); .withPassword("admin"));
public static RetryRegistry elasticSearchRetryRegistry =
RetryRegistry.of(
RetryConfig.custom()
.maxAttempts(30) // about 3 seconds
.waitDuration(Duration.ofMillis(100))
.retryExceptions(RetryableAssertionError.class)
.build());
public static void assertCustomProperties( public static void assertCustomProperties(
List<CustomProperty> expected, List<CustomProperty> actual) { List<CustomProperty> expected, List<CustomProperty> actual) {
if (expected == actual) { // Take care of both being null if (expected == actual) { // Take care of both being null
@ -654,4 +668,29 @@ public final class TestUtils {
// the owner of the async operation // the owner of the async operation
TimeUnit.MILLISECONDS.sleep(milliseconds); TimeUnit.MILLISECONDS.sleep(milliseconds);
} }
public static void assertEventually(String name, CheckedRunnable runnable) {
try {
Retry.decorateCheckedRunnable(
elasticSearchRetryRegistry.retry(name),
() -> {
try {
runnable.run();
} catch (AssertionError e) {
// translate AssertionErrors to Exceptions to that retry processes
// them correctly. This is required because retry library only retries on
// Exceptions and:
// AssertionError -> Error -> Throwable
// RetryableAssertionError -> Exception -> Throwable
throw new RetryableAssertionError(e);
}
})
.run();
} catch (RetryableAssertionError e) {
throw new AssertionFailedError(
"Max retries exceeded polling for eventual assert", e.getCause());
} catch (Throwable e) {
throw new AssertionFailedError("Unexpected error while running retry: " + e.getMessage(), e);
}
}
} }