Skip to content

craft: S5 RAFT entry apply — SyncRSCommitLSN + InternalLogin (SDSTOR-22886, SDSTOR-22887) - #176

Open
sbinmalek wants to merge 12 commits into
eBay:dev/v6.xfrom
sbinmalek:SDSTOR-22886
Open

craft: S5 RAFT entry apply — SyncRSCommitLSN + InternalLogin (SDSTOR-22886, SDSTOR-22887)#176
sbinmalek wants to merge 12 commits into
eBay:dev/v6.xfrom
sbinmalek:SDSTOR-22886

Conversation

@sbinmalek

@sbinmalek sbinmalek commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the apply side of both CRAFT S5 RAFT entries on top of the S5 infrastructure merged in #172:

  • SyncRSCommitLSN (SDSTOR-22886): on_commit dispatches to apply_sync_rs_commit_lsn, which gates
    the whole apply on a client_token match, range-validates empty_slots against rs_commit_lsn,
    reconciles them into empty_lsns_/missing_lsns_, and catches up any local gap via
    CraftPeerFetcher::fetch_data. Catch-up is best-effort: a failed fetch, a malformed/unrequested-LSN
    response, a failed write_slot, or no fetcher wired at all just leaves the LSN in missing_lsns_ for a
    later attempt. commit_lsn/last_append_lsn always advance afterward, never regress.
  • InternalLogin (SDSTOR-22887): apply_internal_login overwrites client_token unconditionally
    (opaque id, no ordering) and advances term with a max-guard against regression, enforcing
    single-writer exclusivity through the existing term-fence check in write().
  • Bounds the peer catch-up fetch with a configurable deadline (peer_fetch_timeout_ms, default 5000ms,
    home_blks_config.fbs) so an unresponsive peer can't hang apply_sync_rs_commit_lsn forever.
  • Fixes a use-after-free in the detached on_commit coroutine: CraftReplDev now derives from
    std::enable_shared_from_this, is constructed only via a new CraftReplDev::create() factory (private
    constructor), and apply_sync_rs_commit_lsn captures shared_from_this() so the object stays alive
    across every co_await even if the last external owner drops its shared_ptr mid-apply.
  • Drops the redundant get_lsns() alias (get_rs_commit_lsn() already covered the identical snapshot)
    and switches empty_lsns_ to unordered_set (no ordering requirement).

Known gaps (tracked, not fixed here)

  • on_commit still detaches the apply coroutine fire-and-forget, so a later-committed entry can dispatch
    before this one's effects are fully applied — breaks strict RAFT apply ordering across entries. Real fix
    needs a per-device serialized apply queue (flagged as a FIXME in on_commit).
  • CraftPeerFetcher is still unwired to a real transport — production peer catch-up stays stubbed until
    CraftConnector (S9).
  • The checkpoint trigger (SDSTOR-22888) is left for a follow-up.

Test plan

  • test_craft_raft_entries.cpp (new): client_token gate, empty_slots range validation +
    reconciliation, watermark advance (incl. never-decrements), best-effort catch-up (success, fetch
    failure, configured timeout threading, malformed/duplicate peer response, write_slot failure, no
    fetcher wired), on_commit dispatch for both entry types incl. malformed-entry rejection,
    InternalLogin session replacement + term monotonicity + write() term-fencing end-to-end.
  • test_craft_peer_exchange.cpp / test_craft_truncate.cpp / test_craft_write.cpp updated for the
    CraftReplDev::create() factory; otherwise unchanged.
  • conan create . full build + ctest.

sbinmalek and others added 7 commits September 1, 2026 12:05
  Implement the apply side of the SyncRSCommitLSN RAFT entry: on_commit
  now parses the entry header/key and dispatches to
  apply_sync_rs_commit_lsn, which reconciles empty_slots, catches up
  missing journal data from a peer, and advances the commit_lsn/
  last_append_lsn watermarks. InternalLogin dispatch and apply
  (SDSTOR-22887) and the checkpoint trigger (SDSTOR-22888) are deliberately
  left as stubs for follow-up PRs.

  - on_commit: validates header/key blob sizes, parses CraftEntryType and
    the SyncRSCommitLSNPayload fixed prefix + empty_slots, and detaches
    apply_sync_rs_commit_lsn as fire-and-forget (on_commit is a
    synchronous HomeStore callback; apply needs to co_await peer fetch +
    journal writes). Logs and no-ops on an unrecognized entry type.
  - apply_sync_rs_commit_lsn: a client_token mismatch gates the entire
    apply (no reconciliation, no catch-up, no watermark advance).
    Otherwise, empty_slots are reconciled into empty_lsns_/missing_lsns_,
    the newly-spanned range is marked missing, and catch-up via
    CraftPeerFetcher::fetch_from_peer + CraftJournalBackend::write_slot is
    best-effort: a failed fetch, a failed write, or no peer_fetcher_ wired
    at all just leaves the affected LSNs in missing_lsns_ for a later
    attempt. commit_lsn/last_append_lsn advance unconditionally afterward
    (never decrement), mirroring truncate()'s existing invariant.
  - Add volume_error::WRONG_TOKEN for the client_token-mismatch case.
  - Add a _PRERELEASE-only test_listener() accessor so tests can drive
    on_commit directly.
  - New test_craft_raft_entries.cpp (with a MockCraftPeerFetcher) covering
    the token gate, empty_slots reconciliation, watermark advance
    (including never-decrements), best-effort catch-up (success, fetch
    failure, write failure, unwired fetcher), and on_commit dispatch
    including malformed-entry rejection.
  - guard CraftRaftEntriesTest friend decl with #ifdef _PRERELEASE
  - rename OnCommitLogsUnrecognizedEntryType -> OnCommitIgnoresUnrecognizedEntryType
  - add tests: mismatched empty_slots count via on_commit, empty_slots
    overlapping the same apply's new gap range
  - reject the whole apply (new volume_error::INVALID_ENTRY) if
    empty_slots has a negative LSN or one above rs_commit_lsn
  - validate a peer's fetch_data response against what was requested;
    discard the whole batch on an unrequested/duplicate lsn
  - document the known use-after-free gap in the detached
    apply_sync_rs_commit_lsn coroutine (not fixed yet)
  - add tests for both validations
  - implement apply_internal_login: overwrite client_token, max-guard
    term against regression; synchronous, called directly from
    on_commit (no detail::detach -- no I/O to await)
  - wire on_commit's InternalLogin dispatch with an exact-size key
    check (no variable trailing data, unlike SyncRSCommitLSN)
  - fix write()'s pre-existing unlocked read of state_.term -- latent
    until now since nothing mutated it; this ticket arms the race
  - add client_token()/term() observability accessors
  - add tests: dispatch success/wrong-size, second-login replaces
    session, term-never-regresses vs token-always-overwrites, write()
    term-fencing end-to-end, and cross-entry-type interaction with
    apply_sync_rs_commit_lsn's token check
…sns_

-  get_rs_commit_lsn() already covered the same snapshot; empty_lsns_ doesn't need ordering.
…timeout

