Summary
Today ConcurrentLookupTable.updateLeft/updateRight (src/main/java/lookup/ConcurrentLookupTable.java, lines ~44-84) perform an unconditional blind write of a neighbor slot and return the previous occupant — they perform no ordering check and no inspection of the current occupant. Because the table does no ordering verification of its own, join has to enforce ordering from the outside: it acquires a separate distributed insertion lock across every level (with random-backoff retry, SkipNode.insert/acquireNeighborLocks, src/main/java/skipnode/SkipNode.java ~70-233), reads a neighbor with a getLeftNeighborOf/getRightNeighborOf RPC, and only then issues a separate write RPC (announceNeighbor, or updateLeftNode/updateRightNode) that blindly overwrites the slot. The ordering decision and the write are distinct steps on distinct nodes, so cross-thread correctness rests entirely on holding that distributed lock rather than on the table.
Introduce a single-critical-section accept-or-forward write on the lookup table that folds the read, the ordering check, and the set into one writeLock() section: inspect the current slot, verify the candidate's ordering against the table owner, then either set the slot (accept) or leave it and report the current occupant as the next hop (forward). Add a relink variant that additionally reports the already-consistent (no-op) and evicted-neighbor cases.
This is the table-layer foundation for lock-free splicing during join and for periodic backpointer repair. Grounding: Aspnes & Shah, "Skip Graphs" (arXiv:cs/0306043) — Algorithm 2 (insert/join) and Algorithm 8 (Section 5.2.1, periodic backpointer repair). In a lock-free join a neighbor receiving a link request verifies ordering against its own table and either accepts the link or forwards the request to the correct neighbor; this primitive is that verify-and-set step made atomic at the table.
Scope
src/main/java/lookup/LookupTable.java — add to the interface (1) an accept-or-forward write method and (2) a relink variant, each taking Direction, a level, and a candidate SkipNodeIdentity. Add a small result type carrying an outcome and the relevant neighbor identity. Outcomes: ACCEPTED (slot set; previous occupant returned), FORWARD (slot unchanged; current occupant returned as next hop), ALREADY_CONSISTENT (candidate already present; no write), EVICTED (a different non-empty neighbor was displaced; returned for repair). Reuse the existing Direction enum. (Names above are illustrative.)
src/main/java/lookup/ConcurrentLookupTable.java — implement both methods within one writeLock() critical section: read the current slot, decide ordering against the stored owner using Identifier.comparedTo (a valid left neighbor sits below the owner and the closer one wins; right is the mirror), then set-or-forward atomically and return the outcome plus the relevant neighbor. Leave existing updateLeft/updateRight/getLeft/getRight in place.
src/test/java/lookup/ConcurrentLookupTableTest.java — extend with unit tests covering every outcome branch in both directions, plus a multi-threaded test that drives the primitive concurrently and asserts the slot converges to a single correctly-ordered neighbor with no lost writes.
Acceptance Criteria
Dependencies
No prerequisites — this is a self-contained lookup-table change. It is the foundation that the lock-free join splice and the periodic backpointer-repair sweep build on. Relates to #25.
Part of #33. Depends on: none — ready to start
Summary
Today
ConcurrentLookupTable.updateLeft/updateRight(src/main/java/lookup/ConcurrentLookupTable.java, lines ~44-84) perform an unconditional blind write of a neighbor slot and return the previous occupant — they perform no ordering check and no inspection of the current occupant. Because the table does no ordering verification of its own, join has to enforce ordering from the outside: it acquires a separate distributed insertion lock across every level (with random-backoff retry,SkipNode.insert/acquireNeighborLocks,src/main/java/skipnode/SkipNode.java~70-233), reads a neighbor with agetLeftNeighborOf/getRightNeighborOfRPC, and only then issues a separate write RPC (announceNeighbor, orupdateLeftNode/updateRightNode) that blindly overwrites the slot. The ordering decision and the write are distinct steps on distinct nodes, so cross-thread correctness rests entirely on holding that distributed lock rather than on the table.Introduce a single-critical-section accept-or-forward write on the lookup table that folds the read, the ordering check, and the set into one
writeLock()section: inspect the current slot, verify the candidate's ordering against the table owner, then either set the slot (accept) or leave it and report the current occupant as the next hop (forward). Add a relink variant that additionally reports the already-consistent (no-op) and evicted-neighbor cases.This is the table-layer foundation for lock-free splicing during join and for periodic backpointer repair. Grounding: Aspnes & Shah, "Skip Graphs" (arXiv:cs/0306043) — Algorithm 2 (insert/join) and Algorithm 8 (Section 5.2.1, periodic backpointer repair). In a lock-free join a neighbor receiving a link request verifies ordering against its own table and either accepts the link or forwards the request to the correct neighbor; this primitive is that verify-and-set step made atomic at the table.
Scope
src/main/java/lookup/LookupTable.java— add to the interface (1) an accept-or-forward write method and (2) a relink variant, each takingDirection, a level, and a candidateSkipNodeIdentity. Add a small result type carrying an outcome and the relevant neighbor identity. Outcomes:ACCEPTED(slot set; previous occupant returned),FORWARD(slot unchanged; current occupant returned as next hop),ALREADY_CONSISTENT(candidate already present; no write),EVICTED(a different non-empty neighbor was displaced; returned for repair). Reuse the existingDirectionenum. (Names above are illustrative.)src/main/java/lookup/ConcurrentLookupTable.java— implement both methods within onewriteLock()critical section: read the current slot, decide ordering against the storedownerusingIdentifier.comparedTo(a valid left neighbor sits below the owner and the closer one wins; right is the mirror), then set-or-forward atomically and return the outcome plus the relevant neighbor. Leave existingupdateLeft/updateRight/getLeft/getRightin place.src/test/java/lookup/ConcurrentLookupTableTest.java— extend with unit tests covering every outcome branch in both directions, plus a multi-threaded test that drives the primitive concurrently and asserts the slot converges to a single correctly-ordered neighbor with no lost writes.Acceptance Criteria
writeLock()section with no intermediate lock release; the primitive contains nogetLeft/getRight-then-updateLeft/updateRighttwo-step.ACCEPTED(slot set to candidate, previous occupant returned) when the candidate belongs in the slot, andFORWARD(slot unchanged, current occupant returned as next hop) when the candidate belongs farther from the owner. An empty slot always accepts.ALREADY_CONSISTENTwhen the slot already holds the candidate (no write performed) andEVICTED, returning the displaced neighbor, when accepting overwrites a different non-empty neighbor.Identifier.comparedTo; behavior is symmetric forLEFTandRIGHT.make test(mvn clean install && mvn test) andmake lint(mvn checkstyle:checkstyle) pass; total diff including tests is ≤ 200 lines.Dependencies
No prerequisites — this is a self-contained lookup-table change. It is the foundation that the lock-free join splice and the periodic backpointer-repair sweep build on. Relates to #25.
Part of #33. Depends on: none — ready to start