mirror of
https://github.com/facebook/rocksdb.git
synced 2026-10-23 08:09:02 +03:00
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14982 **What**: Add reference-counted `DB::AbortCompactions(cf)` / `DB::ResumeCompactions(cf)` to abort and resume compactions for a single column family without stopping unrelated column families. They compose with the DB-wide `AbortAllCompactions()` / `ResumeAllCompactions()` and are no-ops once DB shutdown has begun. **How**: - **Per-CF abort flag.** `ColumnFamilyData` gains `compaction_aborted_`, wrapped in `CacheAlignedWrapper` so the lock-free reads done by compaction workers do not false-share with the DB-mutex-written fields packed around it. A new `IsCompactionAborted(cfd)` helper checks it alongside the DB-wide counter at every entry point (`CompactRange()`, `RunManualCompaction()`, `CompactFilesImpl()`), and `CompactionJob::ProcessKeyValue()` checks both on entry and in its periodic cron check. - **Parking.** Automatic work for an aborted CF is preserved rather than dropped. `PickCompactionFromQueue()` now returns `std::variant<CompactionQueuePicked, CompactionQueueThrottled, CompactionQueueParked>` and moves an aborted CF into `parked_compaction_cfds_` only once it reaches the front of the queue, so CFs behind it keep their place and the scheduler credit is not restored (which would cause a hot re-pick loop). `EnqueuePendingCompaction()` parks newly needed work for an aborted CF and now early-returns for dropped CFs. `RestoreParkedCompaction()` re-enqueues on the final resume; a CF dropped while parked is handled in `DropColumnFamily()`, and one dropped before its aborted queue entry is reached is handled by a fast path in `PickCompactionFromQueue()`. - **Knowing when it is safe to return.** `ColumnFamilyData::num_in_flight_compactions_` counts actually-picked work, and `Defer` balances `BeginInFlightCompaction()` / `EndInFlightCompaction()` on every `BackgroundCompaction()` and `CompactFiles()` exit path. That covers the `NotifyOnCompactionCompleted()` window that `CompactionPicker::compactions_in_progress_` does not, since the picker set is cleared during `Install`'s manifest write. `MustWaitForCompaction()` combines the counter with the picker set (filtered to skip `kExternalSstIngestion` / `kRefitLevel` reservations, which are not real jobs) because only the picker set covers the LOW-to-BOTTOM priority handoff, where the counter briefly dips to 0. - **Waking the waiter.** `AbortCompactions()` cancels matching `manual_compaction_dequeue_` entries and then waits on `bg_cv_`; `per_cf_compaction_abort_waiters_` forces the compaction completion paths to signal, because the picker's `UnregisterCompaction()` does not. `CloseHelper()` drains `parked_compaction_cfds_` and releases their references. - **API surface.** The two methods are new pure virtuals on `DB`, so `StackableDB` forwards them and the `db_test.cc` DB wrapper stubs them. `db.h` documents how the per-CF API composes with DB-wide abort, `DisableManualCompaction()`, `PauseBackgroundWork()`, shutdown, ingestion reservations, level refit and remote compaction, plus the parked-work caveat on `WaitForCompact()`. Comments in `listener.h`, `statistics.h` and `status.h` now name both abort APIs. - **Coverage.** 10 new cases in `db_compaction_abort_test.cc` (CF scoping, counter composition, parked-work restore, CF drop both while parked and before parking, trivial-move rejection, waiting for the completion callback, post-close no-op). `db_stress` gains `abort_and_resume_cf_compactions_one_in` and `TestAbortAndResumeCfCompactions()`, rejected in combination with `clear_column_family_one_in` because that would drop the column family out from under an active abort. Also a `db_crashtest.py` knob and one `new_features` release note. - **Known gap.** A `TODO` records the pre-existing post-pick stale compaction-score problem: aborting after `PickCompaction()` releases the inputs without recomputing the score, so the resume can observe a stale sub-trigger score and re-enqueue nothing. `ResumeAllCompactions()` has the same gap and carries a pointer to the same TODO; fixing it is left to a separate change covering both APIs. **Why**: Atomic-replace bulk loads only conflict with their target column family, but the DB-wide abort suppresses compaction for every column family colocated in the same RocksDB instance. Scoping the abort keeps unrelated column families compacting for the duration of a bulk load. Reviewed By: pdillinger Differential Revision: D113103899 fbshipit-source-id: 9bef924132676ed118a37aee86da6ee8bcefef09
1520 lines
55 KiB
C++
1520 lines
55 KiB
C++
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
// This source code is licensed under both the GPLv2 (found in the
|
|
// COPYING file in the root directory) and Apache 2.0 License
|
|
// (found in the LICENSE.Apache file in the root directory).
|
|
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <mutex>
|
|
#include <set>
|
|
#include <thread>
|
|
#include <unordered_map>
|
|
|
|
#include "db/compaction/compaction_job.h"
|
|
#include "db/db_impl/db_impl_secondary.h"
|
|
#include "db/db_test_util.h"
|
|
#include "options/options_helper.h"
|
|
#include "port/stack_trace.h"
|
|
#include "rocksdb/db.h"
|
|
#include "rocksdb/listener.h"
|
|
#include "rocksdb/sst_file_writer.h"
|
|
#include "test_util/sync_point.h"
|
|
#include "test_util/testharness.h"
|
|
|
|
namespace ROCKSDB_NAMESPACE {
|
|
|
|
// Helper class to manage abort synchronization in tests.
|
|
//
|
|
// Compaction abort could happen at various stage of compaction.
|
|
// To test this, we need to trigger abort at different stage. This requires
|
|
// precise control on the timing of abort API invocation. To achieve this in a
|
|
// consistent way across various tests, we invoke AbortAllCompactions() within
|
|
// the sync point callback, that is added at various stages of compaction.
|
|
// However as the abort API is a blocking call, calling it within the sync point
|
|
// callback on the compaction thread would cause deadlock. This test helper
|
|
// class is designed to solve this challenge.
|
|
//
|
|
// 1. Abort must happen from a different thread:
|
|
// AbortAllCompactions() is typically called from the compaction thread
|
|
// via a sync point callback, so that we could precisely control the time of
|
|
// API invocation to simulate abort at different stage of compaction.
|
|
// However, we can't block the compaction thread waiting for the abort to
|
|
// complete - the compaction needs to continue executing to actually check
|
|
// the abort flag and exit. So we spawn a separate thread to call
|
|
// AbortAllCompactions().
|
|
//
|
|
// 2. We need to know when abort completes:
|
|
// After compaction returns (with aborted status), we often need to:
|
|
// - Verify state (e.g., no output files created)
|
|
// - Call ResumeAllCompactions()
|
|
// - Run compaction again to verify it succeeds
|
|
// We must wait for the abort thread to finish before proceeding, otherwise
|
|
// we might call Resume before Abort completes, causing race conditions.
|
|
//
|
|
// 3. Sync point callbacks may fire multiple times:
|
|
// With multiple subcompactions, a callback like
|
|
// "CompactionJob::ProcessKeyValueCompaction:Start" fires once per
|
|
// subcompaction. We only want to trigger abort once, so we use
|
|
// abort_triggered_ as a guard.
|
|
//
|
|
// 4. Tests may need multiple abort cycles:
|
|
// Some tests (e.g., MultipleAbortResumeSequence) do abort->resume->abort
|
|
// multiple times. The class supports this by auto-resetting when a
|
|
// previous abort has completed.
|
|
class AbortSynchronizer {
|
|
public:
|
|
AbortSynchronizer() : abort_cv_(&abort_mutex_) {}
|
|
|
|
~AbortSynchronizer() {
|
|
// Join the thread if it was started - ensures clean shutdown
|
|
if (abort_thread_.joinable()) {
|
|
abort_thread_.join();
|
|
}
|
|
}
|
|
|
|
// Non-copyable, non-movable due to thread member
|
|
AbortSynchronizer(const AbortSynchronizer&) = delete;
|
|
AbortSynchronizer& operator=(const AbortSynchronizer&) = delete;
|
|
|
|
// Trigger abort from a separate thread.
|
|
// - Safe to call multiple times; only first call in each cycle spawns thread
|
|
// - If a previous abort has completed, automatically resets state first
|
|
// - The spawned thread calls AbortAllCompactions() and signals completion
|
|
void TriggerAbort(DBImpl* db) {
|
|
// If previous abort completed, reset state to allow new abort
|
|
if (abort_triggered_.load() && abort_completed_.load()) {
|
|
Reset();
|
|
}
|
|
|
|
if (!abort_triggered_.exchange(true)) {
|
|
abort_thread_ = std::thread([this, db]() {
|
|
db->AbortAllCompactions();
|
|
SignalAbortCompleted();
|
|
});
|
|
}
|
|
}
|
|
|
|
void TriggerAbort(DBImpl* db, ColumnFamilyHandle* column_family) {
|
|
if (abort_triggered_.load() && abort_completed_.load()) {
|
|
Reset();
|
|
}
|
|
|
|
if (!abort_triggered_.exchange(true)) {
|
|
abort_thread_ = std::thread([this, db, column_family]() {
|
|
db->AbortCompactions(column_family);
|
|
SignalAbortCompleted();
|
|
});
|
|
}
|
|
}
|
|
|
|
// Wait for the abort thread to complete.
|
|
// Call this AFTER compaction returns to ensure the abort thread has finished
|
|
// before proceeding with Resume or other operations.
|
|
void WaitForAbortCompletion() {
|
|
MutexLock l(&abort_mutex_);
|
|
while (!abort_completed_.load()) {
|
|
abort_cv_.Wait();
|
|
}
|
|
}
|
|
|
|
// Reset state for reuse. Joins any previous thread first.
|
|
// Called automatically by TriggerAbort() if previous abort completed,
|
|
// but can also be called explicitly for clarity.
|
|
void Reset() {
|
|
if (abort_thread_.joinable()) {
|
|
abort_thread_.join();
|
|
}
|
|
abort_triggered_.store(false);
|
|
abort_completed_.store(false);
|
|
}
|
|
|
|
bool IsAbortTriggered() const { return abort_triggered_.load(); }
|
|
|
|
private:
|
|
void SignalAbortCompleted() {
|
|
MutexLock l(&abort_mutex_);
|
|
abort_completed_.store(true);
|
|
abort_cv_.SignalAll();
|
|
}
|
|
|
|
std::atomic<bool> abort_triggered_{false}; // Guards against multiple spawns
|
|
std::atomic<bool> abort_completed_{false}; // Signals thread completion
|
|
port::Mutex abort_mutex_;
|
|
port::CondVar abort_cv_;
|
|
std::thread abort_thread_; // The thread that calls AbortAllCompactions()
|
|
};
|
|
|
|
// Helper to clean up SyncPoint state after tests
|
|
inline void CleanupSyncPoints() {
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
|
}
|
|
|
|
// Helper class that combines AbortSynchronizer with sync point setup for
|
|
// deterministic abort triggering. This adds sync point coordination on top
|
|
// of AbortSynchronizer:
|
|
//
|
|
// This is useful when you need deterministic timing - the callback won't
|
|
// return until AbortAllCompactions() has actually set the abort flag,
|
|
// guaranteeing the compaction will see it on the next check.
|
|
class SyncPointAbortHelper {
|
|
public:
|
|
explicit SyncPointAbortHelper(const std::string& trigger_point)
|
|
: trigger_point_(trigger_point) {}
|
|
|
|
// Set up sync points and callbacks. Call this before starting compaction.
|
|
void Setup(DBImpl* db_impl) {
|
|
db_impl_ = db_impl;
|
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
|
{"DBImpl::AbortAllCompactions:FlagSet", kWaitPointName},
|
|
});
|
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
trigger_point_, [this](void* /*arg*/) {
|
|
// Use AbortSynchronizer to handle the abort in a separate thread
|
|
abort_sync_.TriggerAbort(db_impl_);
|
|
|
|
// Wait for abort flag to be set via sync point dependency
|
|
// This ensures deterministic timing - compaction will see the flag
|
|
TEST_SYNC_POINT_CALLBACK(kWaitPointName, nullptr);
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
}
|
|
|
|
// Wait for the abort to complete. Call this after compaction returns.
|
|
void WaitForAbortCompletion() { abort_sync_.WaitForAbortCompletion(); }
|
|
|
|
// Clean up sync points and wait for abort completion in one call
|
|
void CleanupAndWait() {
|
|
CleanupSyncPoints();
|
|
WaitForAbortCompletion();
|
|
}
|
|
|
|
private:
|
|
static constexpr const char* kWaitPointName =
|
|
"SyncPointAbortHelper::WaitForAbort";
|
|
std::string trigger_point_;
|
|
DBImpl* db_impl_{nullptr};
|
|
AbortSynchronizer abort_sync_;
|
|
};
|
|
|
|
class DBCompactionAbortTest : public DBTestBase {
|
|
public:
|
|
DBCompactionAbortTest()
|
|
: DBTestBase("db_compaction_abort_test", /*env_do_fsync=*/false) {}
|
|
|
|
protected:
|
|
// Map to track the latest value of each key for verification
|
|
std::unordered_map<std::string, std::string> expected_values_;
|
|
|
|
// Statistics object for verifying compaction metrics
|
|
std::shared_ptr<Statistics> stats_;
|
|
|
|
// Get current options with statistics enabled
|
|
Options GetOptionsWithStats() {
|
|
Options options = CurrentOptions();
|
|
stats_ = CreateDBStatistics();
|
|
options.statistics = stats_;
|
|
return options;
|
|
}
|
|
|
|
// Populate database with test data.
|
|
// If overlapping=true, uses the same key range (0 to keys_per_file-1) in each
|
|
// file to ensure compaction has work to do.
|
|
// If overlapping=false, uses non-overlapping keys across files.
|
|
void PopulateData(int num_files, int keys_per_file, int value_size,
|
|
bool overlapping = true, int seed = 301) {
|
|
Random rnd(seed);
|
|
for (int i = 0; i < num_files; ++i) {
|
|
for (int j = 0; j < keys_per_file; ++j) {
|
|
int key_index = overlapping ? j : (j + i * keys_per_file);
|
|
std::string key = Key(key_index);
|
|
std::string value = rnd.RandomString(value_size);
|
|
ASSERT_OK(Put(key, value));
|
|
expected_values_[key] = value;
|
|
}
|
|
ASSERT_OK(Flush());
|
|
}
|
|
}
|
|
|
|
// Verify data integrity by reading all keys and comparing with expected
|
|
// values
|
|
void VerifyDataIntegrity(int num_keys, int start_key = 0) {
|
|
std::string val;
|
|
for (int j = start_key; j < start_key + num_keys; ++j) {
|
|
std::string key = Key(j);
|
|
ASSERT_OK(dbfull()->Get(ReadOptions(), key, &val));
|
|
auto it = expected_values_.find(key);
|
|
if (it != expected_values_.end()) {
|
|
ASSERT_EQ(it->second, val) << "Value mismatch for key: " << key;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear expected values (useful when reopening DB or between tests)
|
|
void ClearExpectedValues() { expected_values_.clear(); }
|
|
|
|
// Run the common abort test pattern with SyncPointAbortHelper:
|
|
// 1. Set up sync point abort helper
|
|
// 2. Run compaction and verify it's aborted
|
|
// 3. Verify COMPACTION_ABORTED stat increased (if stats enabled)
|
|
// 4. Clean up, resume, and verify compaction succeeds
|
|
// 5. Verify COMPACT_WRITE_BYTES increased (if stats enabled)
|
|
void RunSyncPointAbortTest(const std::string& trigger_point,
|
|
CompactRangeOptions cro = CompactRangeOptions()) {
|
|
// Capture stats and file counts before abort
|
|
uint64_t aborted_before = 0;
|
|
uint64_t write_bytes_before = 0;
|
|
if (stats_) {
|
|
aborted_before = stats_->getTickerCount(COMPACTION_ABORTED);
|
|
write_bytes_before = stats_->getTickerCount(COMPACT_WRITE_BYTES);
|
|
}
|
|
|
|
SyncPointAbortHelper helper(trigger_point);
|
|
helper.Setup(dbfull());
|
|
|
|
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
|
ASSERT_TRUE(s.IsIncomplete());
|
|
ASSERT_TRUE(s.IsCompactionAborted());
|
|
|
|
// Verify abort was counted
|
|
if (stats_) {
|
|
uint64_t aborted_after = stats_->getTickerCount(COMPACTION_ABORTED);
|
|
ASSERT_GT(aborted_after, aborted_before)
|
|
<< "COMPACTION_ABORTED stat should increase after abort";
|
|
}
|
|
|
|
helper.CleanupAndWait();
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
|
|
|
// Verify compaction completed and wrote bytes
|
|
if (stats_) {
|
|
uint64_t write_bytes_after = stats_->getTickerCount(COMPACT_WRITE_BYTES);
|
|
ASSERT_GT(write_bytes_after, write_bytes_before)
|
|
<< "COMPACT_WRITE_BYTES should increase after successful compaction";
|
|
}
|
|
}
|
|
};
|
|
|
|
// Parameterized test for abort with different number of max subcompactions.
|
|
// This consolidates tests that were essentially duplicates with different
|
|
// max_subcompactions values
|
|
class DBCompactionAbortSubcompactionTest
|
|
: public DBCompactionAbortTest,
|
|
public ::testing::WithParamInterface<int> {};
|
|
|
|
TEST_P(DBCompactionAbortSubcompactionTest, AbortWithVaryingSubcompactions) {
|
|
int max_subcompactions = GetParam();
|
|
|
|
Options options = GetOptionsWithStats();
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.max_subcompactions = max_subcompactions;
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/100);
|
|
|
|
RunSyncPointAbortTest("CompactionJob::RunSubcompactions:BeforeStart");
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
}
|
|
|
|
INSTANTIATE_TEST_CASE_P(SubcompactionVariants,
|
|
DBCompactionAbortSubcompactionTest,
|
|
::testing::Values(1, 2, 4),
|
|
[](const ::testing::TestParamInfo<int>& param_info) {
|
|
return "MaxSubcompactionCount_" +
|
|
std::to_string(param_info.param);
|
|
});
|
|
|
|
// Parameterized test for abort with different compaction styles
|
|
// This consolidates tests for Level, Universal, and FIFO compaction styles
|
|
class DBCompactionAbortStyleTest
|
|
: public DBCompactionAbortTest,
|
|
public ::testing::WithParamInterface<CompactionStyle> {
|
|
protected:
|
|
// Configure options based on compaction style
|
|
void ConfigureOptionsForStyle(Options& options, CompactionStyle style) {
|
|
options.compaction_style = style;
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.disable_auto_compactions = true;
|
|
|
|
switch (style) {
|
|
case kCompactionStyleLevel:
|
|
// Level compaction uses default settings
|
|
break;
|
|
case kCompactionStyleUniversal:
|
|
options.compaction_options_universal.size_ratio = 10;
|
|
break;
|
|
case kCompactionStyleFIFO:
|
|
// Set a large max_table_files_size to avoid deletion compaction
|
|
options.compaction_options_fifo.max_table_files_size =
|
|
100 * 1024 * 1024;
|
|
// Enable intra-L0 compaction which goes through normal compaction path
|
|
options.compaction_options_fifo.allow_compaction = true;
|
|
options.max_open_files = -1; // Required for FIFO compaction
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
TEST_P(DBCompactionAbortStyleTest, AbortCompaction) {
|
|
CompactionStyle style = GetParam();
|
|
|
|
Options options = GetOptionsWithStats();
|
|
options.max_subcompactions = 1;
|
|
ConfigureOptionsForStyle(options, style);
|
|
Reopen(options);
|
|
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/100);
|
|
|
|
RunSyncPointAbortTest("CompactionJob::RunSubcompactions:BeforeStart");
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
}
|
|
|
|
INSTANTIATE_TEST_CASE_P(
|
|
CompactionStyleVariants, DBCompactionAbortStyleTest,
|
|
::testing::Values(kCompactionStyleLevel, kCompactionStyleUniversal,
|
|
kCompactionStyleFIFO),
|
|
[](const ::testing::TestParamInfo<CompactionStyle>& param_info) {
|
|
return OptionsHelper::compaction_style_to_string.at(param_info.param);
|
|
});
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortManualCompaction) {
|
|
Options options = GetOptionsWithStats();
|
|
options.level0_file_num_compaction_trigger = 10;
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
PopulateData(/*num_files=*/5, /*keys_per_file=*/100, /*value_size=*/1000);
|
|
|
|
CompactRangeOptions cro;
|
|
cro.exclusive_manual_compaction = true;
|
|
RunSyncPointAbortTest("CompactionJob::ProcessKeyValueCompaction:Start", cro);
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortAutomaticCompaction) {
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.max_subcompactions = 2;
|
|
options.disable_auto_compactions = false;
|
|
Reopen(options);
|
|
|
|
Random rnd(301);
|
|
AbortSynchronizer abort_sync;
|
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"CompactionJob::ProcessKeyValueCompaction:Start",
|
|
[&](void* /*arg*/) { abort_sync.TriggerAbort(dbfull()); });
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
for (int j = 0; j < 100; ++j) {
|
|
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
|
|
}
|
|
ASSERT_OK(Flush());
|
|
}
|
|
|
|
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
|
|
|
CleanupSyncPoints();
|
|
|
|
abort_sync.WaitForAbortCompletion();
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
for (int j = 0; j < 100; ++j) {
|
|
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
|
|
}
|
|
ASSERT_OK(Flush());
|
|
|
|
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
|
|
|
std::string val;
|
|
for (int j = 0; j < 100; ++j) {
|
|
ASSERT_OK(dbfull()->Get(ReadOptions(), Key(j), &val));
|
|
}
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortScheduledAutomaticCompactionBeforePick) {
|
|
// The goal is to cover an automatic compaction that has been scheduled, but
|
|
// aborts before the worker picks and removes a column family from the
|
|
// compaction queue. This pins the scheduler bookkeeping invariant that
|
|
// ResumeAllCompactions() must be able to schedule that still-queued work.
|
|
constexpr int kCompactionTrigger = 4;
|
|
constexpr int kNumL0Files = kCompactionTrigger + 1;
|
|
Options options = GetOptionsWithStats();
|
|
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
|
options.max_background_compactions = 1;
|
|
options.max_subcompactions = 1;
|
|
options.disable_auto_compactions = false;
|
|
Reopen(options);
|
|
|
|
SyncPointAbortHelper helper("BackgroundCallCompaction:0");
|
|
helper.Setup(dbfull());
|
|
|
|
PopulateData(/*num_files=*/kNumL0Files, /*keys_per_file=*/100,
|
|
/*value_size=*/1000);
|
|
helper.CleanupAndWait();
|
|
|
|
const uint64_t compact_write_bytes_before_resume =
|
|
stats_->getTickerCount(COMPACT_WRITE_BYTES);
|
|
dbfull()->ResumeAllCompactions();
|
|
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
|
|
|
ASSERT_GT(stats_->getTickerCount(COMPACT_WRITE_BYTES),
|
|
compact_write_bytes_before_resume);
|
|
ASSERT_LT(NumTableFilesAtLevel(0), kNumL0Files);
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortCompactionsIsScopedToColumnFamily) {
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 100;
|
|
options.max_background_compactions = 2;
|
|
options.max_subcompactions = 1;
|
|
options.disable_auto_compactions = true;
|
|
CreateAndReopenWithCF({"target", "sibling"}, options);
|
|
|
|
Random rnd(301);
|
|
for (int cf = 1; cf <= 2; ++cf) {
|
|
for (int file = 0; file < 4; ++file) {
|
|
for (int key = 0; key < 100; ++key) {
|
|
ASSERT_OK(Put(cf, Key(key), rnd.RandomString(1000)));
|
|
}
|
|
ASSERT_OK(Flush(cf));
|
|
}
|
|
}
|
|
|
|
AbortSynchronizer abort_sync;
|
|
std::atomic<int> compactions_started{0};
|
|
std::mutex sibling_mutex;
|
|
std::condition_variable sibling_cv;
|
|
bool sibling_started = false;
|
|
bool allow_sibling_to_finish = false;
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
|
{"DBImpl::AbortCompactions:FlagSet",
|
|
"DBCompactionAbortTest::ScopedAbort:WaitForAbort"},
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"CompactionJob::ProcessKeyValueCompaction:Start", [&](void* /*arg*/) {
|
|
if (compactions_started.fetch_add(1) == 0) {
|
|
std::unique_lock<std::mutex> lock(sibling_mutex);
|
|
sibling_started = true;
|
|
sibling_cv.notify_all();
|
|
sibling_cv.wait(lock, [&]() { return allow_sibling_to_finish; });
|
|
return;
|
|
}
|
|
abort_sync.TriggerAbort(dbfull(), handles_[1]);
|
|
TEST_SYNC_POINT_CALLBACK(
|
|
"DBCompactionAbortTest::ScopedAbort:WaitForAbort", nullptr);
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
|
|
Status sibling_status;
|
|
std::thread sibling_thread([&]() {
|
|
sibling_status = dbfull()->CompactRange(CompactRangeOptions(), handles_[2],
|
|
nullptr, nullptr);
|
|
});
|
|
{
|
|
std::unique_lock<std::mutex> lock(sibling_mutex);
|
|
sibling_cv.wait(lock, [&]() { return sibling_started; });
|
|
}
|
|
|
|
Status target_status = dbfull()->CompactRange(CompactRangeOptions(),
|
|
handles_[1], nullptr, nullptr);
|
|
abort_sync.WaitForAbortCompletion();
|
|
{
|
|
std::lock_guard<std::mutex> guard(sibling_mutex);
|
|
allow_sibling_to_finish = true;
|
|
}
|
|
sibling_cv.notify_all();
|
|
sibling_thread.join();
|
|
CleanupSyncPoints();
|
|
|
|
ASSERT_TRUE(target_status.IsIncomplete());
|
|
ASSERT_TRUE(target_status.IsCompactionAborted());
|
|
ASSERT_OK(sibling_status);
|
|
target_status = dbfull()->CompactRange(CompactRangeOptions(), handles_[1],
|
|
nullptr, nullptr);
|
|
ASSERT_TRUE(target_status.IsIncomplete());
|
|
ASSERT_TRUE(target_status.IsCompactionAborted());
|
|
|
|
dbfull()->ResumeCompactions(handles_[1]);
|
|
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
|
|
nullptr));
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, PerColumnFamilyAndGlobalAbortCountersCompose) {
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 100;
|
|
options.disable_auto_compactions = true;
|
|
CreateAndReopenWithCF({"target", "sibling"}, options);
|
|
|
|
for (int cf = 1; cf <= 2; ++cf) {
|
|
for (int file = 0; file < 2; ++file) {
|
|
ASSERT_OK(Put(cf, Key(file), "value"));
|
|
ASSERT_OK(Flush(cf));
|
|
}
|
|
}
|
|
|
|
dbfull()->AbortAllCompactions();
|
|
dbfull()->AbortCompactions(handles_[1]);
|
|
dbfull()->AbortCompactions(handles_[1]);
|
|
dbfull()->ResumeCompactions(handles_[1]);
|
|
|
|
Status sibling_status = dbfull()->CompactRange(CompactRangeOptions(),
|
|
handles_[2], nullptr, nullptr);
|
|
ASSERT_TRUE(sibling_status.IsIncomplete());
|
|
ASSERT_TRUE(sibling_status.IsCompactionAborted());
|
|
|
|
dbfull()->ResumeAllCompactions();
|
|
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[2], nullptr,
|
|
nullptr));
|
|
Status target_status = dbfull()->CompactRange(CompactRangeOptions(),
|
|
handles_[1], nullptr, nullptr);
|
|
ASSERT_TRUE(target_status.IsIncomplete());
|
|
ASSERT_TRUE(target_status.IsCompactionAborted());
|
|
|
|
dbfull()->ResumeCompactions(handles_[1]);
|
|
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), handles_[1], nullptr,
|
|
nullptr));
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortCompactionsRejectsCompactFilesTrivialMove) {
|
|
Options options = CurrentOptions();
|
|
options.disable_auto_compactions = true;
|
|
CreateAndReopenWithCF({"target"}, options);
|
|
|
|
ASSERT_OK(Put(1, "key", "value"));
|
|
ASSERT_OK(Flush(1));
|
|
|
|
ColumnFamilyMetaData metadata;
|
|
dbfull()->GetColumnFamilyMetaData(handles_[1], &metadata);
|
|
ASSERT_EQ(metadata.levels[0].files.size(), 1);
|
|
const std::vector<std::string> input_files = {
|
|
metadata.levels[0].files.front().name};
|
|
|
|
CompactionOptions compaction_options;
|
|
compaction_options.allow_trivial_move = true;
|
|
dbfull()->AbortCompactions(handles_[1]);
|
|
Status status =
|
|
dbfull()->CompactFiles(compaction_options, handles_[1], input_files, 1);
|
|
ASSERT_TRUE(status.IsIncomplete());
|
|
ASSERT_TRUE(status.IsCompactionAborted());
|
|
|
|
dbfull()->ResumeCompactions(handles_[1]);
|
|
ASSERT_OK(
|
|
dbfull()->CompactFiles(compaction_options, handles_[1], input_files, 1));
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, ResumeRestoresParkedAutomaticCompaction) {
|
|
constexpr int kCompactionTrigger = 4;
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
|
options.max_background_compactions = 1;
|
|
options.disable_auto_compactions = false;
|
|
CreateAndReopenWithCF({"target", "sibling"}, options);
|
|
|
|
AbortSynchronizer abort_sync;
|
|
std::mutex parked_mutex;
|
|
std::condition_variable parked_cv;
|
|
bool compaction_parked = false;
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
|
{"DBImpl::AbortCompactions:FlagSet",
|
|
"DBCompactionAbortTest::ParkedCompaction:WaitForAbort"},
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
|
|
abort_sync.TriggerAbort(dbfull(), handles_[1]);
|
|
TEST_SYNC_POINT_CALLBACK(
|
|
"DBCompactionAbortTest::ParkedCompaction:WaitForAbort", nullptr);
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"DBImpl::PickCompactionFromQueue:Parked", [&](void* /*arg*/) {
|
|
std::lock_guard<std::mutex> guard(parked_mutex);
|
|
compaction_parked = true;
|
|
parked_cv.notify_all();
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
|
|
Random rnd(301);
|
|
for (int file = 0; file < kCompactionTrigger; ++file) {
|
|
for (int key = 0; key < 100; ++key) {
|
|
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
|
|
}
|
|
ASSERT_OK(Flush(1));
|
|
}
|
|
{
|
|
std::unique_lock<std::mutex> lock(parked_mutex);
|
|
parked_cv.wait(lock, [&]() { return compaction_parked; });
|
|
}
|
|
|
|
CleanupSyncPoints();
|
|
abort_sync.WaitForAbortCompletion();
|
|
const int l0_files_before_resume = NumTableFilesAtLevel(0, 1);
|
|
ASSERT_GE(l0_files_before_resume, kCompactionTrigger);
|
|
|
|
WaitForCompactOptions wait_options;
|
|
wait_options.timeout = std::chrono::milliseconds(1);
|
|
ASSERT_TRUE(dbfull()->WaitForCompact(wait_options).IsTimedOut());
|
|
|
|
for (int file = 0; file < kCompactionTrigger; ++file) {
|
|
for (int key = 0; key < 100; ++key) {
|
|
ASSERT_OK(Put(2, Key(key), rnd.RandomString(1000)));
|
|
}
|
|
ASSERT_OK(Flush(2));
|
|
}
|
|
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
|
|
ASSERT_LT(NumTableFilesAtLevel(0, 2), kCompactionTrigger);
|
|
ASSERT_EQ(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
|
|
|
|
std::atomic<bool> target_reenqueued{false};
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"DBImpl::RestoreParkedCompaction:cfd", [&](void* arg) {
|
|
auto* cfd = static_cast<ColumnFamilyData*>(arg);
|
|
if (cfd->GetName() == "target") {
|
|
target_reenqueued.store(true);
|
|
}
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
dbfull()->ResumeCompactions(handles_[1]);
|
|
ASSERT_TRUE(target_reenqueued.load());
|
|
CleanupSyncPoints();
|
|
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
|
ASSERT_LT(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, DropColumnFamilyReleasesParkedCompaction) {
|
|
constexpr int kCompactionTrigger = 4;
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
|
options.max_background_compactions = 1;
|
|
options.disable_auto_compactions = false;
|
|
CreateAndReopenWithCF({"target"}, options);
|
|
|
|
AbortSynchronizer abort_sync;
|
|
std::mutex parked_mutex;
|
|
std::condition_variable parked_cv;
|
|
bool compaction_parked = false;
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
|
{"DBImpl::AbortCompactions:FlagSet",
|
|
"DBCompactionAbortTest::DropParkedCompaction:WaitForAbort"},
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
|
|
abort_sync.TriggerAbort(dbfull(), handles_[1]);
|
|
TEST_SYNC_POINT_CALLBACK(
|
|
"DBCompactionAbortTest::DropParkedCompaction:WaitForAbort",
|
|
nullptr);
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"DBImpl::PickCompactionFromQueue:Parked", [&](void* /*arg*/) {
|
|
std::lock_guard<std::mutex> guard(parked_mutex);
|
|
compaction_parked = true;
|
|
parked_cv.notify_all();
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
|
|
Random rnd(301);
|
|
for (int file = 0; file < kCompactionTrigger; ++file) {
|
|
for (int key = 0; key < 100; ++key) {
|
|
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
|
|
}
|
|
ASSERT_OK(Flush(1));
|
|
}
|
|
{
|
|
std::unique_lock<std::mutex> lock(parked_mutex);
|
|
parked_cv.wait(lock, [&]() { return compaction_parked; });
|
|
}
|
|
|
|
CleanupSyncPoints();
|
|
abort_sync.WaitForAbortCompletion();
|
|
ASSERT_OK(dbfull()->DropColumnFamily(handles_[1]));
|
|
|
|
WaitForCompactOptions wait_options;
|
|
wait_options.timeout = std::chrono::seconds(10);
|
|
ASSERT_OK(dbfull()->WaitForCompact(wait_options));
|
|
dbfull()->ResumeCompactions(handles_[1]);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, DropColumnFamilyBeforeAbortedQueueEntryIsParked) {
|
|
constexpr int kCompactionTrigger = 4;
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
|
options.max_background_compactions = 1;
|
|
options.disable_auto_compactions = false;
|
|
CreateAndReopenWithCF({"target"}, options);
|
|
|
|
std::mutex worker_mutex;
|
|
std::condition_variable worker_cv;
|
|
bool worker_started = false;
|
|
bool allow_worker_to_pick = false;
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"BackgroundCallCompaction:0", [&](void* /*arg*/) {
|
|
std::unique_lock<std::mutex> lock(worker_mutex);
|
|
worker_started = true;
|
|
worker_cv.notify_all();
|
|
worker_cv.wait(lock, [&]() { return allow_worker_to_pick; });
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
|
|
for (int file = 0; file < kCompactionTrigger; ++file) {
|
|
ASSERT_OK(Put(1, Key(file), "value"));
|
|
ASSERT_OK(Flush(1));
|
|
}
|
|
{
|
|
std::unique_lock<std::mutex> lock(worker_mutex);
|
|
worker_cv.wait(lock, [&]() { return worker_started; });
|
|
}
|
|
|
|
dbfull()->AbortCompactions(handles_[1]);
|
|
const Status drop_status = dbfull()->DropColumnFamily(handles_[1]);
|
|
{
|
|
std::lock_guard<std::mutex> guard(worker_mutex);
|
|
allow_worker_to_pick = true;
|
|
}
|
|
worker_cv.notify_all();
|
|
|
|
WaitForCompactOptions wait_options;
|
|
wait_options.timeout = std::chrono::seconds(10);
|
|
const Status wait_status = dbfull()->WaitForCompact(wait_options);
|
|
CleanupSyncPoints();
|
|
dbfull()->ResumeCompactions(handles_[1]);
|
|
|
|
ASSERT_OK(drop_status);
|
|
ASSERT_OK(wait_status);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, NewCompactionWorkWhileAbortedIsParked) {
|
|
constexpr int kCompactionTrigger = 4;
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
|
options.max_background_compactions = 1;
|
|
options.disable_auto_compactions = false;
|
|
CreateAndReopenWithCF({"target"}, options);
|
|
|
|
dbfull()->AbortCompactions(handles_[1]);
|
|
for (int file = 0; file < kCompactionTrigger; ++file) {
|
|
ASSERT_OK(Put(1, Key(file), "value"));
|
|
ASSERT_OK(Flush(1));
|
|
}
|
|
|
|
const int l0_files_before_resume = NumTableFilesAtLevel(0, 1);
|
|
WaitForCompactOptions wait_options;
|
|
wait_options.timeout = std::chrono::milliseconds(1);
|
|
const Status wait_status = dbfull()->WaitForCompact(wait_options);
|
|
|
|
dbfull()->ResumeCompactions(handles_[1]);
|
|
ASSERT_TRUE(wait_status.IsTimedOut());
|
|
ASSERT_GE(l0_files_before_resume, kCompactionTrigger);
|
|
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
|
ASSERT_LT(NumTableFilesAtLevel(0, 1), l0_files_before_resume);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortWaitsForCompactionCompletionCallback) {
|
|
constexpr int kCompactionTrigger = 4;
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
|
options.max_background_compactions = 1;
|
|
options.disable_auto_compactions = false;
|
|
options.listeners.emplace_back(std::make_shared<EventListener>());
|
|
CreateAndReopenWithCF({"target"}, options);
|
|
|
|
std::mutex callback_mutex;
|
|
std::condition_variable callback_cv;
|
|
bool callback_started = false;
|
|
bool allow_callback_to_finish = false;
|
|
enum class AbortState { kNotObserved, kWaiting, kComplete };
|
|
AbortState abort_state = AbortState::kNotObserved;
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"DBImpl::NotifyOnCompactionCompleted::UnlockMutex", [&](void* /*arg*/) {
|
|
std::unique_lock<std::mutex> lock(callback_mutex);
|
|
callback_started = true;
|
|
callback_cv.notify_all();
|
|
callback_cv.wait(lock, [&]() { return allow_callback_to_finish; });
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"DBImpl::AbortCompactions:Waiting", [&](void* /*arg*/) {
|
|
std::lock_guard<std::mutex> guard(callback_mutex);
|
|
abort_state = AbortState::kWaiting;
|
|
callback_cv.notify_all();
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"DBImpl::AbortCompactions:Complete", [&](void* /*arg*/) {
|
|
std::lock_guard<std::mutex> guard(callback_mutex);
|
|
abort_state = AbortState::kComplete;
|
|
callback_cv.notify_all();
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
|
|
for (int file = 0; file < kCompactionTrigger; ++file) {
|
|
ASSERT_OK(Put(1, Key(file), "value"));
|
|
ASSERT_OK(Flush(1));
|
|
}
|
|
{
|
|
std::unique_lock<std::mutex> lock(callback_mutex);
|
|
callback_cv.wait(lock, [&]() { return callback_started; });
|
|
}
|
|
|
|
std::thread abort_thread([&]() { dbfull()->AbortCompactions(handles_[1]); });
|
|
bool returned_while_callback_blocked = false;
|
|
{
|
|
std::unique_lock<std::mutex> lock(callback_mutex);
|
|
callback_cv.wait(lock,
|
|
[&]() { return abort_state != AbortState::kNotObserved; });
|
|
returned_while_callback_blocked = abort_state == AbortState::kComplete;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> guard(callback_mutex);
|
|
allow_callback_to_finish = true;
|
|
}
|
|
callback_cv.notify_all();
|
|
abort_thread.join();
|
|
CleanupSyncPoints();
|
|
dbfull()->ResumeCompactions(handles_[1]);
|
|
|
|
ASSERT_FALSE(returned_while_callback_blocked);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest,
|
|
DroppedColumnFamilyFilesPurgedAfterTrackedCompaction) {
|
|
constexpr int kCompactionTrigger = 4;
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = kCompactionTrigger;
|
|
options.max_background_compactions = 1;
|
|
options.disable_auto_compactions = false;
|
|
CreateAndReopenWithCF({"target"}, options);
|
|
|
|
std::mutex compaction_mutex;
|
|
std::condition_variable compaction_cv;
|
|
bool compaction_started = false;
|
|
bool allow_compaction_to_finish = false;
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"CompactionJob::Run():Start", [&](void* /*arg*/) {
|
|
std::unique_lock<std::mutex> lock(compaction_mutex);
|
|
compaction_started = true;
|
|
compaction_cv.notify_all();
|
|
compaction_cv.wait(lock, [&]() { return allow_compaction_to_finish; });
|
|
});
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
|
|
Random rnd(301);
|
|
for (int file = 0; file < kCompactionTrigger; ++file) {
|
|
for (int key = 0; key < 100; ++key) {
|
|
ASSERT_OK(Put(1, Key(key), rnd.RandomString(1000)));
|
|
}
|
|
ASSERT_OK(Flush(1));
|
|
}
|
|
{
|
|
std::unique_lock<std::mutex> lock(compaction_mutex);
|
|
compaction_cv.wait(lock, [&]() { return compaction_started; });
|
|
}
|
|
|
|
const int sst_files_before_drop = GetSstFileCount(dbname_);
|
|
const Status drop_status = dbfull()->DropColumnFamily(handles_[1]);
|
|
Status destroy_handle_status;
|
|
if (drop_status.ok()) {
|
|
destroy_handle_status = dbfull()->DestroyColumnFamilyHandle(handles_[1]);
|
|
if (destroy_handle_status.ok()) {
|
|
handles_.erase(handles_.begin() + 1);
|
|
}
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> guard(compaction_mutex);
|
|
allow_compaction_to_finish = true;
|
|
}
|
|
compaction_cv.notify_all();
|
|
const Status wait_status = dbfull()->TEST_WaitForCompact();
|
|
CleanupSyncPoints();
|
|
|
|
ASSERT_OK(drop_status);
|
|
ASSERT_OK(destroy_handle_status);
|
|
ASSERT_OK(wait_status);
|
|
ASSERT_GT(sst_files_before_drop, 0);
|
|
ASSERT_EQ(0, GetSstFileCount(dbname_));
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, ResumeCompactionsAfterCloseIsNoOp) {
|
|
auto* const default_column_family = dbfull()->DefaultColumnFamily();
|
|
dbfull()->AbortCompactions(default_column_family);
|
|
|
|
ASSERT_OK(dbfull()->Close());
|
|
dbfull()->ResumeCompactions(default_column_family);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortAndVerifyNoOutputFiles) {
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.max_subcompactions = 2;
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
|
|
|
int num_l0_files_before = NumTableFilesAtLevel(0);
|
|
int num_l1_files_before = NumTableFilesAtLevel(1);
|
|
|
|
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
|
|
helper.Setup(dbfull());
|
|
|
|
CompactRangeOptions cro;
|
|
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
|
ASSERT_TRUE(s.IsIncomplete());
|
|
ASSERT_TRUE(s.IsCompactionAborted());
|
|
|
|
CleanupSyncPoints();
|
|
|
|
int num_l0_files_after = NumTableFilesAtLevel(0);
|
|
int num_l1_files_after = NumTableFilesAtLevel(1);
|
|
|
|
ASSERT_EQ(num_l0_files_before, num_l0_files_after);
|
|
ASSERT_EQ(num_l1_files_before, num_l1_files_after);
|
|
|
|
helper.WaitForAbortCompletion();
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
|
|
|
int num_l0_files_final = NumTableFilesAtLevel(0);
|
|
int num_l1_files_final = NumTableFilesAtLevel(1);
|
|
|
|
ASSERT_EQ(0, num_l0_files_final);
|
|
ASSERT_GT(num_l1_files_final, 0);
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, MultipleAbortResumeSequence) {
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.max_subcompactions = 2;
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
|
|
|
for (int round = 0; round < 3; ++round) {
|
|
// Use SyncPointAbortHelper for deterministic abort timing - it waits
|
|
// for the abort flag to be set via sync point dependency
|
|
SyncPointAbortHelper helper(
|
|
"CompactionJob::ProcessKeyValueCompaction:Start");
|
|
helper.Setup(dbfull());
|
|
|
|
CompactRangeOptions cro;
|
|
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
|
ASSERT_TRUE(s.IsIncomplete());
|
|
ASSERT_TRUE(s.IsCompactionAborted());
|
|
|
|
helper.CleanupAndWait();
|
|
dbfull()->ResumeAllCompactions();
|
|
}
|
|
|
|
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortWithOutputFilesCleanup) {
|
|
Options options = CurrentOptions();
|
|
options.num_levels = 2; // Ensure compaction output goes to L1
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.max_subcompactions = 2;
|
|
options.disable_auto_compactions = true;
|
|
options.target_file_size_base = 50 * 1024;
|
|
Reopen(options);
|
|
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/100);
|
|
|
|
SyncPointAbortHelper helper("CompactionJob::RunSubcompactions:BeforeStart");
|
|
helper.Setup(dbfull());
|
|
|
|
CompactRangeOptions cro;
|
|
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
|
ASSERT_TRUE(s.IsIncomplete());
|
|
ASSERT_TRUE(s.IsCompactionAborted());
|
|
|
|
CleanupSyncPoints();
|
|
|
|
int num_l1_files_after_abort = NumTableFilesAtLevel(1);
|
|
ASSERT_EQ(0, num_l1_files_after_abort);
|
|
|
|
helper.WaitForAbortCompletion();
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
|
|
|
// Verify L0 files are compacted and L1 has output files
|
|
int num_l0_files_final = NumTableFilesAtLevel(0);
|
|
int num_l1_files_final = NumTableFilesAtLevel(1);
|
|
ASSERT_EQ(0, num_l0_files_final)
|
|
<< "L0 should be empty after successful compaction";
|
|
ASSERT_GT(num_l1_files_final, 0)
|
|
<< "L1 should have files after successful compaction";
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, NestedAbortResumeCalls) {
|
|
// Test that nested AbortAllCompactions() calls work correctly with the
|
|
// counter
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.max_subcompactions = 2;
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
|
|
|
// First abort call
|
|
dbfull()->AbortAllCompactions();
|
|
|
|
// Nested abort call (counter should be 2)
|
|
dbfull()->AbortAllCompactions();
|
|
|
|
// Compaction should still be blocked after one resume
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
// Compaction should still return aborted because counter is still 1
|
|
CompactRangeOptions cro;
|
|
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
|
ASSERT_TRUE(s.IsIncomplete());
|
|
ASSERT_TRUE(s.IsCompactionAborted());
|
|
|
|
// Second resume - counter should be 0 now
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
// Compaction should succeed now
|
|
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortCompactFilesAPI) {
|
|
// Test that AbortAllCompactions works with CompactFiles API
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 100; // Disable auto compaction
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
|
|
|
// Get the L0 file names
|
|
std::vector<std::string> files_to_compact;
|
|
ColumnFamilyMetaData cf_meta;
|
|
dbfull()->GetColumnFamilyMetaData(dbfull()->DefaultColumnFamily(), &cf_meta);
|
|
for (const auto& file : cf_meta.levels[0].files) {
|
|
files_to_compact.push_back(file.name);
|
|
}
|
|
ASSERT_GE(files_to_compact.size(), 2);
|
|
|
|
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
|
|
helper.Setup(dbfull());
|
|
|
|
CompactionOptions compact_options;
|
|
Status s = dbfull()->CompactFiles(compact_options, files_to_compact, 1);
|
|
ASSERT_TRUE(s.IsIncomplete());
|
|
ASSERT_TRUE(s.IsCompactionAborted());
|
|
|
|
helper.CleanupAndWait();
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
// CompactFiles should work after resume
|
|
ASSERT_OK(dbfull()->CompactFiles(compact_options, files_to_compact, 1));
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortDoesNotAffectFlush) {
|
|
// Test that AbortAllCompactions does not affect flush operations
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 100;
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
Random rnd(301);
|
|
for (int j = 0; j < 100; ++j) {
|
|
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
|
|
}
|
|
|
|
// Abort compactions
|
|
dbfull()->AbortAllCompactions();
|
|
|
|
// Flush should still work
|
|
ASSERT_OK(Flush());
|
|
|
|
// Write more data
|
|
for (int j = 100; j < 200; ++j) {
|
|
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
|
|
}
|
|
|
|
// Flush should still work
|
|
ASSERT_OK(Flush());
|
|
|
|
// Resume compactions
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/200);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortBeforeCompactionStarts) {
|
|
// Test aborting before any compaction has started
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
|
|
|
|
// Abort before starting compaction
|
|
dbfull()->AbortAllCompactions();
|
|
|
|
// Compaction should immediately return aborted
|
|
CompactRangeOptions cro;
|
|
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
|
ASSERT_TRUE(s.IsIncomplete());
|
|
ASSERT_TRUE(s.IsCompactionAborted());
|
|
|
|
// Resume
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
// Now compaction should work
|
|
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
|
|
|
// Verify L0 files are compacted
|
|
ASSERT_EQ(0, NumTableFilesAtLevel(0));
|
|
}
|
|
|
|
// Test that in-progress blob and SST files are properly cleaned up when
|
|
// compaction is aborted. This specifically tests the case where abort happens
|
|
// while files are being written (opened but not yet completed/closed).
|
|
// This catches the bug where files exist on disk but are removed from the
|
|
// outputs_ vector (e.g., by RemoveLastEmptyOutput when file_size is 0 because
|
|
// the builder was abandoned), leaving orphan files.
|
|
TEST_F(DBCompactionAbortTest, AbortWithInProgressFileCleanup) {
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.max_subcompactions =
|
|
1; // Single subcompaction for deterministic behavior
|
|
options.disable_auto_compactions = true;
|
|
options.target_file_size_base = 32 * 1024; // 32KB
|
|
|
|
// Enable BlobDB with garbage collection to force blob rewriting during
|
|
// compaction
|
|
options.enable_blob_files = true;
|
|
options.min_blob_size = 0; // All values go to blob files
|
|
options.blob_file_size =
|
|
1024 * 1024; // 1MB - large enough to not close during test
|
|
// Enable blob garbage collection - this forces blob data to be rewritten
|
|
// during compaction, creating new blob files
|
|
options.enable_blob_garbage_collection = true;
|
|
options.blob_garbage_collection_age_cutoff = 1.0; // Include all blob files
|
|
options.blob_garbage_collection_force_threshold = 0.0; // Always force GC
|
|
|
|
Reopen(options);
|
|
|
|
// Write enough data to trigger the periodic abort check (every 1000 records).
|
|
// 4 files * 2000 keys = 2000 unique overlapping keys processed during
|
|
// compaction. The sync point triggers at 999, 1999, etc.
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/2000, /*value_size=*/500);
|
|
|
|
// Helper function to get blob files on disk with their names
|
|
auto GetBlobFilesOnDisk = [this]() -> std::vector<std::string> {
|
|
std::vector<std::string> blob_files;
|
|
std::vector<std::string> files;
|
|
EXPECT_OK(env_->GetChildren(dbname_, &files));
|
|
for (const auto& f : files) {
|
|
if (f.find(".blob") != std::string::npos) {
|
|
blob_files.push_back(f);
|
|
}
|
|
}
|
|
std::sort(blob_files.begin(), blob_files.end());
|
|
return blob_files;
|
|
};
|
|
|
|
// Helper function to get blob file count in metadata
|
|
auto GetBlobFilesInMetadata = [this]() -> std::vector<uint64_t> {
|
|
std::vector<uint64_t> blob_file_numbers;
|
|
ColumnFamilyMetaData cf_meta;
|
|
dbfull()->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta);
|
|
for (const auto& blob_meta : cf_meta.blob_files) {
|
|
blob_file_numbers.push_back(blob_meta.blob_file_number);
|
|
}
|
|
std::sort(blob_file_numbers.begin(), blob_file_numbers.end());
|
|
return blob_file_numbers;
|
|
};
|
|
|
|
// Helper function to get SST files on disk
|
|
auto GetSstFilesOnDisk = [this]() -> std::vector<std::string> {
|
|
std::vector<std::string> sst_files;
|
|
std::vector<std::string> files;
|
|
EXPECT_OK(env_->GetChildren(dbname_, &files));
|
|
for (const auto& f : files) {
|
|
if (f.find(".sst") != std::string::npos) {
|
|
sst_files.push_back(f);
|
|
}
|
|
}
|
|
std::sort(sst_files.begin(), sst_files.end());
|
|
return sst_files;
|
|
};
|
|
|
|
// Helper function to get SST file numbers in metadata
|
|
auto GetSstFilesInMetadata = [this]() -> std::vector<uint64_t> {
|
|
std::vector<uint64_t> sst_file_numbers;
|
|
ColumnFamilyMetaData cf_meta;
|
|
dbfull()->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta);
|
|
for (const auto& level : cf_meta.levels) {
|
|
for (const auto& file : level.files) {
|
|
// Extract file number from the file name (e.g., "000010.sst" -> 10)
|
|
uint64_t file_num = 0;
|
|
std::string fname = file.name;
|
|
// Remove leading path separators if present
|
|
size_t pos = fname.rfind('/');
|
|
if (pos != std::string::npos) {
|
|
fname = fname.substr(pos + 1);
|
|
}
|
|
if (sscanf(fname.c_str(), "%" PRIu64, &file_num) == 1) {
|
|
sst_file_numbers.push_back(file_num);
|
|
}
|
|
}
|
|
}
|
|
std::sort(sst_file_numbers.begin(), sst_file_numbers.end());
|
|
return sst_file_numbers;
|
|
};
|
|
|
|
std::vector<std::string> initial_blob_files = GetBlobFilesOnDisk();
|
|
std::vector<uint64_t> initial_meta_blobs = GetBlobFilesInMetadata();
|
|
std::vector<std::string> initial_sst_files = GetSstFilesOnDisk();
|
|
std::vector<uint64_t> initial_meta_ssts = GetSstFilesInMetadata();
|
|
|
|
ASSERT_GT(initial_blob_files.size(), 0u) << "Expected initial blob files";
|
|
ASSERT_EQ(initial_blob_files.size(), initial_meta_blobs.size())
|
|
<< "Initial blob files should match between disk and metadata";
|
|
ASSERT_GT(initial_sst_files.size(), 0u) << "Expected initial SST files";
|
|
ASSERT_EQ(initial_sst_files.size(), initial_meta_ssts.size())
|
|
<< "Initial SST files should match between disk and metadata";
|
|
|
|
// Tracking variables for blob file lifecycle
|
|
std::atomic<int> blob_writes{0};
|
|
std::atomic<bool> abort_triggered{false};
|
|
AbortSynchronizer abort_sync;
|
|
|
|
// Set up dependency: the wait point will block until FlagSet is hit
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
|
|
{"DBImpl::AbortAllCompactions:FlagSet",
|
|
"DBCompactionAbortTest::InProgressBlob:WaitForAbort"},
|
|
});
|
|
|
|
// Trigger abort after some blob writes during compaction output.
|
|
// This ensures we have an in-progress blob file when abort happens.
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
|
"BlobFileBuilder::WriteBlobToFile:AddRecord", [&](void* /*arg*/) {
|
|
int count = blob_writes.fetch_add(1) + 1;
|
|
|
|
// Trigger abort after 100 blob writes - this ensures:
|
|
// 1. A blob file has been opened (for writing)
|
|
// 2. Some data has been written to it
|
|
// 3. But it's not yet completed (blob_file_size is 1MB)
|
|
if (count == 100 && !abort_triggered.exchange(true)) {
|
|
abort_sync.TriggerAbort(dbfull());
|
|
// Wait for abort flag to be set - this sync point blocks until
|
|
// FlagSet is processed
|
|
TEST_SYNC_POINT_CALLBACK(
|
|
"DBCompactionAbortTest::InProgressBlob:WaitForAbort", nullptr);
|
|
}
|
|
});
|
|
|
|
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
|
|
|
// Run compaction - it should be aborted while blob file is in-progress
|
|
CompactRangeOptions cro;
|
|
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
|
|
|
ASSERT_TRUE(s.IsIncomplete())
|
|
<< "Expected compaction to be aborted, got: " << s.ToString();
|
|
|
|
CleanupSyncPoints();
|
|
abort_sync.WaitForAbortCompletion();
|
|
|
|
// Check state after abort
|
|
std::vector<std::string> post_abort_disk_blobs = GetBlobFilesOnDisk();
|
|
std::vector<uint64_t> post_abort_meta_blobs = GetBlobFilesInMetadata();
|
|
std::vector<std::string> post_abort_disk_ssts = GetSstFilesOnDisk();
|
|
std::vector<uint64_t> post_abort_meta_ssts = GetSstFilesInMetadata();
|
|
|
|
// This is the key assertion for blob files: files on disk should match
|
|
// metadata. If the in-progress blob file was NOT cleaned up, there will be an
|
|
// extra file on disk that's not in metadata (orphan).
|
|
ASSERT_EQ(post_abort_disk_blobs.size(), post_abort_meta_blobs.size())
|
|
<< "Orphan blob file detected! In-progress blob file was not cleaned up "
|
|
"after abort. Files on disk: "
|
|
<< post_abort_disk_blobs.size()
|
|
<< ", Files in metadata: " << post_abort_meta_blobs.size()
|
|
<< ". The difference indicates orphaned in-progress blob file(s).";
|
|
|
|
// This is the key assertion for SST files: files on disk should match
|
|
// metadata. If the in-progress SST file was NOT cleaned up, there will be an
|
|
// extra file on disk that's not in metadata (orphan).
|
|
ASSERT_EQ(post_abort_disk_ssts.size(), post_abort_meta_ssts.size())
|
|
<< "Orphan SST file detected! In-progress SST file was not cleaned up "
|
|
"after abort. Files on disk: "
|
|
<< post_abort_disk_ssts.size()
|
|
<< ", Files in metadata: " << post_abort_meta_ssts.size()
|
|
<< ". The difference indicates orphaned in-progress SST file(s).";
|
|
|
|
// Resume and complete compaction to verify DB is still functional
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
|
|
|
// Verify data integrity - we wrote 4 files * 2000 keys with overlapping keys
|
|
VerifyDataIntegrity(/*num_keys=*/2000);
|
|
}
|
|
|
|
TEST_F(DBCompactionAbortTest, AbortBottommostLevelCompaction) {
|
|
Options options = CurrentOptions();
|
|
options.num_levels = 4;
|
|
options.level0_file_num_compaction_trigger = 2;
|
|
options.max_bytes_for_level_base = 1024 * 10; // 10KB
|
|
options.max_bytes_for_level_multiplier = 2;
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
// Write data to fill multiple levels (non-overlapping keys)
|
|
PopulateData(/*num_files=*/6, /*keys_per_file=*/100,
|
|
/*value_size=*/500, /*overlapping=*/false);
|
|
|
|
// First compact to push data to lower levels
|
|
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
|
|
|
// Write more data to L0 (overlapping keys)
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/500);
|
|
|
|
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
|
|
helper.Setup(dbfull());
|
|
|
|
// Trigger bottommost level compaction
|
|
CompactRangeOptions cro;
|
|
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
|
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
|
ASSERT_TRUE(s.IsIncomplete());
|
|
ASSERT_TRUE(s.IsCompactionAborted());
|
|
|
|
helper.CleanupAndWait();
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
|
|
|
|
VerifyDataIntegrity(/*num_keys=*/600);
|
|
}
|
|
|
|
// Test that while compactions are aborted, atomic range replace
|
|
// (IngestExternalFiles with atomic_replace_range) works correctly.
|
|
// This verifies that the abort state doesn't block other write operations
|
|
// like atomic range replace.
|
|
TEST_F(DBCompactionAbortTest, AbortThenAtomicRangeReplace) {
|
|
Options options = CurrentOptions();
|
|
options.level0_file_num_compaction_trigger = 4;
|
|
options.max_subcompactions = 2;
|
|
options.disable_auto_compactions = true;
|
|
Reopen(options);
|
|
|
|
// Create a directory for SST files
|
|
std::string sst_files_dir = dbname_ + "_sst_files/";
|
|
ASSERT_OK(env_->CreateDirIfMissing(sst_files_dir));
|
|
|
|
// Populate initial data with overlapping keys
|
|
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/500);
|
|
|
|
// Verify initial data
|
|
VerifyDataIntegrity(/*num_keys=*/100);
|
|
|
|
// Trigger compaction and abort it
|
|
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
|
|
helper.Setup(dbfull());
|
|
|
|
CompactRangeOptions cro;
|
|
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
|
|
ASSERT_TRUE(s.IsIncomplete());
|
|
ASSERT_TRUE(s.IsCompactionAborted());
|
|
|
|
helper.CleanupAndWait();
|
|
|
|
// While compaction is still aborted, perform atomic range replace using
|
|
// IngestExternalFiles with atomic_replace_range. This verifies that the
|
|
// abort state doesn't block other write operations.
|
|
// Using RangeOpt() (empty range) means replace everything in the CF.
|
|
|
|
// Create an SST file with new data for keys 0-49 (replacing keys 0-99)
|
|
std::string sst_file_path = sst_files_dir + "atomic_replace_1.sst";
|
|
SstFileWriter sst_file_writer(EnvOptions(), options);
|
|
ASSERT_OK(sst_file_writer.Open(sst_file_path));
|
|
|
|
// Write new values for keys 0-49
|
|
Random rnd(42);
|
|
std::unordered_map<std::string, std::string> new_values;
|
|
for (int j = 0; j < 50; ++j) {
|
|
std::string key = Key(j);
|
|
std::string value = "replaced_" + rnd.RandomString(100);
|
|
ASSERT_OK(sst_file_writer.Put(key, value));
|
|
new_values[key] = value;
|
|
}
|
|
ASSERT_OK(sst_file_writer.Finish());
|
|
|
|
// Perform atomic range replace for the entire column family.
|
|
// Using RangeOpt() (default constructor) means replace everything in the CF.
|
|
IngestExternalFileArg arg;
|
|
arg.column_family = db_->DefaultColumnFamily();
|
|
arg.external_files = {sst_file_path};
|
|
arg.atomic_replace_range = RangeOpt();
|
|
// snapshot_consistency must be false when using atomic_replace_range
|
|
arg.options.snapshot_consistency = false;
|
|
|
|
// Atomic range replace should work even while compactions are aborted
|
|
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
|
|
|
// Now resume compactions after the atomic range replace
|
|
dbfull()->ResumeAllCompactions();
|
|
|
|
// Verify that the atomic range replace worked correctly:
|
|
// 1. Keys 0-49 should have new replaced values
|
|
std::string val;
|
|
for (int j = 0; j < 50; ++j) {
|
|
std::string key = Key(j);
|
|
ASSERT_OK(db_->Get(ReadOptions(), key, &val));
|
|
auto it = new_values.find(key);
|
|
ASSERT_NE(it, new_values.end());
|
|
ASSERT_EQ(it->second, val) << "Value mismatch for replaced key: " << key;
|
|
}
|
|
|
|
// 2. Keys 50-99 should not exist (they were replaced/deleted by atomic
|
|
// replace)
|
|
for (int j = 50; j < 100; ++j) {
|
|
std::string key = Key(j);
|
|
Status get_status = db_->Get(ReadOptions(), key, &val);
|
|
ASSERT_TRUE(get_status.IsNotFound())
|
|
<< "Key " << key << " should not exist after full CF replace";
|
|
}
|
|
|
|
// Clean up SST files directory
|
|
ASSERT_OK(DestroyDir(env_, sst_files_dir));
|
|
}
|
|
|
|
} // namespace ROCKSDB_NAMESPACE
|
|
|
|
int main(int argc, char** argv) {
|
|
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|