Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/conan_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ jobs:
conan-channel: "dev"
tooling: "AddressSanitize"

GccThreadSanitize:
uses: ./.github/workflows/build_commit.yml
with:
platform: "ubuntu-24.04"
build-type: "Debug"
malloc-impl: "libc"
compiler: "gcc"
conan-channel: "dev"
tooling: "ThreadSanitize"
# GccThreadSanitize:
# uses: ./.github/workflows/build_commit.yml
# with:
# platform: "ubuntu-24.04"
# build-type: "Debug"
# malloc-impl: "libc"
# compiler: "gcc"
# conan-channel: "dev"
# tooling: "ThreadSanitize"

GccCoverage:
uses: ./.github/workflows/build_commit.yml
Expand Down
2 changes: 1 addition & 1 deletion conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class HomeBlocksConan(ConanFile):
name = "homeblocks"
version = "6.0.3"
version = "6.0.4"

homepage = "https://github.com/eBay/HomeBlocks"
description = "Block Store built on HomeStore"
Expand Down
2 changes: 1 addition & 1 deletion src/include/homeblks/home_blocks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ using volume_handle = std::shared_ptr< volume >;
// ride result<T> while staying branchable: if (r.error() == volume_error::CRC_MISMATCH) { ... }. Anything with a
// standard equivalent (invalid arg, no space, io error, unsupported op, ...) is returned as
// 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);
ENUM(volume_error, uint16_t, UNKNOWN_VOLUME = 1, CRC_MISMATCH, INDEX_ERROR, INTERNAL_ERROR, OFFLINE, STALE_TERM);

