1
0
mirror of https://github.com/facebook/rocksdb.git synced 2026-10-23 08:09:02 +03:00
Files
rocksdb/db/db_write_buffer_manager_test.cc
Josh Kang 014e1c43c1 Apply readability fixes (isolate decls, braces) (#15394)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15394

## Summary

RocksDB's `.clang-tidy` enables several readability checks that existing
code still violates: `readability-isolate-declaration` (476 findings),
`readability-braces-around-statements` (49),
`readability-redundant-string-init` (25), `modernize-use-using` (10),
`misc-definitions-in-headers` (3) and `readability-duplicate-include`
(1). They keep getting flagged whenever nearby lines change. This diff
fixes 553 of the 564; upstream has since fixed the other 11.

What changed:
- Multi-variable declarations such as `std::string a, b;` are split into
  one declaration per variable. Pointer and reference declarators are kept
  per variable.
- Braces are added around single-statement `if`/`else`/`for`/`while`
  bodies, and empty spin loops get an empty `{}` body.
- `std::string s = ""` / `member("")` initializers are removed.
- Remaining `typedef`s in the touched C++ files become `using`. C headers
  and `extern "C"` code are untouched.
- `inline` is added to non-template function definitions in test headers.
- A duplicate `#include "util/udt_util.h"` in `db_impl.cc` is removed.

These are clang-tidy's own fix-its, applied mechanically and then
clang-formatted on the changed lines. Three kinds of broken fix-it output
were repaired by hand. Removing a `member("")` initializer left a dangling
comma in constructor initializer lists (`ExternalSstFileInfo`,
`ColumnFamilyMetaData` and others). The function-type typedefs in
`block_test.cc` came out malformed. Empty `while (cond);` spin loops got a
`{;` body. Splitting declarations also exposed two vectors in
`db_etc2_test.cc` to `performance-inefficient-vector-operation`, so a
`reserve()` was added for each.

The lint bot reports every finding in a file a diff touches. This diff
splits declarations in `options/configurable_test.cc`, so it also fixes
that file's 14 older `cert-err58-cpp` and
`cppcoreguidelines-avoid-non-const-global-variables` findings, which come
from seven mutable file-level maps. Six are now `const` and built where
they are used (function-local statics, or locals in their only user).
The unused `imm_struct_option_info` is deleted.

Compatibility: no behavior change. In public headers, the only edits are
removed redundant `""` initializers, split declarations and added braces.

Reviewed By: rban1

Differential Revision: D123186310

fbshipit-source-id: f9c8e88b00e997db0196f8725c7c8d880cf798da
2026-10-13 14:06:55 -07:00

2675 lines
99 KiB
C++

// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// 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).
//
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include "db/db_test_util.h"
#include "db/write_thread.h"
#include "port/stack_trace.h"
#include "util/defer.h"
namespace ROCKSDB_NAMESPACE {
class DBWriteBufferManagerTest : public DBTestBase,
public testing::WithParamInterface<bool> {
public:
DBWriteBufferManagerTest()
: DBTestBase("db_write_buffer_manager_test", /*env_do_fsync=*/false) {}
bool cost_cache_;
};
TEST_P(DBWriteBufferManagerTest, SharedBufferAcrossCFs1) {
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 500000; // this is never hit
std::shared_ptr<Cache> cache = NewLRUCache(4 * 1024 * 1024, 2);
ASSERT_LT(cache->GetUsage(), 256 * 1024);
cost_cache_ = GetParam();
if (cost_cache_) {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, cache, true));
} else {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, nullptr, true));
}
WriteOptions wo;
wo.disableWAL = true;
CreateAndReopenWithCF({"cf1", "cf2", "cf3"}, options);
ASSERT_OK(Put(3, Key(1), DummyString(1), wo));
ASSERT_OK(Flush(3));
ASSERT_OK(Put(3, Key(1), DummyString(1), wo));
ASSERT_OK(Put(0, Key(1), DummyString(1), wo));
ASSERT_OK(Flush(0));
// Write to "Default", "cf2" and "cf3".
ASSERT_OK(Put(3, Key(1), DummyString(30000), wo));
ASSERT_OK(Put(0, Key(1), DummyString(40000), wo));
ASSERT_OK(Put(2, Key(1), DummyString(1), wo));
ASSERT_OK(Put(3, Key(2), DummyString(40000), wo));
// WriteBufferManager::buffer_size_ has exceeded after the previous write is
// completed.
// This make sures write will go through and if stall was in effect, it will
// end.
ASSERT_OK(Put(0, Key(2), DummyString(1), wo));
}
// Test Single DB with multiple writer threads get blocked when
// WriteBufferManager execeeds buffer_size_ and flush is waiting to be
// finished.
TEST_P(DBWriteBufferManagerTest, SharedWriteBufferAcrossCFs2) {
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 500000; // this is never hit
std::shared_ptr<Cache> cache = NewLRUCache(4 * 1024 * 1024, 2);
ASSERT_LT(cache->GetUsage(), 256 * 1024);
cost_cache_ = GetParam();
if (cost_cache_) {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, cache, true));
} else {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, nullptr, true));
}
WriteOptions wo;
wo.disableWAL = true;
CreateAndReopenWithCF({"cf1", "cf2", "cf3"}, options);
ASSERT_OK(Put(3, Key(1), DummyString(1), wo));
ASSERT_OK(Flush(3));
ASSERT_OK(Put(3, Key(1), DummyString(1), wo));
ASSERT_OK(Put(0, Key(1), DummyString(1), wo));
ASSERT_OK(Flush(0));
// Write to "Default", "cf2" and "cf3". No flush will be triggered.
ASSERT_OK(Put(3, Key(1), DummyString(30000), wo));
ASSERT_OK(Put(0, Key(1), DummyString(40000), wo));
ASSERT_OK(Put(2, Key(1), DummyString(1), wo));
ASSERT_OK(Put(3, Key(2), DummyString(40000), wo));
// WriteBufferManager::buffer_size_ has exceeded after the previous write is
// completed.
std::unordered_set<WriteThread::Writer*> w_set;
std::vector<port::Thread> threads;
int wait_count_db = 0;
int num_writers = 4;
InstrumentedMutex mutex;
InstrumentedCondVar cv(&mutex);
std::atomic<int> thread_num(0);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0",
"DBImpl::BackgroundCallFlush:start"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WBMStallInterface::BlockDB", [&](void*) {
InstrumentedMutexLock lock(&mutex);
wait_count_db++;
cv.SignalAll();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WriteThread::WriteStall::Wait", [&](void* arg) {
InstrumentedMutexLock lock(&mutex);
WriteThread::Writer* w = static_cast<WriteThread::Writer*>(arg);
w_set.insert(w);
// Allow the flush to continue if all writer threads are blocked.
if (w_set.size() == (unsigned long)num_writers) {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0");
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
bool s = true;
std::function<void(int)> writer = [&](int cf) {
int a = thread_num.fetch_add(1);
std::string key = "foo" + std::to_string(a);
Status tmp = Put(cf, Slice(key), DummyString(1), wo);
InstrumentedMutexLock lock(&mutex);
s = s && tmp.ok();
};
// Flow:
// main_writer thread will write but will be blocked (as Flush will on hold,
// buffer_size_ has exceeded, thus will create stall in effect).
// |
// |
// multiple writer threads will be created to write across multiple columns
// and they will be blocked.
// |
// |
// Last writer thread will write and when its blocked it will signal Flush to
// continue to clear the stall.
threads.emplace_back(writer, 1);
// Wait untill first thread (main_writer) writing to DB is blocked and then
// create the multiple writers which will be blocked from getting added to the
// queue because stall is in effect.
{
InstrumentedMutexLock lock(&mutex);
while (wait_count_db != 1) {
cv.Wait();
}
}
for (int i = 0; i < num_writers; i++) {
threads.emplace_back(writer, i % 4);
}
for (auto& t : threads) {
t.join();
}
ASSERT_TRUE(s);
// Number of DBs blocked.
ASSERT_EQ(wait_count_db, 1);
// Number of Writer threads blocked.
ASSERT_EQ(w_set.size(), num_writers);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
}
// Test multiple DBs get blocked when WriteBufferManager limit exceeds and flush
// is waiting to be finished but DBs tries to write meanwhile.
TEST_P(DBWriteBufferManagerTest, SharedWriteBufferLimitAcrossDB) {
std::vector<std::string> dbnames;
std::vector<std::unique_ptr<DB>> dbs;
int num_dbs = 3;
for (int i = 0; i < num_dbs; i++) {
dbs.emplace_back();
dbnames.push_back(
test::PerThreadDBPath("db_shared_wb_db" + std::to_string(i)));
}
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 500000; // this is never hit
std::shared_ptr<Cache> cache = NewLRUCache(4 * 1024 * 1024, 2);
ASSERT_LT(cache->GetUsage(), 256 * 1024);
cost_cache_ = GetParam();
if (cost_cache_) {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, cache, true));
} else {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, nullptr, true));
}
CreateAndReopenWithCF({"cf1", "cf2"}, options);
for (int i = 0; i < num_dbs; i++) {
ASSERT_OK(DestroyDB(dbnames[i], options));
ASSERT_OK(DB::Open(options, dbnames[i], &(dbs[i])));
}
WriteOptions wo;
wo.disableWAL = true;
for (int i = 0; i < num_dbs; i++) {
ASSERT_OK(dbs[i]->Put(wo, Key(1), DummyString(20000)));
}
// Insert to db_.
ASSERT_OK(Put(0, Key(1), DummyString(30000), wo));
// WriteBufferManager Limit exceeded.
std::vector<port::Thread> threads;
int wait_count_db = 0;
InstrumentedMutex mutex;
InstrumentedCondVar cv(&mutex);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0",
"DBImpl::BackgroundCallFlush:start"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WBMStallInterface::BlockDB", [&](void*) {
{
InstrumentedMutexLock lock(&mutex);
wait_count_db++;
cv.Signal();
// Since this is the last DB, signal Flush to continue.
if (wait_count_db == num_dbs + 1) {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0");
}
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
bool s = true;
// Write to DB.
std::function<void(DB*)> write_db = [&](DB* db) {
Status tmp = db->Put(wo, Key(3), DummyString(1));
InstrumentedMutexLock lock(&mutex);
s = s && tmp.ok();
};
// Flow:
// db_ will write and will be blocked (as Flush will on hold and will create
// stall in effect).
// |
// multiple dbs writers will be created to write to that db and they will be
// blocked.
// |
// |
// Last writer will write and when its blocked it will signal Flush to
// continue to clear the stall.
threads.emplace_back(write_db, db_.get());
// Wait untill first DB is blocked and then create the multiple writers for
// different DBs which will be blocked from getting added to the queue because
// stall is in effect.
{
InstrumentedMutexLock lock(&mutex);
while (wait_count_db != 1) {
cv.Wait();
}
}
for (int i = 0; i < num_dbs; i++) {
threads.emplace_back(write_db, dbs[i].get());
}
for (auto& t : threads) {
t.join();
}
ASSERT_TRUE(s);
ASSERT_EQ(num_dbs + 1, wait_count_db);
// Clean up DBs.
for (int i = 0; i < num_dbs; i++) {
ASSERT_OK(dbs[i]->Close());
ASSERT_OK(DestroyDB(dbnames[i], options));
dbs[i].reset();
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
}
// Test multiple threads writing across multiple DBs and multiple columns get
// blocked when stall by WriteBufferManager is in effect.
TEST_P(DBWriteBufferManagerTest, SharedWriteBufferLimitAcrossDB1) {
std::vector<std::string> dbnames;
std::vector<std::unique_ptr<DB>> dbs;
int num_dbs = 3;
for (int i = 0; i < num_dbs; i++) {
dbs.emplace_back();
dbnames.push_back(
test::PerThreadDBPath("db_shared_wb_db" + std::to_string(i)));
}
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 500000; // this is never hit
std::shared_ptr<Cache> cache = NewLRUCache(4 * 1024 * 1024, 2);
ASSERT_LT(cache->GetUsage(), 256 * 1024);
cost_cache_ = GetParam();
if (cost_cache_) {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, cache, true));
} else {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, nullptr, true));
}
CreateAndReopenWithCF({"cf1", "cf2"}, options);
for (int i = 0; i < num_dbs; i++) {
ASSERT_OK(DestroyDB(dbnames[i], options));
ASSERT_OK(DB::Open(options, dbnames[i], &(dbs[i])));
}
WriteOptions wo;
wo.disableWAL = true;
for (int i = 0; i < num_dbs; i++) {
ASSERT_OK(dbs[i]->Put(wo, Key(1), DummyString(20000)));
}
// Insert to db_.
ASSERT_OK(Put(0, Key(1), DummyString(30000), wo));
// WriteBufferManager::buffer_size_ has exceeded after the previous write to
// dbs[0] is completed.
std::vector<port::Thread> threads;
int wait_count_db = 0;
InstrumentedMutex mutex;
InstrumentedCondVar cv(&mutex);
std::unordered_set<WriteThread::Writer*> w_set;
std::vector<port::Thread> writer_threads;
std::atomic<int> thread_num(0);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0",
"DBImpl::BackgroundCallFlush:start"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WBMStallInterface::BlockDB", [&](void*) {
{
InstrumentedMutexLock lock(&mutex);
wait_count_db++;
thread_num.fetch_add(1);
cv.Signal();
// Allow the flush to continue if all writer threads are blocked.
if (thread_num.load(std::memory_order_relaxed) == 2 * num_dbs + 1) {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0");
}
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WriteThread::WriteStall::Wait", [&](void* arg) {
WriteThread::Writer* w = static_cast<WriteThread::Writer*>(arg);
{
InstrumentedMutexLock lock(&mutex);
w_set.insert(w);
thread_num.fetch_add(1);
// Allow the flush continue if all writer threads are blocked.
if (thread_num.load(std::memory_order_relaxed) == 2 * num_dbs + 1) {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0");
}
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
bool s1 = true;
bool s2 = true;
// Write to multiple columns of db_.
std::function<void(int)> write_cf = [&](int cf) {
Status tmp = Put(cf, Key(3), DummyString(1), wo);
InstrumentedMutexLock lock(&mutex);
s1 = s1 && tmp.ok();
};
// Write to multiple DBs.
std::function<void(DB*)> write_db = [&](DB* db) {
Status tmp = db->Put(wo, Key(3), DummyString(1));
InstrumentedMutexLock lock(&mutex);
s2 = s2 && tmp.ok();
};
// Flow:
// thread will write to db_ will be blocked (as Flush will on hold,
// buffer_size_ has exceeded and will create stall in effect).
// |
// |
// multiple writers threads writing to different DBs and to db_ across
// multiple columns will be created and they will be blocked due to stall.
// |
// |
// Last writer thread will write and when its blocked it will signal Flush to
// continue to clear the stall.
threads.emplace_back(write_db, db_.get());
// Wait untill first thread is blocked and then create the multiple writer
// threads.
{
InstrumentedMutexLock lock(&mutex);
while (wait_count_db != 1) {
cv.Wait();
}
}
for (int i = 0; i < num_dbs; i++) {
// Write to multiple columns of db_.
writer_threads.emplace_back(write_cf, i % 3);
// Write to different dbs.
threads.emplace_back(write_db, dbs[i].get());
}
for (auto& t : threads) {
t.join();
}
for (auto& t : writer_threads) {
t.join();
}
ASSERT_TRUE(s1);
ASSERT_TRUE(s2);
// Number of DBs blocked.
ASSERT_EQ(num_dbs + 1, wait_count_db);
// Number of Writer threads blocked.
ASSERT_EQ(w_set.size(), num_dbs);
// Clean up DBs.
for (int i = 0; i < num_dbs; i++) {
ASSERT_OK(dbs[i]->Close());
ASSERT_OK(DestroyDB(dbnames[i], options));
dbs[i].reset();
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
}
// Test multiple threads writing across multiple columns of db_ by passing
// different values to WriteOption.no_slown_down.
TEST_P(DBWriteBufferManagerTest, MixedSlowDownOptionsSingleDB) {
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 500000; // this is never hit
std::shared_ptr<Cache> cache = NewLRUCache(4 * 1024 * 1024, 2);
ASSERT_LT(cache->GetUsage(), 256 * 1024);
cost_cache_ = GetParam();
if (cost_cache_) {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, cache, true));
} else {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, nullptr, true));
}
WriteOptions wo;
wo.disableWAL = true;
CreateAndReopenWithCF({"cf1", "cf2", "cf3"}, options);
ASSERT_OK(Put(3, Key(1), DummyString(1), wo));
ASSERT_OK(Flush(3));
ASSERT_OK(Put(3, Key(1), DummyString(1), wo));
ASSERT_OK(Put(0, Key(1), DummyString(1), wo));
ASSERT_OK(Flush(0));
// Write to "Default", "cf2" and "cf3". No flush will be triggered.
ASSERT_OK(Put(3, Key(1), DummyString(30000), wo));
ASSERT_OK(Put(0, Key(1), DummyString(40000), wo));
ASSERT_OK(Put(2, Key(1), DummyString(1), wo));
ASSERT_OK(Put(3, Key(2), DummyString(40000), wo));
// WriteBufferManager::buffer_size_ has exceeded after the previous write to
// db_ is completed.
std::unordered_set<WriteThread::Writer*> w_slowdown_set;
std::vector<port::Thread> threads;
int wait_count_db = 0;
int num_writers = 4;
InstrumentedMutex mutex;
InstrumentedCondVar cv(&mutex);
std::atomic<int> thread_num(0);
std::atomic<int> w_no_slowdown(0);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0",
"DBImpl::BackgroundCallFlush:start"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WBMStallInterface::BlockDB", [&](void*) {
{
InstrumentedMutexLock lock(&mutex);
wait_count_db++;
cv.SignalAll();
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WriteThread::WriteStall::Wait", [&](void* arg) {
{
InstrumentedMutexLock lock(&mutex);
WriteThread::Writer* w = static_cast<WriteThread::Writer*>(arg);
w_slowdown_set.insert(w);
// Allow the flush continue if all writer threads are blocked.
if (w_slowdown_set.size() + (unsigned long)w_no_slowdown.load(
std::memory_order_relaxed) ==
(unsigned long)num_writers) {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0");
}
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
bool s1 = true;
bool s2 = true;
std::function<void(int)> write_slow_down = [&](int cf) {
int a = thread_num.fetch_add(1);
std::string key = "foo" + std::to_string(a);
WriteOptions write_op;
write_op.no_slowdown = false;
Status tmp = Put(cf, Slice(key), DummyString(1), write_op);
InstrumentedMutexLock lock(&mutex);
s1 = s1 && tmp.ok();
};
std::function<void(int)> write_no_slow_down = [&](int cf) {
int a = thread_num.fetch_add(1);
std::string key = "foo" + std::to_string(a);
WriteOptions write_op;
write_op.no_slowdown = true;
Status tmp = Put(cf, Slice(key), DummyString(1), write_op);
{
InstrumentedMutexLock lock(&mutex);
s2 = s2 && !tmp.ok();
w_no_slowdown.fetch_add(1);
// Allow the flush continue if all writer threads are blocked.
if (w_slowdown_set.size() +
(unsigned long)w_no_slowdown.load(std::memory_order_relaxed) ==
(unsigned long)num_writers) {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0");
}
}
};
// Flow:
// main_writer thread will write but will be blocked (as Flush will on hold,
// buffer_size_ has exceeded, thus will create stall in effect).
// |
// |
// multiple writer threads will be created to write across multiple columns
// with different values of WriteOptions.no_slowdown. Some of them will
// be blocked and some of them will return with Incomplete status.
// |
// |
// Last writer thread will write and when its blocked/return it will signal
// Flush to continue to clear the stall.
threads.emplace_back(write_slow_down, 1);
// Wait untill first thread (main_writer) writing to DB is blocked and then
// create the multiple writers which will be blocked from getting added to the
// queue because stall is in effect.
{
InstrumentedMutexLock lock(&mutex);
while (wait_count_db != 1) {
cv.Wait();
}
}
for (int i = 0; i < num_writers; i += 2) {
threads.emplace_back(write_no_slow_down, (i) % 4);
threads.emplace_back(write_slow_down, (i + 1) % 4);
}
for (auto& t : threads) {
t.join();
}
ASSERT_TRUE(s1);
ASSERT_TRUE(s2);
// Number of DBs blocked.
ASSERT_EQ(wait_count_db, 1);
// Number of Writer threads blocked.
ASSERT_EQ(w_slowdown_set.size(), num_writers / 2);
// Number of Writer threads with WriteOptions.no_slowdown = true.
ASSERT_EQ(w_no_slowdown.load(std::memory_order_relaxed), num_writers / 2);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
}
// Test multiple threads writing across multiple columns of db_ and different
// dbs by passing different values to WriteOption.no_slown_down.
TEST_P(DBWriteBufferManagerTest, MixedSlowDownOptionsMultipleDB) {
std::vector<std::string> dbnames;
std::vector<std::unique_ptr<DB>> dbs;
int num_dbs = 4;
for (int i = 0; i < num_dbs; i++) {
dbs.emplace_back();
dbnames.push_back(
test::PerThreadDBPath("db_shared_wb_db" + std::to_string(i)));
}
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 500000; // this is never hit
std::shared_ptr<Cache> cache = NewLRUCache(4 * 1024 * 1024, 2);
ASSERT_LT(cache->GetUsage(), 256 * 1024);
cost_cache_ = GetParam();
if (cost_cache_) {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, cache, true));
} else {
options.write_buffer_manager.reset(
new WriteBufferManager(100000, nullptr, true));
}
CreateAndReopenWithCF({"cf1", "cf2"}, options);
for (int i = 0; i < num_dbs; i++) {
ASSERT_OK(DestroyDB(dbnames[i], options));
ASSERT_OK(DB::Open(options, dbnames[i], &(dbs[i])));
}
WriteOptions wo;
wo.disableWAL = true;
for (int i = 0; i < num_dbs; i++) {
ASSERT_OK(dbs[i]->Put(wo, Key(1), DummyString(20000)));
}
// Insert to db_.
ASSERT_OK(Put(0, Key(1), DummyString(30000), wo));
// WriteBufferManager::buffer_size_ has exceeded after the previous write to
// dbs[0] is completed.
std::vector<port::Thread> threads;
int wait_count_db = 0;
InstrumentedMutex mutex;
InstrumentedCondVar cv(&mutex);
std::unordered_set<WriteThread::Writer*> w_slowdown_set;
std::vector<port::Thread> writer_threads;
std::atomic<int> thread_num(0);
std::atomic<int> w_no_slowdown(0);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0",
"DBImpl::BackgroundCallFlush:start"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WBMStallInterface::BlockDB", [&](void*) {
InstrumentedMutexLock lock(&mutex);
wait_count_db++;
cv.Signal();
// Allow the flush continue if all writer threads are blocked.
if (w_slowdown_set.size() +
(unsigned long)(w_no_slowdown.load(std::memory_order_relaxed) +
wait_count_db) ==
(unsigned long)(2 * num_dbs + 1)) {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0");
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WriteThread::WriteStall::Wait", [&](void* arg) {
WriteThread::Writer* w = static_cast<WriteThread::Writer*>(arg);
InstrumentedMutexLock lock(&mutex);
w_slowdown_set.insert(w);
// Allow the flush continue if all writer threads are blocked.
if (w_slowdown_set.size() +
(unsigned long)(w_no_slowdown.load(std::memory_order_relaxed) +
wait_count_db) ==
(unsigned long)(2 * num_dbs + 1)) {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0");
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
bool s1 = true;
bool s2 = true;
std::function<void(DB*)> write_slow_down = [&](DB* db) {
int a = thread_num.fetch_add(1);
std::string key = "foo" + std::to_string(a);
WriteOptions write_op;
write_op.no_slowdown = false;
Status tmp = db->Put(write_op, Slice(key), DummyString(1));
InstrumentedMutexLock lock(&mutex);
s1 = s1 && tmp.ok();
};
std::function<void(DB*)> write_no_slow_down = [&](DB* db) {
int a = thread_num.fetch_add(1);
std::string key = "foo" + std::to_string(a);
WriteOptions write_op;
write_op.no_slowdown = true;
Status tmp = db->Put(write_op, Slice(key), DummyString(1));
{
InstrumentedMutexLock lock(&mutex);
s2 = s2 && !tmp.ok();
w_no_slowdown.fetch_add(1);
if (w_slowdown_set.size() +
(unsigned long)(w_no_slowdown.load(std::memory_order_relaxed) +
wait_count_db) ==
(unsigned long)(2 * num_dbs + 1)) {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::SharedWriteBufferAcrossCFs:0");
}
}
};
// Flow:
// first thread will write but will be blocked (as Flush will on hold,
// buffer_size_ has exceeded, thus will create stall in effect).
// |
// |
// multiple writer threads will be created to write across multiple columns
// of db_ and different DBs with different values of
// WriteOptions.no_slowdown. Some of them will be blocked and some of them
// will return with Incomplete status.
// |
// |
// Last writer thread will write and when its blocked/return it will signal
// Flush to continue to clear the stall.
threads.emplace_back(write_slow_down, db_.get());
// Wait untill first thread writing to DB is blocked and then
// create the multiple writers.
{
InstrumentedMutexLock lock(&mutex);
while (wait_count_db != 1) {
cv.Wait();
}
}
for (int i = 0; i < num_dbs; i += 2) {
// Write to multiple columns of db_.
writer_threads.emplace_back(write_slow_down, db_.get());
writer_threads.emplace_back(write_no_slow_down, db_.get());
// Write to different DBs.
threads.emplace_back(write_slow_down, dbs[i].get());
threads.emplace_back(write_no_slow_down, dbs[i + 1].get());
}
for (auto& t : threads) {
t.join();
}
for (auto& t : writer_threads) {
t.join();
}
ASSERT_TRUE(s1);
ASSERT_TRUE(s2);
// Number of DBs blocked.
ASSERT_EQ((num_dbs / 2) + 1, wait_count_db);
// Number of writer threads writing to db_ blocked from getting added to the
// queue.
ASSERT_EQ(w_slowdown_set.size(), num_dbs / 2);
// Number of threads with WriteOptions.no_slowdown = true.
ASSERT_EQ(w_no_slowdown.load(std::memory_order_relaxed), num_dbs);
// Clean up DBs.
for (int i = 0; i < num_dbs; i++) {
ASSERT_OK(dbs[i]->Close());
ASSERT_OK(DestroyDB(dbnames[i], options));
dbs[i].reset();
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
}
// Tests a `WriteBufferManager` constructed with `allow_stall == false` does not
// thrash memtable switching when full and a CF receives multiple writes.
// Instead, we expect to switch a CF's memtable for flush only when that CF does
// not have any pending or running flush.
//
// This test uses multiple DBs each with a single CF instead of a single DB
// with multiple CFs. That way we can control which CF is considered for switch
// by writing to that CF's DB.
//
// Not supported in LITE mode due to `GetProperty()` unavailable.
TEST_P(DBWriteBufferManagerTest, StopSwitchingMemTablesOnceFlushing) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10; // 4KB
options.write_buffer_size = 1 << 20; // 1MB
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
ASSERT_LT(cache->GetUsage(), 256 << 10 /* 256KB */);
cost_cache_ = GetParam();
if (cost_cache_) {
options.write_buffer_manager.reset(new WriteBufferManager(
512 << 10 /* buffer_size (512KB) */, cache, false /* allow_stall */));
} else {
options.write_buffer_manager.reset(
new WriteBufferManager(512 << 10 /* buffer_size (512KB) */,
nullptr /* cache */, false /* allow_stall */));
}
Reopen(options);
std::string dbname = test::PerThreadDBPath("db_shared_wbm_db");
std::unique_ptr<DB> shared_wbm_db;
ASSERT_OK(DestroyDB(dbname, options));
ASSERT_OK(DB::Open(options, dbname, &shared_wbm_db));
// The last write will make WBM need flush, but it won't flush yet.
ASSERT_OK(Put(Key(1), DummyString(256 << 10 /* 256KB */), WriteOptions()));
ASSERT_FALSE(options.write_buffer_manager->ShouldFlush());
ASSERT_OK(Put(Key(1), DummyString(256 << 10 /* 256KB */), WriteOptions()));
ASSERT_TRUE(options.write_buffer_manager->ShouldFlush());
// Flushes will be pending, not running because flush threads are blocked.
test::SleepingBackgroundTask sleeping_task_high;
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
&sleeping_task_high, Env::Priority::HIGH);
for (int i = 0; i < 3; ++i) {
ASSERT_OK(
shared_wbm_db->Put(WriteOptions(), Key(1), DummyString(1 /* len */)));
std::string prop;
ASSERT_TRUE(
shared_wbm_db->GetProperty("rocksdb.num-immutable-mem-table", &prop));
ASSERT_EQ(std::to_string(i > 0 ? 1 : 0), prop);
ASSERT_TRUE(
shared_wbm_db->GetProperty("rocksdb.mem-table-flush-pending", &prop));
ASSERT_EQ(std::to_string(i > 0 ? 1 : 0), prop);
}
// Clean up DBs.
sleeping_task_high.WakeUp();
sleeping_task_high.WaitUntilDone();
ASSERT_OK(shared_wbm_db->Close());
ASSERT_OK(DestroyDB(dbname, options));
shared_wbm_db.reset();
}
// kFlushLargest must select by mutable memory rather than creation order.
// Not supported in LITE mode because GetProperty() is unavailable.
TEST_P(DBWriteBufferManagerTest, FlushLargestMemtablePolicy) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10; // 4KB
options.write_buffer_size = 1 << 20; // 1MB, so per-CF flush is never hit
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
cost_cache_ = GetParam();
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
512 << 10 /* buffer_size (512KB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargest);
CreateAndReopenWithCF({"cf1", "cf2"}, options);
// Block the flush background thread so switched memtables stay pending and
// remain observable via the num-immutable-mem-table property.
test::SleepingBackgroundTask sleeping_task_high;
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
&sleeping_task_high, Env::Priority::HIGH);
// Make cf1 the largest mutable memtable while keeping the total under the WBM
// limit (512KB * 7/8 = 448KB) so no flush is triggered yet.
ASSERT_OK(
Put(0 /* default */, Key(1), DummyString(200 << 10), WriteOptions()));
ASSERT_OK(Put(1 /* cf1 */, Key(1), DummyString(300 << 10), WriteOptions()));
ASSERT_TRUE(options.write_buffer_manager->ShouldFlush());
// This write crosses the WBM limit and triggers a flush. Even though it
// targets cf2, kFlushLargest must pick cf1, the largest memtable.
ASSERT_OK(Put(2 /* cf2 */, Key(1), DummyString(1), WriteOptions()));
auto num_immutable = [&](int cf) {
std::string prop;
EXPECT_TRUE(db_->GetProperty(handles_[cf],
"rocksdb.num-immutable-mem-table", &prop));
return prop;
};
EXPECT_EQ("1", num_immutable(1)); // cf1 (largest) was flushed
EXPECT_EQ("0", num_immutable(0)); // default was not
EXPECT_EQ("0", num_immutable(2)); // cf2 was not
sleeping_task_high.WakeUp();
sleeping_task_high.WaitUntilDone();
}
namespace {
// Records which DB instances have completed a flush, so a test can wait for and
// assert on cross-DB flush behavior deterministically.
class FlushedDBRecorder : public EventListener {
public:
void OnFlushCompleted(DB* db, const FlushJobInfo& info) override {
InstrumentedMutexLock l(&mu_);
flushed_.insert(db);
cf_flushes_[db].insert(info.cf_name);
cv_.SignalAll();
}
void OnManualFlushScheduled(
DB* /*db*/,
const std::vector<ManualFlushInfo>& /*manual_flush_info*/) override {
InstrumentedMutexLock l(&mu_);
++manual_flushes_;
}
void WaitForFlush(DB* db) {
InstrumentedMutexLock l(&mu_);
while (flushed_.find(db) == flushed_.end()) {
cv_.Wait();
}
}
// Waits until `db` has flushed at least `n` distinct column families.
void WaitForColumnFamilies(DB* db, size_t n) {
InstrumentedMutexLock l(&mu_);
while (cf_flushes_[db].size() < n) {
cv_.Wait();
}
}
bool HasFlushed(DB* db) {
InstrumentedMutexLock l(&mu_);
return flushed_.find(db) != flushed_.end();
}
size_t ManualFlushes() {
InstrumentedMutexLock l(&mu_);
return manual_flushes_;
}
void Reset() {
InstrumentedMutexLock l(&mu_);
flushed_.clear();
cf_flushes_.clear();
manual_flushes_ = 0;
}
private:
InstrumentedMutex mu_;
InstrumentedCondVar cv_{&mu_};
std::set<DB*> flushed_;
std::map<DB*, std::set<std::string>> cf_flushes_;
size_t manual_flushes_ = 0;
};
} // anonymous namespace
// Soft WBM pressure must flush the largest DB sharing the WBM.
// Not supported in LITE mode because GetProperty() is unavailable.
TEST_P(DBWriteBufferManagerTest, FlushLargestAcrossDBsPolicy) {
auto recorder = std::make_shared<FlushedDBRecorder>();
Options options = CurrentOptions();
options.arena_block_size = 4 << 10; // 4KB
options.write_buffer_size = 1 << 20; // 1MB, so per-CF flush is never hit
options.listeners.push_back(recorder);
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
cost_cache_ = GetParam();
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20 /* buffer_size (10MB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname = test::PerThreadDBPath("wbm_across_dbs_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(400 << 10)));
ASSERT_OK(Put(Key(1), DummyString(300 << 10), WriteOptions()));
ASSERT_FALSE(options.write_buffer_manager->ShouldFlush());
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
// Ignore any flush that may have happened during open/setup.
recorder->Reset();
options.write_buffer_manager->SetBufferSize(768 << 10);
recorder->WaitForFlush(other_db.get());
ASSERT_OK(static_cast_with_check<DBImpl>(other_db.get())
->TEST_WaitForBackgroundWork());
EXPECT_FALSE(recorder->HasFlushed(db_.get()));
EXPECT_EQ(0, recorder->ManualFlushes());
// The replacement memtable must make this DB eligible again after the
// previous completion published has_flushable_cf=false.
options.write_buffer_manager->SetBufferSize(10 << 20);
recorder->Reset();
ASSERT_OK(other_db->Put(WriteOptions(), Key(2), DummyString(500 << 10)));
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
ASSERT_TRUE(
options.write_buffer_manager->TEST_ScheduleFlushOnLargestDB(nullptr));
recorder->WaitForFlush(other_db.get());
EXPECT_FALSE(recorder->HasFlushed(db_.get()));
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
TEST_P(DBWriteBufferManagerTest,
FlushLargestAcrossDBsIncludesWaitingImmutableMemtables) {
auto recorder = std::make_shared<FlushedDBRecorder>();
Options options = CurrentOptions();
options.arena_block_size = 1 << 20;
options.write_buffer_size = 4 << 20;
options.max_write_buffer_number = 5;
options.min_write_buffer_number_to_merge = 4;
options.listeners.push_back(recorder);
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
cost_cache_ = GetParam();
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20 /* buffer_size (10MB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname =
test::PerThreadDBPath("wbm_waiting_immutable_other");
Options other_options = options;
other_options.arena_block_size = 4 << 10;
ASSERT_OK(DestroyDB(other_dbname, other_options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(other_options, other_dbname, &other_db));
const size_t memory_before_first =
options.write_buffer_manager->memory_usage();
ASSERT_OK(Put(Key(1), DummyString(4 << 10), WriteOptions()));
const size_t first_wbm_charge =
options.write_buffer_manager->memory_usage() - memory_before_first;
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
std::string num_immutable;
ASSERT_TRUE(
db_->GetProperty("rocksdb.num-immutable-mem-table", &num_immutable));
ASSERT_EQ("1", num_immutable);
ASSERT_FALSE(recorder->HasFlushed(db_.get()));
const size_t memory_before_second =
options.write_buffer_manager->memory_usage();
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(200 << 10)));
const size_t second_wbm_charge =
options.write_buffer_manager->memory_usage() - memory_before_second;
ASSERT_GT(first_wbm_charge, second_wbm_charge);
uint64_t first_approximate_usage = 0;
uint64_t second_approximate_usage = 0;
ASSERT_TRUE(db_->GetIntProperty("rocksdb.cur-size-all-mem-tables",
&first_approximate_usage));
ASSERT_TRUE(other_db->GetIntProperty("rocksdb.cur-size-all-mem-tables",
&second_approximate_usage));
ASSERT_LT(first_approximate_usage, second_approximate_usage);
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
recorder->Reset();
ASSERT_TRUE(
options.write_buffer_manager->TEST_ScheduleFlushOnLargestDB(nullptr));
recorder->WaitForFlush(db_.get());
EXPECT_FALSE(recorder->HasFlushed(other_db.get()));
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, other_options));
}
TEST_P(DBWriteBufferManagerTest,
FlushLargestAcrossDBsTriggersOnWaitingImmutablePressure) {
auto recorder = std::make_shared<FlushedDBRecorder>();
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 4 << 20;
options.max_write_buffer_number = 5;
options.min_write_buffer_number_to_merge = 4;
options.listeners.push_back(recorder);
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
cost_cache_ = GetParam();
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20 /* buffer_size (10MB) */, cost_cache_ ? cache : nullptr,
true /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
ASSERT_OK(Put(Key(1), DummyString(300 << 10), WriteOptions()));
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
ASSERT_FALSE(recorder->HasFlushed(db_.get()));
ASSERT_LT(options.write_buffer_manager->mutable_memtable_memory_usage(),
options.write_buffer_manager->memory_usage() / 2);
options.write_buffer_manager->SetBufferSize(
options.write_buffer_manager->memory_usage() - 1);
ASSERT_FALSE(options.write_buffer_manager->ShouldFlush());
recorder->WaitForFlush(db_.get());
}
TEST_F(DBWriteBufferManagerTest, FlushOldestIgnoresWaitingImmutableMemtables) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 1 << 20;
options.max_write_buffer_number = 5;
options.min_write_buffer_number_to_merge = 4;
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20, nullptr, false, WriteBufferFlushPolicy::kFlushOldest);
Reopen(options);
ASSERT_OK(Put(Key(1), DummyString(64 << 10), WriteOptions()));
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
// Create and switch another CF after the default CF so its current mutable
// is newer. The default CF is older, but has only a waiting immutable.
CreateColumnFamilies({"newer"}, options);
ColumnFamilyHandle* newer = handles_.back();
ASSERT_OK(db_->Put(WriteOptions(), newer, Key(1), DummyString(1)));
ColumnFamilyData* newer_cfd =
static_cast_with_check<ColumnFamilyHandleImpl>(newer)->cfd();
ASSERT_OK(dbfull()->TEST_SwitchMemtable(newer_cfd));
ASSERT_OK(db_->Put(WriteOptions(), newer, Key(2), DummyString(64 << 10)));
test::SleepingBackgroundTask sleeping_task_high;
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
&sleeping_task_high, Env::Priority::HIGH);
sleeping_task_high.WaitUntilSleeping();
options.write_buffer_manager->SetBufferSize(1);
ASSERT_OK(db_->Put(WriteOptions(), newer, Key(3), DummyString(1)));
std::string prop;
ASSERT_TRUE(db_->GetProperty(db_->DefaultColumnFamily(),
"rocksdb.num-immutable-mem-table", &prop));
EXPECT_EQ("1", prop);
ASSERT_TRUE(
db_->GetProperty(newer, "rocksdb.num-immutable-mem-table", &prop));
EXPECT_EQ("2", prop);
sleeping_task_high.WakeUp();
sleeping_task_high.WaitUntilDone();
}
TEST_F(DBWriteBufferManagerTest,
FlushLargestAcrossDBsRebuildsAfterPolicyChange) {
auto recorder = std::make_shared<FlushedDBRecorder>();
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 1 << 20;
options.listeners.push_back(recorder);
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20, nullptr, false, WriteBufferFlushPolicy::kFlushOldest);
Reopen(options);
std::string other_dbname = test::PerThreadDBPath("wbm_policy_change_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
ASSERT_NE(other_db, nullptr);
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(400 << 10)));
ASSERT_OK(Put(Key(1), DummyString(300 << 10), WriteOptions()));
options.write_buffer_manager->SetFlushPolicy(
WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
recorder->Reset();
options.write_buffer_manager->SetBufferSize(768 << 10);
recorder->WaitForFlush(other_db.get());
EXPECT_FALSE(recorder->HasFlushed(db_.get()));
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
TEST_F(DBWriteBufferManagerTest,
FlushLargestAcrossDBsDoesNotWaitForForeignMutex) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 1 << 20;
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20, nullptr, false, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname =
test::PerThreadDBPath("wbm_busy_foreign_mutex_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(300 << 10)));
ASSERT_OK(Put(Key(1), DummyString(200 << 10), WriteOptions()));
auto* other_dbimpl = static_cast_with_check<DBImpl>(other_db.get());
other_dbimpl->TEST_LockMutex();
options.write_buffer_manager->SetBufferSize(512 << 10);
InstrumentedMutex completion_mu;
InstrumentedCondVar completion_cv(&completion_mu);
bool write_finished = false;
Status write_status;
port::Thread writer([&] {
write_status = Put(Key(2), DummyString(1), WriteOptions());
{
InstrumentedMutexLock lock(&completion_mu);
write_finished = true;
completion_cv.SignalAll();
}
});
bool completed_while_foreign_mutex_locked = true;
{
InstrumentedMutexLock lock(&completion_mu);
const uint64_t deadline = env_->NowMicros() + 5 * 1000 * 1000;
while (!write_finished) {
if (completion_cv.TimedWait(deadline)) {
completed_while_foreign_mutex_locked = write_finished;
break;
}
}
}
other_dbimpl->TEST_UnlockMutex();
writer.join();
EXPECT_TRUE(completed_while_foreign_mutex_locked);
ASSERT_OK(write_status);
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
TEST_F(DBWriteBufferManagerTest,
AtomicFlushNonBlockingJoinReleasesGeneratedCandidates) {
Options options = CurrentOptions();
options.atomic_flush = true;
CreateAndReopenWithCF({"cf1"}, options);
auto* impl = dbfull();
{
impl->TEST_BeginWriteStall();
Defer end_stall([&] { impl->TEST_EndWriteStall(); });
FlushOptions flush_options;
flush_options.wait = false;
flush_options.allow_write_stall = true;
const Status s = impl->TEST_AtomicFlushMemTables(
{} /* provided_candidate_cfds */, flush_options,
true /* non_blocking_write_thread */);
ASSERT_TRUE(s.IsIncomplete());
}
// Closing destroys the ColumnFamilySet and verifies that the failed flush
// did not retain a reference to either column family.
Close();
}
TEST_F(DBWriteBufferManagerTest,
AtomicFlushNonBlockingJoinQueuesBehindOrdinaryLeader) {
Options options = CurrentOptions();
options.atomic_flush = true;
CreateAndReopenWithCF({"cf1"}, options);
DBImpl* const impl = dbfull();
impl->TEST_LockMutex();
void* const leader = impl->TEST_BeginWrite();
impl->TEST_UnlockMutex();
InstrumentedMutex completion_mu;
InstrumentedCondVar completion_cv(&completion_mu);
bool completed = false;
Status flush_status;
port::Thread flush_thread([&] {
FlushOptions flush_options;
flush_options.wait = false;
flush_options.allow_write_stall = true;
flush_status = impl->TEST_AtomicFlushMemTables(
{} /* provided_candidate_cfds */, flush_options,
true /* non_blocking_write_thread */);
InstrumentedMutexLock lock(&completion_mu);
completed = true;
completion_cv.SignalAll();
});
bool completed_while_leader_held = false;
{
InstrumentedMutexLock lock(&completion_mu);
const uint64_t deadline = env_->NowMicros() + 100 * 1000;
while (!completed && !completion_cv.TimedWait(deadline)) {
}
completed_while_leader_held = completed;
}
impl->TEST_LockMutex();
impl->TEST_EndWrite(leader);
impl->TEST_UnlockMutex();
flush_thread.join();
EXPECT_FALSE(completed_while_leader_held);
ASSERT_OK(flush_status);
}
TEST_P(DBWriteBufferManagerTest, AtomicWBMFlushRequestIsDeduplicated) {
Options options = CurrentOptions();
options.atomic_flush = true;
options.arena_block_size = 4 << 10;
options.write_buffer_size = 4 << 20;
options.create_if_missing = true;
options.create_missing_column_families = true;
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
cost_cache_ = GetParam();
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20 /* buffer_size (10MB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
CreateAndReopenWithCF({"cf1"}, options);
test::SleepingBackgroundTask sleeping_task_high;
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
&sleeping_task_high, Env::Priority::HIGH);
sleeping_task_high.WaitUntilSleeping();
ASSERT_OK(Put(0, Key(1), DummyString(300 << 10), WriteOptions()));
ASSERT_OK(Put(1, Key(1), DummyString(300 << 10), WriteOptions()));
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::EnqueuePendingFlush:AtomicWBMRequestDeduplicated",
"DBWriteBufferManagerTest::AtomicWBMFlushRequestIsDeduplicated"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_points([] {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
});
options.write_buffer_manager->SetBufferSize(512 << 10);
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::AtomicWBMFlushRequestIsDeduplicated");
sleeping_task_high.WakeUp();
sleeping_task_high.WaitUntilDone();
}
TEST_F(DBWriteBufferManagerTest, AtomicFlushNonBlockingJoinRechecksWriteStop) {
Options options = CurrentOptions();
options.atomic_flush = true;
CreateAndReopenWithCF({"cf1"}, options);
ASSERT_OK(Put(Key(1), "value"));
DBImpl* const impl = dbfull();
std::unique_ptr<WriteControllerToken> stop_token;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WriteThread::EnterUnbatchedNonBlocking:BeforeLock",
[&](void*) { stop_token = impl->TEST_write_controler().GetStopToken(); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_points([&] {
stop_token.reset();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
});
FlushOptions flush_options;
flush_options.wait = false;
flush_options.allow_write_stall = true;
const Status s = impl->TEST_AtomicFlushMemTables(
{} /* provided_candidate_cfds */, flush_options,
true /* non_blocking_write_thread */);
ASSERT_TRUE(s.IsIncomplete());
std::string num_immutable;
ASSERT_TRUE(
db_->GetProperty("rocksdb.num-immutable-mem-table", &num_immutable));
EXPECT_EQ("0", num_immutable);
}
TEST_F(DBWriteBufferManagerTest, AtomicFlushRefreshesWBMAccountingOnce) {
Options options = CurrentOptions();
options.atomic_flush = true;
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
1 << 30, nullptr, false, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
CreateAndReopenWithCF({"cf1", "cf2"}, options);
ASSERT_OK(Put(0, Key(1), DummyString(1), WriteOptions()));
ASSERT_OK(Put(1, Key(1), DummyString(1), WriteOptions()));
ASSERT_OK(Put(2, Key(1), DummyString(1), WriteOptions()));
size_t refreshes = 0;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::UpdateWBMAccountingAfterMemtableSwitch:Refresh",
[&](void*) { ++refreshes; });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_points([] {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
});
FlushOptions flush_options;
flush_options.wait = false;
ASSERT_OK(dbfull()->TEST_AtomicFlushMemTables(
{} /* provided_candidate_cfds */, flush_options,
false /* non_blocking_write_thread */));
EXPECT_EQ(1, refreshes);
}
// A cross-DB WBM flush must run even when the high-priority pool is empty.
TEST_P(DBWriteBufferManagerTest, FlushLargestAcrossDBsWithEmptyHighPriPool) {
auto recorder = std::make_shared<FlushedDBRecorder>();
Options options = CurrentOptions();
options.arena_block_size = 4 << 10; // 4KB
options.write_buffer_size = 1 << 20; // 1MB, so per-CF flush is never hit
options.listeners.push_back(recorder);
cost_cache_ = GetParam();
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20 /* buffer_size (10MB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname = test::PerThreadDBPath("wbm_empty_high_pri_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(300 << 10)));
ASSERT_OK(Put(Key(1), DummyString(200 << 10), WriteOptions()));
recorder->Reset();
// Empty the shared high-priority pool after DB open, and restore it on exit.
const int saved_high = env_->GetBackgroundThreads(Env::Priority::HIGH);
Defer restore_high_pool(
[&] { env_->SetBackgroundThreads(saved_high, Env::Priority::HIGH); });
env_->SetBackgroundThreads(0, Env::Priority::HIGH);
ASSERT_EQ(0, env_->GetBackgroundThreads(Env::Priority::HIGH));
options.write_buffer_manager->SetBufferSize(512 << 10);
recorder->WaitForFlush(other_db.get());
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
// A write-stopped DB cannot honor a flush and must not win selection.
TEST_P(DBWriteBufferManagerTest, FlushLargestAcrossDBsSkipsWriteStoppedDB) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10; // 4KB
options.write_buffer_size = 1 << 20; // 1MB, so per-CF flush is never hit
cost_cache_ = GetParam();
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20 /* buffer_size (10MB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname = test::PerThreadDBPath("wbm_write_stopped_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(400 << 10)));
auto* other_dbimpl = static_cast_with_check<DBImpl>(other_db.get());
auto stop_token = other_dbimpl->TEST_write_controler().GetStopToken();
ASSERT_TRUE(other_dbimpl->TEST_write_controler().IsStopped());
// Block the flush thread so the memtable switch stays observable.
test::SleepingBackgroundTask sleeping_task_high;
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
&sleeping_task_high, Env::Priority::HIGH);
ASSERT_OK(Put(Key(1), DummyString(300 << 10), WriteOptions()));
ASSERT_FALSE(options.write_buffer_manager->ShouldFlush());
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
options.write_buffer_manager->SetBufferSize(768 << 10);
options.write_buffer_manager->TEST_WaitForFlushHandoffCompletion();
std::string prop;
ASSERT_TRUE(db_->GetProperty("rocksdb.num-immutable-mem-table", &prop));
EXPECT_EQ("1", prop) << "db_ should have flushed itself";
sleeping_task_high.WakeUp();
sleeping_task_high.WaitUntilDone();
stop_token.reset();
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
// Skip a stalled DB so the selected flush can run and release shared memory.
TEST_F(DBWriteBufferManagerTest, FlushLargestAcrossDBsSkipsStalledDB) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 4 << 20; // per-CF flush is never hit
options.create_if_missing = true;
options.create_missing_column_families = true;
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
1 << 20 /* buffer_size (1MB) */, nullptr /* cache */,
true /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
DBImpl* const db_impl = dbfull();
// Keep mutable memory in other_db after it enters the stall.
std::string other_dbname = test::PerThreadDBPath("wbm_stalled_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::vector<ColumnFamilyDescriptor> cf_descs = {
{kDefaultColumnFamilyName, ColumnFamilyOptions(options)},
{"cf1", ColumnFamilyOptions(options)}};
std::vector<ColumnFamilyHandle*> other_handles;
std::unique_ptr<DB> other_db;
ASSERT_OK(
DB::Open(options, other_dbname, cf_descs, &other_handles, &other_db));
auto* const other_dbimpl = static_cast_with_check<DBImpl>(other_db.get());
// Freeze flushes so the stall remains active for the test.
InstrumentedMutex mu;
InstrumentedCondVar cv(&mu);
bool blocked = false;
bool job_done = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBWriteBufferManagerTest::SkipsStalledDB:Done",
"DBImpl::BackgroundCallFlush:start"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WBMStallInterface::BlockDB", [&](void*) {
InstrumentedMutexLock l(&mu);
blocked = true;
cv.SignalAll();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BGWorkWBMFlush:done", [&](void* arg) {
if (arg != db_impl) {
return;
}
InstrumentedMutexLock l(&mu);
job_done = true;
cv.SignalAll();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Stay below both thresholds until the dedicated staller crosses them.
ASSERT_OK(other_db->Put(WriteOptions(), other_handles[1], Key(1),
DummyString(300 << 10)));
ASSERT_OK(other_db->Put(WriteOptions(), other_handles[0], Key(1),
DummyString(250 << 10)));
ASSERT_OK(Put(Key(1), DummyString(200 << 10), WriteOptions()));
ASSERT_FALSE(options.write_buffer_manager->ShouldFlush());
ASSERT_FALSE(options.write_buffer_manager->IsStallActive());
// The first write crosses the limit; the second observes it and parks.
port::Thread staller([&] {
other_db
->Put(WriteOptions(), other_handles[0], Key(2), DummyString(500 << 10))
.PermitUncheckedError();
other_db->Put(WriteOptions(), other_handles[0], Key(3), DummyString(1))
.PermitUncheckedError();
});
{
InstrumentedMutexLock l(&mu);
while (!blocked) {
cv.Wait();
}
}
ASSERT_TRUE(options.write_buffer_manager->IsStallActive());
// Keep manager-wide pressure active even if the stalling writer sealed its
// local memtable before parking. This makes selection of the non-stalled DB
// independent of the race between that writer and the WBM sorter.
constexpr size_t kInjectedPressure = 1 << 20;
options.write_buffer_manager->ReserveMem(kInjectedPressure);
// The smaller, non-stalled DB must be selected despite other_db's larger bid.
{
InstrumentedMutexLock l(&mu);
while (!job_done) {
cv.Wait();
}
}
std::string prop;
ASSERT_TRUE(db_->GetProperty("rocksdb.num-immutable-mem-table", &prop));
EXPECT_EQ("1", prop) << "the non-stalled DB should have been flushed";
// Change policy while other_db is still hidden by its stall. Exiting under
// the local policy must restore its eligibility for a later switch back.
options.write_buffer_manager->SetFlushPolicy(
WriteBufferFlushPolicy::kFlushOldest);
options.write_buffer_manager->SetAllowStall(false);
staller.join();
options.write_buffer_manager->ScheduleFreeMem(kInjectedPressure);
options.write_buffer_manager->FreeMem(kInjectedPressure);
options.write_buffer_manager->SetBufferSize(10 << 20);
options.write_buffer_manager->SetFlushPolicy(
WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
ASSERT_OK(other_db->Put(WriteOptions(), other_handles[0], Key(4),
DummyString(1 << 10)));
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
EXPECT_TRUE(other_dbimpl->TEST_IsWBMFlushInitiatorFlushable());
TEST_SYNC_POINT("DBWriteBufferManagerTest::SkipsStalledDB:Done");
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
for (auto* handle : other_handles) {
ASSERT_OK(other_db->DestroyColumnFamilyHandle(handle));
}
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
// A DB that deferred must still seal locally before entering a shared stall.
TEST_F(DBWriteBufferManagerTest, FlushLargestAcrossDBsSelfFlushesBeforeStall) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 4 << 20; // per-CF flush is never hit
options.create_if_missing = true;
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
1 << 20 /* buffer_size (1MB) */, nullptr /* cache */,
true /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname = test::PerThreadDBPath("wbm_selfflush_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
// Freeze the larger DB's job so only db_ can release memory.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBWriteBufferManagerTest::SelfFlushesBeforeStall:Release",
"WriteBufferManager::FlushHandoffExecutor:BeforeExecute"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(700 << 10)));
ASSERT_OK(Put(Key(1), DummyString(350 << 10), WriteOptions()));
ASSERT_TRUE(options.write_buffer_manager->ShouldFlush());
ASSERT_TRUE(options.write_buffer_manager->ShouldStall());
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
// This write defers to other_db and then has to stall. It can only return if
// it sealed its own memtable on the way in.
InstrumentedMutex mu;
InstrumentedCondVar cv(&mu);
bool write_done = false;
port::Thread writer([&] {
Status s = Put(Key(2), DummyString(1), WriteOptions());
InstrumentedMutexLock l(&mu);
write_done = true;
s.PermitUncheckedError();
cv.SignalAll();
});
bool timed_out = false;
{
InstrumentedMutexLock l(&mu);
const uint64_t deadline = env_->NowMicros() + 30 * 1000 * 1000;
while (!write_done) {
if (cv.TimedWait(deadline)) {
timed_out = !write_done;
break;
}
}
}
EXPECT_FALSE(timed_out)
<< "writer never came back: it deferred to an idle DB and then parked "
"with no flush in flight, so nothing can ever call FreeMem()";
if (!timed_out) {
EXPECT_GT(NumTableFilesAtLevel(0), 0)
<< "the deferring DB must seal its own memtable rather than rely on "
"the other DB's job";
}
options.write_buffer_manager->SetAllowStall(false);
TEST_SYNC_POINT("DBWriteBufferManagerTest::SelfFlushesBeforeStall:Release");
writer.join();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
// A queued WBM-executor handoff does not push work onto a writer below the
// hard limit.
TEST_P(DBWriteBufferManagerTest,
FlushLargestAcrossDBsPendingJobStaysInBackground) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 1 << 20;
cost_cache_ = GetParam();
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
768 << 10 /* buffer_size (768KB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname = test::PerThreadDBPath("wbm_pending_handoff_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBWriteBufferManagerTest::PendingHandoff:ReleaseWBM",
"WriteBufferManager::FlushHandoffExecutor:BeforeExecute"},
{"DBWriteBufferManagerTest::PendingHandoff:ReleaseFlush",
"DBImpl::BackgroundCallFlush:start"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
bool released = false;
auto release_background_work = [&] {
if (released) {
return;
}
TEST_SYNC_POINT("DBWriteBufferManagerTest::PendingHandoff:ReleaseWBM");
TEST_SYNC_POINT("DBWriteBufferManagerTest::PendingHandoff:ReleaseFlush");
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
released = true;
};
Defer release_on_exit([&] { release_background_work(); });
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(400 << 10)));
ASSERT_OK(Put(Key(1), DummyString(300 << 10), WriteOptions()));
ASSERT_TRUE(options.write_buffer_manager->ShouldFlush());
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
// The sorter queues a handoff to other_db. Additional writes below the hard
// limit must not seal db_ while that WBM-executor job is blocked.
options.write_buffer_manager->TEST_WaitForFlushHandoff();
ASSERT_OK(Put(Key(2), DummyString(1), WriteOptions()));
std::string prop;
ASSERT_TRUE(db_->GetProperty("rocksdb.num-immutable-mem-table", &prop));
EXPECT_EQ("0", prop) << "soft pressure must remain background-managed";
ASSERT_TRUE(other_db->GetProperty("rocksdb.num-immutable-mem-table", &prop));
EXPECT_EQ("0", prop) << "the WBM-executor handoff should still be blocked";
release_background_work();
options.write_buffer_manager->TEST_WaitForFlushHandoffCompletion();
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
TEST_F(DBWriteBufferManagerTest, WaitForCompactDrainsReservedWBMFlushHandoff) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 1 << 20;
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
2 << 20, nullptr, false, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname =
test::PerThreadDBPath("wbm_wait_for_compact_handoff_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(400 << 10)));
ASSERT_OK(Put(Key(1), DummyString(300 << 10), WriteOptions()));
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
std::mutex handoff_mu;
std::condition_variable handoff_cv;
bool handoff_reserved = false;
bool release_handoff = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"WriteBufferManager::GetNextCandidateForHandoff:CandidateSelected",
[&](void*) {
std::unique_lock<std::mutex> lock(handoff_mu);
handoff_reserved = true;
handoff_cv.notify_all();
handoff_cv.wait(lock, [&] { return release_handoff; });
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_points([&] {
{
std::lock_guard<std::mutex> lock(handoff_mu);
release_handoff = true;
}
handoff_cv.notify_all();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
});
options.write_buffer_manager->SetBufferSize(768 << 10);
{
std::unique_lock<std::mutex> lock(handoff_mu);
handoff_cv.wait(lock, [&] { return handoff_reserved; });
}
std::mutex wait_mu;
std::condition_variable wait_cv;
bool wait_finished = false;
Status wait_status;
port::Thread waiter([&] {
wait_status = other_db->WaitForCompact(WaitForCompactOptions());
std::lock_guard<std::mutex> lock(wait_mu);
wait_finished = true;
wait_cv.notify_all();
});
{
std::unique_lock<std::mutex> lock(wait_mu);
EXPECT_FALSE(wait_cv.wait_for(lock, std::chrono::milliseconds(100),
[&] { return wait_finished; }));
}
{
std::lock_guard<std::mutex> lock(handoff_mu);
release_handoff = true;
}
handoff_cv.notify_all();
waiter.join();
ASSERT_OK(wait_status);
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
// A handoff that finds no flushable CF advances to another background
// candidate instead of pushing work onto a writer.
TEST_P(DBWriteBufferManagerTest,
FlushLargestAcrossDBsNoProgressTriesNextCandidate) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 2 << 20;
cost_cache_ = GetParam();
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
2304 << 10 /* buffer_size (2.25MB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname =
test::PerThreadDBPath("wbm_no_progress_handoff_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
test::SleepingBackgroundTask sleeping_task_high;
const int saved_high = env_->GetBackgroundThreads(Env::Priority::HIGH);
env_->SetBackgroundThreads(1, Env::Priority::HIGH);
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
&sleeping_task_high, Env::Priority::HIGH);
sleeping_task_high.WaitUntilSleeping();
bool high_pool_restored = false;
auto restore_high_pool = [&] {
if (high_pool_restored) {
return;
}
sleeping_task_high.WakeUp();
sleeping_task_high.WaitUntilDone();
env_->SetBackgroundThreads(saved_high, Env::Priority::HIGH);
high_pool_restored = true;
};
Defer restore_high_pool_on_exit([&] { restore_high_pool(); });
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(100 << 10)));
FlushOptions flush_options;
flush_options.wait = false;
ASSERT_OK(other_db->Flush(flush_options));
ASSERT_OK(other_db->Put(WriteOptions(), Key(2), DummyString(1400 << 10)));
ASSERT_OK(Put(Key(1), DummyString(500 << 10), WriteOptions()));
ASSERT_FALSE(options.write_buffer_manager->ShouldFlush());
InstrumentedMutex mu;
InstrumentedCondVar cv(&mu);
bool handoff_done = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BGWorkWBMFlush:done", [&](void*) {
InstrumentedMutexLock l(&mu);
handoff_done = true;
cv.SignalAll();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_points([] {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
});
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
ASSERT_OK(Put(Key(2), DummyString(200 << 10), WriteOptions()));
{
InstrumentedMutexLock l(&mu);
const uint64_t deadline = env_->NowMicros() + 30 * 1000 * 1000;
while (!handoff_done) {
ASSERT_FALSE(cv.TimedWait(deadline));
}
}
options.write_buffer_manager->TEST_WaitForFlushHandoffCompletion();
std::string prop;
ASSERT_TRUE(db_->GetProperty("rocksdb.num-immutable-mem-table", &prop));
EXPECT_EQ("1", prop);
restore_high_pool();
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
TEST_F(DBWriteBufferManagerTest,
FlushLargestAcrossDBsDetectsNoOpAfterSelection) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 2 << 20;
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
1 << 20, nullptr, false, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname =
test::PerThreadDBPath("wbm_no_op_after_selection_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
test::SleepingBackgroundTask sleeping_task_high;
const int saved_high = env_->GetBackgroundThreads(Env::Priority::HIGH);
env_->SetBackgroundThreads(1, Env::Priority::HIGH);
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
&sleeping_task_high, Env::Priority::HIGH);
sleeping_task_high.WaitUntilSleeping();
bool high_pool_restored = false;
auto restore_high_pool = [&] {
if (high_pool_restored) {
return;
}
sleeping_task_high.WakeUp();
sleeping_task_high.WaitUntilDone();
env_->SetBackgroundThreads(saved_high, Env::Priority::HIGH);
high_pool_restored = true;
};
Defer restore_high_pool_on_exit([&] { restore_high_pool(); });
InstrumentedMutex mu;
InstrumentedCondVar cv(&mu);
bool candidate_picked = false;
bool release_handoff = false;
bool handoff_done = false;
bool made_progress = true;
bool already_flushing_rejected = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BackgroundCallWBMFlush:AfterPick", [&](void*) {
InstrumentedMutexLock l(&mu);
candidate_picked = true;
cv.SignalAll();
while (!release_handoff) {
cv.Wait();
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BackgroundCallWBMFlush:MadeProgress", [&](void* arg) {
InstrumentedMutexLock l(&mu);
made_progress = *static_cast<bool*>(arg);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::FlushMemTableImpl:AlreadyFlushing", [&](void*) {
InstrumentedMutexLock l(&mu);
already_flushing_rejected = true;
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BGWorkWBMFlush:done", [&](void*) {
InstrumentedMutexLock l(&mu);
handoff_done = true;
cv.SignalAll();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_points([&] {
{
InstrumentedMutexLock l(&mu);
release_handoff = true;
cv.SignalAll();
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
});
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(600 << 10)));
ASSERT_OK(Put(Key(1), DummyString(400 << 10), WriteOptions()));
ASSERT_TRUE(options.write_buffer_manager->ShouldFlush());
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
ASSERT_OK(Put(Key(2), DummyString(1), WriteOptions()));
{
InstrumentedMutexLock l(&mu);
const uint64_t deadline = env_->NowMicros() + 30 * 1000 * 1000;
while (!candidate_picked) {
ASSERT_FALSE(cv.TimedWait(deadline));
}
}
FlushOptions flush_options;
flush_options.wait = false;
ASSERT_OK(other_db->Flush(flush_options));
ASSERT_OK(other_db->Put(WriteOptions(), Key(2), DummyString(1)));
{
InstrumentedMutexLock l(&mu);
release_handoff = true;
cv.SignalAll();
const uint64_t deadline = env_->NowMicros() + 30 * 1000 * 1000;
while (!handoff_done) {
ASSERT_FALSE(cv.TimedWait(deadline));
}
}
EXPECT_FALSE(made_progress);
EXPECT_TRUE(already_flushing_rejected);
std::string prop;
ASSERT_TRUE(other_db->GetProperty("rocksdb.num-immutable-mem-table", &prop));
EXPECT_EQ("1", prop);
restore_high_pool();
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
TEST_F(DBWriteBufferManagerTest,
FlushLargestAcrossDBsSkipsDroppedCandidateAfterSelection) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 2 << 20;
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
1 << 20, nullptr, false, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
std::string other_dbname =
test::PerThreadDBPath("wbm_dropped_after_selection_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
ColumnFamilyHandle* other_cfh = nullptr;
ASSERT_OK(other_db->CreateColumnFamily(ColumnFamilyOptions(), "victim",
&other_cfh));
InstrumentedMutex mu;
InstrumentedCondVar cv(&mu);
bool candidate_picked = false;
bool release_handoff = false;
bool handoff_done = false;
bool dropped_candidate_rejected = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BackgroundCallWBMFlush:AfterPick", [&](void*) {
InstrumentedMutexLock l(&mu);
candidate_picked = true;
cv.SignalAll();
while (!release_handoff) {
cv.Wait();
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::FlushMemTableImpl:Dropped", [&](void*) {
InstrumentedMutexLock l(&mu);
dropped_candidate_rejected = true;
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BGWorkWBMFlush:done", [&](void*) {
InstrumentedMutexLock l(&mu);
handoff_done = true;
cv.SignalAll();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_points([&] {
{
InstrumentedMutexLock l(&mu);
release_handoff = true;
cv.SignalAll();
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
});
ASSERT_OK(
other_db->Put(WriteOptions(), other_cfh, Key(1), DummyString(600 << 10)));
ASSERT_OK(Put(Key(1), DummyString(400 << 10), WriteOptions()));
ASSERT_TRUE(options.write_buffer_manager->ShouldFlush());
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
ASSERT_OK(Put(Key(2), DummyString(1), WriteOptions()));
{
InstrumentedMutexLock l(&mu);
const uint64_t deadline = env_->NowMicros() + 30 * 1000 * 1000;
while (!candidate_picked) {
ASSERT_FALSE(cv.TimedWait(deadline));
}
}
ASSERT_OK(other_db->DropColumnFamily(other_cfh));
ASSERT_OK(other_db->DestroyColumnFamilyHandle(other_cfh));
{
InstrumentedMutexLock l(&mu);
release_handoff = true;
cv.SignalAll();
const uint64_t deadline = env_->NowMicros() + 30 * 1000 * 1000;
while (!handoff_done) {
ASSERT_FALSE(cv.TimedWait(deadline));
}
}
EXPECT_TRUE(dropped_candidate_rejected);
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
// Cross-DB handoffs use WBM-owned capacity and do not wait behind compactions
// in Env::LOW.
TEST_P(DBWriteBufferManagerTest, FlushLargestAcrossDBsBypassesBusyLowPool) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 1 << 20;
cost_cache_ = GetParam();
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
1 << 20 /* buffer_size (1MB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
InstrumentedMutex mu;
InstrumentedCondVar cv(&mu);
bool handoff_executed = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BackgroundCallWBMFlush:AfterPick", [&](void*) {
InstrumentedMutexLock lock(&mu);
handoff_executed = true;
cv.SignalAll();
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Defer cleanup_sync_points([] {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
});
Reopen(options);
std::string other_dbname = test::PerThreadDBPath("wbm_cancel_on_close_other");
ASSERT_OK(DestroyDB(other_dbname, options));
std::unique_ptr<DB> other_db;
ASSERT_OK(DB::Open(options, other_dbname, &other_db));
test::SleepingBackgroundTask sleeping_task_low;
const int saved_low = env_->GetBackgroundThreads(Env::Priority::LOW);
env_->SetBackgroundThreads(1, Env::Priority::LOW);
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task_low,
Env::Priority::LOW);
sleeping_task_low.WaitUntilSleeping();
Defer restore_low_pool([&] {
sleeping_task_low.WakeUp();
sleeping_task_low.WaitUntilDone();
env_->SetBackgroundThreads(saved_low, Env::Priority::LOW);
});
ASSERT_OK(other_db->Put(WriteOptions(), Key(1), DummyString(600 << 10)));
ASSERT_OK(Put(Key(1), DummyString(300 << 10), WriteOptions()));
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
ASSERT_OK(Put(Key(2), DummyString(1), WriteOptions()));
{
InstrumentedMutexLock lock(&mu);
const uint64_t deadline = env_->NowMicros() + 30 * 1000 * 1000;
while (!handoff_executed) {
ASSERT_FALSE(cv.TimedWait(deadline));
}
}
ASSERT_OK(other_db->Close());
other_db.reset();
ASSERT_OK(DestroyDB(other_dbname, options));
}
// A read-only DB consumes shared WBM memory but cannot honor a flush request.
TEST_P(DBWriteBufferManagerTest, FlushLargestAcrossDBsSkipsReadOnlyDB) {
auto recorder = std::make_shared<FlushedDBRecorder>();
Options options = CurrentOptions();
options.arena_block_size = 4 << 10; // 4KB
options.write_buffer_size = 1 << 20; // 1MB, so per-CF flush is never hit
cost_cache_ = GetParam();
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
options.listeners.push_back(recorder);
// Stage a DB whose data lives only in the WAL, so opening it read-only
// rebuilds that data into memtables.
std::string ro_dbname = test::PerThreadDBPath("wbm_read_only_db");
{
Options staging = options;
staging.create_if_missing = true;
staging.avoid_flush_during_shutdown = true; // keep the data in the WAL
ASSERT_OK(DestroyDB(ro_dbname, staging));
std::unique_ptr<DB> staging_db;
ASSERT_OK(DB::Open(staging, ro_dbname, &staging_db));
ASSERT_OK(staging_db->Put(WriteOptions(), Key(1), DummyString(300 << 10)));
ASSERT_OK(staging_db->Close());
}
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
512 << 10 /* buffer_size (512KB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
const size_t mem_before = options.write_buffer_manager->memory_usage();
std::unique_ptr<DB> read_only_db;
ASSERT_OK(DB::OpenForReadOnly(options, ro_dbname, &read_only_db));
EXPECT_GT(options.write_buffer_manager->memory_usage(), mem_before);
ASSERT_OK(Put(Key(1), DummyString(200 << 10), WriteOptions()));
// db_ holds less than the read-only DB, so without the read-only exclusion
// the selector would pick a DB that cannot flush. The coordinator instead
// switches db_'s memtable asynchronously.
recorder->WaitForFlush(db_.get());
EXPECT_FALSE(recorder->HasFlushed(read_only_db.get()));
ASSERT_OK(read_only_db->Close());
read_only_db.reset();
ASSERT_OK(DestroyDB(ro_dbname, options));
}
// Rank an atomic-flush DB by the memory reclaimed across all its CFs.
TEST_P(DBWriteBufferManagerTest, FlushLargestAcrossDBsPicksAtomicFlushDB) {
auto recorder = std::make_shared<FlushedDBRecorder>();
Options options = CurrentOptions();
options.arena_block_size = 4 << 10; // 4KB
options.write_buffer_size = 1 << 20; // 1MB, so per-CF flush is never hit
options.listeners.push_back(recorder);
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
cost_cache_ = GetParam();
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20 /* buffer_size (10MB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
Options atomic_options = options;
atomic_options.atomic_flush = true;
atomic_options.create_if_missing = true;
atomic_options.create_missing_column_families = true;
std::string atomic_dbname = test::PerThreadDBPath("wbm_atomic_db");
ASSERT_OK(DestroyDB(atomic_dbname, atomic_options));
std::vector<ColumnFamilyDescriptor> cf_descs = {
{kDefaultColumnFamilyName, ColumnFamilyOptions(atomic_options)},
{"cf1", ColumnFamilyOptions(atomic_options)}};
std::vector<ColumnFamilyHandle*> atomic_handles;
std::unique_ptr<DB> atomic_db;
ASSERT_OK(DB::Open(atomic_options, atomic_dbname, cf_descs, &atomic_handles,
&atomic_db));
// atomic_db reclaims 600KB across two CFs versus db_'s single 350KB CF.
ASSERT_OK(atomic_db->Put(WriteOptions(), atomic_handles[0], Key(1),
DummyString(300 << 10)));
ASSERT_OK(atomic_db->Put(WriteOptions(), atomic_handles[1], Key(1),
DummyString(300 << 10)));
ASSERT_OK(Put(Key(1), DummyString(350 << 10), WriteOptions()));
ASSERT_FALSE(options.write_buffer_manager->ShouldFlush());
options.write_buffer_manager->TEST_RefreshFlushInitiatorCandidate();
// Ignore any flush that may have happened during open/setup.
recorder->Reset();
options.write_buffer_manager->SetBufferSize(1 << 20);
recorder->WaitForColumnFamilies(atomic_db.get(), 2);
EXPECT_FALSE(recorder->HasFlushed(db_.get()));
EXPECT_EQ(0, recorder->ManualFlushes());
for (auto* handle : atomic_handles) {
ASSERT_OK(atomic_db->DestroyColumnFamilyHandle(handle));
}
ASSERT_OK(atomic_db->Close());
atomic_db.reset();
ASSERT_OK(DestroyDB(atomic_dbname, atomic_options));
}
TEST_P(DBWriteBufferManagerTest,
AtomicFlushRankingIncludesWaitingImmutableMemory) {
Options options = CurrentOptions();
options.arena_block_size = 4 << 10;
options.write_buffer_size = 1 << 20;
options.max_write_buffer_number = 5;
options.min_write_buffer_number_to_merge = 4;
std::shared_ptr<Cache> cache =
NewLRUCache(4 << 20 /* capacity (4MB) */, 2 /* num_shard_bits */);
cost_cache_ = GetParam();
options.write_buffer_manager = std::make_shared<WriteBufferManager>(
10 << 20 /* buffer_size (10MB) */, cost_cache_ ? cache : nullptr,
false /* allow_stall */, WriteBufferFlushPolicy::kFlushLargestAcrossDBs);
Reopen(options);
Options atomic_options = options;
atomic_options.atomic_flush = true;
atomic_options.create_if_missing = true;
atomic_options.create_missing_column_families = true;
std::string atomic_dbname =
test::PerThreadDBPath("wbm_atomic_waiting_immutable");
ASSERT_OK(DestroyDB(atomic_dbname, atomic_options));
std::vector<ColumnFamilyDescriptor> cf_descs = {
{kDefaultColumnFamilyName, ColumnFamilyOptions(atomic_options)},
{"cf1", ColumnFamilyOptions(atomic_options)}};
std::vector<ColumnFamilyHandle*> atomic_handles;
std::unique_ptr<DB> atomic_db;
ASSERT_OK(DB::Open(atomic_options, atomic_dbname, cf_descs, &atomic_handles,
&atomic_db));
auto* atomic_db_impl = static_cast_with_check<DBImpl>(atomic_db.get());
// The atomic DB has about 300KB waiting immutable plus 300KB mutable.
// Its combined reclaimable memory should rank above db_'s 450KB mutable.
ASSERT_OK(atomic_db->Put(WriteOptions(), atomic_handles[0], Key(1),
DummyString(300 << 10)));
ColumnFamilyData* atomic_default_cfd =
static_cast_with_check<ColumnFamilyHandleImpl>(atomic_handles[0])->cfd();
ASSERT_OK(atomic_db_impl->TEST_SwitchMemtable(atomic_default_cfd));
ASSERT_OK(atomic_db->Put(WriteOptions(), atomic_handles[1], Key(1),
DummyString(300 << 10)));
ASSERT_OK(Put(Key(1), DummyString(450 << 10), WriteOptions()));
EXPECT_GT(atomic_db_impl->TEST_GetWBMFlushableMemUsage(),
dbfull()->TEST_GetWBMFlushableMemUsage());
for (auto* handle : atomic_handles) {
ASSERT_OK(atomic_db->DestroyColumnFamilyHandle(handle));
}
ASSERT_OK(atomic_db->Close());
atomic_db.reset();
ASSERT_OK(DestroyDB(atomic_dbname, atomic_options));
}
TEST_F(DBWriteBufferManagerTest, RuntimeChangeableAllowStall) {
constexpr int kBigValue = 10000;
Options options = CurrentOptions();
options.write_buffer_manager.reset(
new WriteBufferManager(1, nullptr /* cache */, true /* allow_stall */));
DestroyAndReopen(options);
// Pause flush thread so that
// (a) the only way to exist write stall below is to change the `allow_stall`
// (b) the write stall is "stable" without being interfered by flushes so that
// we can check it without flakiness
std::unique_ptr<test::SleepingBackgroundTask> sleeping_task(
new test::SleepingBackgroundTask());
env_->SetBackgroundThreads(1, Env::HIGH);
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
sleeping_task.get(), Env::Priority::HIGH);
sleeping_task->WaitUntilSleeping();
// Test 1: test setting `allow_stall` from true to false
//
// Assert existence of a write stall
WriteOptions wo_no_slowdown;
wo_no_slowdown.no_slowdown = true;
Status s = Put(Key(0), DummyString(kBigValue), wo_no_slowdown);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.ToString().find("Write stall") != std::string::npos);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"WBMStallInterface::BlockDB",
"DBWriteBufferManagerTest::RuntimeChangeableThreadSafeParameters::"
"ChangeParameter"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Test `SetAllowStall()`
port::Thread thread1([&] { ASSERT_OK(Put(Key(0), DummyString(kBigValue))); });
port::Thread thread2([&] {
TEST_SYNC_POINT(
"DBWriteBufferManagerTest::RuntimeChangeableThreadSafeParameters::"
"ChangeParameter");
options.write_buffer_manager->SetAllowStall(false);
});
// Verify `allow_stall` is successfully set to false in thread2.
// Othwerwise, thread1's write will be stalled and this test will hang
// forever.
thread1.join();
thread2.join();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
// Test 2: test setting `allow_stall` from false to true
//
// Assert no write stall
ASSERT_OK(Put(Key(0), DummyString(kBigValue), wo_no_slowdown));
// Test `SetAllowStall()`
options.write_buffer_manager->SetAllowStall(true);
// Verify `allow_stall` is successfully set to true.
// Otherwise the following write will not be stalled and therefore succeed.
s = Put(Key(0), DummyString(kBigValue), wo_no_slowdown);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.ToString().find("Write stall") != std::string::npos);
sleeping_task->WakeUp();
}
// Test that enforce_write_buffer_manager_during_recovery option controls
// whether WriteBufferManager limits are respected during WAL recovery.
// When enabled, flushes are triggered to keep memory bounded.
// When disabled (default), memory can grow beyond the configured limit.
TEST_F(DBWriteBufferManagerTest,
WriteBufferManagerLimitDuringWALRecoverySingleDB) {
const size_t kWbmLimit = 1 * 1024 * 1024; // 1 MB
const size_t kWbmLimitForWrites = 100 * 1024 * 1024; // 100 MB (no flush)
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 10 * 1024 * 1024; // 10MB per CF, never hit
options.max_write_buffer_number = 10; // Allow many memtables
options.disable_auto_compactions = true;
// Use avoid_flush_during_recovery = true to prevent any flushes triggered
// during the recovery
options.avoid_flush_during_recovery = true;
const int kNumKeys = 50;
const int kValueSize = 50 * 1024; // 50 KB each, total ~2.5 MB > 1MB limit
// ========== Part 1: Test with enforcement DISABLED (default behavior) =====
// WBM limits are not enforced during recovery
options.enforce_write_buffer_manager_during_recovery = false;
// Use large WBM limit during writes to avoid triggering flushes
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForWrites, nullptr, true);
DestroyAndReopen(options);
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put(Key(i), DummyString(kValueSize)));
}
// Check to make sure there's no L0 file
ASSERT_EQ(0, TotalTableFiles());
Close();
// Use smaller WBM limit for recovery
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimit, nullptr, true);
// Recovery without enforcement - memory should exceed the limit
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
size_t memory_without_enforcement =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_GT(memory_without_enforcement, kWbmLimit)
<< "Without enforcement, memory (" << memory_without_enforcement
<< ") should exceed the WBM limit (" << kWbmLimit << ")";
// Still no L0 file since avoid_flush_during_recovery is true
ASSERT_EQ(0, TotalTableFiles());
// ========== Part 2: Test with enforcement ENABLED ==========================
options.enforce_write_buffer_manager_during_recovery = true;
// Use large WBM limit during writes to avoid triggering flushes
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForWrites, nullptr, true);
DestroyAndReopen(options);
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put(Key(i), DummyString(kValueSize)));
}
// Check to make sure there's no L0 file
ASSERT_EQ(0, TotalTableFiles());
Close();
// Use smaller WBM limit for recovery
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimit, nullptr, true);
// Recovery with enforcement - memory should be bounded
Reopen(options);
// Wait for flush to finish
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// WBM's ShouldFlush() compares active memtable mem usage against
// mutable_limit_ which is 7/8 of buffer_size.
size_t expected_num_l0_files =
memory_without_enforcement / (kWbmLimit * 7 / 8) + 1;
ASSERT_EQ(expected_num_l0_files, TotalTableFiles());
size_t memory_with_enforcement =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_LT(memory_with_enforcement, kWbmLimit)
<< "With enforcement, memory (" << memory_with_enforcement
<< ") should be less than the limit " << kWbmLimit << ")";
}
TEST_F(DBWriteBufferManagerTest,
WriteBufferManagerLimitDuringWALRecoveryMultipleDBs) {
// Two DBs with 4MB WBM limit.
// First DB writes 2.5MB and closes, no flush (mem usage goes back to 0)
// Second DB writes 2.5MB then first DB reopens.
const size_t kWbmLimitForTwoDbs = 4 * 1024 * 1024;
Options options = CurrentOptions();
options.arena_block_size = 2048;
options.write_buffer_size = 10 * 1024 * 1024; // 10MB per CF, never hit
options.max_write_buffer_number = 10; // Allow many memtables
options.disable_auto_compactions = true;
// Use avoid_flush_during_recovery = true to prevent any flushes triggered
// during the recovery
options.avoid_flush_during_recovery = true;
const int kNumKeys = 50;
const int kValueSize = 50 * 1024;
// ========== Part 1: Test with enforcement DISABLED (default behavior) =====
// WBM limits are not enforced during recovery
options.enforce_write_buffer_manager_during_recovery = false;
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForTwoDbs, nullptr, true);
DestroyAndReopen(options);
// Use of 2.5MB shouldn't trigger flush
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put(Key(i), DummyString(kValueSize)));
}
ASSERT_EQ(0, TotalTableFiles());
size_t mem_usage_first_db_only =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_LT(mem_usage_first_db_only, kWbmLimitForTwoDbs)
<< "Memory (" << mem_usage_first_db_only
<< ") should be less than the limit " << kWbmLimitForTwoDbs << ")";
Close();
ASSERT_EQ(0, options.write_buffer_manager->mutable_memtable_memory_usage());
// Open a second DB sharing the same WBM, write data to consume memory
std::string second_dbname = test::PerThreadDBPath("db_shared_wbm_recovery");
std::unique_ptr<DB> second_db;
ASSERT_OK(DestroyDB(second_dbname, options));
ASSERT_OK(DB::Open(options, second_dbname, &second_db));
WriteOptions wo;
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(second_db->Put(wo, Key(i), DummyString(kValueSize)));
}
// First DB reopens without enforcement
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// No flush
ASSERT_EQ(0, TotalTableFiles());
size_t memory_usage_for_both =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_GT(memory_usage_for_both, kWbmLimitForTwoDbs)
<< "Without enforcement + shared WBM, memory (" << memory_usage_for_both
<< ") should be greater than the limit (" << kWbmLimitForTwoDbs << ")";
// Close the first DB
Close();
// ========== Part 2: Test with enforcement ENABLED =====
// WBM limits enforced during recovery
options.enforce_write_buffer_manager_during_recovery = true;
// Reopen the first DB with enforcement option enabled.
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// With enforcement enabled, there were flushes
ASSERT_GT(TotalTableFiles(), 0);
memory_usage_for_both =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_LT(memory_usage_for_both, kWbmLimitForTwoDbs)
<< "With enforcement + shared WBM, memory (" << memory_usage_for_both
<< ") should be less than the limit (" << kWbmLimitForTwoDbs << ")";
Close();
// Clean up second DB
ASSERT_OK(second_db->Close());
ASSERT_OK(DestroyDB(second_dbname, options));
second_db.reset();
}
// Regression test: a WriteBatch that exceeds both per-CF memtable limit and
// WBM global limit during WAL recovery should not double-schedule a CF on
// flush_scheduler_ (which crashes debug builds via assert).
TEST_F(DBWriteBufferManagerTest, DoubleSchedulingBugDuringWALRecovery) {
Options options = CurrentOptions();
options.arena_block_size = 4096;
// Small per-CF limit so memtable triggers CheckMemtableFull during recovery
options.write_buffer_size = 64 * 1024; // 64KB
options.max_write_buffer_number = 10;
options.disable_auto_compactions = true;
options.avoid_flush_during_recovery = true;
options.enforce_write_buffer_manager_during_recovery = true;
// WBM limit also small so the WBM loop in InsertLogRecordToMemtable fires
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(128 * 1024, nullptr, true);
DestroyAndReopen(options);
// Write enough data to exceed both limits during recovery replay
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(Key(i), DummyString(4096))); // ~200KB total > both limits
}
Close();
// Reopen triggers WAL recovery. Without the fix, this crashes in debug
// builds with assert(checking_set_.count(cfd) == 0) in ScheduleWork().
ASSERT_OK(TryReopen(options));
}
// Read-only WAL recovery with enforce_write_buffer_manager_during_recovery
// should not crash due to duplicate ScheduleWork() calls on the
// flush_scheduler_. The flush scheduler was not drained in read-only mode,
// causing the same CFD to be scheduled twice on successive WAL records,
// triggering assert(checking_set_.count(cfd) == 0).
TEST_F(DBWriteBufferManagerTest, ReadOnlyRecoveryWithEnforceWBMDoesNotAssert) {
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 10 * 1024 * 1024; // 10MB, never hit
options.max_write_buffer_number = 10;
options.disable_auto_compactions = true;
options.avoid_flush_during_recovery = true;
options.enforce_write_buffer_manager_during_recovery = true;
const size_t kWbmLimitForWrites = 100 * 1024 * 1024; // 100MB (no flush)
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForWrites, nullptr, true);
DestroyAndReopen(options);
// Write enough data so that WAL recovery will trigger
// WriteBufferManager::ShouldFlush() multiple times with a small WBM limit.
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(Key(i), DummyString(50 * 1024))); // ~2.5MB total
}
ASSERT_EQ(0, TotalTableFiles());
Close();
// Reopen read-only with a small WBM limit that will trigger ShouldFlush()
// during WAL recovery. Without the fix, this crashes in debug builds with
// assert(checking_set_.count(cfd) == 0) in FlushScheduler::ScheduleWork().
const size_t kWbmLimit = 1 * 1024 * 1024; // 1MB
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimit, nullptr, true);
ASSERT_OK(ReadOnlyReopen(options));
// Verify data is readable
for (int i = 0; i < 50; i++) {
ASSERT_EQ(DummyString(50 * 1024), Get(Key(i)));
}
}
INSTANTIATE_TEST_CASE_P(DBWriteBufferManagerTest, DBWriteBufferManagerTest,
testing::Bool());
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
::testing::InitGoogleTest(&argc, argv);
RegisterCustomObjects(argc, argv);
return RUN_ALL_TESTS();
}