Skip to content

Commit 5a68da8

Browse files
ATLAS-4766: java.lang.IllegalStateException from JanuGraph while purging entities at Atlas
1 parent 5e90fee commit 5a68da8

2 files changed

Lines changed: 322 additions & 18 deletions

File tree

repository/src/main/java/org/apache/atlas/repository/store/graph/v1/DeleteHandlerV1.java

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -717,11 +717,15 @@ public boolean isRelationshipEdge(AtlasEdge edge) {
717717
boolean ret = false;
718718

719719
if (edge != null) {
720-
String outVertexType = getTypeName(edge.getOutVertex());
721-
String inVertexType = getTypeName(edge.getInVertex());
720+
try {
721+
String outVertexType = getTypeName(edge.getOutVertex());
722+
String inVertexType = getTypeName(edge.getInVertex());
722723

723-
ret = GraphHelper.isRelationshipEdge(edge) || edge.getPropertyKeys().contains(RELATIONSHIP_GUID_PROPERTY_KEY) ||
724-
(typeRegistry.getEntityTypeByName(outVertexType) != null && typeRegistry.getEntityTypeByName(inVertexType) != null);
724+
ret = GraphHelper.isRelationshipEdge(edge) || edge.getPropertyKeys().contains(RELATIONSHIP_GUID_PROPERTY_KEY) ||
725+
(typeRegistry.getEntityTypeByName(outVertexType) != null && typeRegistry.getEntityTypeByName(inVertexType) != null);
726+
} catch (IllegalStateException e) {
727+
LOG.warn("Skipping edge {} because one of its vertices was removed", edge.getIdForDisplay(), e);
728+
}
725729
}
726730

727731
return ret;
@@ -1246,31 +1250,42 @@ protected void deleteEdgeBetweenVertices(AtlasVertex outVertex, AtlasVertex inVe
12461250
}
12471251

12481252
protected void deleteVertex(AtlasVertex instanceVertex, boolean force) throws AtlasBaseException {
1249-
if (LOG.isDebugEnabled()) {
1250-
LOG.debug("Setting the external references to {} to null(removing edges)", string(instanceVertex));
1251-
}
1253+
LOG.debug("Setting the external references to {} to null(removing edges)", string(instanceVertex));
12521254

12531255
// Delete external references to this vertex - incoming edges from lineage or glossary term edges
12541256
final Iterable<AtlasEdge> incomingEdges = instanceVertex.getEdges(AtlasEdgeDirection.IN);
12551257
final boolean isPurgeRequested = RequestContext.get().isPurgeRequested();
12561258

12571259
for (AtlasEdge edge : incomingEdges) {
1258-
AtlasEntity.Status edgeStatus = getStatus(edge);
1259-
boolean isProceed = edgeStatus == (isPurgeRequested ? DELETED : ACTIVE);
1260+
try {
1261+
if (!edge.exists()) {
1262+
LOG.debug("deleteVertex(): skipping edge {} - already removed in this transaction", edge.getIdForDisplay());
1263+
continue;
1264+
}
12601265

1261-
if (isProceed) {
1262-
if (isRelationshipEdge(edge)) {
1263-
deleteRelationship(edge);
1264-
} else {
1265-
AtlasVertex outVertex = edge.getOutVertex();
1266+
AtlasVertex outVertex = edge.getOutVertex();
1267+
if (!outVertex.exists()) {
1268+
LOG.debug("deleteVertex(): skipping edge {} - out-vertex {} already removed in this transaction", edge.getIdForDisplay(), outVertex.getIdForDisplay());
1269+
continue;
1270+
}
12661271

1267-
if (!isDeletedEntity(outVertex)) {
1268-
AtlasVertex inVertex = edge.getInVertex();
1269-
AtlasAttribute attribute = getAttributeForEdge(edge.getLabel());
1272+
AtlasEntity.Status edgeStatus = getStatus(edge);
1273+
boolean isProceed = edgeStatus == (isPurgeRequested ? DELETED : ACTIVE);
12701274

1271-
deleteEdgeBetweenVertices(outVertex, inVertex, attribute);
1275+
if (isProceed) {
1276+
if (isRelationshipEdge(edge)) {
1277+
deleteRelationship(edge);
1278+
} else {
1279+
if (!isDeletedEntity(outVertex)) {
1280+
AtlasVertex inVertex = edge.getInVertex();
1281+
AtlasAttribute attribute = getAttributeForEdge(edge.getLabel());
1282+
1283+
deleteEdgeBetweenVertices(outVertex, inVertex, attribute);
1284+
}
12721285
}
12731286
}
1287+
} catch (IllegalStateException e) {
1288+
LOG.warn("deleteVertex(): graph element referenced by edge was already removed; skipping", e);
12741289
}
12751290
}
12761291

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
* <p>
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* <p>
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.atlas.repository.store.graph.v1;
19+
20+
import org.apache.atlas.DeleteType;
21+
import org.apache.atlas.RequestContext;
22+
import org.apache.atlas.TestModules;
23+
import org.apache.atlas.TestUtilsV2;
24+
import org.apache.atlas.model.instance.AtlasEntity;
25+
import org.apache.atlas.model.instance.AtlasEntity.AtlasEntitiesWithExtInfo;
26+
import org.apache.atlas.model.instance.AtlasEntityHeader;
27+
import org.apache.atlas.model.instance.EntityMutationResponse;
28+
import org.apache.atlas.model.typedef.AtlasTypesDef;
29+
import org.apache.atlas.repository.AtlasTestBase;
30+
import org.apache.atlas.repository.graphdb.AtlasEdge;
31+
import org.apache.atlas.repository.graphdb.AtlasEdgeDirection;
32+
import org.apache.atlas.repository.graphdb.AtlasVertex;
33+
import org.apache.atlas.repository.store.graph.v2.AtlasEntityStoreV2;
34+
import org.apache.atlas.repository.store.graph.v2.AtlasEntityStream;
35+
import org.apache.atlas.repository.store.graph.v2.AtlasGraphUtilsV2;
36+
import org.apache.atlas.store.AtlasTypeDefStore;
37+
import org.apache.atlas.type.AtlasTypeRegistry;
38+
import org.testng.annotations.AfterClass;
39+
import org.testng.annotations.BeforeClass;
40+
import org.testng.annotations.Guice;
41+
import org.testng.annotations.Test;
42+
43+
import javax.inject.Inject;
44+
import java.util.ArrayList;
45+
import java.util.Collection;
46+
import java.util.Collections;
47+
import java.util.HashSet;
48+
import java.util.Iterator;
49+
import java.util.Set;
50+
import java.util.stream.Collectors;
51+
52+
import static org.apache.atlas.TestRelationshipUtilsV2.DEPARTMENT_TYPE;
53+
import static org.apache.atlas.TestRelationshipUtilsV2.EMPLOYEE_TYPE;
54+
import static org.apache.atlas.TestRelationshipUtilsV2.MANAGER_TYPE;
55+
import static org.apache.atlas.TestRelationshipUtilsV2.getDepartmentEmployeeTypes;
56+
import static org.apache.atlas.TestUtilsV2.NAME;
57+
import static org.apache.atlas.type.AtlasTypeUtil.getAtlasObjectId;
58+
import static org.testng.Assert.assertEquals;
59+
import static org.testng.Assert.assertFalse;
60+
import static org.testng.Assert.assertNotNull;
61+
import static org.testng.Assert.assertNull;
62+
import static org.testng.Assert.assertTrue;
63+
64+
/**
65+
* Tests for ATLAS-4766: resilience of DeleteHandlerV1 when graph elements
66+
* are removed during iteration ({@link DeleteHandlerV1#isRelationshipEdge} and
67+
* {@link DeleteHandlerV1#deleteVertex}).
68+
*/
69+
@Guice(modules = TestModules.TestOnlyModule.class)
70+
public class DeleteHandlerV1Test extends AtlasTestBase {
71+
72+
@Inject
73+
private AtlasTypeRegistry typeRegistry;
74+
75+
@Inject
76+
private AtlasTypeDefStore typeDefStore;
77+
78+
@Inject
79+
private AtlasEntityStoreV2 entityStore;
80+
81+
@Inject
82+
private DeleteHandlerDelegate deleteDelegate;
83+
84+
@BeforeClass
85+
public void setUp() throws Exception {
86+
RequestContext.clear();
87+
RequestContext.get().setUser(TestUtilsV2.TEST_USER, null);
88+
89+
super.initialize();
90+
91+
AtlasTypesDef employeeTypes = getDepartmentEmployeeTypes();
92+
typeDefStore.createTypesDef(employeeTypes);
93+
}
94+
95+
@AfterClass
96+
public void clear() throws Exception {
97+
Thread.sleep(1000);
98+
super.cleanup();
99+
}
100+
101+
// ---------------------------------------------------------------
102+
// isRelationshipEdge resilience
103+
// ---------------------------------------------------------------
104+
105+
@Test
106+
public void testIsRelationshipEdgeWithPurgedEndpoint() throws Exception {
107+
EntityMutationResponse createResp = createManagerWithSubordinates("is_rel_edge", 1);
108+
String subGuid = getGuidForName(createResp, "is_rel_edge_sub1");
109+
String mgrGuid = getGuidForName(createResp, "is_rel_edge_mgr");
110+
111+
assertNotNull(subGuid);
112+
assertNotNull(mgrGuid);
113+
114+
AtlasVertex subVertex = AtlasGraphUtilsV2.findByGuid(subGuid);
115+
AtlasEdge managerEdge = pickRelationshipEdge(subVertex);
116+
117+
assertNotNull(subVertex);
118+
assertNotNull(managerEdge, "Expected a relationship edge from subordinate to manager");
119+
120+
DeleteHandlerV1 handler = deleteDelegate.getHandler();
121+
assertTrue(handler.isRelationshipEdge(managerEdge));
122+
123+
softDeleteGuids(Collections.singletonList(mgrGuid));
124+
// Soft-deleted endpoint still exists in the graph; must not throw.
125+
handler.isRelationshipEdge(managerEdge);
126+
127+
purgeGuids(Collections.singleton(mgrGuid));
128+
// After hard purge the stale edge handle may still report true (label starts with
129+
// "r:") when the surviving endpoint is readable. ATLAS-4766 only requires that
130+
// removed vertices do not cause IllegalStateException to propagate.
131+
assertDoesNotThrow(() -> handler.isRelationshipEdge(managerEdge));
132+
}
133+
134+
@Test
135+
public void testIsRelationshipEdgeWithNullEdge() {
136+
DeleteHandlerV1 handler = deleteDelegate.getHandler();
137+
assertFalse(handler.isRelationshipEdge(null));
138+
}
139+
140+
// ---------------------------------------------------------------
141+
// deleteVertex resilience during purge
142+
// ---------------------------------------------------------------
143+
144+
/**
145+
* Manager has incoming subordinate edges. Purge one subordinate first, then purge
146+
* the manager. Remaining incoming edges from the already-purged subordinate must
147+
* be skipped via {@code outVertex.exists()} without throwing.
148+
*/
149+
@Test
150+
public void testPurgeManagerAfterSubordinateAlreadyPurged() throws Exception {
151+
EntityMutationResponse createResp = createManagerWithSubordinates("seq_purge", 2);
152+
String mgrGuid = getGuidForName(createResp, "seq_purge_mgr");
153+
String sub1Guid = getGuidForName(createResp, "seq_purge_sub1");
154+
String sub2Guid = getGuidForName(createResp, "seq_purge_sub2");
155+
156+
softDeleteGuids(Collections.singletonList(sub1Guid));
157+
assertEntityPurged(sub1Guid, purgeGuids(Collections.singleton(sub1Guid)));
158+
159+
softDeleteGuids(Collections.singletonList(mgrGuid));
160+
assertEntityPurged(mgrGuid, purgeGuids(Collections.singleton(mgrGuid)));
161+
162+
assertNotNull(AtlasGraphUtilsV2.findByGuid(sub2Guid),
163+
"Other subordinate should remain until explicitly deleted");
164+
}
165+
166+
/**
167+
* Purge manager and multiple subordinates in a single transaction. While iterating
168+
* incoming edges during {@code deleteVertex()}, edges whose out-vertex was already
169+
* removed earlier in the same batch must be skipped safely.
170+
*/
171+
@Test
172+
public void testBatchPurgeManagerAndSubordinates() throws Exception {
173+
EntityMutationResponse createResp = createManagerWithSubordinates("batch_purge", 2);
174+
String mgrGuid = getGuidForName(createResp, "batch_purge_mgr");
175+
String sub1Guid = getGuidForName(createResp, "batch_purge_sub1");
176+
String sub2Guid = getGuidForName(createResp, "batch_purge_sub2");
177+
178+
Set<String> guidsToPurge = new HashSet<>();
179+
guidsToPurge.add(mgrGuid);
180+
guidsToPurge.add(sub1Guid);
181+
guidsToPurge.add(sub2Guid);
182+
183+
softDeleteGuids(guidsToPurge);
184+
185+
EntityMutationResponse purgeResp = purgeGuids(guidsToPurge);
186+
187+
assertEquals(purgeResp.getPurgedEntities().size(), guidsToPurge.size());
188+
assertEntitiesPurged(guidsToPurge, purgeResp);
189+
}
190+
191+
// ---------------------------------------------------------------
192+
// Helpers
193+
// ---------------------------------------------------------------
194+
195+
private EntityMutationResponse createManagerWithSubordinates(String prefix, int subordinateCount) throws Exception {
196+
AtlasEntitiesWithExtInfo batch = new AtlasEntitiesWithExtInfo();
197+
198+
AtlasEntity dept = new AtlasEntity(DEPARTMENT_TYPE, "name", prefix + "_dept");
199+
200+
AtlasEntity manager = new AtlasEntity(MANAGER_TYPE);
201+
manager.setAttribute(NAME, prefix + "_mgr");
202+
manager.setRelationshipAttribute("department", getAtlasObjectId(dept));
203+
204+
batch.addEntity(dept);
205+
batch.addEntity(manager);
206+
207+
for (int i = 1; i <= subordinateCount; i++) {
208+
AtlasEntity subordinate = new AtlasEntity(EMPLOYEE_TYPE);
209+
subordinate.setAttribute(NAME, prefix + "_sub" + i);
210+
subordinate.setRelationshipAttribute("department", getAtlasObjectId(dept));
211+
subordinate.setRelationshipAttribute("manager", getAtlasObjectId(manager));
212+
batch.addEntity(subordinate);
213+
}
214+
215+
EntityMutationResponse response = entityStore.createOrUpdate(new AtlasEntityStream(batch), false);
216+
assertNotNull(response);
217+
assertTrue(response.getCreatedEntities().size() >= subordinateCount + 2);
218+
return response;
219+
}
220+
221+
private String getGuidForName(EntityMutationResponse response, String name) {
222+
for (AtlasEntityHeader header : response.getCreatedEntities()) {
223+
if (name.equals(header.getAttribute(NAME))) {
224+
return header.getGuid();
225+
}
226+
}
227+
return null;
228+
}
229+
230+
private AtlasEdge pickRelationshipEdge(AtlasVertex vertex) {
231+
Iterator<AtlasEdge> edges = vertex.getEdges(AtlasEdgeDirection.BOTH).iterator();
232+
while (edges.hasNext()) {
233+
AtlasEdge edge = edges.next();
234+
if (!edge.getLabel().startsWith("__")) {
235+
return edge;
236+
}
237+
}
238+
return null;
239+
}
240+
241+
private void initRequestContext() {
242+
RequestContext.clear();
243+
RequestContext.get().setUser(TestUtilsV2.TEST_USER, null);
244+
}
245+
246+
private void softDeleteGuids(Collection<String> guids) throws Exception {
247+
initRequestContext();
248+
RequestContext.get().setDeleteType(DeleteType.SOFT);
249+
entityStore.deleteByIds(new ArrayList<>(guids));
250+
}
251+
252+
private EntityMutationResponse purgeGuids(Set<String> guids) throws Exception {
253+
initRequestContext();
254+
RequestContext.get().setDeleteType(DeleteType.HARD);
255+
RequestContext.get().setPurgeRequested(true);
256+
return entityStore.purgeByIds(guids);
257+
}
258+
259+
private void assertDoesNotThrow(Runnable runnable) {
260+
try {
261+
runnable.run();
262+
} catch (Exception e) {
263+
throw new AssertionError("Expected no exception but got: " + e.getMessage(), e);
264+
}
265+
}
266+
267+
private void assertEntityPurged(String guid, EntityMutationResponse purgeResp) {
268+
assertNotNull(purgeResp);
269+
assertNotNull(purgeResp.getPurgedEntities());
270+
assertTrue(purgeResp.getPurgedEntities().stream().anyMatch(h -> guid.equals(h.getGuid())),
271+
"Expected guid " + guid + " in purged entities");
272+
assertNull(AtlasGraphUtilsV2.findByGuid(guid), "Entity should be removed from graph after purge");
273+
}
274+
275+
private void assertEntitiesPurged(Set<String> expectedGuids, EntityMutationResponse purgeResp) {
276+
assertNotNull(purgeResp);
277+
assertNotNull(purgeResp.getPurgedEntities());
278+
279+
Set<String> purgedGuids = purgeResp.getPurgedEntities().stream()
280+
.map(AtlasEntityHeader::getGuid)
281+
.collect(Collectors.toSet());
282+
283+
assertEquals(purgedGuids, expectedGuids);
284+
285+
for (String guid : expectedGuids) {
286+
assertNull(AtlasGraphUtilsV2.findByGuid(guid), "Entity " + guid + " should be removed from graph after purge");
287+
}
288+
}
289+
}

0 commit comments

Comments
 (0)