Skip to content

Commit

Permalink
Add test cases for blob file GC with online compaction (WIP)
Browse files Browse the repository at this point in the history
  • Loading branch information
umegane committed Feb 19, 2025
1 parent 04620fa commit 5d8473b
Show file tree
Hide file tree
Showing 2 changed files with 125 additions and 27 deletions.
4 changes: 2 additions & 2 deletions src/limestone/datastore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,6 @@ void datastore::ready() {
online_compaction_worker_future_ = std::async(std::launch::async, &datastore::online_compaction_worker, this);
if (epoch_id_switched_.load() != 0) {
write_epoch_callback_(epoch_id_informed_.load());
available_boundary_version_ = write_version_type{epoch_id_informed_.load(), 0};
}
cleanup_rotated_epoch_files(location_);
state_ = state::ready;
Expand Down Expand Up @@ -764,6 +763,7 @@ std::unique_ptr<blob_pool> datastore::acquire_blob_pool() {
do {
current = next_blob_id_.load(std::memory_order_acquire); // Load the current ID atomically.
if (current == std::numeric_limits<blob_id_type>::max()) {
LOG_LP(ERROR) << "Blob ID overflow detected.";
return current; // Return max value to indicate overflow.
}
} while (!next_blob_id_.compare_exchange_weak(
Expand Down Expand Up @@ -794,7 +794,7 @@ blob_file datastore::get_blob_file(blob_id_type reference) {
available = false;
}
}
TRACE_END;
TRACE_END << "path=" << path.string() << ", available=" << available;
return blob_file(path, available);
}

Expand Down
148 changes: 123 additions & 25 deletions test/limestone/blob/datastore_blob_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,14 @@ class datastore_blob_test : public ::testing::Test {
datastore_->ready();
}

void create_dummy_file(api::blob_id_type existing_blob_id) {
auto file = datastore_->get_blob_file(existing_blob_id);
boost::filesystem::path create_dummy_file(api::blob_id_type blob_id) {
auto file = datastore_->get_blob_file(blob_id);
auto path = file.path();
boost::filesystem::create_directories(path.parent_path());
boost::filesystem::ofstream dummy_file(path);
dummy_file << "test data";
dummy_file.close();
return path;
}
};

Expand All @@ -77,45 +78,95 @@ TEST_F(datastore_blob_test, acquire_blob_pool_basic) {
ASSERT_NE(pool, nullptr);
}

TEST_F(datastore_blob_test, acquire_blob_pool_overflow_boundary) {
// Set next_blob_id to max - 1.
auto max_id = std::numeric_limits<blob_id_type>::max();
datastore_->set_next_blob_id(max_id - 1);

// Acquire the blob pool.
auto pool = datastore_->acquire_blob_pool();
ASSERT_NE(pool, nullptr);

std::string test_data1 = "test data 1";
std::string test_data2 = "test data 2";

// First call: the id_generator should return max - 1 and update next_blob_id to max.
auto id1 = pool->register_data(test_data1);
EXPECT_EQ(id1, max_id - 1) << "Expected first registered blob ID to be max - 1";

// Second call: since next_blob_id is now max, the lambda should return max.
auto id2 = pool->register_data(test_data2);
EXPECT_EQ(id2, max_id) << "Expected second registered blob ID to be max, indicating overflow";
}



// Environment-independent part
TEST_F(datastore_blob_test, get_blob_file_basic) {
int next_blob_id = 12345;
int existing_blob_id = 12344;
datastore_->set_next_blob_id(next_blob_id);

create_dummy_file(existing_blob_id);
create_dummy_file(next_blob_id);
// create_dummy_file now returns the generated file's path.
boost::filesystem::path expected_existing_path = create_dummy_file(existing_blob_id);
boost::filesystem::path expected_next_path = create_dummy_file(next_blob_id);

// Case 1: Normal case - file exists and is accessible
// Case 1: Normal case - file exists and is accessible.
auto file = datastore_->get_blob_file(existing_blob_id);
EXPECT_TRUE(static_cast<bool>(file));
EXPECT_EQ(file.path().string(), expected_existing_path.string())
<< "Returned path should match the dummy file path for existing_blob_id";
EXPECT_TRUE(static_cast<bool>(file))
<< "File should be available when it exists";

// Case 2: File is removed after being confirmed to exist
// Case 2: File is removed after being confirmed to exist.
boost::filesystem::remove(file.path());
auto file_removed = datastore_->get_blob_file(existing_blob_id);
EXPECT_FALSE(static_cast<bool>(file_removed));
EXPECT_FALSE(static_cast<bool>(file_removed))
<< "File should be marked as unavailable if it has been removed";
EXPECT_EQ(file_removed.path().string(), expected_existing_path.string())
<< "Returned path should still match the dummy file path even if the file is unavailable";

// Case 3: Boundary condition - ID equal to next_blob_id
// Case 3: Boundary condition - ID equal to next_blob_id.
auto file_next_blob_id = datastore_->get_blob_file(next_blob_id);
EXPECT_TRUE(boost::filesystem::exists(file_next_blob_id.path()));
EXPECT_EQ(file_next_blob_id.path().string(), expected_next_path.string())
<< "Returned path should match the dummy file path for next_blob_id";
EXPECT_FALSE(static_cast<bool>(file_next_blob_id))
<< "File should be available for next_blob_id if it exists";
}

// Environment-dependent part (disabled in CI environment)
// Reason: This test modifies directory permissions to simulate a "permission denied" scenario.
// However, in certain environments, such as CI, the behavior might differ, making the test unreliable.
TEST_F(datastore_blob_test, DISABLED_get_blob_file_permission_error) {
int existing_blob_id = 12344;
create_dummy_file(existing_blob_id);

// This test simulates a permission error so that boost::filesystem::exists() fails,
// causing the catch block to mark the file as unavailable.
TEST_F(datastore_blob_test, DISABLED_get_blob_file_filesystem_error) {
FLAGS_v = 70;
int existing_blob_id = 12345;
datastore_->set_next_blob_id(existing_blob_id + 1);
boost::filesystem::path expected_path = create_dummy_file(existing_blob_id);

// Normally, the file should exist.
auto file = datastore_->get_blob_file(existing_blob_id);

// Simulate a permission denied scenario by modifying directory permissions
boost::filesystem::permissions(file.path().parent_path(), boost::filesystem::perms::no_perms);
EXPECT_THROW(boost::filesystem::exists(file.path()), boost::filesystem::filesystem_error);

// Cleanup: Restore permissions for subsequent tests
boost::filesystem::permissions(file.path().parent_path(), boost::filesystem::perms::all_all);
EXPECT_TRUE(boost::filesystem::exists(file.path()))
<< "File should exist before permission change";
EXPECT_TRUE(static_cast<bool>(file))
<< "File should be available before permission change";
EXPECT_EQ(file.path().string(), expected_path.string())
<< "Returned path should match the dummy file path";

// Get the parent directory of the file and save its original permissions.
boost::filesystem::path parent_dir = file.path().parent_path();
auto original_perms = boost::filesystem::status(parent_dir).permissions();

// Set the directory permissions to no permissions to simulate a filesystem error.
boost::filesystem::permissions(parent_dir, boost::filesystem::perms::no_perms);

// Now, get_blob_file should mark the file as unavailable.
auto file_error = datastore_->get_blob_file(existing_blob_id);
EXPECT_FALSE(static_cast<bool>(file_error))
<< "Expected file to be marked unavailable due to permission error";
EXPECT_EQ(file_error.path().string(), expected_path.string())
<< "Returned path should still match the dummy file path even if file is unavailable";

// Restore the original permissions so that subsequent tests are not affected.
boost::filesystem::permissions(parent_dir, original_perms);
}

TEST_F(datastore_blob_test, add_persistent_blob_ids) {
Expand Down Expand Up @@ -432,7 +483,7 @@ TEST_F(datastore_blob_test, available_boundary_version_after_reboot) {
// --- Step 4: Check after restart ---
// Verify that available_boundary_version_ is properly restored after restart

EXPECT_EQ(datastore_->get_available_boundary_version().get_major(), 115);
EXPECT_EQ(datastore_->get_available_boundary_version().get_major(), 0);
EXPECT_EQ(datastore_->get_available_boundary_version().get_minor(), 0);

// Verify data consistency
Expand All @@ -449,4 +500,51 @@ TEST_F(datastore_blob_test, available_boundary_version_after_reboot) {
EXPECT_FALSE(cursor->next());
}

TEST_F(datastore_blob_test, initial_available_boundary_version_) {
auto boundary_version = datastore_->get_available_boundary_version();
EXPECT_EQ(boundary_version.get_major(), 0);
EXPECT_EQ(boundary_version.get_minor(), 0);

// Change available_boundary_version_ and reboot
datastore_->switch_epoch(1);
lc0_->begin_session();
lc0_->add_entry(1, "key1", "value1", {1,1});
lc0_->end_session();
datastore_->switch_epoch(2);
datastore_->switch_available_boundary_version({1, 5});

boundary_version = datastore_->get_available_boundary_version();
EXPECT_EQ(boundary_version.get_major(), 1);
EXPECT_EQ(boundary_version.get_minor(), 5);

datastore_->shutdown();
datastore_.reset();
gen_datastore();

boundary_version = datastore_->get_available_boundary_version();
EXPECT_EQ(boundary_version.get_major(), 0);
EXPECT_EQ(boundary_version.get_minor(), 0);
}

TEST_F(datastore_blob_test, switch_available_boundary_version) {
auto boundary_version = datastore_->get_available_boundary_version();
EXPECT_EQ(boundary_version.get_major(), 0);
EXPECT_EQ(boundary_version.get_minor(), 0);

datastore_->switch_available_boundary_version({1, 5});
boundary_version = datastore_->get_available_boundary_version();
EXPECT_EQ(boundary_version.get_major(), 1);
EXPECT_EQ(boundary_version.get_minor(), 5);

datastore_->switch_available_boundary_version({1, 3});
boundary_version = datastore_->get_available_boundary_version();
EXPECT_EQ(boundary_version.get_major(), 1);
EXPECT_EQ(boundary_version.get_minor(), 5);

datastore_->switch_available_boundary_version({2, 3});
boundary_version = datastore_->get_available_boundary_version();
EXPECT_EQ(boundary_version.get_major(), 2);
EXPECT_EQ(boundary_version.get_minor(), 3);
}

} // namespace limestone::testing

0 comments on commit 5d8473b

Please sign in to comment.