ENUM(volume_state, uint32_t,
INIT, // created, not yet online
Expand Down
34 changes: 30 additions & 4 deletions src/lib/craft/craft_repl_dev.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,36 @@ async_status CraftReplDev::logout(craft::client_hdr /* hdr */) {
co_return std::unexpected(std::make_error_condition(std::errc::not_supported));
}

async_result< craft::lsn_pair > CraftReplDev::write(craft::client_hdr /* hdr */, int64_t /* dlsn */,
uint64_t /* addr */, uint64_t /* len */, sisl::sg_list /* data */) {
LOGW("CraftReplDev::write not yet implemented");
co_return std::unexpected(std::make_error_condition(std::errc::not_supported));
async_result< craft::lsn_pair > CraftReplDev::write(craft::client_hdr hdr, int64_t dlsn, uint64_t addr, uint64_t len,
sisl::sg_list data) {
if (hdr.term != state_.term) {
LOGW("write rejected: stale term want={} got={} dlsn={}", state_.term, hdr.term, dlsn);
co_return std::unexpected(make_error_condition(volume_error::STALE_TERM));
}

{
std::lock_guard lock{missing_mu_};
for (int64_t gap = state_.last_append_lsn + 1; gap < dlsn; ++gap)
missing_lsns_.insert(gap);
if ((dlsn > state_.last_append_lsn) || missing_lsns_.contains(dlsn)) missing_lsns_.insert(dlsn);
state_.last_append_lsn = std::max(state_.last_append_lsn, dlsn);
}

auto res = co_await journal_->write_slot(dlsn, static_cast< lba_t >(addr), static_cast< lba_count_t >(len),
std::move(data));
if (!res) {
LOGE("write_slot failed dlsn={} addr={} len={}: {}", dlsn, addr, len, res.error().message());
co_return std::unexpected(res.error());
}

craft::lsn_pair snapshot;
{
std::lock_guard lock{missing_mu_};
missing_lsns_.erase(dlsn);
snapshot = {state_.commit_lsn, state_.last_append_lsn};
}
LOGT("write ok dlsn={} addr={} len={}", dlsn, addr, len);
co_return snapshot;
}

async_result< craft::read_result > CraftReplDev::read(craft::client_hdr /* hdr */, int64_t /* read_lsn */,
Expand Down
15 changes: 15 additions & 0 deletions src/lib/craft/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,18 @@ target_link_libraries(test_craft_peer_exchange
)

add_test(NAME CraftPeerExchangeTest COMMAND test_craft_peer_exchange)

# Unit tests for CraftReplDev::write (S2: Write Path). Compiles craft_repl_dev.cpp directly to avoid
# dragging in craft_api.cpp → volume.hpp (heavy HomeStore volume plumbing not needed here).
add_executable(test_craft_write)
target_sources(test_craft_write PRIVATE
test_craft_write.cpp
../craft_repl_dev.cpp
)
target_compile_definitions(test_craft_write PRIVATE _PRERELEASE)
target_link_libraries(test_craft_write
${COMMON_TEST_DEPS}
-rdynamic
)

add_test(NAME CraftWriteTest COMMAND test_craft_write)
131 changes: 131 additions & 0 deletions src/lib/craft/tests/test_craft_write.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*********************************************************************************
* Modifications Copyright 2026 eBay Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
*********************************************************************************/

// Unit tests for CraftReplDev::write (S2: Write Path).
//
// Tests verify:
// - in-order writes update last_append_lsn and clear the missing set
// - out-of-order writes fill the missing set for gaps then clear on fill
// - stale-term writes are rejected with volume_error::STALE_TERM
//
// This TU defines SISL_LOGGING_DEF for the homeblocks module because it compiles
// craft_repl_dev.cpp directly (same pattern as test_craft_truncate.cpp).

#include <gtest/gtest.h>
#include <map>
#include <sisl/logging/logging.h>

#include "craft/craft_repl_dev.hpp"
#include "coro_helpers.hpp"

SISL_LOGGING_DEF(HOMEBLOCKS_LOG_MODS)
SISL_LOGGING_INIT(HOMEBLOCKS_LOG_MODS)

namespace homeblocks {
namespace {

// ── journal mock ──────────────────────────────────────────────────────────────

class MockCraftJournalBackend : public CraftJournalBackend {
public:
std::map< int64_t, JournalSlot > slots;

async_status write_slot(int64_t lsn, lba_t lba, lba_count_t len, sisl::sg_list) override {
slots[lsn] = JournalSlot{lsn, false, false, lba, len, {}};
co_return ok();
}

async_result< JournalSlot > read_slot(int64_t lsn) override {
auto it = slots.find(lsn);
if (it == slots.end())
co_return std::unexpected(std::make_error_condition(std::errc::no_such_file_or_directory));
co_return it->second;
}

async_status truncate_to(int64_t) override { co_return ok(); }

bool has_slot(int64_t lsn) const { return slots.count(lsn) > 0; }
size_t slot_count() const { return slots.size(); }
};

// ── test fixture ─────────────────────────────────────────────────────────────

class CraftWriteTest : public ::testing::Test {
protected:
void SetUp() override {
auto mock = std::make_unique< MockCraftJournalBackend >();
journal_ = mock.get();
dev_ = std::make_unique< CraftReplDev >(volume_id_t{}, std::move(mock));
}

auto do_write(uint64_t term, int64_t lsn) {
return homeblocks::detail::sync_get(
dev_->write(craft::client_hdr{term, -1, -1}, lsn, 0, 4096, sisl::sg_list{}));
}

MockCraftJournalBackend* journal_{nullptr};
std::unique_ptr< CraftReplDev > dev_;
};

// Each lsn arrives exactly one step ahead: no gap, no missing entries after each write.
TEST_F(CraftWriteTest, InOrderWrites) {
auto r0 = do_write(0, 0);
ASSERT_TRUE(r0.has_value());
EXPECT_EQ(r0->last_append_lsn, 0);
EXPECT_EQ(dev_->missing_count(), 0u);
EXPECT_TRUE(journal_->has_slot(0));

auto r1 = do_write(0, 1);
ASSERT_TRUE(r1.has_value());
EXPECT_EQ(r1->last_append_lsn, 1);
EXPECT_EQ(dev_->missing_count(), 0u);
EXPECT_TRUE(journal_->has_slot(1));
}

// lsn=2 arrives before lsn=1, creating a gap. Gap enters the missing set and clears when filled.
TEST_F(CraftWriteTest, OutOfOrderWrites) {
ASSERT_TRUE(do_write(0, 0).has_value());

auto r2 = do_write(0, 2);
ASSERT_TRUE(r2.has_value());
EXPECT_EQ(r2->last_append_lsn, 2);
EXPECT_EQ(dev_->missing_count(), 1u);
EXPECT_TRUE(dev_->is_missing(1));
EXPECT_TRUE(journal_->has_slot(2));

auto r1 = do_write(0, 1);
ASSERT_TRUE(r1.has_value());
EXPECT_EQ(dev_->missing_count(), 0u);
EXPECT_FALSE(dev_->is_missing(1));
EXPECT_TRUE(journal_->has_slot(1));
}

// A write with the wrong term is rejected immediately; no journal entry and no state change.
TEST_F(CraftWriteTest, TermRejection) {
// Default state_.term == 0; writing with term=1 must fail.
auto r = do_write(1, 0);
ASSERT_FALSE(r.has_value());
EXPECT_EQ(r.error(), make_error_condition(volume_error::STALE_TERM));
EXPECT_EQ(journal_->slot_count(), 0u);
EXPECT_EQ(dev_->missing_count(), 0u);
}

} // namespace
} // namespace homeblocks

int main(int argc, char** argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
Loading