This is an automated email from the ASF dual-hosted git repository.

chaitalicod pushed a commit to branch atlas-2.6
in repository https://gitbox.apache.org/repos/asf/atlas.git


The following commit(s) were added to refs/heads/atlas-2.6 by this push:
     new d523e6546 ATLAS-4766: java.lang.IllegalStateException from JanuGraph 
while purging entities in Atlas (#682) (cherrypicked from 
7caaabcf94910d5bf452785a39a091755334ebcf) Co-authored-by: Sheetal Shah 
<[email protected]>
d523e6546 is described below

commit d523e6546f407819426a2e2ed35212440f1d4e42
Author: sheetalshah1007 <[email protected]>
AuthorDate: Thu Jul 9 14:01:05 2026 +0530

    ATLAS-4766: java.lang.IllegalStateException from JanuGraph while purging 
entities in Atlas (#682)
    (cherrypicked from 7caaabcf94910d5bf452785a39a091755334ebcf)
    Co-authored-by: Sheetal Shah <[email protected]>
---
 .../repository/store/graph/v1/DeleteHandlerV1.java |  78 ++++-
 .../store/graph/v1/DeleteHandlerV1Test.java        | 315 +++++++++++++++++++++
 2 files changed, 383 insertions(+), 10 deletions(-)

diff --git 
a/repository/src/main/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1.java
 
b/repository/src/main/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1.java
index fd7b42f9f..5041d2874 100644
--- 
a/repository/src/main/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1.java
+++ 
b/repository/src/main/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1.java
@@ -223,8 +223,21 @@ public abstract class DeleteHandlerV1 {
     */
     public void deleteTraitsAndVertices(Collection<AtlasVertex> 
deletionCandidateVertices) throws AtlasBaseException {
         for (AtlasVertex deletionCandidateVertex : deletionCandidateVertices) {
-            deleteAllClassifications(deletionCandidateVertex);
-            deleteTypeVertex(deletionCandidateVertex, 
isInternalType(deletionCandidateVertex));
+            if (deletionCandidateVertex == null) {
+                continue;
+            }
+
+            if (!deletionCandidateVertex.exists()) {
+                LOG.warn("deleteTraitsAndVertices(): skipping vertex - already 
removed");
+                continue;
+            }
+
+            try {
+                deleteAllClassifications(deletionCandidateVertex);
+                deleteTypeVertex(deletionCandidateVertex, 
isInternalType(deletionCandidateVertex));
+            } catch (IllegalStateException e) {
+                LOG.warn("deleteTraitsAndVertices(): skipping vertex - already 
removed", e);
+            }
         }
     }
 
@@ -717,11 +730,15 @@ public abstract class DeleteHandlerV1 {
         boolean ret = false;
 
         if (edge != null) {
-            String outVertexType = getTypeName(edge.getOutVertex());
-            String inVertexType  = getTypeName(edge.getInVertex());
+            try {
+                String outVertexType = getTypeName(edge.getOutVertex());
+                String inVertexType  = getTypeName(edge.getInVertex());
 
-            ret = GraphHelper.isRelationshipEdge(edge) || 
edge.getPropertyKeys().contains(RELATIONSHIP_GUID_PROPERTY_KEY) ||
-                    (typeRegistry.getEntityTypeByName(outVertexType) != null 
&& typeRegistry.getEntityTypeByName(inVertexType) != null);
+                ret = GraphHelper.isRelationshipEdge(edge) || 
edge.getPropertyKeys().contains(RELATIONSHIP_GUID_PROPERTY_KEY) ||
+                        (typeRegistry.getEntityTypeByName(outVertexType) != 
null && typeRegistry.getEntityTypeByName(inVertexType) != null);
+            } catch (IllegalStateException e) {
+                LOG.warn("Skipping edge {} because one of its vertices was 
removed", edge.getIdForDisplay(), e);
+            }
         }
 
         return ret;
@@ -1255,6 +1272,24 @@ public abstract class DeleteHandlerV1 {
         final boolean             isPurgeRequested = 
RequestContext.get().isPurgeRequested();
 
         for (AtlasEdge edge : incomingEdges) {
+            if (!edge.exists()) {
+                LOG.debug("deleteVertex(): skipping edge {} - already removed 
in this transaction", edge.getIdForDisplay());
+                continue;
+            }
+
+            AtlasVertex outVertex;
+            try {
+                outVertex = edge.getOutVertex();
+            } catch (IllegalStateException e) {
+                LOG.warn("deleteVertex(): skipping edge {} - out-vertex was 
already removed", edge.getIdForDisplay(), e);
+                continue;
+            }
+
+            if (!outVertex.exists()) {
+                LOG.debug("deleteVertex(): skipping edge {} - out-vertex {} 
already removed in this transaction", edge.getIdForDisplay(), 
outVertex.getIdForDisplay());
+                continue;
+            }
+
             AtlasEntity.Status edgeStatus = getStatus(edge);
             boolean            isProceed  = edgeStatus == (isPurgeRequested ? 
DELETED : ACTIVE);
 
@@ -1262,10 +1297,20 @@ public abstract class DeleteHandlerV1 {
                 if (isRelationshipEdge(edge)) {
                     deleteRelationship(edge);
                 } else {
-                    AtlasVertex outVertex = edge.getOutVertex();
-
                     if (!isDeletedEntity(outVertex)) {
-                        AtlasVertex    inVertex  = edge.getInVertex();
+                        AtlasVertex inVertex;
+                        try {
+                            inVertex = edge.getInVertex();
+                        } catch (IllegalStateException e) {
+                            LOG.warn("deleteVertex(): skipping edge {} - 
in-vertex was already removed", edge.getIdForDisplay(), e);
+                            continue;
+                        }
+
+                        if (!inVertex.exists()) {
+                            LOG.debug("deleteVertex(): skipping edge {} - 
in-vertex {} already removed in this transaction", edge.getIdForDisplay(), 
inVertex.getIdForDisplay());
+                            continue;
+                        }
+
                         AtlasAttribute attribute = 
getAttributeForEdge(edge.getLabel());
 
                         deleteEdgeBetweenVertices(outVertex, inVertex, 
attribute);
@@ -1347,7 +1392,20 @@ public abstract class DeleteHandlerV1 {
      * @throws AtlasBaseException
      */
     private void deleteAllClassifications(AtlasVertex instanceVertex) throws 
AtlasBaseException {
-        List<AtlasEdge> classificationEdges = 
getAllClassificationEdges(instanceVertex);
+        if (instanceVertex == null || !instanceVertex.exists()) {
+            LOG.warn("deleteAllClassifications(): skipping vertex - already 
removed");
+            return;
+        }
+
+        List<AtlasEdge> classificationEdges;
+
+        try {
+            classificationEdges = getAllClassificationEdges(instanceVertex);
+        } catch (IllegalStateException e) {
+            LOG.warn("deleteAllClassifications(): skipping vertex - already 
removed", e);
+            return;
+        }
+
         boolean isImportInProgress = RequestContext.get().isImportInProgress();
 
         for (AtlasEdge edge : classificationEdges) {
diff --git 
a/repository/src/test/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1Test.java
 
b/repository/src/test/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1Test.java
new file mode 100644
index 000000000..cc3d5abad
--- /dev/null
+++ 
b/repository/src/test/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1Test.java
@@ -0,0 +1,315 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.atlas.repository.store.graph.v1;
+
+import org.apache.atlas.DeleteType;
+import org.apache.atlas.RequestContext;
+import org.apache.atlas.TestModules;
+import org.apache.atlas.TestUtilsV2;
+import org.apache.atlas.model.instance.AtlasEntity;
+import org.apache.atlas.model.instance.AtlasEntity.AtlasEntitiesWithExtInfo;
+import org.apache.atlas.model.instance.AtlasEntityHeader;
+import org.apache.atlas.model.instance.EntityMutationResponse;
+import org.apache.atlas.model.typedef.AtlasTypesDef;
+import org.apache.atlas.repository.AtlasTestBase;
+import org.apache.atlas.repository.graphdb.AtlasEdge;
+import org.apache.atlas.repository.graphdb.AtlasEdgeDirection;
+import org.apache.atlas.repository.graphdb.AtlasVertex;
+import org.apache.atlas.repository.store.graph.v2.AtlasEntityStoreV2;
+import org.apache.atlas.repository.store.graph.v2.AtlasEntityStream;
+import org.apache.atlas.repository.store.graph.v2.AtlasGraphUtilsV2;
+import org.apache.atlas.store.AtlasTypeDefStore;
+import org.apache.atlas.type.AtlasTypeRegistry;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import javax.inject.Inject;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.apache.atlas.TestRelationshipUtilsV2.DEPARTMENT_TYPE;
+import static org.apache.atlas.TestRelationshipUtilsV2.EMPLOYEE_TYPE;
+import static org.apache.atlas.TestRelationshipUtilsV2.MANAGER_TYPE;
+import static 
org.apache.atlas.TestRelationshipUtilsV2.getDepartmentEmployeeTypes;
+import static org.apache.atlas.TestUtilsV2.NAME;
+import static org.apache.atlas.type.AtlasTypeUtil.getAtlasObjectId;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+/**
+ * Tests for ATLAS-4766: resilience of DeleteHandlerV1 when graph elements
+ * are removed during iteration ({@link DeleteHandlerV1#isRelationshipEdge},
+ * {@link DeleteHandlerV1#deleteVertex}, {@link 
DeleteHandlerV1#deleteTraitsAndVertices}, and
+ * {@code deleteAllClassifications}).
+ */
+@Guice(modules = TestModules.TestOnlyModule.class)
+public class DeleteHandlerV1Test extends AtlasTestBase {
+    @Inject
+    private AtlasTypeRegistry typeRegistry;
+
+    @Inject
+    private AtlasTypeDefStore typeDefStore;
+
+    @Inject
+    private AtlasEntityStoreV2 entityStore;
+
+    @Inject
+    private DeleteHandlerDelegate deleteDelegate;
+
+    @BeforeClass
+    public void setUp() throws Exception {
+        RequestContext.clear();
+        RequestContext.get().setUser(TestUtilsV2.TEST_USER, null);
+
+        super.initialize();
+
+        AtlasTypesDef employeeTypes = getDepartmentEmployeeTypes();
+        typeDefStore.createTypesDef(employeeTypes);
+    }
+
+    @AfterClass
+    public void clear() throws Exception {
+        Thread.sleep(1000);
+        super.cleanup();
+    }
+
+    // ---------------------------------------------------------------
+    // isRelationshipEdge resilience
+    // ---------------------------------------------------------------
+
+    @Test
+    public void testIsRelationshipEdgeWithPurgedEndpoint() throws Exception {
+        EntityMutationResponse createResp = 
createManagerWithSubordinates("is_rel_edge", 1);
+        String                 subGuid    = getGuidForName(createResp, 
"is_rel_edge_sub1");
+        String                 mgrGuid    = getGuidForName(createResp, 
"is_rel_edge_mgr");
+
+        assertNotNull(subGuid);
+        assertNotNull(mgrGuid);
+
+        AtlasVertex subVertex   = AtlasGraphUtilsV2.findByGuid(subGuid);
+        AtlasEdge   managerEdge = pickRelationshipEdge(subVertex);
+
+        assertNotNull(subVertex);
+        assertNotNull(managerEdge, "Expected a relationship edge from 
subordinate to manager");
+
+        DeleteHandlerV1 handler = deleteDelegate.getHandler();
+        assertTrue(handler.isRelationshipEdge(managerEdge));
+
+        softDeleteGuids(Collections.singletonList(mgrGuid));
+        // Soft-deleted endpoint still exists in the graph; must not throw.
+        handler.isRelationshipEdge(managerEdge);
+
+        purgeGuids(Collections.singleton(mgrGuid));
+        // After hard purge the stale edge handle may still report true (label 
starts with
+        // "r:") when the surviving endpoint is readable. ATLAS-4766 only 
requires that
+        // removed vertices do not cause IllegalStateException to propagate.
+        assertDoesNotThrow(() -> handler.isRelationshipEdge(managerEdge));
+    }
+
+// ---------------------------------------------------------------
+    // deleteTraitsAndVertices / deleteAllClassifications resilience
+    // ---------------------------------------------------------------
+
+    /**
+     * After hard purge, a stale entity vertex handle must not cause
+     * {@code IllegalStateException} when {@link 
DeleteHandlerV1#deleteTraitsAndVertices}
+     * iterates the collection (e.g. classification edge lookup on a removed 
vertex).
+     */
+    @Test
+    public void testDeleteTraitsAndVerticesWithPurgedVertex() throws Exception 
{
+        EntityMutationResponse createResp = 
createManagerWithSubordinates("traits_purge", 1);
+        String                 mgrGuid    = getGuidForName(createResp, 
"traits_purge_mgr");
+        String                 subGuid    = getGuidForName(createResp, 
"traits_purge_sub1");
+
+        assertNotNull(mgrGuid);
+        assertNotNull(subGuid);
+
+        AtlasVertex mgrVertex = AtlasGraphUtilsV2.findByGuid(mgrGuid);
+        assertNotNull(mgrVertex);
+
+        softDeleteGuids(Arrays.asList(mgrGuid, subGuid));
+
+        EntityMutationResponse purgeResp = purgeGuids(new 
HashSet<>(Arrays.asList(mgrGuid, subGuid)));
+        assertEquals(purgeResp.getPurgedEntities().size(), 2);
+        assertNull(AtlasGraphUtilsV2.findByGuid(mgrGuid), "Entity should be 
removed from graph after purge");
+
+        DeleteHandlerV1 handler = deleteDelegate.getHandler();
+        handler.deleteTraitsAndVertices(Collections.singleton(mgrVertex));
+    }
+
+    // ---------------------------------------------------------------
+    // deleteVertex resilience during purge
+    // ---------------------------------------------------------------
+
+    /**
+     * Manager has incoming subordinate edges. Purge one subordinate first, 
then purge
+     * the manager. Remaining incoming edges from the already-purged 
subordinate must
+     * be skipped via {@code outVertex.exists()} without throwing.
+     */
+    @Test
+    public void testPurgeManagerAfterSubordinateAlreadyPurged() throws 
Exception {
+        EntityMutationResponse createResp = 
createManagerWithSubordinates("seq_purge", 2);
+        String                 mgrGuid    = getGuidForName(createResp, 
"seq_purge_mgr");
+        String                 sub1Guid   = getGuidForName(createResp, 
"seq_purge_sub1");
+        String                 sub2Guid   = getGuidForName(createResp, 
"seq_purge_sub2");
+
+        softDeleteGuids(Collections.singletonList(sub1Guid));
+        assertEntityPurged(sub1Guid, 
purgeGuids(Collections.singleton(sub1Guid)));
+
+        softDeleteGuids(Collections.singletonList(mgrGuid));
+        assertEntityPurged(mgrGuid, 
purgeGuids(Collections.singleton(mgrGuid)));
+
+        assertNotNull(AtlasGraphUtilsV2.findByGuid(sub2Guid),
+                "Other subordinate should remain until explicitly deleted");
+    }
+
+    /**
+     * Purge manager and multiple subordinates in a single transaction. While 
iterating
+     * incoming edges during {@code deleteVertex()}, edges whose out-vertex 
was already
+     * removed earlier in the same batch must be skipped safely.
+     */
+    @Test
+    public void testBatchPurgeManagerAndSubordinates() throws Exception {
+        EntityMutationResponse createResp = 
createManagerWithSubordinates("batch_purge", 2);
+        String                 mgrGuid    = getGuidForName(createResp, 
"batch_purge_mgr");
+        String                 sub1Guid   = getGuidForName(createResp, 
"batch_purge_sub1");
+        String                 sub2Guid   = getGuidForName(createResp, 
"batch_purge_sub2");
+
+        Set<String> guidsToPurge = new HashSet<>();
+        guidsToPurge.add(mgrGuid);
+        guidsToPurge.add(sub1Guid);
+        guidsToPurge.add(sub2Guid);
+
+        softDeleteGuids(guidsToPurge);
+
+        EntityMutationResponse purgeResp = purgeGuids(guidsToPurge);
+
+        assertEquals(purgeResp.getPurgedEntities().size(), 
guidsToPurge.size());
+        assertEntitiesPurged(guidsToPurge, purgeResp);
+    }
+
+    // ---------------------------------------------------------------
+    // Helpers
+    // ---------------------------------------------------------------
+
+    private EntityMutationResponse createManagerWithSubordinates(String 
prefix, int subordinateCount) throws Exception {
+        AtlasEntitiesWithExtInfo batch = new AtlasEntitiesWithExtInfo();
+
+        AtlasEntity dept = new AtlasEntity(DEPARTMENT_TYPE, "name", prefix + 
"_dept");
+
+        AtlasEntity manager = new AtlasEntity(MANAGER_TYPE);
+        manager.setAttribute(NAME, prefix + "_mgr");
+        manager.setRelationshipAttribute("department", getAtlasObjectId(dept));
+
+        batch.addEntity(dept);
+        batch.addEntity(manager);
+
+        for (int i = 1; i <= subordinateCount; i++) {
+            AtlasEntity subordinate = new AtlasEntity(EMPLOYEE_TYPE);
+            subordinate.setAttribute(NAME, prefix + "_sub" + i);
+            subordinate.setRelationshipAttribute("department", 
getAtlasObjectId(dept));
+            subordinate.setRelationshipAttribute("manager", 
getAtlasObjectId(manager));
+            batch.addEntity(subordinate);
+        }
+
+        EntityMutationResponse response = entityStore.createOrUpdate(new 
AtlasEntityStream(batch), false);
+        assertNotNull(response);
+        assertTrue(response.getCreatedEntities().size() >= subordinateCount + 
2);
+        return response;
+    }
+
+    private String getGuidForName(EntityMutationResponse response, String 
name) {
+        for (AtlasEntityHeader header : response.getCreatedEntities()) {
+            if (name.equals(header.getAttribute(NAME))) {
+                return header.getGuid();
+            }
+        }
+        return null;
+    }
+
+    private AtlasEdge pickRelationshipEdge(AtlasVertex vertex) {
+        Iterator<AtlasEdge> edges = 
vertex.getEdges(AtlasEdgeDirection.BOTH).iterator();
+        while (edges.hasNext()) {
+            AtlasEdge edge = edges.next();
+            if (!edge.getLabel().startsWith("__")) {
+                return edge;
+            }
+        }
+        return null;
+    }
+
+    private void initRequestContext() {
+        RequestContext.clear();
+        RequestContext.get().setUser(TestUtilsV2.TEST_USER, null);
+    }
+
+    private void softDeleteGuids(Collection<String> guids) throws Exception {
+        initRequestContext();
+        RequestContext.get().setDeleteType(DeleteType.SOFT);
+        entityStore.deleteByIds(new ArrayList<>(guids));
+    }
+
+    private EntityMutationResponse purgeGuids(Set<String> guids) throws 
Exception {
+        initRequestContext();
+        RequestContext.get().setDeleteType(DeleteType.HARD);
+        RequestContext.get().setPurgeRequested(true);
+        return entityStore.purgeByIds(guids);
+    }
+
+    private void assertDoesNotThrow(Runnable runnable) {
+        try {
+            runnable.run();
+        } catch (Exception e) {
+            throw new AssertionError("Expected no exception but got: " + 
e.getMessage(), e);
+        }
+    }
+
+    private void assertEntityPurged(String guid, EntityMutationResponse 
purgeResp) {
+        assertNotNull(purgeResp);
+        assertNotNull(purgeResp.getPurgedEntities());
+        assertTrue(purgeResp.getPurgedEntities().stream().anyMatch(h -> 
guid.equals(h.getGuid())),
+                "Expected guid " + guid + " in purged entities");
+        assertNull(AtlasGraphUtilsV2.findByGuid(guid), "Entity should be 
removed from graph after purge");
+    }
+
+    private void assertEntitiesPurged(Set<String> expectedGuids, 
EntityMutationResponse purgeResp) {
+        assertNotNull(purgeResp);
+        assertNotNull(purgeResp.getPurgedEntities());
+
+        Set<String> purgedGuids = purgeResp.getPurgedEntities().stream()
+                .map(AtlasEntityHeader::getGuid)
+                .collect(Collectors.toSet());
+
+        assertEquals(purgedGuids, expectedGuids);
+
+        for (String guid : expectedGuids) {
+            assertNull(AtlasGraphUtilsV2.findByGuid(guid), "Entity " + guid + 
" should be removed from graph after purge");
+        }
+    }
+}

Reply via email to