Sdstor 22886 - #2
Conversation
|
@copilot review this pr |
There was a problem hiding this comment.
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 tohome_blocks.hpp:203-205andcraft_repl_dev.hpp:161-162. Reject the dLSN undermissing_mu_before callingjournal_->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.
| for (int64_t lsn : empty_slots) { | ||
| empty_lsns_.insert(lsn); | ||
| missing_lsns_.erase(lsn); | ||
| } |
| for (auto& slot : *fetched) { | ||
| if (slot.is_empty) { | ||
| std::lock_guard lk{missing_mu_}; | ||
| empty_lsns_.insert(slot.lsn); | ||
| missing_lsns_.erase(slot.lsn); |
| detail::detach(owner_->apply_sync_rs_commit_lsn(payload->rs_commit_lsn, payload->client_token, | ||
| std::move(*empty_slots))); |
There was a problem hiding this comment.
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.
Addressed the three issues flagged in the automated review (commit
|
fc6827d to
6cfe388
Compare
There was a problem hiding this comment.
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_lsneven when there is no fetcher, fetching fails, orwrite_slotfails, while unresolved LSNs remain inmissing_lsns_. That contradicts the repository's definition ofcommit_lsnas 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_tokencomes from the new payload buttermremains 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); assigntermdirectly, 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);
| detail::detach(owner_->apply_sync_rs_commit_lsn(payload->rs_commit_lsn, payload->client_token, | ||
| std::move(*empty_slots))); |
6cfe388 to
9dd260f
Compare
…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.
d29e9dc to
6f88b8e
Compare
f322ca7 to
7253e77
Compare
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.
6f88b8e to
94ddacf
Compare
…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>
cd23823 to
05b5efa
Compare
No description provided.