Skip to content

craft: S4 truncate path — CraftReplDev::truncate#170

Merged
shosseinimotlagh merged 3 commits into
eBay:dev/v6.xfrom
shosseinimotlagh:craft_truncate
Jul 20, 2026
Merged

craft: S4 truncate path — CraftReplDev::truncate#170
shosseinimotlagh merged 3 commits into
eBay:dev/v6.xfrom
shosseinimotlagh:craft_truncate

Conversation

@shosseinimotlagh

Copy link
Copy Markdown
Contributor

Drop all journal entries with dLSN > lsn via HomeStore rollback(), clamp last_append_lsn, and erase missing-set entries above lsn. commit_lsn is not touched. A journal I/O failure short-circuits before state is mutated.

Introduces CraftJournalBackend::truncate_to() as the injectable seam so unit tests drive the path with MockCraftJournalBackend without HomeStore. HomeStoreCraftJournalBackend (wrapping home_log_store::rollback) is the production implementation; make_homestore_journal_backend() is the factory for callers that own a log store (volume.cpp, wired up in a later PR).

Observability accessors last_append_lsn(), commit_lsn(), missing_count(), and is_missing() are unconditional public methods usable in production. seed_lsns() is compiled only when _PRERELEASE is defined (set by the test target in CMake) so the symbol is absent from production binaries.

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

Adds the S4 “truncate” path to CraftReplDev by introducing an injectable journal-backend seam (CraftJournalBackend::truncate_to()), implementing a HomeStore-backed production backend, and adding a focused unit test that exercises rollback/clamp/missing-set behavior.

Changes:

  • Implement CraftReplDev::truncate(lsn) to rollback the journal, clamp last_append_lsn, and erase missing-set entries above lsn (without changing commit_lsn).
  • Introduce CraftJournalBackend::truncate_to() plus make_homestore_journal_backend() (HomeStore production implementation).
  • Add a new unit test target test_craft_truncate that compiles craft_repl_dev.cpp directly and drives truncate behavior with a mock backend.

Reviewed changes

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

Show a summary per file
File Description
src/lib/craft/tests/test_craft_truncate.cpp New unit tests covering truncate success cases and journal-failure no-mutation invariant.
src/lib/craft/tests/CMakeLists.txt Adds test_craft_truncate target and registers it with CTest.
src/lib/craft/craft_repl_dev.hpp Extends journal backend interface; adds observability helpers and prerelease-only seeding helper.
src/lib/craft/craft_repl_dev.cpp Implements HomeStore-backed journal backend and CraftReplDev::truncate().
src/lib/craft/CMakeLists.txt Adds craft tests subdirectory.
conanfile.py Bumps package version to 6.0.3.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/craft/craft_repl_dev.hpp Outdated

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 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread src/lib/craft/tests/test_craft_truncate.cpp
Comment thread src/lib/craft/craft_repl_dev.cpp

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 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread src/lib/craft/craft_repl_dev.cpp
Comment thread src/lib/craft/craft_repl_dev.hpp
@codecov-commenter

codecov-commenter commented Jul 16, 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 42.42424% with 19 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (dev/v6.x@d07d1d7). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/lib/craft/craft_repl_dev.cpp 33.33% 7 Missing and 7 partials ⚠️
src/lib/craft/craft_repl_dev.hpp 58.33% 0 Missing and 5 partials ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@             Coverage Diff             @@
##             dev/v6.x     #170   +/-   ##
===========================================
  Coverage            ?   45.00%           
===========================================
  Files               ?       18           
  Lines               ?     1020           
  Branches            ?      442           
===========================================
  Hits                ?      459           
  Misses              ?      274           
  Partials            ?      287           

☔ 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.

Drop all journal entries with dLSN > lsn via HomeStore rollback(), clamp
last_append_lsn, and erase missing-set entries above lsn. commit_lsn is
not touched. A journal I/O failure short-circuits before state is mutated.

Introduces CraftJournalBackend::truncate_to() as the injectable seam so
unit tests drive the path with MockCraftJournalBackend without HomeStore.
HomeStoreCraftJournalBackend (wrapping home_log_store::rollback) is the
production implementation; make_homestore_journal_backend() is the factory
for callers that own a log store (volume.cpp, wired up in a later PR).

Observability accessors last_append_lsn(), commit_lsn(), missing_count(),
and is_missing() are unconditional public methods usable in production.
seed_lsns() is compiled only when _PRERELEASE is defined (set by the test
target in CMake) so the symbol is absent from production binaries.
@szmyd

szmyd commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Reviewed the S4 truncate path in isolation; the core logic is correct and the test coverage is thorough (clamp above/at/below last_append_lsn, partial/full/boundary missing-set erase, commit_lsn invariance, and the journal-error state-shield are all covered). The failing TSAN job is unrelated to this change: VolumeIOTest fails under TSAN on the base commit too, the volume tests don't touch craft code, and the new CraftTruncateTest passes clean.

Three notes on the concurrency contract this PR establishes. All are latent today (these methods have no callers yet), but worth settling now while the locking discipline is being set; #1 in particular is the exact race a later TSAN run will surface once the peer/login paths land.

1. Inconsistent locking of the partition watermarks - craft_repl_dev.cpp:63-73, .hpp:167-184

This PR makes truncate() the first writer of state_.last_append_lsn (under missing_mu_), and the new last_append_lsn() / commit_lsn() accessors read the watermarks under that same lock - but get_lsns() / get_rs_commit_lsn() still read state_.commit_lsn / state_.last_append_lsn lock-free. The "callers are serialised by login" justification doesn't cover get_rs_commit_lsn(), which the header documents as an inbound peer RPC ("exposed to peer servers during GetRSCommitLSN broadcast"). Once the peer/login paths are wired, peer B's GetRSCommitLSN landing on node A while A's login runs truncate() is an unsynchronized read/write of last_append_lsn (and of commit_lsn once S5's apply_sync_rs_commit_lsn writes it from the RAFT commit thread).

Since this PR sets the locking contract, suggest making it consistent: either take missing_mu_ in those two snapshots, or make the two watermarks std::atomic< int64_t > and drop the mutex for them.

2. truncate() doesn't enforce lsn >= commit_lsn - craft_repl_dev.cpp:88-99

If a caller ever passes lsn < commit_lsn, step 1 rolls the journal below the committed prefix (dropping committed entries) and the clamp at line 95 sets last_append_lsn = lsn < commit_lsn, breaking the commit_lsn <= last_append_lsn invariant; even though the comment states commit_lsn is deliberately left untouched. Right now that contract lives only in prose. A cheap guard inside the lock turns caller misuse into a pre-release failure instead of silent watermark corruption:

DEBUG_ASSERT_GE(lsn, state_.commit_lsn, "truncate below committed prefix");

3. missing_mu_ now guards more than the missing set - craft_repl_dev.hpp:256-257

As of this PR missing_mu_ also protects the state_ watermarks (last_append_lsn, plus commit_lsn going forward), not just missing_lsns_. The name and the missing_lsns_ comment above it ("gaps between commit_lsn and last_append_lsn") undersell that scope, so a future reader could assume it only covers the set and add an unguarded state_ access. Suggest renaming to state_mu_, or documenting that it guards state_ + missing_lsns_ together.

Fix two concurrency issues flagged by szmyd:

1. get_lsns() / get_rs_commit_lsn() now hold missing_mu_ when
   snapshotting state_.commit_lsn / state_.last_append_lsn, consistent
   with truncate() (writer) and the observability accessors.
   get_rs_commit_lsn() is documented as a peer RPC that can race with
   truncate() from a concurrent login; the lock-free read was a latent
   data race.

2. truncate() now asserts lsn >= state_.commit_lsn inside missing_mu_
   before clamping last_append_lsn.  Truncating below the committed
   prefix would drop committed journal entries and break the
   commit_lsn <= last_append_lsn invariant; the assert turns caller
   misuse into a prerelease failure instead of silent watermark
   corruption.

@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.

The DEBUG_ASSERT_GE fires after the journal has already been rolled back (craft_repl_dev.cpp:145):

if (auto r = co_await journal_->truncate_to(lsn); !r) co_return r;  // journal truncated here
{
    std::lock_guard lk{missing_mu_};
    DEBUG_ASSERT_GE(lsn, state_.commit_lsn, "truncate below committed prefix");  // too late

If a caller passes lsn < state_.commit_lsn, the journal is already truncated below the committed prefix before the assert catches it — committed entries are permanently gone. The check needs to move before step 1:

async_status CraftReplDev::truncate(int64_t lsn) {
    {
        std::lock_guard lk{missing_mu_};
        DEBUG_ASSERT_GE(lsn, state_.commit_lsn, "truncate below committed prefix");
    }
    if (auto r = co_await journal_->truncate_to(lsn); !r) co_return r;
    ...

The DEBUG_ASSERT_GE was placed after journal_->truncate_to(), so a
caller passing lsn < commit_lsn would permanently lose committed journal
entries before the assert had a chance to fire.

Move the check into its own lock scope before step 1 so misuse is caught
before any irreversible I/O is issued.

@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.

LG!

@shosseinimotlagh
shosseinimotlagh merged commit 4847a0d into eBay:dev/v6.x Jul 20, 2026
28 of 29 checks passed
@shosseinimotlagh
shosseinimotlagh deleted the craft_truncate branch July 20, 2026 20:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants