Skip to content

Sdstor 22886 - #2

Draft
sbinmalek wants to merge 13 commits into
craft_S5_basefrom
SDSTOR-22886
Draft

Sdstor 22886#2
sbinmalek wants to merge 13 commits into
craft_S5_basefrom
SDSTOR-22886

Conversation

@sbinmalek

Copy link
Copy Markdown
Owner

No description provided.

@sbinmalek

Copy link
Copy Markdown
Owner Author

@copilot review this pr

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Implements SyncRSCommitLSN RAFT entry dispatch, reconciliation, peer catch-up, and related tests.

Changes:

  • Parses and dispatches committed CRAFT RAFT entries.
  • Applies commit watermarks, Empty verdicts, and peer-fetched slots.
  • Adds error handling and comprehensive unit tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/lib/craft/craft_repl_dev.cpp Implements RAFT dispatch and synchronization apply logic.
src/lib/craft/craft_repl_dev.hpp Adds async apply signature and test accessors.
src/lib/craft/tests/test_craft_raft_entries.cpp Tests reconciliation, catch-up, and dispatch.
src/lib/craft/tests/CMakeLists.txt Registers the new test target.
src/include/homeblks/home_blocks.hpp Adds the WRONG_TOKEN error.
Suppressed comments (1)

src/lib/craft/craft_repl_dev.cpp:318

  • Populating empty_lsns_ does not enforce the documented “Empty beats data” rule on the write path. write() never checks this set, so a late write to an Empty-verdicted dLSN is still persisted and acknowledged, contrary to home_blocks.hpp:203-205 and craft_repl_dev.hpp:161-162. Reject the dLSN under missing_mu_ before calling journal_->write_slot, and cover that behavior in the write tests.
            empty_lsns_.insert(lsn);
            missing_lsns_.erase(lsn);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +316 to +319
for (int64_t lsn : empty_slots) {
empty_lsns_.insert(lsn);
missing_lsns_.erase(lsn);
}
Comment on lines +341 to +345
for (auto& slot : *fetched) {
if (slot.is_empty) {
std::lock_guard lk{missing_mu_};
empty_lsns_.insert(slot.lsn);
missing_lsns_.erase(slot.lsn);
Comment thread src/lib/craft/craft_repl_dev.cpp Outdated
Comment on lines +281 to +282
detail::detach(owner_->apply_sync_rs_commit_lsn(payload->rs_commit_lsn, payload->client_token,
std::move(*empty_slots)));

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Approach 1 — shared_ptr keep-alive

  class CraftReplDev : public std::enable_shared_from_this< CraftReplDev > { ... };                                                                                                                                                                                                                                                                  
                                                                                                                                                                                                                                                                                                                                                   
  async_status CraftReplDev::apply_sync_rs_commit_lsn(...) {                                                                                                                                                                                                                                                                                         
      auto self = shared_from_this();   // lives in the coroutine frame across every co_await                                                                                                                                                                                                                                                        
                                  // rest of the function unchanged                                                                                                                                                                                                                                                                            
  }
  • Every construction site (test_craft_raft_entries.cpp's fixture today, whatever S8 adds later) must switch from make_unique to make_shared.
  • Destruction is never blocked. If a volume is removed mid-apply, the object's other owners can drop their references immediately; the coroutine's own self copy keeps the memory alive until it finishes, then the object is freed normally.
  • Standard idiom for this exact problem (detached async work outliving its nominal owner) — same pattern asio/networking code uses.
  • Risk is purely mechanical: if a future call site ever constructs CraftReplDev without a shared_ptr (stack, make_unique, raw new) and that instance's apply_sync_rs_commit_lsn runs, shared_from_this() throws std::bad_weak_ptr immediately — loud and at the call site, not a silent corruption.

Approach 2 — Drain-on-destroy

  CraftReplDev::~CraftReplDev() {                                                                                                                                                                                                                                                                                                                    
      std::unique_lock lk{drain_mu_};                                                                                                                                                                                                                                                                                                                
      drain_cv_.wait(lk, [this] { return inflight_applies_.load() == 0; });                                                                                                                                                                                                                                                                          
  }                                                                                                                                                                                                                                                                                                                                                  

with an RAII guard incrementing/decrementing inflight_applies_ around the body of apply_sync_rs_commit_lsn.

  • No change to how CraftReplDev is constructed anywhere — unique_ptr stays fine.
  • But ~CraftReplDev() becomes a blocking call: whatever thread destroys the object (e.g. a volume-removal path) stalls until any in-flight fetch_from_peer/write_slot completes. Neither CraftJournalBackend nor CraftPeerFetcher has a timeout or cancellation today, so a slow/unresponsive peer during catch-up could hang that thread
    indefinitely.
  • Fixable by making it a co_await-able drain() method instead of a blocking destructor — but then it only helps if whoever writes S8's volume-destroy path remembers to call and await it first, which is the same "easy to forget" risk the first question already flagged for deferring outright.

Copilot AI commented Aug 11, 2026

Copy link
Copy Markdown

@copilot review this pr

Addressed the three issues flagged in the automated review (commit craft: fix review findings):

  1. empty_slots out-of-range (lines 316–319): LSNs that are ≤ 0 or > rs_commit_lsn are now filtered before insertion into empty_lsns_, preventing future fetch_data() from reporting phantom Empty verdicts. New test: EmptySlotsOutOfRangeAreIgnored.

  2. Untrusted peer response (lines 341–345): An expected-LSN set is built from to_fetch before processing any fetched slot; slots not in the request are silently dropped. This prevents a malformed or mismatched server response from writing arbitrary journal entries or corrupting empty_lsns_/missing_lsns_. New test: PeerResponseWithUnexpectedLsnIsIgnored.

  3. Use-after-free in detached coroutine (lines 281–282): CraftReplDev now inherits enable_shared_from_this<CraftReplDev>. In on_commit, shared_from_this() is captured in a wrapper lambda before the coroutine is detached, keeping the device alive across co_await suspension points. The test fixture was updated to use shared_ptr accordingly.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

src/lib/craft/craft_repl_dev.cpp:423

  • This advances commit_lsn even when there is no fetcher, fetching fails, or write_slot fails, while unresolved LSNs remain in missing_lsns_. That contradicts the repository's definition of commit_lsn as a contiguous applied prefix (docs/craft/README.md:35) and the S5 sequence that fetches missing slots before advancing it (docs/craft/subtasks.md:135); callers can therefore observe a synced watermark for data this replica does not hold. Keep the watermark behind the first unresolved LSN, retry catch-up, or mark the replica unhealthy instead of reporting success.
    // Unconditional: commit_lsn is a replica-set-wide watermark RAFT already agreed on, independent of
    // whether this replica's local catch-up succeeded.
    {
        std::lock_guard lk{missing_mu_};
        state_.commit_lsn = std::max(state_.commit_lsn, rs_commit_lsn);

src/lib/craft/craft_repl_dev.cpp:443

  • When a lower-term entry is encountered, this creates a session pair that was never committed: client_token comes from the new payload but term remains from the previous login. Subsequent I/O using the old term is then accepted while Sync token checks use the new token. The S5 contract applies both payload fields together (docs/craft/subtasks.md:139-142); assign term directly, or reject the entire stale pair atomically rather than mixing sessions.
    state_.client_token = client_token; // opaque id, no ordering semantics -- plain overwrite
    // term is RAFT-ordered in practice (the leader always proposes strictly increasing terms), but
    // guard against regression the same way commit_lsn/last_append_lsn already do rather than trusting
    // log order blindly.
    state_.term = std::max(state_.term, term);

Comment thread src/lib/craft/craft_repl_dev.cpp Outdated
Comment on lines +308 to +309
detail::detach(owner_->apply_sync_rs_commit_lsn(payload->rs_commit_lsn, payload->client_token,
std::move(*empty_slots)));

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Added FIXME comments

sbinmalek added a commit that referenced this pull request Aug 17, 2026
…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 force-pushed the craft_S5_base branch 3 times, most recently from f322ca7 to 7253e77 Compare August 31, 2026 22:19
sbinmalek and others added 8 commits August 31, 2026 16:44
craft: S5 infrastructure — RAFT entry types, CraftPeerFetcher, write_counter
  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 and others added 4 commits September 1, 2026 13:47
…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.
…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>
…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.

3 participants