Skip to content

Dev/v6.x#167

Merged
szmyd merged 1 commit into
eBay:dev/v6.xfrom
szmyd:dev/v6.x
Jul 9, 2026
Merged

Dev/v6.x#167
szmyd merged 1 commit into
eBay:dev/v6.xfrom
szmyd:dev/v6.x

Conversation

@szmyd

@szmyd szmyd commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@szmyd
szmyd force-pushed the dev/v6.x branch 2 times, most recently from dd27272 to 4ce88c1 Compare July 9, 2026 14:02
Adds dlsn_tracker -- the client-side dLSN bookkeeping for one CRAFT partition --
and rewires craft_client onto it. It owns the dLSN counter, the contiguous
commit frontier F that client_hdr.commit_lsn piggybacks, and the per-read
horizon H.

The spine is sisl::StreamTracker keyed by dLSN. The load-bearing choice is that
its "completed" bit means RESOLVED (acked or empty), not "acked", which yields
the invariant "every slot <= F is resolved". So "is anything unresolved at or
below H?" collapses to "F < H": the common read is two atomic loads and never
touches the tracker. It also means a sub-quorum write pins F, which is exactly
the commit_lsn the protocol wants, since commit must not pass a slot the leader
has not yet filled or Emptied. Advancing F is completed_upto() over the
completion bitset, so the std::mutex around a std::set that used to fold acked
dLSNs on every ack is gone from the I/O path.

The horizon is a per-LBA property, and a read carries one. For each block, with
A the highest acked slot covering it and u* the lowest unresolved slot covering
it above A, that block is safe at H = u*-1: nothing unresolved remains at or
below it, and A < u*, so no acked write is hidden. With no such u* the block is
safe at the full horizon, because a replica serves the highest dLSN <= H per
LBA and an unresolved slot beneath a durable one is superseded -- it can never
be the version a read sees. When blocks in a range disagree, the read
partitions into segments issued in parallel, each landing in its own slice of
the caller's buffer via sisl::sg_iterator. That is CRAFT-Design.md's split
read, and it is why an unresolved write never blocks a read: a safe per-block
horizon always exists, so in_flight and failed slots fence identically.

Reads wait only in three pathological branches -- an index whose create() is not
yet visible, a window past the scan cap, or a plan wider than the fan-out cap --
on a re-armable resolution_gate whose Dekker interlock lets signal() skip its
allocation when nobody is stalled without losing a wakeup. A failed slot in one
of those branches fails instead of waiting, since nothing resolves it without a
leader resolution round.

Three bugs found while reading the client:

  - A recovery login seeded next_dlsn but left the horizon at -1, so every read
    carried H = -1 and returned zeros. reset_at() now seeds both watermarks from
    the set's rs_commit_lsn.
  - make_hdr() sent its own frontier as all_committed_lsn. That is an upper
    bound on the set-wide minimum, never the minimum, so a replica could reclaim
    journal a lagging peer still needs. It now sends -1 (unknown).
  - A follower login returns a value with term == 0 and leader_hint, not an
    error. The client turned that into a hard NOT_LEADER and discarded the hint;
    it now follows the redirect.

Requires the companion sisl commit "AtomicBitset: publish with release, observe
with acquire". The read path scans slots the write path is concurrently
creating, and the slot bit is the only thing that publishes the payload.

NOT included, and drawn explicitly in the new README: read routing. Every
segment goes to replicas_[leader_] and that peer's error is surfaced directly.
There is no failover, no eligibility check, and no per-replica Missing map. The
tracker answers which version a read may see; the Missing map answers which
replica can serve it. Only the first is built, so read() is correct only when
the leader is in the write quorum of every winner in the range.

Tests: test_craft_dlsn_tracker drives reserve/resolve directly (14 cases; links
without homestore::homestore) and ends with a concurrent reader/writer stress
meant for TSAN -- readers scan slots writers are creating, which reports ~12
races with relaxed bit ordering and none with release/acquire. test_craft_client
adds the end-to-end cases against the reference model, including one where the
leader physically holds a sub-quorum write spanning both blocks of a split read.
craft_test_util.hpp collects the scaffolding the four craft test binaries had
each copied.

Verified end to end: craft_ublk --replicas 3 exposes /dev/ublkb0; unwritten
blocks read as zeros, direct and buffered writes round-trip, overwrite takes,
8-block IO round-trips, FLUSH succeeds, and DISCARD is refused.

Also folds in a clang-format pass over src/test/craft, src/test/legacy_ublk and
src/lib/craft (no behaviour change), and fixes the version-bump CI check, which
globbed a src/tests/ directory that is actually named src/test/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@szmyd
szmyd merged commit 594f1eb into eBay:dev/v6.x Jul 9, 2026
24 of 25 checks passed
@szmyd
szmyd deleted the dev/v6.x branch July 9, 2026 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant