Skip to content

[Refactor] Add accept-or-forward table write primitive #34

Description

@thep2p

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

  • The interface gains the accept-or-forward method, the relink variant, and the result/outcome type, each documented with Javadoc.
  • Both methods perform read → ordering check → set entirely inside a single writeLock() section with no intermediate lock release; the primitive contains no getLeft/getRight-then-updateLeft/updateRight two-step.
  • Accept-or-forward returns ACCEPTED (slot set to candidate, previous occupant returned) when the candidate belongs in the slot, and FORWARD (slot unchanged, current occupant returned as next hop) when the candidate belongs farther from the owner. An empty slot always accepts.
  • The relink variant additionally returns ALREADY_CONSISTENT when the slot already holds the candidate (no write performed) and EVICTED, returning the displaced neighbor, when accepting overwrites a different non-empty neighbor.
  • Ordering decisions use Identifier.comparedTo; behavior is symmetric for LEFT and RIGHT.
  • Unit tests exercise every outcome branch in both directions and pass; a multi-threaded test demonstrates no lost/torn writes and a consistently-ordered final slot.
  • make test (mvn clean install && mvn test) and make 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions