fix(#5667): GraphBatch resumes over an already-promoted super-node vertex instead of hard-failing - #5948
Conversation
…rtex instead of hard-failing GraphBatch's bulk segment path used to reject any vertex already promoted to the super-node striped layout (#5156): the sequential flush path threw a documented IllegalStateException, but the parallel flush path had no guard at all and threw a raw, undocumented ClassCastException (StripeDirectory cast to EdgeSegment). Both paths now detect a promoted vertex and route that group's edges through the standard, MVCC-safe StripedEdgeList write path instead of failing the batch. The parallel-path detection is done with purely local variables (not the shared instance-field pattern used by the sequential path) since connectOutEdgesRangeLocal/ connectIncomingEdgesRangeLocal run concurrently across async-executor threads. GraphBatch still does not promote a vertex to the striped layout during a bulk load itself - that remains a follow-up; the class javadoc now documents the limitation.
|
Tick the box to add this pull request to the merge queue (same as
|
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| CodeStyle | 3 minor |
🟢 Metrics 5 complexity
Metric Results Complexity 5
🟢 Coverage 100.00% diff coverage · -6.37% coverage variation
Metric Results Coverage variation ✅ -6.37% coverage variation Diff coverage ✅ 100.00% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (ea4f6af) 152855 117429 76.82% Head commit (41a4ffb) 185521 (+32666) 130714 (+13285) 70.46% (-6.37%) Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch:
<coverage of head commit> - <coverage of common ancestor commit>Diff coverage details
Coverable lines Covered lines Diff coverage Pull request (#5948) 32 32 100.00% Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified:
<covered lines added or modified>/<coverable lines added or modified> * 100%
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
ReviewReviewed against the code merged into this checkout ( CorrectnessThe core fix is sound. All four call sites that used to hard-fail on a promoted vertex now route through
I traced the neighbour-vertex argument through all four call sites (
Minor / non-blocking observations
Style / conventions
Test coverage assessmentThe two new tests ( Overall: this is a careful, well-scoped fix with good reasoning about the concurrency hazard (and evidence the thread-safety bug was actually caught during development, not just asserted). Nothing here blocks merging; the notes above are suggestions for a possible follow-up rather than requested changes. |
|
Reviewed the diff ( Code quality
Potential bugs
Performance
SecurityNothing notable - purely internal storage-engine logic, no new external input surface, no new dependencies. Test coverage
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5948 +/- ##
==========================================
+ Coverage 68.03% 68.11% +0.07%
==========================================
Files 1797 1797
Lines 152855 153178 +323
Branches 32397 32442 +45
==========================================
+ Hits 103999 104338 +339
+ Misses 35466 35461 -5
+ Partials 13390 13379 -11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Closes #5667
Root cause
GraphBatch(the bulk graph importer) manages edge segments directly, bypassingEdgeLinkedList/StripedEdgeList. It never callsEdgeLinkedList.tryPromoteToSuperNode(), so a bulk-loaded hub never gets the striped super-node layout (#5156) no matter how many edges land on it.Worse, if the graph already contains a promoted vertex (created through the standard API, or by a previous non-bulk write), resuming a bulk load over it used to fail outright:
getOrCreateOutSegmentDeferred/getOrCreateInSegmentDeferred, used byconnectOutgoingEdgesSorted/connectIncomingEdgesSequential): threw a documentedIllegalStateException("Bulk edge import into the super-node promoted vertex ... is not supported").getOrCreateOutEdgeChunk/getOrCreateInEdgeChunk, used byconnectOutEdgesRangeLocal/connectIncomingEdgesRangeLocalvia the async executor): had no guard at all - it blindly cast the vertex's head chunk record toEdgeSegment, which threw a raw, undocumentedClassCastException(StripeDirectorycannot be cast toEdgeSegment) instead.Scope of this fix
This PR fixes the hard failure (both shapes above, sequential and parallel), which is the more severe half of the issue - an unhandled exception that aborts the whole batch mid-import, arriving after potentially hours of work. Edges for an already-promoted vertex are now routed through the standard, MVCC-safe
StripedEdgeListwrite path instead of being rejected.Out of scope:
GraphBatchstill does not promote a vertex to the super-node layout during a bulk load - a very high-degree vertex loaded entirely throughGraphBatchstill ends up as a long chained-segment list rather than striped. Implementing promotion inside GraphBatch's own bulk segment-management code (four independent overflow call sites across OUT/IN x sequential/parallel, each needing correct cross-flush degree tracking) is a materially larger change with its own performance and correctness surface; it is intentionally left as a follow-up. TheGraphBatchclass javadoc now documents this limitation explicitly, and points atarcadedb.graph.supernodeThreshold=0as the database-wide workaround if a bulk-loaded super-node's degraded traversal performance is a concern.Changes
engine/src/main/java/com/arcadedb/graph/GraphBatch.java:getOrCreateOutSegmentDeferred/getOrCreateInSegmentDeferred(sequential flush): instead of throwing, set new instance fields (lastSegmentPromoted,lastPromotedVertex,lastPromotedDirectory) and returnnull. Safe as instance-field signalling because these two methods run only on the single-threaded sequential connect path.connectOutgoingEdgesSorted/connectIncomingEdgesSequential: checklastSegmentPromotedfirst and, if set, route the group through the newaddGroupThroughStripedEdgeListhelper instead of the bulk segment write.connectOutEdgesRangeLocal/connectIncomingEdgesRangeLocal(parallel flush, runs concurrently across async-executor threads): check for aStripeDirectoryhead locally (no shared instance state - see the thread-safety note below) before callinggetOrCreateOutEdgeChunk/getOrCreateInEdgeChunk, and route throughaddGroupThroughStripedEdgeListwhen found.getOrCreateOutEdgeChunk/getOrCreateInEdgeChunkthemselves are left unchanged (noStripeDirectoryawareness) since the promoted case is now fully handled by their callers.addGroupThroughStripedEdgeList: constructs aStripedEdgeListover the resolved vertex + direction + directory and calls itsadd()once per buffered edge in the group.supernodeThreshold=0workaround.Thread-safety pitfall caught during development
The first version of this fix used the shared
lastSegmentPromotedinstance-field pattern forgetOrCreateOutEdgeChunk/getOrCreateInEdgeChunktoo, mirroring the sequential-path methods. That is wrong: those two methods are also called fromconnectOutEdgesRangeLocal/connectIncomingEdgesRangeLocal, which the parallel flush path dispatches to the async executor - multiple buckets run concurrently on different threads. A shared mutable instance field read immediately after being set raced across threads and silently attributed one group's promoted vertex to a different, unrelated group, inflating an edge count by one in testing. Fixed by keeping the parallel path's promotion check entirely in local variables, never touching shared state. See the regression test's parallel-flush variant, which reproduces both the originalClassCastException(fix reverted) and this thread-safety bug (during development, not present in the final diff).Tests
engine/src/test/java/com/arcadedb/graph/Issue5667GraphBatchSuperNodeResumeTest.java(new):sequentialFlushResumesOverPromotedHub- pre-promotes a hub's OUT and IN lists via the standard API, then bulk-loads one more edge in each direction withparallelFlush(false). Verifies no exception, both edges land, both lists remainStripeDirectory.parallelFlushResumesOverPromotedHub- same scenario withparallelFlush(true)(the default), exercising the async range-local path.Both tests were verified to fail with the pre-fix code: the sequential test reproduces the documented
IllegalStateException, the parallel test reproduces the previously-undocumentedClassCastException(StripeDirectory->EdgeSegment).Verification run
mvn -q -pl engine -am compile/test-compile: clean.Issue5667GraphBatchSuperNodeResumeTest- 2/2 pass.GraphBatchTest,GraphBatchCommitRetryTest,GraphBatchUniqueIndexTest,GraphBatchWALRestoreTest,SuperNodeStripingTest,SuperNodeDefaultThresholdTest,SuperNodeBothSizeQueryTest,Issue5147SuperNodeChunkRaceTest,Issue5666ConcurrentGraphBatchTest- 47/47 pass.com.arcadedb.graph.*Testsweep (-DexcludedGroups=slow,benchmark) - 208/208 pass, 0 failures/errors.Test plan
Issue5667GraphBatchSuperNodeResumeTest(sequential and parallel flush variants) - both verified to fail against the pre-fix code (IllegalStateException/ClassCastExceptionrespectively) and pass against the fix.mvn -q -pl engine -am compileandtest-compileclean.com.arcadedb.graph.*Testsweep (-DexcludedGroups=slow,benchmark) - 208/208 pass, 0 failures/errors.Review cycle 1
claude[bot]reviewed and confirmed the core fix is sound (traced the neighbour-vertex argument through all four call sites againstEdgeLinkedList.add()'s expected semantics, confirmed the thread-safety fix is correct). "Nothing here blocks merging" - all notes below are optional follow-up, not requested changes, so none were applied in this cycle:GraphBatchinstance is running could have its promotion missed by the RID cache /knownNewVertexKeysfast path. Out of scope per this PR's own stated limits ("promoted before this bulk load ran").groupSize == 1) against a promoted vertex; a multi-edge-group test would additionally coverStripedEdgeList.add()'s stripe-rollover branches.instanceof StripeDirectoryon every flush group rather than caching "this vertex is promoted" - consistent with the PR's documented "ordinary (non-bulk) speed" trade-off, not treated as a bug.getOrCreateOutEdgeChunkExact/getOrCreateInEdgeChunkExactare unused dead code, unrelated to this PR.