Summary
#5670 (PR #5678) closed the window where deleting an EDGE could leave its back-reference behind: a
chunk that is momentarily unreadable - a concurrent commit publishes its pages one at a time, so a
vertex page can expose a new edge-list head RID before that head's page is visible, and an emptied
chunk is relinked out of the chain under a walker's feet - was read as "nothing to remove here"
while the edge record was deleted anyway.
GraphEngine.deleteVertex still reads its own edge lists through the best-effort
getEdgeHeadChunk, and wraps the collection loop in catch (Exception):
try {
outEdges = getEdgeHeadChunk(vertex, Vertex.DIRECTION.OUT);
if (outEdges != null) { ... collect edges to delete ... }
} catch (Exception e) {
// LINKED LIST COULD BE BROKEN
LogManager.instance().log(this, Level.WARNING, "Error on deleting outgoing edges connected to vertex %s", e, ...);
}
...
vertex.getDatabase().getSchema().getBucketById(...).deleteRecord(vertex.getIdentity(), force);
So if the head chunk is unreadable at that instant, outEdges is null, no edges are collected,
and the vertex record is deleted regardless - leaving edges pointing at a vertex that no longer
exists (ghost edges), reported by check database as invalid links.
Why it was left out of #5678
This is a deliberate, tested contract, not an oversight: deleteVertex's javadoc states that edge
disconnection is best-effort, and Issue4420TolerantDeleteTest / Issue4432CorruptVertexDeleteTest
pin that a vertex whose chain is structurally broken stays deletable. Making the collection strict
would trade repairability for strictness, and that is a decision worth taking on its own merits
rather than as a side effect of an edge-delete fix.
Note that deleteVertex does get #5670's fix transitively where it matters most: every edge it does
collect is removed via edge.delete() -> deleteEdge, which is now strict, so a NEIGHBOUR's
back-reference is no longer left dangling.
What a fix would look like
Distinguish the two cases the single getEdgeHeadChunk call currently conflates:
The open question is what an ordinary deleteVertex should do when the chain is genuinely broken
(not transiently unreadable), since the two are indistinguishable at that point: today it deletes and
warns; strict handling would require force to get the vertex out. That is the trade-off to decide.
Verification
A test in the shape of Issue5670EdgeDeleteDanglingBackRefTest - make the vertex's head chunk
unreadable, delete the vertex, and assert its edges do not outlive it - would pin whichever answer is
chosen.
Summary
#5670 (PR #5678) closed the window where deleting an EDGE could leave its back-reference behind: a
chunk that is momentarily unreadable - a concurrent commit publishes its pages one at a time, so a
vertex page can expose a new edge-list head RID before that head's page is visible, and an emptied
chunk is relinked out of the chain under a walker's feet - was read as "nothing to remove here"
while the edge record was deleted anyway.
GraphEngine.deleteVertexstill reads its own edge lists through the best-effortgetEdgeHeadChunk, and wraps the collection loop incatch (Exception):So if the head chunk is unreadable at that instant,
outEdgesis null, no edges are collected,and the vertex record is deleted regardless - leaving edges pointing at a vertex that no longer
exists (ghost edges), reported by
check databaseas invalid links.Why it was left out of #5678
This is a deliberate, tested contract, not an oversight:
deleteVertex's javadoc states that edgedisconnection is best-effort, and
Issue4420TolerantDeleteTest/Issue4432CorruptVertexDeleteTestpin that a vertex whose chain is structurally broken stays deletable. Making the collection strict
would trade repairability for strictness, and that is a decision worth taking on its own merits
rather than as a side effect of an edge-delete fix.
Note that
deleteVertexdoes get #5670's fix transitively where it matters most: every edge it doescollect is removed via
edge.delete()->deleteEdge, which is now strict, so a NEIGHBOUR'sback-reference is no longer left dangling.
What a fix would look like
Distinguish the two cases the single
getEdgeHeadChunkcall currently conflates:force == true(explicit repair of a structurally broken record): keep today's tolerance.force == false(ordinary delete): usegetEdgeHeadChunkForWrite, so a transiently unreadablehead is a retryable conflict and the transaction re-reads a consistent view - the same line fix(engine) #5670: an unreadable edge-list chunk retries the delete instead of skipping it #5678
drew for
deleteEdge. The surroundingcatch (Exception)would have to letNeedRetryExceptionthrough.
The open question is what an ordinary
deleteVertexshould do when the chain is genuinely broken(not transiently unreadable), since the two are indistinguishable at that point: today it deletes and
warns; strict handling would require
forceto get the vertex out. That is the trade-off to decide.Verification
A test in the shape of
Issue5670EdgeDeleteDanglingBackRefTest- make the vertex's head chunkunreadable, delete the vertex, and assert its edges do not outlive it - would pin whichever answer is
chosen.