-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Fix flaky "Entity not found: <service>" on list (concurrent cascade delete) #29150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ | |
| import org.openmetadata.it.util.TestNamespaceExtension; | ||
| import org.openmetadata.it.util.UpdateType; | ||
| import org.openmetadata.schema.EntityInterface; | ||
| import org.openmetadata.schema.ServiceEntityInterface; | ||
| import org.openmetadata.schema.api.policies.CreatePolicy; | ||
| import org.openmetadata.schema.api.teams.CreateRole; | ||
| import org.openmetadata.schema.api.teams.CreateUser; | ||
|
|
@@ -45,7 +46,10 @@ | |
| import org.openmetadata.schema.entity.teams.User; | ||
| import org.openmetadata.schema.type.ApiStatus; | ||
| import org.openmetadata.schema.type.ChangeDescription; | ||
| import org.openmetadata.schema.type.EntityReference; | ||
| import org.openmetadata.schema.type.Include; | ||
| import org.openmetadata.schema.type.MetadataOperation; | ||
| import org.openmetadata.schema.type.Relationship; | ||
| import org.openmetadata.schema.type.TagLabel; | ||
| import org.openmetadata.schema.type.api.BulkOperationResult; | ||
| import org.openmetadata.schema.type.api.BulkResponse; | ||
|
|
@@ -58,6 +62,10 @@ | |
| import org.openmetadata.sdk.services.policies.PolicyService; | ||
| import org.openmetadata.sdk.services.teams.RoleService; | ||
| import org.openmetadata.sdk.services.teams.UserService; | ||
| import org.openmetadata.service.Entity; | ||
| import org.openmetadata.service.jdbi3.CollectionDAO; | ||
| import org.openmetadata.service.jdbi3.EntityDAO; | ||
| import org.openmetadata.service.jdbi3.EntityRepository; | ||
| import org.openmetadata.service.util.TestUtils; | ||
|
|
||
| /** | ||
|
|
@@ -1048,6 +1056,76 @@ void get_entityListWithInvalidPaginationCursors_4xx(TestNamespace ns) { | |
| "Listing with invalid before cursor should fail"); | ||
| } | ||
|
|
||
| /** | ||
| * Regression for the flaky {@code Entity not found: <service> <id>} failure on the LIST path (seen | ||
| * in CI on {@code ContainerResourceIT}/{@code MlModelResourceIT}). A list resolves every row's | ||
| * parent service in bulk, in a statement separate from the relationship lookup; when a sibling | ||
| * test cascade-hard-deletes the service in between, the {@code CONTAINS} relationship row is still | ||
| * seen but the service entity row is already gone. A non-lenient bulk resolver then threw {@code | ||
| * EntityNotFoundException} and failed the whole list instead of the single affected row. | ||
| * | ||
| * <p>Runs for every entity directly contained by a service this test created (skips entities with | ||
| * no owned service parent). Reproduces the window deterministically: delete only the service | ||
| * entity row (the {@code CONTAINS} relationship survives) and evict it from the entity cache — | ||
| * exactly the state a concurrent cascade delete leaves mid-flight — then list scoped by the | ||
| * now-dangling service FQN. The list must succeed; the bulk resolver must tolerate the deleted | ||
| * service rather than fail the whole page. | ||
| */ | ||
| @Test | ||
| void list_toleratesConcurrentlyHardDeletedService(TestNamespace ns) { | ||
| T entity = createEntity(createMinimalRequest(ns)); | ||
| EntityReference serviceRef = findOwnedServiceParent(ns, entity); | ||
| Assumptions.assumeTrue( | ||
| serviceRef != null, | ||
| getEntityType() + " is not directly contained by a service it owns; tolerance test N/A"); | ||
|
|
||
| String serviceType = serviceRef.getType(); | ||
| EntityDAO<?> serviceDao = Entity.getEntityRepository(serviceType).getDao(); | ||
| int rowsRemoved = serviceDao.delete(serviceDao.getTableName(), serviceRef.getId()); | ||
| assertEquals(1, rowsRemoved, "Setup: should have removed exactly the service entity row"); | ||
| EntityRepository.invalidateCacheForEntity( | ||
| serviceType, serviceRef.getId(), serviceRef.getFullyQualifiedName()); | ||
|
|
||
| org.openmetadata.sdk.models.ListParams params = new org.openmetadata.sdk.models.ListParams(); | ||
| params.setService(serviceRef.getFullyQualifiedName()); | ||
| params.setLimit(1000); | ||
|
|
||
| org.openmetadata.sdk.models.ListResponse<T> response = listEntities(params); | ||
| assertNotNull(response); | ||
| assertNotNull( | ||
| response.getData(), | ||
| "List must tolerate the concurrently hard-deleted service, not fail the whole page"); | ||
| } | ||
|
|
||
| /** | ||
| * Find this entity's immediate {@code CONTAINS} parent that is a service AND was created by this | ||
| * test (tracked as a namespace root) so deleting it can never disturb a shared fixture. Returns | ||
| * {@code null} when the entity is not directly service-scoped or its parent service is not owned | ||
| * here. | ||
| */ | ||
| private EntityReference findOwnedServiceParent(TestNamespace ns, T entity) { | ||
| EntityReference serviceRef = null; | ||
| List<CollectionDAO.EntityRelationshipRecord> parents = | ||
| Entity.getCollectionDAO() | ||
| .relationshipDAO() | ||
| .findFrom(entity.getId(), getEntityType(), Relationship.CONTAINS.ordinal()); | ||
| for (CollectionDAO.EntityRelationshipRecord parent : parents) { | ||
| boolean ownedByThisTest = | ||
| ns.trackedRoots().stream().anyMatch(root -> root.id().equals(parent.getId())); | ||
| if (ownedByThisTest && isServiceEntity(parent.getType(), parent.getId())) { | ||
| serviceRef = Entity.getEntityReferenceById(parent.getType(), parent.getId(), Include.ALL); | ||
| break; | ||
| } | ||
| } | ||
| return serviceRef; | ||
| } | ||
|
|
||
| private boolean isServiceEntity(String entityType, UUID id) { | ||
| EntityInterface candidate = | ||
| Entity.getEntity(new EntityReference().withId(id).withType(entityType), "", Include.ALL); | ||
| return candidate instanceof ServiceEntityInterface; | ||
| } | ||
|
Comment on lines
+1123
to
+1127
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
|
|
||
| @Test | ||
| void get_entityWithInvalidFields_4xx(TestNamespace ns) { | ||
| if (!supportsFieldsQueryParam) return; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
| import static org.openmetadata.service.Entity.FIELD_PARENT; | ||
| import static org.openmetadata.service.Entity.FIELD_TAGS; | ||
| import static org.openmetadata.service.Entity.STORAGE_SERVICE; | ||
| import static org.openmetadata.service.Entity.getEntityReferenceById; | ||
| import static org.openmetadata.service.Entity.getEntityReferenceByIdOrNull; | ||
| import static org.openmetadata.service.resources.tags.TagLabelUtil.addDerivedTagsGracefully; | ||
| import static org.openmetadata.service.resources.tags.TagLabelUtil.addDerivedTagsWithPreFetched; | ||
| import static org.openmetadata.service.resources.tags.TagLabelUtil.batchFetchDerivedTags; | ||
|
|
@@ -187,9 +187,11 @@ private Map<UUID, EntityReference> batchFetchParents(List<Container> containers) | |
| // Only consider container parents, not service parents | ||
| if (CONTAINER.equals(record.getFromEntity())) { | ||
| EntityReference parentRef = | ||
| getEntityReferenceById( | ||
| getEntityReferenceByIdOrNull( | ||
| record.getFromEntity(), UUID.fromString(record.getFromId()), NON_DELETED); | ||
| parentsMap.put(containerId, parentRef); | ||
| if (parentRef != null) { | ||
| parentsMap.put(containerId, parentRef); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -226,8 +228,10 @@ private Map<UUID, List<EntityReference>> batchFetchChildren(List<Container> cont | |
| for (CollectionDAO.EntityRelationshipObject record : records) { | ||
| UUID parentId = UUID.fromString(record.getFromId()); | ||
| EntityReference childRef = | ||
| getEntityReferenceById(CONTAINER, UUID.fromString(record.getToId()), NON_DELETED); | ||
| childrenMap.get(parentId).add(childRef); | ||
| getEntityReferenceByIdOrNull(CONTAINER, UUID.fromString(record.getToId()), NON_DELETED); | ||
| if (childRef != null) { | ||
| childrenMap.get(parentId).add(childRef); | ||
| } | ||
| } | ||
|
|
||
| return childrenMap; | ||
|
|
@@ -273,8 +277,10 @@ private Map<UUID, EntityReference> batchFetchServices(List<Container> containers | |
| UUID serviceId = UUID.fromString(record.getFromId()); | ||
| EntityReference serviceRef = | ||
| serviceRefById.computeIfAbsent( | ||
| serviceId, id -> getEntityReferenceById(STORAGE_SERVICE, id, NON_DELETED)); | ||
| serviceMap.put(containerId, serviceRef); | ||
| serviceId, id -> getEntityReferenceByIdOrNull(STORAGE_SERVICE, id, NON_DELETED)); | ||
| if (serviceRef != null) { | ||
| serviceMap.put(containerId, serviceRef); | ||
| } | ||
|
Comment on lines
278
to
+283
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When |
||
| } | ||
|
|
||
| return serviceMap; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Quality: Fully-qualified names used instead of imports in BaseEntityIT
In the new
list_toleratesConcurrentlyHardDeletedServicetest,ListParamsandListResponseare referenced via fully-qualified names rather than imports:The project's code standards explicitly forbid fully-qualified names ("No fully qualified names"). Add
import org.openmetadata.sdk.models.ListParams;andimport org.openmetadata.sdk.models.ListResponse;and use the simple names. (NoteListResponse/ListParamsmay already be imported elsewhere in this file givenlistEntitiesis used; verify to avoid duplicate-import.)Was this helpful? React with 👍 / 👎