Sitelet https://github.com/ArcadeData/arcadedb/commit/0561988b1bfaf5255c908a05aedadc9a2f54839d
Skip to content

Commit 0561988

Browse files
committed
test(#687): mirror the compound-corruption regression to the OUT list
Extract handleDanglingEdgeListEntry() so checkIncomingEdges and checkOutgoingEdges share one copy of the DanglingEdgeListEntryException handler instead of two independently-maintained ones - the exact kind of drift that caused the catch-scope bug fixed in the previous commit. Add classCastDuringSiblingRescanDoesNotMaskAGenuinelyCorruptEdgeOnOutList, the OUT-list counterpart of the IN-list compound-corruption test: a mismatched edge sharing checkOutgoingEdges' "CHECK ALL OUT EDGES" rescan with a separate dangling entry. Verified it fails when only the OUT-side narrowing is reverted, while the IN-list test and the rest of the suite stay green.
1 parent 7f835ed commit 0561988

2 files changed

Lines changed: 75 additions & 18 deletions

File tree

‎engine/src/main/java/com/arcadedb/graph/GraphDatabaseChecker.java‎

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,22 @@ private static Edge asEdgeOrDanglingEntry(final RID edgeRID) {
628628
}
629629
}
630630

631+
/**
632+
* Shared by the {@code DanglingEdgeListEntryException} catch in both {@link #checkIncomingEdges} and
633+
* {@link #checkOutgoingEdges}: the record loaded fine, only the (Edge) cast on {@code edgeRID} failed. Drop
634+
* just the dangling LIST entry and NEVER schedule the pointed-to record for deletion - fix mode raw-deletes
635+
* every {@code corruptedRecords} RID with {@code bucket.deleteRecord}, which would destroy that valid record
636+
* and bypass graph-aware cleanup (a deleted vertex would leave its OWN edges dangling, cascading the
637+
* damage). Report it and repair the list, nothing else. Extracted so the two copies - drifting apart is
638+
* exactly what caused the bug this handler fixes - cannot go out of sync again.
639+
*/
640+
private static void handleDanglingEdgeListEntry(final CheckReport report, final RID edgeRID,
641+
final DanglingEdgeListEntryException e, final boolean fix) {
642+
report.warn("edge " + edgeRID + " error on loading (error: " + describe(e.getCause()) + ")"
643+
+ (fix ? ", dropping the dangling list entry (record preserved)" : ""));
644+
++report.invalidLinks;
645+
}
646+
631647
private void checkIncomingEdges(boolean fix, Vertex vertex, RID vertexIdentity, Set<RID> reconnectInEdges,
632648
Set<RID> reconnectOutEdges, Map<RID, Long> missingReferences, Map<RID, String> missingReferenceErrors,
633649
CheckReport report) {
@@ -811,16 +827,8 @@ else if (edgeIn.equals(edge.getIn()))
811827
removeEntry = true;
812828
++report.invalidLinks;
813829
} catch (final DanglingEdgeListEntryException e) {
814-
// edgeRID ITSELF resolves to a record that is NOT an edge (e.g. a vertex wrongly linked into
815-
// this adjacency list by an older build): the record loaded fine, only the (Edge) cast failed.
816-
// Drop just the dangling LIST entry and NEVER schedule the pointed-to record for deletion:
817-
// fix mode raw-deletes every corruptedRecords RID with bucket.deleteRecord, which would
818-
// destroy that valid record and bypass graph-aware cleanup (a deleted vertex would leave its
819-
// OWN edges dangling, cascading the damage). Report it and repair the list, nothing else.
820-
report.warn("edge " + edgeRID + " error on loading (error: " + describe(e.getCause()) + ")"
821-
+ (fix ? ", dropping the dangling list entry (record preserved)" : ""));
830+
handleDanglingEdgeListEntry(report, edgeRID, e, fix);
822831
removeEntry = true;
823-
++report.invalidLinks;
824832
} catch (final Exception e) {
825833
// UNKNOWN ERROR ON LOADING - also catches a ClassCastException thrown while processing a
826834
// DIFFERENT RID than edgeRID (the sibling-entry rescan or the far-vertex resolution above):
@@ -1060,16 +1068,8 @@ else if (edgeOut.equals(edge.getOut()))
10601068
removeEntry = true;
10611069
++report.invalidLinks;
10621070
} catch (final DanglingEdgeListEntryException e) {
1063-
// edgeRID ITSELF resolves to a record that is NOT an edge (e.g. a vertex wrongly linked into
1064-
// this adjacency list by an older build): the record loaded fine, only the (Edge) cast failed.
1065-
// Drop just the dangling LIST entry and NEVER schedule the pointed-to record for deletion:
1066-
// fix mode raw-deletes every corruptedRecords RID with bucket.deleteRecord, which would
1067-
// destroy that valid record and bypass graph-aware cleanup (a deleted vertex would leave its
1068-
// OWN edges dangling, cascading the damage). Report it and repair the list, nothing else.
1069-
report.warn("edge " + edgeRID + " error on loading (error: " + describe(e.getCause()) + ")"
1070-
+ (fix ? ", dropping the dangling list entry (record preserved)" : ""));
1071+
handleDanglingEdgeListEntry(report, edgeRID, e, fix);
10711072
removeEntry = true;
1072-
++report.invalidLinks;
10731073
} catch (final Exception e) {
10741074
// UNKNOWN ERROR ON LOADING - also catches a ClassCastException thrown while processing a
10751075
// DIFFERENT RID than edgeRID (the sibling-entry rescan or the far-vertex resolution above):

‎engine/src/test/java/com/arcadedb/graph/Issue687ClassCastEdgeCheckTest.java‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,28 @@ void classCastDuringSiblingRescanDoesNotMaskAGenuinelyCorruptEdge() {
180180
assertThat(corrupted).as("the dangling entry's bystander record must never be flagged corrupt").doesNotContain(bystander);
181181
}
182182

183+
/**
184+
* Same compound-corruption scenario as {@link #classCastDuringSiblingRescanDoesNotMaskAGenuinelyCorruptEdge()},
185+
* exercised through the OUT-list handler ({@code checkOutgoingEdges}, the "CHECK ALL OUT EDGES" rescan at
186+
* {@code nextEntry.getFirst().asEdge(true).getOut()}) instead of the IN-list one. The root cause here was
187+
* literally two independently-maintained copies of the same handler, so the compound case needs its own
188+
* coverage on each side - a fix mirrored to only one copy must fail this test.
189+
*/
190+
@Test
191+
void classCastDuringSiblingRescanDoesNotMaskAGenuinelyCorruptEdgeOnOutList() {
192+
final RID[] ids = buildCompoundCorruptionScenarioOnOutList();
193+
final RID mismatchedEdge = ids[1];
194+
final RID bystander = ids[2];
195+
196+
final Map<String, Object> stats = new GraphDatabaseChecker((DatabaseInternal) database).checkVertices(VERTEX_TYPE, false, 0);
197+
198+
@SuppressWarnings("unchecked")
199+
final Collection<RID> corrupted = (Collection<RID>) stats.get("corruptedRecords");
200+
assertThat(corrupted).as("the genuinely mismatched edge must still be flagged corrupt despite the sibling cast failure")
201+
.contains(mismatchedEdge);
202+
assertThat(corrupted).as("the dangling entry's bystander record must never be flagged corrupt").doesNotContain(bystander);
203+
}
204+
183205
/**
184206
* Builds: a hub vertex whose IN list holds (1) a real edge from {@code source} to some OTHER vertex
185207
* (mismatched: {@code edge.getIn()} != hub, wired into the hub's list anyway) and (2) a dangling entry
@@ -216,6 +238,41 @@ private RID[] buildCompoundCorruptionScenario() {
216238
return ids;
217239
}
218240

241+
/**
242+
* Same as {@link #buildCompoundCorruptionScenario()}, but wired into the hub's OUT list: a real edge FROM
243+
* some other vertex ({@code edge.getOut()} != hub) plus a separate dangling entry.
244+
*/
245+
private RID[] buildCompoundCorruptionScenarioOnOutList() {
246+
database.transaction(() -> {
247+
database.getSchema().createVertexType(VERTEX_TYPE, 1);
248+
database.getSchema().createEdgeType(EDGE_TYPE, 1);
249+
});
250+
251+
final RID[] ids = new RID[3];
252+
database.transaction(() -> {
253+
final Vertex hub = database.newVertex(VERTEX_TYPE).set("name", "hub").save();
254+
final Vertex wrongSource = database.newVertex(VERTEX_TYPE).set("role", "wrongSource").save();
255+
final Vertex target = database.newVertex(VERTEX_TYPE).set("role", "target").save();
256+
257+
// Genuine edge, but its OUT endpoint is NOT the hub: this must stay flagged corrupt.
258+
final Edge mismatchedEdge = wrongSource.newEdge(EDGE_TYPE, target);
259+
260+
final VertexInternal hubInternal = (VertexInternal) hub.getIdentity().asVertex(true);
261+
final EdgeLinkedList hubOutList = ((DatabaseInternal) database).getGraphEngine()
262+
.getOrCreateEdgeList(hubInternal, Vertex.DIRECTION.OUT);
263+
hubOutList.add(mismatchedEdge.getIdentity(), target.getIdentity());
264+
265+
// A separate dangling entry: a plain vertex where an edge RID is expected.
266+
final Vertex bystander = database.newVertex(VERTEX_TYPE).set("role", "bystander").save();
267+
hubOutList.add(bystander.getIdentity(), hub.getIdentity());
268+
269+
ids[0] = hub.getIdentity();
270+
ids[1] = mismatchedEdge.getIdentity();
271+
ids[2] = bystander.getIdentity();
272+
});
273+
return ids;
274+
}
275+
219276
/** Injects one corrupt IN-list entry on the hub: edgeRID = a plain vertex (not an edge). Returns that vertex RID. */
220277
private RID injectClassCastEntry(final RID hubRid) {
221278
final RID[] bystander = new RID[1];

0 commit comments

Comments
 (0)