CraftPeerFetcher::fetch_from_peer() had no deadline, so an unresponsive peer
could hang apply_sync_rs_commit_lsn's catch-up path forever. Adds
peer_fetch_timeout_ms (home_blks_config.fbs, default 5000ms) as a
CraftReplDev member with a setter, threaded through to fetch_from_peer's new
timeout_ms parameter -- kept off the global config singleton so the standalone
craft test binaries (which don't link homeblocks_core) still build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d_ptr ownership

- CraftReplDev now extends std::enable_shared_from_this; apply_sync_rs_commit_lsn opens
  with `auto self = shared_from_this()` so the detached coroutine holds a strong reference
  across every co_await, keeping CraftReplDev alive even if the last external owner (e.g. a
  volume-removal path) drops its shared_ptr mid-apply. Closes the KNOWN GAP flagged in
  review (PR #2, discussion r3761568811).
- CraftReplDev's constructor is now private; construction only via the new
  CraftReplDev::create() factory, so shared_from_this()'s "must already be shared_ptr-owned"
  precondition is enforced by the compiler instead of a comment.
- Update the four craft test fixtures from make_unique/unique_ptr to
  CraftReplDev::create()/shared_ptr.
@sbinmalek sbinmalek changed the title SDSTOR-22886 craft: S5 RAFT entry apply — SyncRSCommitLSN + InternalLogin (SDSTOR-22886, SDSTOR-22887) Sep 1, 2026
@codecov-commenter

codecov-commenter commented Sep 1, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 71.11111% with 13 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (dev/v6.x@8aed11c). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/lib/craft/craft_repl_dev.cpp 71.42% 2 Missing and 8 partials ⚠️
src/lib/craft/craft_repl_dev.hpp 70.00% 0 Missing and 3 partials ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@             Coverage Diff             @@
##             dev/v6.x     #176   +/-   ##
===========================================
  Coverage            ?   47.62%           
===========================================
  Files               ?       19           
  Lines               ?     1096           
  Branches            ?      473           
===========================================
  Hits                ?      522           
  Misses              ?      261           
  Partials            ?      313           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

// std::make_error_condition(std::errc::*) directly rather than duplicated here.
ENUM(volume_error, uint16_t, UNKNOWN_VOLUME = 1, CRC_MISMATCH, INDEX_ERROR, INTERNAL_ERROR, OFFLINE, STALE_TERM,
EMPTY_SLOT);
EMPTY_SLOT, WRONG_TOKEN, INVALID_ENTRY);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raakella1 PTAL

if (!res) {
LOGE("apply_sync_rs_commit_lsn: write_slot failed lsn={}: {} -- leaving as missing", slot.lsn,
res.error().message());
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Block leak: if alloc_write_data succeeded but write_slot fails here, blkid holds
allocated HomeStore data blocks that nobody will ever free. The slot stays in
missing_lsns_, so the next SyncRSCommitLSN will re-fetch it and allocate a new
blkid — but the original one is already gone from any tracking structure.

The write() path (S3) calls journal_->free_data(blkid) in the equivalent failure arm.
Same guard is needed here:

    if (!res) {
        LOGE(...);
        if (!slot.all_zeros)
            journal_->free_data(blkid);   // reclaim the blocks we just allocated
        continue;
    }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Step 1 — SyncRSCommitLSN arrives, replica is behind.

apply_sync_rs_commit_lsn(rs_commit_lsn=3, client_token=0, empty_slots=[])
→ missing_lsns_ = {1, 2, 3}
→ to_fetch = [1, 2, 3]

Step 2 — fetch_data returns slot 2 with real data.

fetcher_.response = { JournalSlot{.lsn=2, .lba_off_bytes=4096, .len_bytes=4096} }

Step 3 — catch-up loop processes slot 2.

// slot.all_zeros = false → allocate blocks
auto alloc_res = co_await journal_->alloc_write_data(slot.data, slot.len_bytes);
// alloc_res = OK, blkid = B1 ← real HomeStore blocks allocated here

blkid = B1;

auto res = co_await journal_->write_slot(2, term, 4096, 4096, B1, false);
// write_slot fails → res = error (disk I/O error, log full, whatever)

if (!res) {
LOGE("write_slot failed lsn=2 ...");
continue; // ← B1 is dropped here, never passed to free_data()
}

Result: B1 sits in HomeStore's block allocator permanently. lsn=2 stays in missing_lsns_. The next SyncRSCommitLSN triggers another catch-up, fetches slot 2 again, allocates B2 — but B1 is already gone from all tracking. Repeat enough times and the data service bleeds blocks.


The fix is one line in the failure arm:

if (!res) {
    LOGE("write_slot failed lsn={} ...", slot.lsn, ...);
    if (!slot.all_zeros)
        journal_->free_data(blkid);   // reclaim B1 before losing the reference
    continue;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to address the case when blkid is not set. How would write_slot handle that?

for (int64_t lsn : empty_slots) {
empty_lsns_.insert(lsn);
missing_lsns_.erase(lsn);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AC (SDSTOR-22886) says: "mark every slot in empty_slots[] as Empty and discard any
local data held at those slots
." The in-memory set updates here cover the tracking
side, but if an LSN in empty_slots was already successfully written on this replica
(journaled via write() or a prior catch-up), the blkid at that journal slot is not freed.

The protocol invariant is that the leader should never verdict Empty an LSN that a client
successfully committed through CRAFT, so in a correctly-operating cluster this case won't
arise. But the AC is explicit about it and it is worth either:

(a) reading the journal slot and calling journal_->free_data(blkid) if one exists, or
(b) adding a comment here acknowledging the gap and filing a follow-up JIRA so it is
not silently absent from the next reviewer's view.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read_slot is async so it can't run under missing_mu_. The fix is a two-phase
approach: collect LSNs that had live data while holding the lock, then free them after
releasing it. Sketch:


    std::vector< int64_t > to_free;
    {
        std::lock_guard lk{missing_mu_};
        for (int64_t lsn : empty_slots) {
            // Not missing and not already empty → was successfully written on this replica.
            // The leader has now verdicted it Empty; discard the local copy.
            if (!missing_lsns_.contains(lsn) && !empty_lsns_.contains(lsn))
                to_free.push_back(lsn);
            empty_lsns_.insert(lsn);
            missing_lsns_.erase(lsn);
        }
        // ... rest of existing lock body unchanged
    }

    // Free existing journal blocks for slots now verdicted Empty.
    // Runs outside the lock because read_slot co_awaits.
    for (int64_t lsn : to_free) {
        if (auto slot_r = co_await journal_->read_slot(lsn);
                slot_r && !slot_r->all_zeros) {
            co_await journal_->free_data(slot_r->blkid);
        }
    }

The protocol invariant is that the leader can only verdict Empty an LSN that no client
successfully committed, so to_free should be empty in correct operation. But the AC
requires handling it, and the block leak on the wrong-invariant path is real.

example:
Step 1 — Write arrives and is journaled successfully.

write(dlsn=1, token=100, term=1) succeeds
→ journal slot 1 written, blkid = B1 (real HomeStore blocks allocated)
→ missing_lsns_ = {} (1 was written, not missing)

Step 2 — Network partition. Replica is isolated briefly. New leader elected.

apply_internal_login(token=200, term=2)
→ state_.client_token = 200, state_.term = 2

Step 3 — SyncRSCommitLSN arrives from the new session.

The new leader says: "in session B's view, lsn=1 was never committed — it was from the old session and the quorum rejected it. lsn=1 is Empty."

apply_sync_rs_commit_lsn(rs_commit_lsn=5, client_token=200, empty_slots=[1])

What happens inside the reconciliation loop:

for (int64_t lsn : empty_slots) { // lsn = 1
empty_lsns_.insert(lsn); // 1 → empty, fine
missing_lsns_.erase(lsn); // no-op, 1 wasn't missing
}
// lsn=1 is not in missing_lsns_, not in empty_lsns_ before this → was_written = true
// but no read_slot(1) → no free_data(B1)

Result: B1 — the HomeStore data blocks allocated for the session-A write — are never freed. The journal slot 1 is now shadowed as Empty in empty_lsns_, but the underlying blocks sit in the allocator permanently leaked.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK

…ots apply

- fetch_data: classify all requested LSNs under one missing_mu_ acquisition
  instead of re-locking per LSN.
- apply_sync_rs_commit_lsn: range-insert empty_slots into empty_lsns_
  instead of inserting one at a time.
@sbinmalek
sbinmalek marked this pull request as ready for review September 3, 2026 17:57
@sbinmalek
sbinmalek requested review from raakella1 and szmyd September 3, 2026 18:01
…p paths

- Empty-verdict reconciliation (to_free): an LSN that was in missing_lsns_
  and gets verdicted Empty by this SyncRSCommitLSN may still hold a locally
  written block from an earlier write() attempt. That block was never
  reclaimed -- only missing_lsns_ was cleared. Added
  CraftJournalBackend::free_slot(lsn), which reads the raw local journal
  entry back off the log store and frees the blkid it references (skipping
  all_zeros entries, which never allocated one) via the existing free_data.
  It bypasses read_slot/JournalSlot deliberately: that type is wire-shared
  with craft::JournalSlot for peer fetch_data responses and carries no
  blkid (meaningless to a remote peer), so it can't serve this local-only
  need.
- Peer-catchup write_slot failure: alloc_write_data can succeed and then
  write_slot fail, leaving an allocated block referenced by nothing. This
  path had no cleanup at all. Now frees it, guarded by blkid_allocated so
  all_zeros slots (which never allocate) aren't passed to free_data --
  mirroring the guard write() already has. The free itself is dispatched
  via detail::detach() as its own coroutine capturing `self` (not just
  journal_), since it can outlive the enclosing apply_sync_rs_commit_lsn
  coroutine, which may return -- and drop its own `self` -- first.
- Added free_slot to the four MockCraftJournalBackend test doubles; factored
  the now-duplicated read_slot/free_slot bodies (identical across
  test_craft_write.cpp, test_craft_raft_entries.cpp, and
  test_craft_peer_exchange.cpp) into a new mock_journal_backend.hpp.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@szmyd szmyd left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blocking issues on the apply path. Both are semantic rather than mechanical: each inverts a rule CRAFT-Design states explicitly, and in both cases craft_client's reference replica already implements it the other way, so there is something concrete to copy from rather than a judgment call to make.

The rest of what I found on this path -- the blocking read_sync that free_slot runs on the commit thread, the dangling blkid it leaves behind, the inverted to_free condition, and the uncapped gap loop -- I'll hold until these two settle, since resolving them changes the shape of that code anyway.

Comment thread src/lib/craft/craft_repl_dev.cpp Outdated
uint64_t term;
{
std::lock_guard lk{missing_mu_};
if (client_token != state_.client_token) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gate vetoes the login's own SyncRSCommitLSN on every replica.

Login applies SyncRSCommitLSN before InternalLogin -- Phase 2 then Phase 3 in CRAFT-Design's login sequence, and the same order in craft_client's reference transport (src/mem/cluster.cpp:352-361):

// Phase 2: SyncRSCommitLSN(rs) applied to all live members.
for (auto* r : live) r->cold_apply_sync(rs, client_token);
// Phase 3: InternalLogin(token, term+1) applied to all live members.
for (auto* r : live) r->cold_apply_login(client_token, nt);

The reference discards the parameter deliberately: void MemCraftReplica::cold_apply_sync(int64_t rs_commit_lsn, uint64_t /*client_token*/).

So at Phase 2 apply time state_.client_token is still the previous session's token, or 0 on a member that has never had one. The gate mismatches every time, and the entry it discards is the one carrying Phase 1b's Empty verdicts and driving every member to rs_commit_lsn. Per the read-eligibility deep-dive, "login can't finish until >= 1 member is filled to L" -- this makes that unreachable, on a fresh cluster included.

A second reachable case with the same root: state_ is in-memory only, so a restarted replica comes back with client_token == 0 and vetoes every subsequent watchdog SyncRSCommitLSN. It never catches up, and nothing says so louder than a LOGW.

"Session verification" in the glossary is not an equality fence against local state. InternalLogin is what establishes the session, so a fence requiring the session to already exist cannot be applied to the entry that precedes it. Either drop the check (the reference's choice), or accept the login-path entry explicitly and let the term fence do the exclusion work -- which is the argument apply_internal_login's own header comment already makes for itself.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped the check, matching craft_client's reference (MemCraftReplica::cold_apply_sync discards the client_token parameter outright) — same option you called out. Exclusivity now comes from RAFT's commit ordering (SyncRSCommitLSN always applies before the InternalLogin that would invalidate it) plus the term fence every other IO already checks, per apply_internal_login's own header comment.

Also found docs/craft/subtasks.md and docs/craft/rpcs.md both documented the removed behavior ("on apply: verify token") — that wording was itself wrong/stale relative to craft_client's reference and the canonical CRAFT-Design wiki, not just the HomeBlocks code. Corrected in 05b5efa.

Fixed in 12b68b9.

Comment thread src/lib/craft/craft_repl_dev.cpp Outdated
// the time this advance actually runs, breaking strict RAFT apply ordering.
{
std::lock_guard lk{missing_mu_};
state_.commit_lsn = std::max(state_.commit_lsn, rs_commit_lsn);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commit_lsn is advanced past unresolved Missing slots.

The comment above justifies this as a set-wide watermark RAFT already agreed on. That is rs_commit_lsn. commit_lsn is a different quantity, and CRAFT-Design defines it as the local contiguous prefix in three separate places:

  • Glossary: "commit_lsn (= Synced) -- The contiguous committed prefix on a replica (Empty slots skipped): everything <= it is present and applied. The fill watermark."
  • Resync: "Commit advances past Empty slots but never past an unresolved Missing one."
  • Read-eligibility deep-dive: "a member must be fully filled to L (Synced >= L) before it serves any read [...] Synced >= L is the same 'reach the watermark' gate as reconfig promotion (commit_lsn >= startLSN)."

craft_client's reference replica implements exactly that (src/mem/replica.cpp:358):

void MemCraftReplica::apply_up_to(int64_t target) {
    int64_t next = state_.commit_lsn + 1;
    while (next <= target) {
        auto it = journal_.find(next);
        if (it == journal_.end()) break; // Missing hole -> stall (best-effort)
        if (!it->second.is_empty) apply_slot(next, it->second);
        state_.commit_lsn = next; // Empty slots are skipped on apply but still advance the frontier
        ++next;
    }
}

Every catch-up failure path above is reachable today -- no peer_fetcher_ wired at all (the production case until S9), a fetch error, a rejected batch, a write_slot failure -- and each one leaves entries in missing_lsns_ below the watermark this line then advances past. Three consequences, none of them local to this function:

  1. commit_lsn is the read-eligibility gate below L. The replica advertises "filled to L" while holding holes, so the client routes prior-session reads to it and gets zeros for writes that were acked.
  2. all_committed_lsn is min(commit_lsn) over voting members, and it is the journal reclaim floor. One replica reporting a false commit_lsn lets the whole set reclaim below it, deleting the last copies of the slots that replica still needs to fetch.
  3. get_rs_commit_lsn() (line 233) hands commit_lsn to the next login as a contiguity certificate -- per the Login section it "bounds the leader's fetch-or-verdict work to (min commit, rs_commit]". Falsely advanced, the leader never resolves those slots at all. That is the false-exclude direction the recovery-watermark deep-dive exists to rule out, and it is the one the design calls catastrophic.

Advance to min(rs_commit_lsn, first unresolved missing LSN - 1) with Empty skipped, and keep rs_commit_lsn separately if the catch-up target is still needed after that. Note test_craft_raft_entries.cpp:25-26 currently asserts the present behavior ("a failed fetch, a failed write_slot, or no peer_fetcher_ at all still lets commit_lsn advance"), so those move with the fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced the unconditional max() with a walk-forward loop that stalls at the first unresolved Missing slot (skipping Empty ones), mirroring craft_client's reference apply_up_to — exactly the min(rs_commit_lsn, first unresolved missing LSN - 1) semantics you described.

Updated test_craft_raft_entries.cpp:25-26 and the 6 assertions in scope, plus one more I found during review (OnCommitDispatchesSyncRSCommitLSN, which goes through the real on_commit dispatch path with no peer_fetcher_ wired, so commit_lsn can't reach the value it used to assert either).

Same doc pages as the other thread (docs/craft/subtasks.md, docs/craft/rpcs.md) also said "commit_lsn = rs_commit_lsn" — that discrepancy is fixed alongside the token-check wording in 05b5efa, since both sentences shared the same lines.

Fixed in 12b68b9.

sbinmalek added a commit to sbinmalek/HomeBlocks that referenced this pull request Sep 4, 2026
…s_commit_lsn

Addresses two blocking review comments on PR eBay#176 (szmyd):

- The client_token != state_.client_token gate vetoed the login sequence's own
  SyncRSCommitLSN: per CRAFT-Design, SyncRSCommitLSN applies before the
  InternalLogin that establishes client_token, so the check always mismatched
  on login (and on every post-restart watchdog SyncRSCommitLSN, since state_
  is in-memory-only). Dropped the check, matching craft_client's reference
  (MemCraftReplica::cold_apply_sync discards the parameter outright).
  Exclusivity comes from RAFT's commit ordering plus the term fence every
  other IO already checks.

- commit_lsn was advancing unconditionally to rs_commit_lsn regardless of
  local catch-up outcome, conflating it with the replica-set-wide watermark.
  CRAFT-Design defines commit_lsn as the local contiguous prefix: skip Empty
  slots, but never advance past an unresolved Missing one. Replaced the
  unconditional max() with a walk-forward loop mirroring craft_client's
  reference apply_up_to.

Updated test_craft_raft_entries.cpp accordingly: repurposed the two tests
that asserted the old token-gate behavior into regression guards for the new
behavior, and corrected 7 commit_lsn assertions (6 from the review scope plus
one found during review, OnCommitDispatchesSyncRSCommitLSN) to the new
stall-at-first-missing semantics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sbinmalek and others added 2 commits September 4, 2026 13:39
…s_commit_lsn

Addresses two blocking review comments on PR eBay#176 (szmyd):

- The client_token != state_.client_token gate vetoed the login sequence's own
  SyncRSCommitLSN: per CRAFT-Design, SyncRSCommitLSN applies before the
  InternalLogin that establishes client_token, so the check always mismatched
  on login (and on every post-restart watchdog SyncRSCommitLSN, since state_
  is in-memory-only). Dropped the check, matching craft_client's reference
  (MemCraftReplica::cold_apply_sync discards the parameter outright).
  Exclusivity comes from RAFT's commit ordering plus the term fence every
  other IO already checks.

- commit_lsn was advancing unconditionally to rs_commit_lsn regardless of
  local catch-up outcome, conflating it with the replica-set-wide watermark.
  CRAFT-Design defines commit_lsn as the local contiguous prefix: skip Empty
  slots, but never advance past an unresolved Missing one. Replaced the
  unconditional max() with a walk-forward loop mirroring craft_client's
  reference apply_up_to.

Updated test_craft_raft_entries.cpp accordingly: repurposed the two tests
that asserted the old token-gate behavior into regression guards for the new
behavior, and corrected 7 commit_lsn assertions (6 from the review scope plus
one found during review, OnCommitDispatchesSyncRSCommitLSN) to the new
stall-at-first-missing semantics.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mit_lsn fixes

docs/craft/subtasks.md and docs/craft/rpcs.md both said the apply "verifies
token" and "commit_lsn = rs_commit_lsn" -- exactly the behavior removed in
the previous commit. Reworded both to describe the actual behavior:
client_token is carried on the entry but not checked against local state,
and commit_lsn advances to the contiguous prefix bounded by rs_commit_lsn,
skipping Empty slots but never past an unresolved Missing one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

4 participants