/
GraphTreeHeap
/
museumdb2
Обзор
Документация
Войти
/
GraphTreeHeap
/
museumdb2
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
db/db_write_buffer_manager_test.cc
1 190 строк
42 KB
Jay Huh
Skip flush during recovery if read_only (#14440)
10 мар 2026, 01:17
10 мар 2026, 01:17
e27114b
Код
Авторство
О чём код?
// 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 "db/db_test_util.h" #include "db/write_thread.h" #include "port/stack_trace.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, 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, 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, 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(); } 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(); }