1
0
mirror of https://github.com/facebook/rocksdb.git synced 2026-10-23 08:09:02 +03:00
Files
rocksdb/db/options_file_test.cc
Anand Ananthabhotla 4b9070bccb Defer obsolete OPTIONS file deletion on open (#15004)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15004

This change reduces DB open latency on remote storage by avoiding foreground deletion of obsolete `OPTIONS-*` files during open when `avoid_unnecessary_blocking_io` is enabled. DB open now avoids foreground deletion of obsolete `OPTIONS-*` files on the pre-open `WriteOptionsFile()` path. `RenameTempFileToOptionsFile()` records the new OPTIONS file and chooses an explicit cleanup mode: skip while open is still in progress, schedule deletion when background work is available, or delete immediately when blocking IO is allowed or background jobs are rejected.

`DBImpl::Open()` makes the deferred cleanup decision once final open status is known. A successful open schedules obsolete OPTIONS files on the purge queue, while a failed open deletes them synchronously before tearing down the partially opened DB. The default synchronous behavior remains unchanged when `avoid_unnecessary_blocking_io` is disabled, and the latest-two OPTIONS retention policy is preserved.

Reviewed By: pdillinger

Differential Revision: D113593680

fbshipit-source-id: 8aee99b3f38a729647d0bcb644d212181fe86895
2026-07-29 12:44:27 -07:00

302 lines
9.9 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).
#include <atomic>
#include <memory>
#include <string>
#include <utility>
#include "db/db_impl/db_impl.h"
#include "db/db_test_util.h"
#include "rocksdb/options.h"
#include "rocksdb/table.h"
#include "test_util/testharness.h"
#include "util/cast_util.h"
namespace ROCKSDB_NAMESPACE {
class OptionsFileTest : public testing::Test {
public:
OptionsFileTest() : dbname_(test::PerThreadDBPath("options_file_test")) {}
std::string dbname_;
};
namespace {
std::string Basename(const std::string& fname) {
const size_t basename_pos = fname.find_last_of("/\\");
return basename_pos == std::string::npos ? fname
: fname.substr(basename_pos + 1);
}
bool IsOptionsFile(const std::string& fname) {
return Basename(fname).find(kOptionsFileNamePrefix) == 0;
}
class FailOptionsDirFsyncFileSystem : public FileSystemWrapper {
public:
explicit FailOptionsDirFsyncFileSystem(std::shared_ptr<FileSystem> target)
: FileSystemWrapper(std::move(target)) {}
static const char* kClassName() { return "FailOptionsDirFsyncFileSystem"; }
const char* Name() const override { return kClassName(); }
void FailOptionsDirFsyncAfterNextOptionsFile() {
observe_options_file_.store(true, std::memory_order_release);
fail_options_dir_fsync_.store(false, std::memory_order_release);
options_dir_fsync_failures_.store(0, std::memory_order_release);
}
int options_dir_fsync_failures() const {
return options_dir_fsync_failures_.load(std::memory_order_acquire);
}
IOStatus NewWritableFile(const std::string& fname,
const FileOptions& file_opts,
std::unique_ptr<FSWritableFile>* result,
IODebugContext* dbg) override {
IOStatus s =
FileSystemWrapper::NewWritableFile(fname, file_opts, result, dbg);
if (!s.ok()) {
return s;
}
if (observe_options_file_.load(std::memory_order_acquire) &&
IsOptionsFile(fname)) {
fail_options_dir_fsync_.store(true, std::memory_order_release);
observe_options_file_.store(false, std::memory_order_release);
}
return s;
}
IOStatus NewDirectory(const std::string& name, const IOOptions& io_opts,
std::unique_ptr<FSDirectory>* result,
IODebugContext* dbg) override {
IOStatus s = FileSystemWrapper::NewDirectory(name, io_opts, result, dbg);
if (s.ok()) {
*result = std::make_unique<FailOptionsDirFsyncDirectory>(
std::move(*result), *this);
}
return s;
}
private:
class FailOptionsDirFsyncDirectory : public FSDirectoryWrapper {
public:
FailOptionsDirFsyncDirectory(std::unique_ptr<FSDirectory>&& dir,
FailOptionsDirFsyncFileSystem& fs)
: FSDirectoryWrapper(std::move(dir)), fs_(fs) {}
IOStatus FsyncWithDirOptions(
const IOOptions& options, IODebugContext* dbg,
const DirFsyncOptions& dir_fsync_options) override {
if (fs_.fail_options_dir_fsync_.load(std::memory_order_acquire) &&
IsOptionsFile(dir_fsync_options.renamed_new_name)) {
fs_.fail_options_dir_fsync_.store(false, std::memory_order_release);
fs_.options_dir_fsync_failures_.fetch_add(1, std::memory_order_acq_rel);
return IOStatus::IOError("Injected OPTIONS directory fsync failure");
}
return FSDirectoryWrapper::FsyncWithDirOptions(options, dbg,
dir_fsync_options);
}
private:
FailOptionsDirFsyncFileSystem& fs_;
};
std::atomic<bool> observe_options_file_{false};
std::atomic<bool> fail_options_dir_fsync_{false};
std::atomic<int> options_dir_fsync_failures_{0};
};
void UpdateOptionsFiles(DB* db,
std::unordered_set<std::string>* filename_history,
int* options_files_count) {
std::vector<std::string> filenames;
EXPECT_OK(db->GetEnv()->GetChildren(db->GetName(), &filenames));
uint64_t number;
FileType type;
*options_files_count = 0;
for (const auto& filename : filenames) {
if (ParseFileName(filename, &number, &type) && type == kOptionsFile) {
filename_history->insert(filename);
(*options_files_count)++;
}
}
}
// Verify whether the current Options Files are the latest ones.
void VerifyOptionsFileName(
DB* db, const std::unordered_set<std::string>& past_filenames) {
std::vector<std::string> filenames;
std::unordered_set<std::string> current_filenames;
EXPECT_OK(db->GetEnv()->GetChildren(db->GetName(), &filenames));
uint64_t number;
FileType type;
for (const auto& filename : filenames) {
if (ParseFileName(filename, &number, &type) && type == kOptionsFile) {
current_filenames.insert(filename);
}
}
for (const auto& past_filename : past_filenames) {
if (current_filenames.find(past_filename) != current_filenames.end()) {
continue;
}
for (const auto& filename : current_filenames) {
ASSERT_GT(filename, past_filename);
}
}
}
int CountOptionsFiles(Env* env, const std::string& dbname) {
std::vector<std::string> filenames;
EXPECT_OK(env->GetChildren(dbname, &filenames));
int options_files_count = 0;
uint64_t number;
FileType type;
for (const auto& filename : filenames) {
if (ParseFileName(filename, &number, &type) && type == kOptionsFile) {
++options_files_count;
}
}
return options_files_count;
}
int CountOptionsFiles(DB* db) {
return CountOptionsFiles(db->GetEnv(), db->GetName());
}
void GenerateStaleOptionsFiles(DB* db, int options_file_count) {
ASSERT_OK(db->DisableFileDeletions());
for (int i = 0; i < options_file_count; ++i) {
ASSERT_OK(db->SetOptions(
{{"level0_file_num_compaction_trigger", std::to_string(8 + i)}}));
}
ASSERT_GT(CountOptionsFiles(db), 2);
}
} // anonymous namespace
TEST_F(OptionsFileTest, NumberOfOptionsFiles) {
const int kReopenCount = 20;
Options opt;
opt.create_if_missing = true;
ASSERT_OK(DestroyDB(dbname_, opt));
std::unordered_set<std::string> filename_history;
std::unique_ptr<DB> db;
for (int i = 0; i < kReopenCount; ++i) {
ASSERT_OK(DB::Open(opt, dbname_, &db));
int num_options_files = 0;
UpdateOptionsFiles(db.get(), &filename_history, &num_options_files);
ASSERT_GT(num_options_files, 0);
ASSERT_LE(num_options_files, 2);
// Make sure we always keep the latest option files.
VerifyOptionsFileName(db.get(), filename_history);
db.reset();
}
}
TEST_F(OptionsFileTest, ObsoleteOptionsFilesPurgedSynchronouslyOnOpen) {
Options opt;
opt.create_if_missing = true;
ASSERT_OK(DestroyDB(dbname_, opt));
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(opt, dbname_, &db));
GenerateStaleOptionsFiles(db.get(), 5);
db.reset();
opt.create_if_missing = false;
opt.avoid_unnecessary_blocking_io = false;
ASSERT_OK(DB::Open(opt, dbname_, &db));
ASSERT_LE(CountOptionsFiles(db.get()), 2);
}
TEST_F(OptionsFileTest, ObsoleteOptionsFilesPurgedInBackgroundOnOpen) {
Options opt;
opt.create_if_missing = true;
ASSERT_OK(DestroyDB(dbname_, opt));
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(opt, dbname_, &db));
GenerateStaleOptionsFiles(db.get(), 5);
db.reset();
opt.create_if_missing = false;
opt.avoid_unnecessary_blocking_io = true;
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->LoadDependency(
{{"OptionsFileTest::ObsoleteOptionsFilesPurgedInBackgroundOnOpen:"
"ReleasePurge",
"DBImpl::BGWorkPurge:start"}});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(DB::Open(opt, dbname_, &db));
ASSERT_GT(CountOptionsFiles(db.get()), 2);
TEST_SYNC_POINT(
"OptionsFileTest::ObsoleteOptionsFilesPurgedInBackgroundOnOpen:"
"ReleasePurge");
ASSERT_OK(static_cast_with_check<DBImpl>(db.get())->TEST_WaitForPurge());
ASSERT_LE(CountOptionsFiles(db.get()), 2);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(OptionsFileTest, ObsoleteOptionsFilesPurgedSynchronouslyOnOpenFailure) {
auto fail_fs =
std::make_shared<FailOptionsDirFsyncFileSystem>(FileSystem::Default());
std::unique_ptr<Env> env(NewCompositeEnv(fail_fs));
Options opt;
opt.env = env.get();
opt.create_if_missing = true;
ASSERT_OK(DestroyDB(dbname_, opt));
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(opt, dbname_, &db));
GenerateStaleOptionsFiles(db.get(), 5);
db.reset();
opt.create_if_missing = false;
opt.avoid_unnecessary_blocking_io = true;
fail_fs->FailOptionsDirFsyncAfterNextOptionsFile();
ASSERT_NOK(DB::Open(opt, dbname_, &db));
ASSERT_EQ(fail_fs->options_dir_fsync_failures(), 1);
ASSERT_LE(CountOptionsFiles(opt.env, dbname_), 2);
}
TEST_F(OptionsFileTest, OptionsFileName) {
const uint64_t kOptionsFileNum = 12345;
uint64_t number;
FileType type;
auto options_file_name = OptionsFileName("", kOptionsFileNum);
ASSERT_TRUE(ParseFileName(options_file_name, &number, &type, nullptr));
ASSERT_EQ(type, kOptionsFile);
ASSERT_EQ(number, kOptionsFileNum);
const uint64_t kTempOptionsFileNum = 54352;
auto temp_options_file_name = TempOptionsFileName("", kTempOptionsFileNum);
ASSERT_TRUE(ParseFileName(temp_options_file_name, &number, &type, nullptr));
ASSERT_NE(temp_options_file_name.find(kTempFileNameSuffix),
std::string::npos);
ASSERT_EQ(type, kTempFile);
ASSERT_EQ(number, kTempOptionsFileNum);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
#if !(defined NDEBUG) || !defined(OS_WIN)
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
#else
return 0;
#endif // !(defined NDEBUG) || !defined(OS_WIN)
}