From 99c46afff8fde89ef52d0104358b3b9f3df18a2e Mon Sep 17 00:00:00 2001 From: Himanshu Verma Date: Sat, 29 Aug 2026 21:27:43 +0530 Subject: [PATCH] perf(core): optimize queryVerticesByIds and queryEdgesByIds for one id Querying a vertex or an edge by a single id is the hot path behind graph.vertex(id), graph.vertices(id), graph.edge(id) and the adjacent vertex loading in HugeVertex.ensureFilledProperties(). It went through the general multi-id code and allocated an id list, a HashMap and a MapperIterator on every call. Add a dedicated single-id path for vertices and edges that keeps the semantics of the multi-id path (removed, updated, expired and missing records, null and invalid ids, IN direction edge ids, undefined adjacent vertices, NotFoundException when the record must exist) but only does the map lookups and, on a miss, one OneIdQuery against the backend. The multi-id path is unchanged. The "vertex not found" handling (NotFoundException, undefined adjacent vertex, or skip) is shared by both paths. Follow-up to #2859, original work by Jermy Li (javeme). Co-authored-by: Jermy Li --- .../backend/tx/GraphTransaction.java | 392 ++++++++---------- .../apache/hugegraph/core/EdgeCoreTest.java | 216 ++++++++++ .../apache/hugegraph/core/VertexCoreTest.java | 171 ++++++++ 3 files changed, 561 insertions(+), 218 deletions(-) diff --git a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java index 5150430975..52656fd2ab 100644 --- a/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java +++ b/hugegraph-server/hugegraph-core/src/main/java/org/apache/hugegraph/backend/tx/GraphTransaction.java @@ -102,7 +102,6 @@ import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; import jakarta.ws.rs.ForbiddenException; @@ -308,8 +307,8 @@ protected final boolean removingEdgeOwner(HugeEdge edge) { return false; } + @Watched(prefix = "tx") @Override - @Watched(prefix = "graph") protected BackendMutation prepareCommit() { // Serialize and add updates into super.deletions if (!this.removedVertices.isEmpty() || !this.removedEdges.isEmpty()) { @@ -515,7 +514,6 @@ private void commitPartOfEdgeDeletions(Map removedEdges) { } @Override - @Watched(prefix = "graph") public void commit() throws BackendException { try { super.commit(); @@ -525,7 +523,6 @@ public void commit() throws BackendException { } @Override - @Watched(prefix = "graph") public void rollback() throws BackendException { // Rollback properties changes for (HugeProperty prop : this.updatedOldestProps) { @@ -539,7 +536,6 @@ public void rollback() throws BackendException { } @Override - @Watched(prefix = "graph") public QueryResults query(Query query) { if (!(query instanceof ConditionQuery)) { // It's a sysprop-query, don't need to optimize @@ -554,7 +550,6 @@ public QueryResults query(Query query) { } @Override - @Watched(prefix = "graph") public Number queryNumber(Query query) { boolean isConditionQuery = query instanceof ConditionQuery; boolean hasUpdate = this.hasUpdate(); @@ -714,7 +709,6 @@ public void removeVertex(HugeVertex vertex) { this.afterWrite(); } - @Watched(prefix = "graph") public Iterator queryAdjacentVertices(Iterator edges) { if (this.lazyLoadAdjacentVertex) { return new MapperIterator<>(edges, edge -> { @@ -732,18 +726,15 @@ public Iterator queryAdjacentVertices(Iterator edges) { }); } - @Watched(prefix = "graph") public Iterator queryAdjacentVertices(Object... vertexIds) { return this.queryVerticesByIds(vertexIds, true, this.checkAdjacentVertexExist); } - @Watched(prefix = "graph") public Iterator queryVertices(Object... vertexIds) { return this.queryVerticesByIds(vertexIds, false, false); } - @Watched(prefix = "graph") public Vertex queryVertex(Object vertexId) { Iterator iter = this.queryVerticesByIds(new Object[]{vertexId}, false, true); @@ -784,131 +775,117 @@ protected Iterator queryVerticesByIds(Object[] vertexIds, boolean adjace return this.queryVerticesByIds(vertexIds, adjacentVertex, checkMustExist, HugeType.VERTEX); } - @Watched(prefix = "graph") protected Iterator queryVerticesByIds(Object[] vertexIds, boolean adjacentVertex, boolean checkMustExist, HugeType type) { Query.checkForceCapacity(vertexIds.length); - List ids; - Map vertices; - boolean verticesUpdated = this.verticesInTxSize() > 0; - if (vertexIds.length == 1) { - Id id = HugeVertex.getIdValue(vertexIds[0]); + // Fast path: skip the id list, map and mapper iterator for one id + return this.queryVertexById(vertexIds[0], adjacentVertex, + checkMustExist, type); + } - boolean tryQueryBackend = true; - if (id == null) { - tryQueryBackend = false; - ids = ImmutableList.of(); - } else { - ids = ImmutableList.of(id); - } - - HugeVertex vertex = null; - if (id != null && verticesUpdated) { - if (this.removedVertices.containsKey(id)) { - // The record has been deleted - tryQueryBackend = false; - } else if ((vertex = this.addedVertices.get(id)) != null || - (vertex = this.updatedVertices.get(id)) != null) { - // Found from local tx - tryQueryBackend = false; - if (vertex.expired()) { - vertex = null; - } else { - assert vertex != null; - } - } - } + // NOTE: allowed duplicated vertices if query by duplicated ids + List ids = InsertionOrderUtil.newList(); + Map vertices = new HashMap<>(vertexIds.length); - if (vertex != null) { - assert !tryQueryBackend; - vertices = ImmutableMap.of(vertex.id(), vertex); - } else if (!tryQueryBackend) { - assert vertex == null; - vertices = ImmutableMap.of(); - } else { - // Query from backend store - IdQuery query = new IdQuery.OneIdQuery(type, id); - Iterator it = this.queryVerticesFromBackend(query); - vertex = QueryResults.one(it); - if (vertex == null) { - vertices = ImmutableMap.of(); - } else { - vertices = ImmutableMap.of(vertex.id(), vertex); - } - } - } else { - // NOTE: allowed duplicated vertices if query by duplicated ids - ids = InsertionOrderUtil.newList(); - vertices = new HashMap<>(vertexIds.length); - - IdQuery query = new IdQuery(type); - for (Object vertexId : vertexIds) { - Id id = HugeVertex.getIdValue(vertexId); - if (id == null) { + IdQuery query = new IdQuery(type); + for (Object vertexId : vertexIds) { + HugeVertex vertex; + Id id = HugeVertex.getIdValue(vertexId); + if (id == null || this.removedVertices.containsKey(id)) { + // The record has been deleted + continue; + } else if ((vertex = this.addedVertices.get(id)) != null || + (vertex = this.updatedVertices.get(id)) != null) { + if (vertex.expired()) { continue; } - boolean foundLocal = false; - if (verticesUpdated) { - HugeVertex vertex; - if (this.removedVertices.containsKey(id)) { - // The record has been deleted - continue; - } - if ((vertex = this.addedVertices.get(id)) != null || - (vertex = this.updatedVertices.get(id)) != null) { - if (vertex.expired()) { - continue; - } - // Found from local tx - foundLocal = true; - vertices.put(vertex.id(), vertex); - } else { - assert !foundLocal; - } - } - if (!foundLocal) { - // Prepare to query from backend store - query.query(id); - } - ids.add(id); + // Found from local tx + vertices.put(vertex.id(), vertex); + } else { + // Prepare to query from backend store + query.query(id); } + ids.add(id); + } - if (!query.empty()) { - // Query from backend store - query.mustSortByInput(false); - Iterator it = this.queryVerticesFromBackend(query); - QueryResults.fillMap(it, vertices); - } + if (!query.empty()) { + // Query from backend store + query.mustSortByInput(false); + Iterator it = this.queryVerticesFromBackend(query); + QueryResults.fillMap(it, vertices); } return new MapperIterator<>(ids.iterator(), id -> { - HugeVertex vertex = vertices.get(id); + return this.resolveVertex(vertices.get(id), id, + adjacentVertex, checkMustExist); + }); + } + + /** + * Query a single vertex by id, with the same semantics as the multi-id + * path of {@link #queryVerticesByIds(Object[], boolean, boolean, HugeType)} + * but without allocating the id list, the result map and the mapper + * iterator; only an {@link IdQuery.OneIdQuery} is created on a miss. + */ + private Iterator queryVertexById(Object vertexId, boolean adjacentVertex, + boolean checkMustExist, HugeType type) { + Id id = HugeVertex.getIdValue(vertexId); + if (id == null) { + return QueryResults.emptyIterator(); + } + + HugeVertex vertex = null; + if (this.verticesInTxSize() > 0) { + if (this.removedVertices.containsKey(id)) { + // The record has been deleted + return QueryResults.emptyIterator(); + } + vertex = this.addedVertices.get(id); if (vertex == null) { - if (checkMustExist) { - throw new NotFoundException( - "Vertex '%s' does not exist", id); - } else if (adjacentVertex) { - assert !checkMustExist; - // Return undefined if adjacentVertex but !checkMustExist - vertex = HugeVertex.undefined(this.graph(), id); - } else { - // Return null - assert vertex == null; - } + vertex = this.updatedVertices.get(id); + } + if (vertex != null && vertex.expired()) { + // Found from local tx but expired + return QueryResults.emptyIterator(); } + } + + if (vertex == null) { + // Query from backend store + IdQuery query = new IdQuery.OneIdQuery(type, id); + vertex = QueryResults.one(this.queryVerticesFromBackend(query)); + } + + vertex = this.resolveVertex(vertex, id, adjacentVertex, checkMustExist); + if (vertex == null) { + return QueryResults.emptyIterator(); + } + return QueryResults.iterator(vertex); + } + + private HugeVertex resolveVertex(HugeVertex vertex, Id id, + boolean adjacentVertex, boolean checkMustExist) { + if (vertex != null) { return vertex; - }); + } + if (checkMustExist) { + throw new NotFoundException("Vertex '%s' does not exist", id); + } + if (adjacentVertex) { + // Return undefined if adjacentVertex but !checkMustExist + return HugeVertex.undefined(this.graph(), id); + } + // Return null to skip the vertex + return null; } - @Watched(prefix = "graph") public Iterator queryVertices() { Query q = new Query(HugeType.VERTEX); return this.queryVertices(q); } - @Watched(prefix = "graph") public Iterator queryVertices(Query query) { if (this.hasUpdate()) { E.checkArgument(query.noLimitAndOffset(), @@ -930,7 +907,6 @@ public Iterator queryVertices(Query query) { return this.skipOffsetOrStopLimit(r, query); } - @Watched(prefix = "graph") protected Iterator queryVerticesFromBackend(Query query) { assert query.resultType().isVertex(); @@ -992,17 +968,14 @@ public void removeEdge(HugeEdge edge) { this.afterWrite(); } - @Watched(prefix = "graph") public Iterator queryEdgesByVertex(Id id) { return this.queryEdges(constructEdgesQuery(id, Directions.BOTH, new Id[0])); } - @Watched(prefix = "graph") public Iterator queryEdges(Object... edgeIds) { return this.queryEdgesByIds(edgeIds, false); } - @Watched(prefix = "graph") public Edge queryEdge(Object edgeId) { Iterator iter = this.queryEdgesByIds(new Object[]{edgeId}, true); Edge edge = QueryResults.one(iter); @@ -1012,121 +985,62 @@ public Edge queryEdge(Object edgeId) { return edge; } - @Watched(prefix = "graph") protected Iterator queryEdgesByIds(Object[] edgeIds, boolean verifyId) { Query.checkForceCapacity(edgeIds.length); - List ids; - Map edges; - boolean edgesUpdated = this.edgesInTxSize() > 0; - if (edgeIds.length == 1) { - EdgeId id = HugeEdge.getIdValue(edgeIds[0], !verifyId); + // Fast path: skip the id list, map and mapper iterator for one id + return this.queryEdgeById(edgeIds[0], verifyId); + } + + // NOTE: allowed duplicated edges if query by duplicated ids + List ids = InsertionOrderUtil.newList(); + Map edges = new HashMap<>(edgeIds.length); - boolean tryQueryBackend = true; + IdQuery query = new IdQuery(HugeType.EDGE); + for (Object edgeId : edgeIds) { + HugeEdge edge; + EdgeId id = HugeEdge.getIdValue(edgeId, !verifyId); if (id == null) { - tryQueryBackend = false; - ids = ImmutableList.of(); - } else { - if (id.direction() == Directions.IN) { - id = id.switchDirection(); - } - ids = ImmutableList.of(id); - } - - HugeEdge edge = null; - if (id != null && edgesUpdated) { - if (this.removedEdges.containsKey(id)) { - // The record has been deleted - tryQueryBackend = false; - } else if ((edge = this.addedEdges.get(id)) != null || - (edge = this.updatedEdges.get(id)) != null) { - // Found from local tx - tryQueryBackend = false; - if (edge.expired()) { - edge = null; - } else { - assert edge != null; - } - } + continue; } - - if (edge != null) { - assert !tryQueryBackend; - edges = ImmutableMap.of(edge.id(), edge); - } else if (!tryQueryBackend) { - assert edge == null; - edges = ImmutableMap.of(); - } else { - // Query from backend store - IdQuery query = new IdQuery.OneIdQuery(HugeType.EDGE, id); - Iterator it = this.queryEdgesFromBackend(query); - edge = QueryResults.one(it); - if (edge == null) { - edges = ImmutableMap.of(); - } else { - edges = ImmutableMap.of(edge.id(), edge); - } + if (id.direction() == Directions.IN) { + id = id.switchDirection(); } - } else { - // NOTE: allowed duplicated edges if query by duplicated ids - ids = InsertionOrderUtil.newList(); - edges = new HashMap<>(edgeIds.length); - - IdQuery query = new IdQuery(HugeType.EDGE); - for (Object edgeId : edgeIds) { - HugeEdge edge; - EdgeId id = HugeEdge.getIdValue(edgeId, !verifyId); - if (id == null) { + if (this.removedEdges.containsKey(id)) { + // The record has been deleted + continue; + } else if ((edge = this.addedEdges.get(id)) != null || + (edge = this.updatedEdges.get(id)) != null) { + if (edge.expired()) { continue; } - if (id.direction() == Directions.IN) { - id = id.switchDirection(); - } - - boolean foundLocal = false; - if (edgesUpdated) { - if (this.removedEdges.containsKey(id)) { - // The record has been deleted - continue; - } - if ((edge = this.addedEdges.get(id)) != null || - (edge = this.updatedEdges.get(id)) != null) { - if (edge.expired()) { - continue; - } - // Found from local tx - foundLocal = true; - edges.put(edge.id(), edge); - } else { - assert !foundLocal; - } - } - if (!foundLocal) { - // Prepare to query from backend store - query.query(id); - } - ids.add(id); + // Found from local tx + edges.put(edge.id(), edge); + } else { + // Prepare to query from backend store + query.query(id); } + ids.add(id); + } - if (!query.empty()) { - // Query from backend store - if (edges.isEmpty() && query.idsSize() == ids.size()) { - /* - * Sort at the lower layer and return directly if there is - * no local vertex and duplicated id. - */ - Iterator it = this.queryEdgesFromBackend(query); - @SuppressWarnings({ "unchecked", "rawtypes" }) - Iterator r = (Iterator) it; - return r; - } - - query.mustSortByInput(false); + if (!query.empty()) { + // Query from backend store + if (edges.isEmpty() && query.idsSize() == ids.size()) { + /* + * Sort at the lower layer and return directly if there is no + * local vertex and duplicated id. + */ Iterator it = this.queryEdgesFromBackend(query); - QueryResults.fillMap(it, edges); + @SuppressWarnings({"unchecked", "rawtypes"}) + Iterator r = (Iterator) it; + return r; } + + query.mustSortByInput(false); + Iterator it = this.queryEdgesFromBackend(query); + QueryResults.fillMap(it, edges); } return new MapperIterator<>(ids.iterator(), id -> { @@ -1135,7 +1049,50 @@ protected Iterator queryEdgesByIds(Object[] edgeIds, }); } - @Watched(prefix = "graph") + /** + * Query a single edge by id, with the same semantics as the multi-id + * path of {@link #queryEdgesByIds(Object[], boolean)} but without + * allocating the id list, the result map and the mapper iterator; only + * an {@link IdQuery.OneIdQuery} is created on a miss. + */ + private Iterator queryEdgeById(Object edgeId, boolean verifyId) { + EdgeId id = HugeEdge.getIdValue(edgeId, !verifyId); + if (id == null) { + return QueryResults.emptyIterator(); + } + if (id.direction() == Directions.IN) { + id = id.switchDirection(); + } + + if (this.edgesInTxSize() > 0) { + if (this.removedEdges.containsKey(id)) { + // The record has been deleted + return QueryResults.emptyIterator(); + } + HugeEdge edge = this.addedEdges.get(id); + if (edge == null) { + edge = this.updatedEdges.get(id); + } + if (edge != null) { + // Found from local tx + if (edge.expired()) { + return QueryResults.emptyIterator(); + } + return QueryResults.iterator(edge); + } + } + + /* + * Query from backend store and return the results directly, just + * like the multi-id path does when there is no local edge. + */ + IdQuery query = new IdQuery.OneIdQuery(HugeType.EDGE, id); + Iterator it = this.queryEdgesFromBackend(query); + @SuppressWarnings({"unchecked", "rawtypes"}) + Iterator r = (Iterator) it; + return r; + } + public Iterator queryEdges() { Query q = new Query(HugeType.EDGE); return this.queryEdges(q); @@ -1189,7 +1146,6 @@ public Iterator queryEdges(Query query) { return this.skipOffsetOrStopLimit(r, query); } - @Watched(prefix = "graph") protected Iterator queryEdgesFromBackend(Query query) { assert query.resultType().isEdge(); @@ -1687,7 +1643,7 @@ private Query optimizeQuery(ConditionQuery query) { * Just query by primary-key(id), ignore other user-props(if exists) * that it will be filtered by queryVertices(Query) */ - return new IdQuery.OneIdQuery(query, id); + return new IdQuery(query, id); } } } diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java index 265d408742..12a8a66e2b 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/EdgeCoreTest.java @@ -23,6 +23,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; @@ -33,6 +34,7 @@ import org.apache.hugegraph.HugeException; import org.apache.hugegraph.HugeGraph; import org.apache.hugegraph.backend.BackendException; +import org.apache.hugegraph.backend.id.EdgeId; import org.apache.hugegraph.backend.id.Id; import org.apache.hugegraph.backend.id.IdGenerator; import org.apache.hugegraph.backend.page.PageInfo; @@ -46,6 +48,7 @@ import org.apache.hugegraph.config.CoreOptions; import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.exception.NoIndexException; +import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.schema.Userdata; import org.apache.hugegraph.structure.HugeEdge; @@ -2728,6 +2731,176 @@ public void testQueryEdgesByIdWithGraphAPIAndNotCommittedRemoved() { Assert.assertTrue(graph.edges(edge1.id(), edge2.id()).hasNext()); } + @Test + public void testQueryEdgesByIdsWithLocalAndDuplicateIds() { + HugeGraph graph = graph(); + init18Edges(); + + Object id1 = graph.traversal().E().toList().get(0).id(); + // Added but not committed + Vertex james = graph.addVertex(T.label, "author", "id", 3, + "name", "Dennis Ritchie", "age", 70, + "lived", "New York"); + Vertex book = graph.addVertex(T.label, "book", "name", "c-book"); + Edge local = james.addEdge("authored", book, "score", 5); + Object id2 = local.id(); + + List edges = ImmutableList.copyOf(graph.edges(id2, id1, id2)); + Assert.assertEquals(3, edges.size()); + Assert.assertSame(local, edges.get(0)); + Assert.assertEquals(id1, edges.get(1).id()); + Assert.assertSame(local, edges.get(2)); + + graph.tx().rollback(); + } + + @Test + public void testQuerySingleEdgeByIdInLocalTx() { + HugeGraph graph = graph(); + init18Edges(); + + // Added but not committed + Vertex james = graph.addVertex(T.label, "author", "id", 3, + "name", "Dennis Ritchie", "age", 70, + "lived", "New York"); + Vertex book = graph.addVertex(T.label, "book", "name", "c-book"); + Edge edge = james.addEdge("authored", book, "score", 5); + Object id = edge.id(); + + List edges = ImmutableList.copyOf(graph.edges(id)); + Assert.assertEquals(1, edges.size()); + Assert.assertSame(edge, edges.get(0)); + Assert.assertSame(edge, graph.edge(id)); + graph.tx().commit(); + + // Updated but not committed + edge = graph.edge(id); + edge.property("score", 6); + + edges = ImmutableList.copyOf(graph.edges(id)); + Assert.assertEquals(1, edges.size()); + Assert.assertSame(edge, edges.get(0)); + Assert.assertEquals(6, (int) graph.edge(id).value("score")); + graph.tx().rollback(); + + Assert.assertEquals(5, (int) graph.edge(id).value("score")); + } + + @Test + public void testQuerySingleEdgeByIdRemovedInLocalTx() { + HugeGraph graph = graph(); + init18Edges(); + + Edge edge = graph.traversal().E().toList().get(0); + Object id = edge.id(); + Assert.assertTrue(graph.edges(id).hasNext()); + + edge.remove(); + Assert.assertFalse(graph.edges(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + + graph.tx().rollback(); + Assert.assertTrue(graph.edges(id).hasNext()); + Assert.assertEquals(id, graph.edge(id).id()); + } + + @Test + public void testQuerySingleEdgeByIdNotFound() { + HugeGraph graph = graph(); + init18Edges(); + + String id = graph.traversal().E().toList().get(0).id() + "-not-exist"; + Assert.assertFalse(graph.edges(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + } + + @Test + public void testQuerySingleEdgeByInvalidId() { + HugeGraph graph = graph(); + init18Edges(); + + // Invalid id is skipped by edges() and rejected by edge() + Assert.assertFalse(graph.edges("invalid-edge-id").hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge("invalid-edge-id"); + }, e -> { + Assert.assertContains("Edge id must be formatted", e.getMessage()); + }); + + Assert.assertFalse(graph.edges((Object) null).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge(null); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + } + + @Test + public void testQuerySingleEdgeByIdWithInDirection() { + HugeGraph graph = graph(); + init18Edges(); + + HugeEdge edge = (HugeEdge) graph.traversal().E().toList().get(0); + EdgeId inId = edge.idWithDirection().switchDirection(); + Assert.assertEquals(Directions.IN, inId.direction()); + + List edges = ImmutableList.copyOf(graph.edges(inId)); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals(edge.id(), edges.get(0).id()); + Assert.assertEquals(edge.id(), graph.edge(inId).id()); + + edges = ImmutableList.copyOf(graph.edges(inId.asString())); + Assert.assertEquals(1, edges.size()); + Assert.assertEquals(edge.id(), edges.get(0).id()); + } + + @Test + public void testQuerySingleEdgeByIdExpiredInLocalTx() { + HugeGraph graph = graph(); + + Vertex baby = graph.addVertex(T.label, "person", "name", "Baby", + "age", 3, "city", "Beijing"); + Vertex java = graph.addVertex(T.label, "book", + "name", "Java in action"); + Edge edge = baby.addEdge("read", java, "place", "library of school", + "date", "2019-12-23 12:00:00"); + Object id = edge.id(); + graph.tx().commit(); + + edge = graph.edge(id); + Assert.assertTrue(graph.edges(id).hasNext()); + graph.tx().rollback(); + + try { + Thread.sleep(3100L); + } catch (InterruptedException e) { + // Ignore + } + + // Update the expired edge in a new tx (not committed) + edge.property("place", "home"); + Map updated = Whitebox.getInternalState( + params().graphTransaction(), "updatedEdges"); + Assert.assertTrue(updated.containsKey(edge.id())); + Assert.assertTrue(((HugeEdge) edge).expired()); + + Assert.assertFalse(graph.edges(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.edge(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + graph.tx().rollback(); + } + @Test public void testQueryEdgesByIdNotFound() { HugeGraph graph = graph(); @@ -3326,6 +3499,49 @@ public void testQueryAdjacentVerticesOfEdgesWithoutVertex() } } + @Test + public void testQueryAdjacentVertexRemovedInLocalTx() + throws InterruptedException, ExecutionException { + HugeGraph graph = graph(); + + Vertex james = graph.addVertex(T.label, "author", "id", 1, + "name", "James Gosling", "age", 62, + "lived", "Canadian"); + Vertex java = graph.addVertex(T.label, "book", "name", "java"); + james.addEdge("authored", java, "score", 3); + graph.tx().commit(); + params().graphEventHub().notify(Events.CACHE, "clear", null).get(); + + Edge edge = graph.traversal().V(james.id()).outE().next(); + HugeVertex adjacent = (HugeVertex) edge.inVertex(); + Assert.assertFalse(adjacent.isPropLoaded()); + Assert.assertEquals("book", adjacent.label()); + + // Remove the adjacent vertex but don't commit + graph.vertex(java.id()).remove(); + Assert.assertFalse(graph.vertices(java.id()).hasNext()); + Assert.assertFalse(graph.adjacentVertex(java.id()).hasNext()); + // Querying by one id must agree with querying by multiple ids + List vertices = ImmutableList.copyOf( + params().graphTransaction() + .queryAdjacentVertices(java.id(), james.id())); + Assert.assertEquals(1, vertices.size()); + Assert.assertEquals(james.id(), vertices.get(0).id()); + + /* + * Loading the adjacent vertex of an edge held before the removal + * must not turn it into an undefined vertex + */ + adjacent.forceLoad(); + Assert.assertFalse(adjacent.schemaLabel().undefined()); + Assert.assertEquals("book", adjacent.label()); + + graph.tx().rollback(); + Assert.assertTrue(graph.vertices(java.id()).hasNext()); + Assert.assertEquals("book", graph.adjacentVertex(java.id()) + .next().label()); + } + @Test public void testQueryAdjacentVerticesOfEdgesWithInvalidVertexLabel() throws InterruptedException, ExecutionException { diff --git a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java index a329de3afb..861f295975 100644 --- a/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java +++ b/hugegraph-server/hugegraph-test/src/main/java/org/apache/hugegraph/core/VertexCoreTest.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Random; @@ -51,11 +52,13 @@ import org.apache.hugegraph.exception.LimitExceedException; import org.apache.hugegraph.exception.NoIndexException; import org.apache.hugegraph.exception.NotAllowException; +import org.apache.hugegraph.exception.NotFoundException; import org.apache.hugegraph.schema.PropertyKey; import org.apache.hugegraph.schema.SchemaManager; import org.apache.hugegraph.schema.Userdata; import org.apache.hugegraph.schema.VertexLabel; import org.apache.hugegraph.structure.HugeElement; +import org.apache.hugegraph.structure.HugeVertex; import org.apache.hugegraph.testutil.Assert; import org.apache.hugegraph.testutil.FakeObjects; import org.apache.hugegraph.testutil.Utils; @@ -3124,6 +3127,174 @@ public void testQueryByIdWithGraphAPIAndNotCommittedRemoved() { Assert.assertTrue(graph.vertices(vertex1.id(), vertex2.id()).hasNext()); } + @Test + public void testQueryVerticesByNonConsecutiveDuplicateIds() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + List all = graph.traversal().V().toList(); + Object id1 = all.get(0).id(); + Object id2 = all.get(1).id(); + + List vertices = ImmutableList.copyOf(graph.vertices(id1, id2, id1)); + Assert.assertEquals(3, vertices.size()); + Assert.assertEquals(id1, vertices.get(0).id()); + Assert.assertEquals(id2, vertices.get(1).id()); + Assert.assertEquals(id1, vertices.get(2).id()); + } + + @Test + public void testQueryVerticesByIdsWithLocalAndDuplicateIds() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + Object id1 = graph.traversal().V().toList().get(0).id(); + // Added but not committed + Vertex local = graph.addVertex(T.label, "author", "id", 3, + "name", "Dennis Ritchie", "age", 70, + "lived", "New York"); + Object id2 = local.id(); + + List vertices = ImmutableList.copyOf(graph.vertices(id2, id1, id2)); + Assert.assertEquals(3, vertices.size()); + Assert.assertSame(local, vertices.get(0)); + Assert.assertEquals(id1, vertices.get(1).id()); + Assert.assertSame(local, vertices.get(2)); + + graph.tx().rollback(); + } + + @Test + public void testQuerySingleVertexByIdInLocalTx() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + // Added but not committed + Vertex vertex = graph.addVertex(T.label, "author", "id", 3, + "name", "Dennis Ritchie", "age", 70, + "lived", "New York"); + Object id = vertex.id(); + + List vertices = ImmutableList.copyOf(graph.vertices(id)); + Assert.assertEquals(1, vertices.size()); + Assert.assertSame(vertex, vertices.get(0)); + Assert.assertSame(vertex, graph.vertex(id)); + Assert.assertSame(vertex, graph.adjacentVertex(id).next()); + this.commitTx(); + + // Updated but not committed + vertex = graph.vertex(id); + vertex.property("age", 71); + + vertices = ImmutableList.copyOf(graph.vertices(id)); + Assert.assertEquals(1, vertices.size()); + Assert.assertSame(vertex, vertices.get(0)); + Assert.assertEquals(71, (int) graph.vertex(id).value("age")); + graph.tx().rollback(); + + Assert.assertEquals(70, (int) graph.vertex(id).value("age")); + } + + @Test + public void testQuerySingleVertexByIdRemovedInLocalTx() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + Vertex vertex = graph.traversal().V().hasLabel("author") + .has("id", 1).next(); + Object id = vertex.id(); + Assert.assertTrue(graph.vertices(id).hasNext()); + Assert.assertTrue(graph.adjacentVertex(id).hasNext()); + + vertex.remove(); + Assert.assertFalse(graph.vertices(id).hasNext()); + // Removed vertex must not be reported as an undefined adjacent vertex + Assert.assertFalse(graph.adjacentVertex(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.vertex(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + + graph.tx().rollback(); + Assert.assertTrue(graph.vertices(id).hasNext()); + Assert.assertTrue(graph.adjacentVertex(id).hasNext()); + } + + @Test + public void testQuerySingleVertexByIdNotFound() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + Id id = SplicingIdGenerator.splicing("author", "not-exists-id"); + Assert.assertFalse(graph.vertices(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.vertex(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + + // Adjacent vertex not found is returned as an undefined vertex + // (vertex.check_adjacent_vertex_exist=false, the default) + List vertices = ImmutableList.copyOf(graph.adjacentVertex(id)); + Assert.assertEquals(1, vertices.size()); + Assert.assertEquals(id, vertices.get(0).id()); + Assert.assertEquals("~undefined", vertices.get(0).label()); + } + + @Test + public void testQuerySingleVertexByNullId() { + HugeGraph graph = graph(); + this.init10VerticesAndCommit(); + + Assert.assertFalse(graph.vertices((Object) null).hasNext()); + Assert.assertFalse(graph.adjacentVertex(null).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.vertex(null); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + + // Same as querying by multiple ids + Assert.assertFalse(graph.vertices(null, null).hasNext()); + } + + @Test + public void testQuerySingleVertexByIdExpiredInLocalTx() { + HugeGraph graph = graph(); + + Vertex vertex = graph.addVertex(T.label, "fan", "name", "Baby", + "age", 3, "city", "Beijing"); + Object id = vertex.id(); + this.commitTx(); + + vertex = graph.vertex(id); + Assert.assertTrue(graph.vertices(id).hasNext()); + graph.tx().rollback(); + + try { + Thread.sleep(3100L); + } catch (InterruptedException e) { + // Ignore + } + + // Update the expired vertex in a new tx (not committed) + vertex.property("age", 4); + Map updated = Whitebox.getInternalState( + params().graphTransaction(), "updatedVertices"); + Assert.assertTrue(updated.containsKey(vertex.id())); + Assert.assertTrue(((HugeVertex) vertex).expired()); + + Assert.assertFalse(graph.vertices(id).hasNext()); + Assert.assertFalse(graph.adjacentVertex(id).hasNext()); + Assert.assertThrows(NotFoundException.class, () -> { + graph.vertex(id); + }, e -> { + Assert.assertContains("does not exist", e.getMessage()); + }); + graph.tx().rollback(); + } + @Test public void testQueryByInvalidSysprop() { HugeGraph graph = graph();