From 21723bbbef39a0e525dd0ecc98af2f83cfa5faac Mon Sep 17 00:00:00 2001 From: Xingbo Wang Date: Fri, 15 May 2026 10:59:47 -0700 Subject: [PATCH] Add async WAL precreation Summary: - Add experimental immutable `DBOptions::async_wal_precreate` to reserve and open one future WAL on a background HIGH-priority task, with sanitization that disables the optimization when WAL recycling is configured. - Split WAL creation into open/preallocate and start phases so `SwitchMemtable()` can consume a prepared WAL after writing normal WAL metadata, wait for in-flight precreation, fall back to synchronous creation, and delete an unstarted prepared WAL on start failure. - Keep WAL numbering, close, recovery, and read-only open safe for empty future WAL files left by async precreation; `error_if_wal_file_exists=true` now rejects non-empty WALs while tolerating empty WALs. - Add public option plumbing for the C API, options parsing/stringification, random option testing, `db_bench`, `db_stress`, and crash-test configuration. - Add WAL precreate statistics counters plus Java `TickerType`/JNI mappings, and update C++, C, and Java read-only-open documentation for the empty-WAL behavior. - Add focused WAL/option/C/Java tests for async precreate ready/wait/failure/recovery paths, read-only WAL detection, option sanitization, and API plumbing, plus write-flow docs and unreleased history entries for the new feature and behavior change. PR https://github.com/facebook/rocksdb/pull/14738 Reviewed By: pdillinger Differential Revision: D105020559 fbshipit-source-id: 5059b424702e021abb8de65ceeb6d3b975280ffc --- db/c.cc | 9 + db/c_test.c | 3 + db/db_impl/db_impl.cc | 15 + db/db_impl/db_impl.h | 87 ++++- db/db_impl/db_impl_open.cc | 211 ++++++++++-- db/db_impl/db_impl_write.cc | 43 ++- db/db_options_test.cc | 34 ++ db/db_wal_test.cc | 310 ++++++++++++++++++ db_stress_tool/db_stress_common.h | 1 + db_stress_tool/db_stress_gflags.cc | 4 + db_stress_tool/db_stress_test_base.cc | 1 + docs/components/write_flow/03_wal.md | 14 +- .../write_flow/04_memtable_insert.md | 2 +- include/rocksdb/c.h | 9 + include/rocksdb/db.h | 8 + include/rocksdb/options.h | 11 + include/rocksdb/statistics.h | 10 + java/rocksjni/portal.h | 20 ++ java/src/main/java/org/rocksdb/RocksDB.java | 4 +- .../src/main/java/org/rocksdb/TickerType.java | 25 ++ .../test/java/org/rocksdb/ReadOnlyTest.java | 5 +- monitoring/statistics.cc | 5 + options/db_options.cc | 7 + options/db_options.h | 2 + options/options_helper.cc | 1 + options/options_settable_test.cc | 1 + options/options_test.cc | 6 + test_util/testutil.cc | 1 + tools/db_bench_tool.cc | 3 + tools/db_crashtest.py | 1 + .../error_if_wal_file_exists_empty_wals.md | 1 + .../new_features/async_wal_precreate.md | 1 + 32 files changed, 823 insertions(+), 32 deletions(-) create mode 100644 unreleased_history/behavior_changes/error_if_wal_file_exists_empty_wals.md create mode 100644 unreleased_history/new_features/async_wal_precreate.md diff --git a/db/c.cc b/db/c.cc index 7b3dee3ea6..6e568fc11b 100644 --- a/db/c.cc +++ b/db/c.cc @@ -5286,6 +5286,15 @@ size_t rocksdb_options_get_recycle_log_file_num(rocksdb_options_t* opt) { return opt->rep.recycle_log_file_num; } +void rocksdb_options_set_async_wal_precreate(rocksdb_options_t* opt, + unsigned char v) { + opt->rep.async_wal_precreate = v; +} + +unsigned char rocksdb_options_get_async_wal_precreate(rocksdb_options_t* opt) { + return opt->rep.async_wal_precreate; +} + void rocksdb_options_set_soft_pending_compaction_bytes_limit( rocksdb_options_t* opt, size_t v) { opt->rep.soft_pending_compaction_bytes_limit = v; diff --git a/db/c_test.c b/db/c_test.c index 76a8866625..fc65997703 100644 --- a/db/c_test.c +++ b/db/c_test.c @@ -2869,6 +2869,9 @@ int main(int argc, char** argv) { rocksdb_options_set_track_and_verify_wals_in_manifest(o, 42); CheckCondition(1 == rocksdb_options_get_track_and_verify_wals_in_manifest(o)); + CheckCondition(0 == rocksdb_options_get_async_wal_precreate(o)); + rocksdb_options_set_async_wal_precreate(o, 1); + CheckCondition(1 == rocksdb_options_get_async_wal_precreate(o)); /* Blob Options */ rocksdb_options_set_enable_blob_files(o, 1); diff --git a/db/db_impl/db_impl.cc b/db/db_impl/db_impl.cc index 401ecf75f9..603e1d4844 100644 --- a/db/db_impl/db_impl.cc +++ b/db/db_impl/db_impl.cc @@ -789,12 +789,27 @@ Status DBImpl::CloseHelper() { bg_flush_scheduled_ || bg_purge_scheduled_ || bg_pressure_callback_in_progress_ || bg_async_file_open_state_ == AsyncFileOpenState::kScheduled || + async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled || pending_purge_obsolete_files_ || error_handler_.IsRecoveryInProgress()) { TEST_SYNC_POINT("DBImpl::~DBImpl:WaitJob"); bg_cv_.Wait(); } + // Release any opened-but-unpublished WAL writer after the in-flight worker + // has published its result. Clear the DB-owned async slot while holding + // mutex_, but destroy the detached writer after dropping mutex_ because + // log::Writer / WritableFileWriter destruction can flush and close the file. + // The file itself can be left behind as an empty future WAL; recovery already + // tolerates it and marks its file number used if observed. + UnpublishedWAL unused_async_wal = std::move(async_wal_precreate_wal_); + async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled; + if (unused_async_wal.writer) { + mutex_.Unlock(); + unused_async_wal.Reset(); + mutex_.Lock(); + } + // Ensure subclasses don't forget to schedule async file opening assert(!immutable_db_options_.open_files_async || !opened_successfully_ || bg_async_file_open_state_ != AsyncFileOpenState::kNotScheduled); diff --git a/db/db_impl/db_impl.h b/db/db_impl/db_impl.h index 3e693076be..6632219881 100644 --- a/db/db_impl/db_impl.h +++ b/db/db_impl/db_impl.h @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -2810,7 +2812,68 @@ class DBImpl : public DB { size_t GetWalPreallocateBlockSize(uint64_t write_buffer_size) const; Env::WriteLifeTimeHint CalculateWALWriteHint() { return Env::WLTH_SHORT; } - IOStatus CreateWAL(const WriteOptions& write_options, uint64_t log_file_num, + // Returns true when async WAL precreation is enabled and compatible with the + // active WAL strategy. WAL recycling already avoids file creation latency, so + // precreation is disabled when recycle_log_file_num is non-zero. + bool AsyncWALPrecreateEnabled() const; + + // A WAL file that has a reserved file number and may have an opened writer, + // but has not been added to DBImpl's in-memory logical WAL tracking lists + // (logs_ and alive_wal_files_). + struct UnpublishedWAL { + uint64_t log_number = 0; + std::unique_ptr writer; + + UnpublishedWAL() = default; + UnpublishedWAL(const UnpublishedWAL&) = delete; + UnpublishedWAL& operator=(const UnpublishedWAL&) = delete; + + UnpublishedWAL(UnpublishedWAL&& other) noexcept { + *this = std::move(other); + } + UnpublishedWAL& operator=(UnpublishedWAL&& other) noexcept { + if (this != &other) { + log_number = other.log_number; + writer = std::move(other.writer); + other.Reset(); + } + return *this; + } + + void Reset() { + log_number = 0; + writer.reset(); + } + }; + + // Reserves the next WAL file number and schedules a HIGH-priority background + // task to precreate that WAL file. A precreated WAL is not a logical WAL + // until a foreground WAL rotation consumes it. + void MaybeScheduleAsyncWALPrecreate(size_t preallocate_block_size); + + // Background task for opening the reserved future WAL and publishing the + // result under mutex_. + static void BGWorkAsyncWALPrecreate(void* arg); + + // Waits for an in-flight async WAL precreation and returns a prepared WAL if + // one is available. If precreation failed, returns an empty WAL and lets the + // foreground rotation create the WAL synchronously. Caller must hold mutex_. + UnpublishedWAL WaitForAsyncWALPrecreate(); + + // Opens and preallocates a WAL writer without writing logical WAL records. + // Used by async WAL precreation and by synchronous WAL creation. + IOStatus CreateWALWriter(const DBOptions& db_options, uint64_t log_file_num, + uint64_t recycle_log_number, + size_t preallocate_block_size, + UnpublishedWAL* new_wal); + + // Starts an opened WAL file by writing the initial records required before it + // can be installed as the current WAL for foreground writes. + IOStatus StartWALFile(const WriteOptions& write_options, + const PredecessorWALInfo& predecessor_wal_info, + log::Writer* new_log); + IOStatus CreateWAL(const DBOptions& db_options, + const WriteOptions& write_options, uint64_t log_file_num, uint64_t recycle_log_number, size_t preallocate_block_size, const PredecessorWALInfo& predecessor_wal_info, log::Writer** new_log); @@ -3306,6 +3369,28 @@ class DBImpl : public DB { AsyncFileOpenState bg_async_file_open_state_ = AsyncFileOpenState::kNotScheduled; + // State machine for the single async WAL precreation slot protected by + // mutex_. Background precreation failure returns to kNotScheduled; foreground + // rotation handles it the same as no prepared WAL and creates one + // synchronously. kScheduled owns a reserved file number; kReady owns an + // opened writer that has not been started or added to logical WAL tracking. + enum class AsyncWALPrecreateState : uint8_t { + kNotScheduled = 0, // No WAL precreate work is in-flight or ready. + kScheduled, // Background task owns creation of the reserved WAL. + kReady, // Reserved WAL writer is open but not logically live. + }; + + // Protected by mutex_. Tracks at most one background precreated WAL. A + // precreated WAL is only reserved empty storage until SwitchMemtable() + // consumes it and installs it in DBImpl's in-memory logical WAL tracking + // lists (logs_ and alive_wal_files_). + AsyncWALPrecreateState async_wal_precreate_state_ = + AsyncWALPrecreateState::kNotScheduled; + + // Reserved in-flight/ready precreated WAL. The writer is populated only while + // state is kReady. + UnpublishedWAL async_wal_precreate_wal_; + std::deque manual_compaction_dequeue_; // shall we disable deletion of obsolete files diff --git a/db/db_impl/db_impl_open.cc b/db/db_impl/db_impl_open.cc index a76410c2ae..721468c14b 100644 --- a/db/db_impl/db_impl_open.cc +++ b/db/db_impl/db_impl_open.cc @@ -21,6 +21,7 @@ #include "file/writable_file_writer.h" #include "logging/logging.h" #include "monitoring/persistent_stats_history.h" +#include "monitoring/statistics_impl.h" #include "monitoring/thread_status_util.h" #include "options/options_helper.h" #include "rocksdb/options.h" @@ -122,6 +123,16 @@ DBOptions SanitizeOptions(const std::string& dbname, const DBOptions& src, result.recycle_log_file_num = 0; } + if (result.async_wal_precreate && result.recycle_log_file_num != 0) { + // Async WAL precreation reserves a future log number, while WAL recycling + // chooses from old WAL files. Keep the recycling behavior and disable only + // the async optimization. + result.async_wal_precreate = false; + ROCKS_LOG_WARN(result.info_log, + "async_wal_precreate is disabled since " + "recycle_log_file_num is non-zero"); + } + if (result.db_paths.size() == 0) { result.db_paths.emplace_back(dbname, std::numeric_limits::max()); } else if (result.wal_dir.empty()) { @@ -799,9 +810,19 @@ Status DBImpl::Recover( if (!wal_files.empty()) { if (error_if_wal_file_exists) { - return Status::Corruption( - "The db was opened in readonly mode with error_if_wal_file_exists" - "flag but a WAL file already exists"); + for (const auto& wal_file : wal_files) { + uint64_t bytes; + s = env_->GetFileSize(wal_file.second, &bytes); + if (!s.ok()) { + return s; + } + if (bytes > 0) { + return Status::Corruption( + "The db was opened in readonly mode with " + "error_if_wal_file_exists flag but a non-empty WAL file " + "already exists"); + } + } } else if (error_if_data_exists_in_wals) { for (auto& wal_file : wal_files) { uint64_t bytes; @@ -2377,16 +2398,17 @@ Status DB::OpenAndTrimHistory( return s; } -IOStatus DBImpl::CreateWAL(const WriteOptions& write_options, - uint64_t log_file_num, uint64_t recycle_log_number, - size_t preallocate_block_size, - const PredecessorWALInfo& predecessor_wal_info, - log::Writer** new_log) { +IOStatus DBImpl::CreateWALWriter(const DBOptions& db_options, + uint64_t log_file_num, + uint64_t recycle_log_number, + size_t preallocate_block_size, + UnpublishedWAL* new_wal) { + assert(new_wal); + assert(new_wal->log_number == 0); + assert(new_wal->writer == nullptr); IOStatus io_s; std::unique_ptr lfile; - DBOptions db_options = - BuildDBOptions(immutable_db_options_, mutable_db_options_); FileOptions opt_file_options = fs_->OptimizeForLogWrite(file_options_, db_options); opt_file_options.write_hint = CalculateWALWriteHint(); @@ -2424,21 +2446,162 @@ IOStatus DBImpl::CreateWAL(const WriteOptions& write_options, Histograms::HISTOGRAM_ENUM_MAX /* hist_type */, listeners, nullptr, tmp_set.Contains(FileType::kWalFile), tmp_set.Contains(FileType::kWalFile))); - *new_log = new log::Writer(std::move(file_writer), log_file_num, - immutable_db_options_.recycle_log_file_num > 0, - immutable_db_options_.manual_wal_flush, - immutable_db_options_.wal_compression, - immutable_db_options_.track_and_verify_wals); - io_s = (*new_log)->AddCompressionTypeRecord(write_options); - if (io_s.ok()) { - io_s = (*new_log)->MaybeAddPredecessorWALInfo(write_options, - predecessor_wal_info); - } + new_wal->log_number = log_file_num; + new_wal->writer = std::make_unique( + std::move(file_writer), log_file_num, + immutable_db_options_.recycle_log_file_num > 0, + immutable_db_options_.manual_wal_flush, + immutable_db_options_.wal_compression, + immutable_db_options_.track_and_verify_wals); } return io_s; } +IOStatus DBImpl::StartWALFile(const WriteOptions& write_options, + const PredecessorWALInfo& predecessor_wal_info, + log::Writer* new_log) { + assert(new_log); + IOStatus io_s = new_log->AddCompressionTypeRecord(write_options); + TEST_SYNC_POINT_CALLBACK("DBImpl::StartWALFile:AfterCompressionTypeRecord", + &io_s); + if (io_s.ok()) { + io_s = new_log->MaybeAddPredecessorWALInfo(write_options, + predecessor_wal_info); + } + return io_s; +} + +IOStatus DBImpl::CreateWAL(const DBOptions& db_options, + const WriteOptions& write_options, + uint64_t log_file_num, uint64_t recycle_log_number, + size_t preallocate_block_size, + const PredecessorWALInfo& predecessor_wal_info, + log::Writer** new_log) { + assert(new_log); + assert(*new_log == nullptr); + UnpublishedWAL wal; + IOStatus io_s = CreateWALWriter(db_options, log_file_num, recycle_log_number, + preallocate_block_size, &wal); + if (io_s.ok()) { + io_s = StartWALFile(write_options, predecessor_wal_info, wal.writer.get()); + } + if (io_s.ok()) { + *new_log = wal.writer.release(); + } + return io_s; +} + +bool DBImpl::AsyncWALPrecreateEnabled() const { + return immutable_db_options_.async_wal_precreate && + immutable_db_options_.recycle_log_file_num == 0; +} + +struct AsyncWALPrecreateContext { + DBImpl* db = nullptr; + DBOptions db_options; + uint64_t log_number = 0; + size_t preallocate_block_size = 0; +}; + +void DBImpl::MaybeScheduleAsyncWALPrecreate(size_t preallocate_block_size) { + mutex_.AssertHeld(); + + if (!AsyncWALPrecreateEnabled() || + shutting_down_.load(std::memory_order_acquire) || + error_handler_.IsBGWorkStopped() || + async_wal_precreate_state_ != AsyncWALPrecreateState::kNotScheduled) { + return; + } + + async_wal_precreate_wal_.Reset(); + async_wal_precreate_wal_.log_number = versions_->NewFileNumber(); + async_wal_precreate_state_ = AsyncWALPrecreateState::kScheduled; + + auto* ctx = new AsyncWALPrecreateContext(); + ctx->db = this; + ctx->db_options = BuildDBOptions(immutable_db_options_, mutable_db_options_); + ctx->log_number = async_wal_precreate_wal_.log_number; + ctx->preallocate_block_size = preallocate_block_size; + env_->Schedule(&DBImpl::BGWorkAsyncWALPrecreate, ctx, Env::Priority::HIGH, + nullptr); +} + +DBImpl::UnpublishedWAL DBImpl::WaitForAsyncWALPrecreate() { + mutex_.AssertHeld(); + UnpublishedWAL result; + + if (!AsyncWALPrecreateEnabled()) { + return result; + } + + TEST_SYNC_POINT("DBImpl::WaitForAsyncWALPrecreate:Begin"); + bool waited = false; + uint64_t wait_start_micros = 0; + while (async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled) { + TEST_SYNC_POINT("DBImpl::WaitForAsyncWALPrecreate:BeforeWait"); + if (!waited) { + waited = true; + wait_start_micros = immutable_db_options_.clock->NowMicros(); + RecordTick(stats_, WAL_PRECREATE_WAITED); + } + bg_cv_.Wait(); + } + if (waited) { + RecordTick(stats_, WAL_PRECREATE_WAIT_MICROS, + immutable_db_options_.clock->NowMicros() - wait_start_micros); + } + + if (async_wal_precreate_state_ == AsyncWALPrecreateState::kReady) { + result = std::move(async_wal_precreate_wal_); + async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled; + RecordTick(stats_, WAL_PRECREATE_HIT); + return result; + } + + assert(async_wal_precreate_state_ == AsyncWALPrecreateState::kNotScheduled); + RecordTick(stats_, WAL_PRECREATE_MISS); + return result; +} + +void DBImpl::BGWorkAsyncWALPrecreate(void* arg) { + TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Start"); + std::unique_ptr ctx( + static_cast(arg)); + DBImpl* db = ctx->db; + + UnpublishedWAL new_wal; + IOStatus io_s = db->CreateWALWriter(ctx->db_options, ctx->log_number, + 0 /* recycle_log_number */, + ctx->preallocate_block_size, &new_wal); + if (!io_s.ok()) { + RecordTick(db->stats_, WAL_PRECREATE_FAILED); + ROCKS_LOG_WARN(db->immutable_db_options_.info_log, + "Async WAL precreate failed for #%" PRIu64 ": %s", + ctx->log_number, io_s.ToString().c_str()); + } + + TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:BeforePublish"); + TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Publish"); + { + InstrumentedMutexLock l(&db->mutex_); + if (db->async_wal_precreate_state_ == AsyncWALPrecreateState::kScheduled && + db->async_wal_precreate_wal_.log_number == ctx->log_number) { + if (io_s.ok()) { + db->async_wal_precreate_wal_ = std::move(new_wal); + db->async_wal_precreate_state_ = AsyncWALPrecreateState::kReady; + } else { + // Async WAL precreation is best-effort. Clear the slot so foreground + // rotation falls back to normal synchronous WAL creation. + db->async_wal_precreate_wal_.Reset(); + db->async_wal_precreate_state_ = AsyncWALPrecreateState::kNotScheduled; + } + } + db->bg_cv_.SignalAll(); + } + TEST_SYNC_POINT("DBImpl::BGWorkAsyncWALPrecreate:Done"); +} + void DBImpl::TrackExistingDataFiles( const std::vector& existing_data_files) { TrackOrUntrackFiles(existing_data_files, /*track=*/true); @@ -2534,8 +2697,10 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname, // created during DB open with predecessor WALs from previous DB session due // to `avoid_flush_during_recovery == true`. This can protect the last WAL // recovered. - s = impl->CreateWAL(write_options, new_log_number, 0 /*recycle_log_number*/, - preallocate_block_size, + const DBOptions db_options_snapshot = + BuildDBOptions(impl->immutable_db_options_, impl->mutable_db_options_); + s = impl->CreateWAL(db_options_snapshot, write_options, new_log_number, + 0 /*recycle_log_number*/, preallocate_block_size, PredecessorWALInfo() /* predecessor_wal_info */, &new_log); if (s.ok()) { @@ -2740,6 +2905,8 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname, if (impl->immutable_db_options_.open_files_async) { impl->ScheduleAsyncFileOpening(); } + impl->MaybeScheduleAsyncWALPrecreate( + impl->GetWalPreallocateBlockSize(max_write_buffer_size)); impl->mutex_.Unlock(); } diff --git a/db/db_impl/db_impl_write.cc b/db/db_impl/db_impl_write.cc index dd89f1f51b..96a483c51b 100644 --- a/db/db_impl/db_impl_write.cc +++ b/db/db_impl/db_impl_write.cc @@ -16,6 +16,7 @@ #include "db/error_handler.h" #include "db/event_helpers.h" #include "db/wide/wide_columns_helper.h" +#include "file/filename.h" #include "logging/logging.h" #include "memtable/wbwi_memtable.h" #include "monitoring/perf_context_imp.h" @@ -3107,6 +3108,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context, const WriteOptions write_options; log::Writer* new_log = nullptr; + UnpublishedWAL prepared_wal; MemTable* new_mem = nullptr; IOStatus io_s; @@ -3134,8 +3136,16 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context, !wal_recycle_files_.empty() && IsFileDeletionsEnabled()) { recycle_log_number = wal_recycle_files_.front(); } - uint64_t new_log_number = - creating_new_log ? versions_->NewFileNumber() : cur_wal_number_; + uint64_t new_log_number = cur_wal_number_; + if (creating_new_log) { + prepared_wal = WaitForAsyncWALPrecreate(); + if (prepared_wal.writer) { + new_log_number = prepared_wal.log_number; + } else { + assert(prepared_wal.log_number == 0); + new_log_number = versions_->NewFileNumber(); + } + } // For use outside of holding DB mutex const MutableCFOptions mutable_cf_options_copy = cfd->GetLatestMutableCFOptions(); @@ -3156,6 +3166,8 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context, // Log this later after lock release. It may be outdated, e.g., if background // flush happens before logging, but that should be ok. int num_imm_unflushed = cfd->imm()->NumNotFlushed(); + const DBOptions db_options_snapshot = + BuildDBOptions(immutable_db_options_, mutable_db_options_); const auto preallocate_block_size = GetWalPreallocateBlockSize(mutable_cf_options_copy.write_buffer_size); mutex_.Unlock(); @@ -3171,13 +3183,35 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context, wal_write_mutex_.Unlock(); // TODO: Write buffer size passed in should be max of all CF's instead // of mutable_cf_options.write_buffer_size. - io_s = CreateWAL(write_options, new_log_number, recycle_log_number, - preallocate_block_size, info, &new_log); + if (prepared_wal.writer) { + io_s = StartWALFile(write_options, info, prepared_wal.writer.get()); + if (io_s.ok()) { + new_log = prepared_wal.writer.release(); + } + } else { + io_s = + CreateWAL(db_options_snapshot, write_options, new_log_number, + recycle_log_number, preallocate_block_size, info, &new_log); + } TEST_SYNC_POINT_CALLBACK("DBImpl::SwitchMemtable:AfterCreateWAL", &io_s); if (s.ok()) { s = io_s; } } + if (!s.ok() && prepared_wal.writer) { + const uint64_t prepared_wal_number = prepared_wal.log_number; + const std::string prepared_wal_fname = + LogFileName(immutable_db_options_.GetWalDir(), prepared_wal_number); + prepared_wal.Reset(); + Status delete_s = env_->DeleteFile(prepared_wal_fname); + if (!delete_s.ok() && !delete_s.IsNotFound()) { + ROCKS_LOG_WARN(immutable_db_options_.info_log, + "Failed to delete unstarted async precreated WAL #%" PRIu64 + ": %s", + prepared_wal_number, delete_s.ToString().c_str()); + } + delete_s.PermitUncheckedError(); + } if (s.ok()) { // FIXME: from the comment for GetEarliestSequenceNumber(), any key with // seqno >= earliest_seqno should be in this or later memtable. This means @@ -3359,6 +3393,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context, new_mem->Ref(); cfd->SetMemtable(new_mem); InstallSuperVersionAndScheduleWork(cfd, &context->superversion_context); + MaybeScheduleAsyncWALPrecreate(preallocate_block_size); // Notify client that memtable is sealed, now that we have successfully // installed a new memtable diff --git a/db/db_options_test.cc b/db/db_options_test.cc index 07e5d27f23..26b5a51f46 100644 --- a/db/db_options_test.cc +++ b/db/db_options_test.cc @@ -102,6 +102,40 @@ TEST_F(DBOptionsTest, ImmutableTrackAndVerifyWalsInManifest) { ASSERT_FALSE(s.ok()); } +TEST_F(DBOptionsTest, ImmutableAsyncWalPrecreate) { + Options options; + options.env = env_; + options.async_wal_precreate = true; + + ImmutableDBOptions db_options(options); + ASSERT_TRUE(db_options.async_wal_precreate); + + Reopen(options); + ASSERT_TRUE(dbfull()->GetDBOptions().async_wal_precreate); + + Status s = dbfull()->SetDBOptions({{"async_wal_precreate", "false"}}); + ASSERT_FALSE(s.ok()); +} + +TEST_F(DBOptionsTest, SanitizeAsyncWalPrecreateWithWalRecycle) { + // This checks the option-level contract for the incompatible WAL paths: + // SanitizeOptions() preserves WAL recycling and disables only the + // opportunistic async precreation path, so the immutable options reflect + // effective behavior. + Options options; + options.env = env_; + options.async_wal_precreate = true; + options.recycle_log_file_num = 1; + + Options sanitized_options = SanitizeOptions(dbname_, options); + ASSERT_FALSE(sanitized_options.async_wal_precreate); + ASSERT_EQ(1U, sanitized_options.recycle_log_file_num); + + options.recycle_log_file_num = 0; + sanitized_options = SanitizeOptions(dbname_, options); + ASSERT_TRUE(sanitized_options.async_wal_precreate); +} + TEST_F(DBOptionsTest, ImmutableVerifySstUniqueIdInManifest) { Options options; options.env = env_; diff --git a/db/db_wal_test.cc b/db/db_wal_test.cc index d3e9d553b4..c34750d089 100644 --- a/db/db_wal_test.cc +++ b/db/db_wal_test.cc @@ -7,6 +7,8 @@ // 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 + #include "db/db_test_util.h" #include "db/db_with_timestamp_test_util.h" #include "options/options_helper.h" @@ -25,6 +27,36 @@ class DBWALTestBase : public DBTestBase { explicit DBWALTestBase(const std::string& dir_name) : DBTestBase(dir_name, /*env_do_fsync=*/true) {} + std::vector ListWalNumbers() { + std::vector wal_numbers; + std::vector files; + EXPECT_OK(env_->GetChildren(dbname_, &files)); + for (const auto& file : files) { + uint64_t number = 0; + FileType type = kWalFile; + if (ParseFileName(file, &number, &type) && type == kWalFile) { + wal_numbers.push_back(number); + } + } + std::sort(wal_numbers.begin(), wal_numbers.end()); + return wal_numbers; + } + + void CreateEmptyWal(uint64_t wal_number) { + std::unique_ptr file; + ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, wal_number), &file, + EnvOptions())); + ASSERT_OK(file->Close()); + } + + void CreateWalWithContents(uint64_t wal_number, const Slice& contents) { + std::unique_ptr file; + ASSERT_OK(env_->NewWritableFile(LogFileName(dbname_, wal_number), &file, + EnvOptions())); + ASSERT_OK(file->Append(contents)); + ASSERT_OK(file->Close()); + } + #if defined(ROCKSDB_PLATFORM_POSIX) public: #if defined(ROCKSDB_FALLOCATE_PRESENT) @@ -3071,6 +3103,284 @@ TEST_F(DBWALTest, EmptyWalReopenTest) { } } +TEST_F(DBWALTest, AsyncWalPrecreateWaitsForPendingFile) { + Options options = CurrentOptions(); + options.env = env_; + options.statistics = CreateDBStatistics(); + options.async_wal_precreate = true; + options.recycle_log_file_num = 0; + options.write_buffer_size = 1024; + options.max_write_buffer_number = 4; + + // Goal: cover the race where foreground WAL rotation reaches the switch + // before the background precreate publishes its result. The sync points hold + // the async worker after it has created the file but before it marks it + // ready, then verify the foreground writer waits instead of allocating a + // higher WAL number. + SyncPoint::GetInstance()->LoadDependency({ + {"DBImpl::BGWorkAsyncWALPrecreate:BeforePublish", + "DBWALTest::AsyncWalPrecreateWaitsForPendingFile:WorkerBlocked"}, + {"DBWALTest::AsyncWalPrecreateWaitsForPendingFile:AllowPublish", + "DBImpl::BGWorkAsyncWALPrecreate:Publish"}, + {"DBImpl::WaitForAsyncWALPrecreate:BeforeWait", + "DBWALTest::AsyncWalPrecreateWaitsForPendingFile:ForegroundWaiting"}, + }); + SyncPoint::GetInstance()->EnableProcessing(); + Defer cleanup_sync_point([] { + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->ClearAllCallBacks(); + }); + + Reopen(options); + TEST_SYNC_POINT( + "DBWALTest::AsyncWalPrecreateWaitsForPendingFile:WorkerBlocked"); + + ASSERT_OK(Put("fill", "value")); + Status writer_status; + port::Thread writer( + [&]() { writer_status = dbfull()->TEST_SwitchMemtable(); }); + + TEST_SYNC_POINT( + "DBWALTest::AsyncWalPrecreateWaitsForPendingFile:ForegroundWaiting"); + TEST_SYNC_POINT( + "DBWALTest::AsyncWalPrecreateWaitsForPendingFile:AllowPublish"); + writer.join(); + ASSERT_OK(writer_status); + ASSERT_OK(Put("trigger", "value")); + ASSERT_EQ("value", Get("trigger")); + ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT)); + ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_WAITED)); + ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_MISS)); +} + +TEST_F(DBWALTest, AsyncWalPrecreateConsumesReadyFile) { + Options options = CurrentOptions(); + options.env = env_; + options.statistics = CreateDBStatistics(); + options.async_wal_precreate = true; + options.recycle_log_file_num = 0; + options.write_buffer_size = 1024; + options.max_write_buffer_number = 4; + + // Goal: exercise the no-wait consumption path. The background task publishes + // a prepared future WAL before the foreground memtable switch, and the switch + // must install exactly that reserved WAL number instead of allocating + // another. + SyncPoint::GetInstance()->LoadDependency({ + {"DBImpl::BGWorkAsyncWALPrecreate:Done", + "DBWALTest::AsyncWalPrecreateConsumesReadyFile:PrecreateDone"}, + }); + SyncPoint::GetInstance()->EnableProcessing(); + Defer cleanup_sync_point([] { + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->ClearAllCallBacks(); + }); + + Reopen(options); + TEST_SYNC_POINT( + "DBWALTest::AsyncWalPrecreateConsumesReadyFile:PrecreateDone"); + + const std::vector wal_numbers = ListWalNumbers(); + ASSERT_GE(wal_numbers.size(), 2); + const uint64_t prepared_wal_number = wal_numbers.back(); + ASSERT_NE(prepared_wal_number, dbfull()->TEST_GetCurrentLogNumber()); + + ASSERT_OK(Put("fill", "value")); + ASSERT_OK(dbfull()->TEST_SwitchMemtable()); + ASSERT_EQ(prepared_wal_number, dbfull()->TEST_GetCurrentLogNumber()); + ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT)); + ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_MISS)); + ASSERT_EQ(0, options.statistics->getTickerCount(WAL_PRECREATE_WAITED)); +} + +TEST_F(DBWALTest, AsyncWalPrecreateRecoveryToleratesActualFutureWal) { + Options options = CurrentOptions(); + options.env = env_; + options.async_wal_precreate = true; + options.recycle_log_file_num = 0; + options.track_and_verify_wals = true; + options.track_and_verify_wals_in_manifest = true; + options.avoid_flush_during_shutdown = true; + options.avoid_flush_during_recovery = true; + + // Goal: cover the actual async precreation path, not a hand-forged empty WAL. + // The background task opens the future WAL, clean close releases the + // unpublished writer and leaves the empty future WAL behind, and recovery + // must tolerate that file while replaying the preceding WAL with real user + // data. + SyncPoint::GetInstance()->LoadDependency({ + {"DBImpl::BGWorkAsyncWALPrecreate:Done", + "DBWALTest::AsyncWalPrecreateRecoveryToleratesActualFutureWal:" + "PrecreateDone"}, + }); + SyncPoint::GetInstance()->EnableProcessing(); + Defer cleanup_sync_point([] { + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->ClearAllCallBacks(); + }); + + DestroyAndReopen(options); + TEST_SYNC_POINT( + "DBWALTest::AsyncWalPrecreateRecoveryToleratesActualFutureWal:" + "PrecreateDone"); + + const std::vector wal_numbers = ListWalNumbers(); + ASSERT_GE(wal_numbers.size(), 2); + const uint64_t future_wal_number = wal_numbers.back(); + uint64_t future_wal_size = 0; + ASSERT_OK(env_->GetFileSize(LogFileName(dbname_, future_wal_number), + &future_wal_size)); + ASSERT_EQ(0, future_wal_size); + + ASSERT_OK(Put("key", "value")); + Close(); + + ASSERT_OK(TryReopen(options)); + ASSERT_EQ("value", Get("key")); + ASSERT_OK(Put("after-recovery", "value2")); + ASSERT_EQ("value2", Get("after-recovery")); +} + +TEST_F(DBWALTest, AsyncWalPrecreateDeletesFileOnStartFailure) { + Options options = CurrentOptions(); + options.env = env_; + options.statistics = CreateDBStatistics(); + options.async_wal_precreate = true; + options.recycle_log_file_num = 0; + options.write_buffer_size = 1024; + options.max_write_buffer_number = 4; + + // Goal: force failure after consuming a prepared WAL but before it becomes a + // logical WAL. The failed, unpublished file must be deleted and its writer + // destroyed outside mutex_. + SyncPoint::GetInstance()->LoadDependency({ + {"DBImpl::BGWorkAsyncWALPrecreate:Done", + "DBWALTest::AsyncWalPrecreateDeletesFileOnStartFailure:PrecreateDone"}, + }); + SyncPoint::GetInstance()->EnableProcessing(); + Defer cleanup_sync_point([] { + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->ClearAllCallBacks(); + }); + + Reopen(options); + TEST_SYNC_POINT( + "DBWALTest::AsyncWalPrecreateDeletesFileOnStartFailure:PrecreateDone"); + SyncPoint::GetInstance()->SetCallBack( + "DBImpl::StartWALFile:AfterCompressionTypeRecord", [](void* arg) { + *static_cast(arg) = + IOStatus::IOError("injected async WAL start failure"); + }); + const std::vector wal_numbers = ListWalNumbers(); + ASSERT_GE(wal_numbers.size(), 2); + const uint64_t prepared_wal_number = wal_numbers.back(); + + ASSERT_OK(Put("fill", "value")); + ASSERT_NOK(dbfull()->TEST_SwitchMemtable()); + ASSERT_TRUE( + env_->FileExists(LogFileName(dbname_, prepared_wal_number)).IsNotFound()); + ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_HIT)); +} + +TEST_F(DBWALTest, AsyncWalPrecreateFailureFallsBackToSyncCreation) { + Options options = CurrentOptions(); + options.env = env_; + options.statistics = CreateDBStatistics(); + options.async_wal_precreate = true; + options.recycle_log_file_num = 0; + options.write_buffer_size = 1024; + options.max_write_buffer_number = 4; + + // Goal: fail the background precreate file open after normal DB open has + // created the current WAL. The foreground switch should observe no prepared + // WAL, create one synchronously, and expose both failure and miss counters. + SyncPoint::GetInstance()->LoadDependency({ + {"DBImpl::BGWorkAsyncWALPrecreate:Done", + "DBWALTest::AsyncWalPrecreateFailureFallsBackToSyncCreation:" + "PrecreateDone"}, + }); + SyncPoint::GetInstance()->SetCallBack( + "DBImpl::Open:AfterDeleteFiles", + [&](void*) { env_->non_writable_count_ = 1; }); + SyncPoint::GetInstance()->EnableProcessing(); + Defer cleanup_sync_point([&] { + env_->non_writable_count_ = 0; + SyncPoint::GetInstance()->DisableProcessing(); + SyncPoint::GetInstance()->ClearAllCallBacks(); + }); + + Reopen(options); + TEST_SYNC_POINT( + "DBWALTest::AsyncWalPrecreateFailureFallsBackToSyncCreation:" + "PrecreateDone"); + ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_FAILED)); + + ASSERT_OK(Put("fill", "value")); + ASSERT_OK(dbfull()->TEST_SwitchMemtable()); + ASSERT_EQ(1, options.statistics->getTickerCount(WAL_PRECREATE_MISS)); +} + +TEST_F(DBWALTest, ReadOnlyErrorIfWalFileExistsIgnoresEmptyWals) { + Options options = CurrentOptions(); + options.env = env_; + DestroyAndReopen(options); + Close(); + + // Goal: preserve error_if_wal_file_exists for WALs containing data while + // allowing empty future WALs left by async precreation. This creates empty + // WAL files directly so the test isolates the read-only open contract. + const std::vector existing_wal_numbers = ListWalNumbers(); + const uint64_t empty_wal_number = + existing_wal_numbers.empty() ? 1 : existing_wal_numbers.back() + 1; + CreateEmptyWal(empty_wal_number); + + DBOptions db_options(options); + std::vector column_families; + column_families.emplace_back(kDefaultColumnFamilyName, + ColumnFamilyOptions(options)); + std::vector handles; + std::unique_ptr read_only_db; + ASSERT_OK(DB::OpenForReadOnly(db_options, dbname_, column_families, &handles, + &read_only_db, + true /* error_if_wal_file_exists */)); + for (ColumnFamilyHandle* handle : handles) { + ASSERT_OK(read_only_db->DestroyColumnFamilyHandle(handle)); + } + read_only_db.reset(); + handles.clear(); + + CreateWalWithContents(empty_wal_number + 1, Slice("not empty")); + ASSERT_NOK(DB::OpenForReadOnly(db_options, dbname_, column_families, &handles, + &read_only_db, + true /* error_if_wal_file_exists */)); +} + +TEST_F(DBWALTest, RecoveryToleratesEmptyFutureWal) { + Options options = CurrentOptions(); + options.env = env_; + options.track_and_verify_wals = true; + options.track_and_verify_wals_in_manifest = true; + options.avoid_flush_during_recovery = true; + DestroyAndReopen(options); + + ASSERT_OK(Put("key", "value")); + Close(); + + const std::vector wal_numbers = ListWalNumbers(); + ASSERT_FALSE(wal_numbers.empty()); + const uint64_t future_wal_number = wal_numbers.back() + 1; + CreateEmptyWal(future_wal_number); + + // Goal: model a crash after async WAL precreation but before consumption. + // Recovery should process the zero-byte future WAL, mark its file number + // used, and keep replaying the real preceding WAL without reporting a WAL + // chain or MANIFEST-tracking error. + ASSERT_OK(TryReopen(options)); + ASSERT_EQ("value", Get("key")); + ASSERT_OK(Put("after-recovery", "value2")); + ASSERT_EQ("value2", Get("after-recovery")); +} + TEST_F(DBWALTest, RecoveryFlushSwitchWALOnEmptyMemtable) { Options options = CurrentOptions(); auto fault_fs = std::make_shared(FileSystem::Default()); diff --git a/db_stress_tool/db_stress_common.h b/db_stress_tool/db_stress_common.h index 7279f2dcab..e1e4f4faec 100644 --- a/db_stress_tool/db_stress_common.h +++ b/db_stress_tool/db_stress_common.h @@ -114,6 +114,7 @@ DECLARE_double(memtable_prefix_bloom_size_ratio); DECLARE_bool(memtable_whole_key_filtering); DECLARE_int32(open_files); DECLARE_bool(open_files_async); +DECLARE_bool(async_wal_precreate); DECLARE_uint64(compressed_secondary_cache_size); DECLARE_int32(compressed_secondary_cache_numshardbits); DECLARE_int32(secondary_cache_update_interval); diff --git a/db_stress_tool/db_stress_gflags.cc b/db_stress_tool/db_stress_gflags.cc index 86e08010df..e881d01175 100644 --- a/db_stress_tool/db_stress_gflags.cc +++ b/db_stress_tool/db_stress_gflags.cc @@ -206,6 +206,10 @@ DEFINE_int32(open_files, ROCKSDB_NAMESPACE::Options().max_open_files, DEFINE_bool(open_files_async, ROCKSDB_NAMESPACE::Options().open_files_async, "Options.open_files_async"); +DEFINE_bool(async_wal_precreate, + ROCKSDB_NAMESPACE::Options().async_wal_precreate, + "Options.async_wal_precreate"); + DEFINE_uint64(compressed_secondary_cache_size, 0, "Number of bytes to use as a cache of compressed data." " 0 means use default settings."); diff --git a/db_stress_tool/db_stress_test_base.cc b/db_stress_tool/db_stress_test_base.cc index 9af954826f..6bb315b47e 100644 --- a/db_stress_tool/db_stress_test_base.cc +++ b/db_stress_tool/db_stress_test_base.cc @@ -4602,6 +4602,7 @@ void InitializeOptionsFromFlags( } options.max_open_files = FLAGS_open_files; options.open_files_async = FLAGS_open_files_async; + options.async_wal_precreate = FLAGS_async_wal_precreate; if (FLAGS_open_files_async && !FLAGS_skip_stats_update_on_db_open) { FLAGS_skip_stats_update_on_db_open = true; fprintf(stderr, diff --git a/docs/components/write_flow/03_wal.md b/docs/components/write_flow/03_wal.md index f07484e80f..18ff1a5be8 100644 --- a/docs/components/write_flow/03_wal.md +++ b/docs/components/write_flow/03_wal.md @@ -94,11 +94,23 @@ Step 3 - **Synced**: `fsync()` is called when `WriteOptions::sync` is true, or w Step 4 - **Obsolete**: After all memtables referencing this WAL are flushed, the WAL becomes obsolete and is either recycled or deleted. +## Async WAL Precreation + +When `DBOptions::async_wal_precreate` is enabled, RocksDB keeps at most one future WAL file precreated in the background. RocksDB reserves a file number before scheduling the background task; the task opens the file/writer, but deliberately leaves the file as reserved empty storage: + +- It does not write compression metadata. +- It does not write predecessor WAL info. +- It does not add the file to `logs_`, `alive_wal_files_`, or MANIFEST WAL tracking. + +The file becomes a logical WAL only when foreground `SwitchMemtable()` consumes it. At that point RocksDB writes the normal WAL metadata, flushes the previous WAL writer buffer, and installs the new WAL in the same order as synchronous WAL creation. If foreground rotation reaches the switch while the background task is still running, the writer waits for the reserved file number instead of allocating a higher numbered WAL. This avoids a late lower-numbered WAL appearing after a newer WAL has become live. If background precreation fails, the background task logs the failure and foreground rotation falls back to normal synchronous WAL creation. + +Unused precreated WALs are safe across crashes and clean shutdowns because they are zero-record future WAL files. Recovery processes WAL files by log number, marks every observed WAL number as used, and treats an empty future WAL as EOF. On clean close, RocksDB releases the unpublished WAL writer but does not need to delete the empty file. The option is sanitized to false when `recycle_log_file_num > 0`. + ## WAL Tracking in MANIFEST `WalSet` (see `db/wal_edit.h`, tracked in `VersionSet`) manages WAL metadata in the MANIFEST: -- `WalAddition` records are written when a WAL is created or when a closed/inactive WAL's synced size is finalized +- `WalAddition` records are written when a closed/inactive WAL's synced size is finalized - `WalDeletion` records are written when a WAL becomes obsolete after flush - Live-WAL syncs (via `DB::SyncWAL()` or `WriteOptions::sync`) are intentionally not tracked in MANIFEST diff --git a/docs/components/write_flow/04_memtable_insert.md b/docs/components/write_flow/04_memtable_insert.md index fd93da44ba..f01f28bb59 100644 --- a/docs/components/write_flow/04_memtable_insert.md +++ b/docs/components/write_flow/04_memtable_insert.md @@ -103,7 +103,7 @@ When a flush is triggered, `DBImpl::SwitchMemtable()` (see `db/db_impl/db_impl_w Step 1 - Writes any recoverable state to the current memtable. -Step 2 - Creates a new WAL file (unless the current WAL is empty) with the next file number from `VersionSet::NewFileNumber()`. +Step 2 - Creates a new WAL file (unless the current WAL is empty) with the next file number from `VersionSet::NewFileNumber()`. If `DBOptions::async_wal_precreate` has prepared a future WAL, this step consumes that reserved file number and finalizes the WAL metadata instead of creating the file synchronously. Step 3 - Constructs a new `MemTable` with `earliest_seq` set to the current last sequence. diff --git a/include/rocksdb/c.h b/include/rocksdb/c.h index 39e53f2056..ee15b1b6e4 100644 --- a/include/rocksdb/c.h +++ b/include/rocksdb/c.h @@ -205,6 +205,8 @@ extern ROCKSDB_LIBRARY_API rocksdb_t* rocksdb_open( extern ROCKSDB_LIBRARY_API rocksdb_t* rocksdb_open_with_ttl( const rocksdb_options_t* options, const char* name, int ttl, char** errptr); +/* If error_if_wal_file_exists is non-zero, returns an error when a non-empty + * WAL file exists. Empty WAL files are tolerated. */ extern ROCKSDB_LIBRARY_API rocksdb_t* rocksdb_open_for_read_only( const rocksdb_options_t* options, const char* name, unsigned char error_if_wal_file_exists, char** errptr); @@ -450,6 +452,8 @@ extern ROCKSDB_LIBRARY_API rocksdb_t* rocksdb_open_column_families_with_ttl( rocksdb_column_family_handle_t** column_family_handles, const int* ttls, char** errptr); +/* If error_if_wal_file_exists is non-zero, returns an error when a non-empty + * WAL file exists. Empty WAL files are tolerated. */ extern ROCKSDB_LIBRARY_API rocksdb_t* rocksdb_open_for_read_only_column_families( const rocksdb_options_t* options, const char* name, int num_column_families, @@ -1818,6 +1822,11 @@ extern ROCKSDB_LIBRARY_API void rocksdb_options_set_recycle_log_file_num( rocksdb_options_t*, size_t); extern ROCKSDB_LIBRARY_API size_t rocksdb_options_get_recycle_log_file_num(rocksdb_options_t*); +// Set/get DBOptions::async_wal_precreate. +extern ROCKSDB_LIBRARY_API void rocksdb_options_set_async_wal_precreate( + rocksdb_options_t*, unsigned char); +extern ROCKSDB_LIBRARY_API unsigned char +rocksdb_options_get_async_wal_precreate(rocksdb_options_t*); extern ROCKSDB_LIBRARY_API void rocksdb_options_set_soft_pending_compaction_bytes_limit(rocksdb_options_t* opt, size_t v); diff --git a/include/rocksdb/db.h b/include/rocksdb/db.h index 45d98b1b02..78614b12c2 100644 --- a/include/rocksdb/db.h +++ b/include/rocksdb/db.h @@ -172,6 +172,10 @@ class DB { // Open the database for read only. // + // If error_if_wal_file_exists is true, returns an error when a non-empty + // WAL file exists. Empty WAL files can be left behind by features such as + // async WAL precreation and are tolerated. + // static Status OpenForReadOnly(const Options& options, const std::string& name, std::unique_ptr* dbptr, bool error_if_wal_file_exists = false); @@ -183,6 +187,10 @@ class DB { // to specify default column family. The default column family name is // 'default' and it's stored in ROCKSDB_NAMESPACE::kDefaultColumnFamilyName // + // If error_if_wal_file_exists is true, returns an error when a non-empty + // WAL file exists. Empty WAL files can be left behind by features such as + // async WAL precreation and are tolerated. + // static Status OpenForReadOnly( const DBOptions& db_options, const std::string& name, const std::vector& column_families, diff --git a/include/rocksdb/options.h b/include/rocksdb/options.h index 706a2e390d..48233a3c1b 100644 --- a/include/rocksdb/options.h +++ b/include/rocksdb/options.h @@ -975,6 +975,17 @@ struct DBOptions { // Default: 0 size_t recycle_log_file_num = 0; + // EXPERIMENTAL: If true, RocksDB asynchronously precreates the next WAL file + // so foreground memtable switching can usually avoid the filesystem latency + // of creating a new WAL. The precreated file is only reserved empty storage; + // it does not become a logical WAL and is not added to WAL tracking until it + // is consumed by a foreground WAL rotation. + // + // The option is sanitized to false when recycle_log_file_num is non-zero. + // + // Default: false + bool async_wal_precreate = false; + // The manifest file is rolled over on reaching this limit AND the // space amp limit described in max_manifest_space_amp_pct. More trade-off // details there. diff --git a/include/rocksdb/statistics.h b/include/rocksdb/statistics.h index c79a682d59..58d81b8edb 100644 --- a/include/rocksdb/statistics.h +++ b/include/rocksdb/statistics.h @@ -223,6 +223,16 @@ enum Tickers : uint32_t { GET_UPDATES_SINCE_CALLS, WAL_FILE_SYNCED, // Number of times WAL sync is done WAL_FILE_BYTES, // Number of bytes written to WAL + // Number of WAL rotations that consumed an async precreated WAL. + WAL_PRECREATE_HIT, + // Number of WAL rotations that found no async precreated WAL to consume. + WAL_PRECREATE_MISS, + // Number of WAL rotations that waited for an in-flight WAL precreation. + WAL_PRECREATE_WAITED, + // Total foreground wait time for in-flight WAL precreation. + WAL_PRECREATE_WAIT_MICROS, + // Number of async WAL precreation attempts that failed. + WAL_PRECREATE_FAILED, // Writes can be processed by requesting thread or by the thread at the // head of the writers queue. diff --git a/java/rocksjni/portal.h b/java/rocksjni/portal.h index 2e28f17c29..e65f9a0237 100644 --- a/java/rocksjni/portal.h +++ b/java/rocksjni/portal.h @@ -5011,6 +5011,16 @@ class TickerTypeJni { return 0x53; case ROCKSDB_NAMESPACE::Tickers::WAL_FILE_BYTES: return 0x54; + case ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_HIT: + return -0x6C; + case ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_MISS: + return -0x6D; + case ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_WAITED: + return -0x6E; + case ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_WAIT_MICROS: + return -0x6F; + case ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_FAILED: + return -0x70; case ROCKSDB_NAMESPACE::Tickers::WRITE_DONE_BY_SELF: return 0x55; case ROCKSDB_NAMESPACE::Tickers::WRITE_DONE_BY_OTHER: @@ -5509,6 +5519,16 @@ class TickerTypeJni { return ROCKSDB_NAMESPACE::Tickers::WAL_FILE_SYNCED; case 0x54: return ROCKSDB_NAMESPACE::Tickers::WAL_FILE_BYTES; + case -0x6C: + return ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_HIT; + case -0x6D: + return ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_MISS; + case -0x6E: + return ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_WAITED; + case -0x6F: + return ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_WAIT_MICROS; + case -0x70: + return ROCKSDB_NAMESPACE::Tickers::WAL_PRECREATE_FAILED; case 0x55: return ROCKSDB_NAMESPACE::Tickers::WRITE_DONE_BY_SELF; case 0x56: diff --git a/java/src/main/java/org/rocksdb/RocksDB.java b/java/src/main/java/org/rocksdb/RocksDB.java index ebe1347269..4097a2f43b 100644 --- a/java/src/main/java/org/rocksdb/RocksDB.java +++ b/java/src/main/java/org/rocksdb/RocksDB.java @@ -404,7 +404,7 @@ public class RocksDB extends RocksObject { * @param options {@link Options} instance. * @param path the path to the RocksDB. * @param errorIfWalFileExists true to raise an error when opening the db - * if a Write Ahead Log file exists, false otherwise. + * if a non-empty Write Ahead Log file exists, false otherwise. * @return a {@link RocksDB} instance on success, null if the specified * {@link RocksDB} can not be opened. * @@ -493,7 +493,7 @@ public class RocksDB extends RocksObject { * @param columnFamilyHandles will be filled with ColumnFamilyHandle instances * on open. * @param errorIfWalFileExists true to raise an error when opening the db - * if a Write Ahead Log file exists, false otherwise. + * if a non-empty Write Ahead Log file exists, false otherwise. * @return a {@link RocksDB} instance on success, null if the specified * {@link RocksDB} can not be opened. * diff --git a/java/src/main/java/org/rocksdb/TickerType.java b/java/src/main/java/org/rocksdb/TickerType.java index 24d76b8c51..ff71b11d13 100644 --- a/java/src/main/java/org/rocksdb/TickerType.java +++ b/java/src/main/java/org/rocksdb/TickerType.java @@ -394,6 +394,31 @@ public enum TickerType { */ WAL_FILE_BYTES((byte) 0x54), + /** + * Number of WAL rotations that consumed an async precreated WAL. + */ + WAL_PRECREATE_HIT((byte) -0x6C), + + /** + * Number of WAL rotations that found no async precreated WAL to consume. + */ + WAL_PRECREATE_MISS((byte) -0x6D), + + /** + * Number of WAL rotations that waited for an in-flight WAL precreation. + */ + WAL_PRECREATE_WAITED((byte) -0x6E), + + /** + * Total foreground wait time for in-flight WAL precreation. + */ + WAL_PRECREATE_WAIT_MICROS((byte) -0x6F), + + /** + * Number of async WAL precreation attempts that failed. + */ + WAL_PRECREATE_FAILED((byte) -0x70), + /** * Writes can be processed by requesting thread or by the thread at the * head of the writers queue. diff --git a/java/src/test/java/org/rocksdb/ReadOnlyTest.java b/java/src/test/java/org/rocksdb/ReadOnlyTest.java index 99549b61ba..fccf8a8d58 100644 --- a/java/src/test/java/org/rocksdb/ReadOnlyTest.java +++ b/java/src/test/java/org/rocksdb/ReadOnlyTest.java @@ -6,6 +6,8 @@ package org.rocksdb; import static org.assertj.core.api.Assertions.assertThat; +import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -200,11 +202,12 @@ public class ReadOnlyTest { } @Test(expected = RocksDBException.class) - public void errorIfWalFileExists() throws RocksDBException { + public void errorIfWalFileExists() throws RocksDBException, IOException { try (final Options options = new Options().setCreateIfMissing(true); final RocksDB ignored = RocksDB.open(options, dbFolder.getRoot().getAbsolutePath())) { // no-op } + Files.write(dbFolder.getRoot().toPath().resolve("999999.log"), new byte[] {1}); try (final ColumnFamilyOptions cfOpts = new ColumnFamilyOptions()) { final List cfDescriptors = Collections.singletonList( diff --git a/monitoring/statistics.cc b/monitoring/statistics.cc index 2ef31accbb..8b977b5061 100644 --- a/monitoring/statistics.cc +++ b/monitoring/statistics.cc @@ -122,6 +122,11 @@ const std::vector> TickersNameMap = { {GET_UPDATES_SINCE_CALLS, "rocksdb.getupdatessince.calls"}, {WAL_FILE_SYNCED, "rocksdb.wal.synced"}, {WAL_FILE_BYTES, "rocksdb.wal.bytes"}, + {WAL_PRECREATE_HIT, "rocksdb.wal.precreate.hit"}, + {WAL_PRECREATE_MISS, "rocksdb.wal.precreate.miss"}, + {WAL_PRECREATE_WAITED, "rocksdb.wal.precreate.waited"}, + {WAL_PRECREATE_WAIT_MICROS, "rocksdb.wal.precreate.wait.micros"}, + {WAL_PRECREATE_FAILED, "rocksdb.wal.precreate.failed"}, {WRITE_DONE_BY_SELF, "rocksdb.write.self"}, {WRITE_DONE_BY_OTHER, "rocksdb.write.other"}, {WRITE_WITH_WAL, "rocksdb.write.wal"}, diff --git a/options/db_options.cc b/options/db_options.cc index ee3d0a48fb..403b1584a7 100644 --- a/options/db_options.cc +++ b/options/db_options.cc @@ -315,6 +315,10 @@ static std::unordered_map {offsetof(struct ImmutableDBOptions, recycle_log_file_num), OptionType::kSizeT, OptionVerificationType::kNormal, OptionTypeFlags::kNone}}, + {"async_wal_precreate", + {offsetof(struct ImmutableDBOptions, async_wal_precreate), + OptionType::kBoolean, OptionVerificationType::kNormal, + OptionTypeFlags::kNone}}, {"log_file_time_to_roll", {offsetof(struct ImmutableDBOptions, log_file_time_to_roll), OptionType::kSizeT, OptionVerificationType::kNormal, @@ -770,6 +774,7 @@ ImmutableDBOptions::ImmutableDBOptions(const DBOptions& options) log_file_time_to_roll(options.log_file_time_to_roll), keep_log_file_num(options.keep_log_file_num), recycle_log_file_num(options.recycle_log_file_num), + async_wal_precreate(options.async_wal_precreate), table_cache_numshardbits(options.table_cache_numshardbits), WAL_ttl_seconds(options.WAL_ttl_seconds), WAL_size_limit_MB(options.WAL_size_limit_MB), @@ -893,6 +898,8 @@ void ImmutableDBOptions::Dump(Logger* log) const { ROCKS_LOG_HEADER( log, " Options.recycle_log_file_num: %" ROCKSDB_PRIszt, recycle_log_file_num); + ROCKS_LOG_HEADER(log, " Options.async_wal_precreate: %d", + async_wal_precreate); ROCKS_LOG_HEADER(log, " Options.allow_fallocate: %d", allow_fallocate); ROCKS_LOG_HEADER(log, " Options.allow_mmap_reads: %d", diff --git a/options/db_options.h b/options/db_options.h index 76a0403c15..e96bb83c18 100644 --- a/options/db_options.h +++ b/options/db_options.h @@ -48,6 +48,8 @@ struct ImmutableDBOptions { size_t log_file_time_to_roll; size_t keep_log_file_num; size_t recycle_log_file_num; + // Immutable copy of DBOptions::async_wal_precreate. + bool async_wal_precreate; int table_cache_numshardbits; uint64_t WAL_ttl_seconds; uint64_t WAL_size_limit_MB; diff --git a/options/options_helper.cc b/options/options_helper.cc index 0eee99ebeb..840902d3e4 100644 --- a/options/options_helper.cc +++ b/options/options_helper.cc @@ -101,6 +101,7 @@ void BuildDBOptions(const ImmutableDBOptions& immutable_db_options, options.log_file_time_to_roll = immutable_db_options.log_file_time_to_roll; options.keep_log_file_num = immutable_db_options.keep_log_file_num; options.recycle_log_file_num = immutable_db_options.recycle_log_file_num; + options.async_wal_precreate = immutable_db_options.async_wal_precreate; options.max_manifest_file_size = mutable_db_options.max_manifest_file_size; options.max_manifest_space_amp_pct = mutable_db_options.max_manifest_space_amp_pct; diff --git a/options/options_settable_test.cc b/options/options_settable_test.cc index d78dce3769..24a0498d07 100644 --- a/options/options_settable_test.cc +++ b/options/options_settable_test.cc @@ -431,6 +431,7 @@ TEST_F(OptionsSettableTest, DBOptionsAllFieldsSettable) { "strict_bytes_per_sync=true;" "enable_thread_tracking=false;" "recycle_log_file_num=0;" + "async_wal_precreate=true;" "create_missing_column_families=true;" "log_file_time_to_roll=3097;" "max_background_flushes=35;" diff --git a/options/options_test.cc b/options/options_test.cc index 0c8c701c67..065defe790 100644 --- a/options/options_test.cc +++ b/options/options_test.cc @@ -148,6 +148,7 @@ TEST_F(OptionsTest, GetOptionsFromMapTest) { {"track_and_verify_wals_in_manifest", "true"}, {"track_and_verify_wals", "true"}, {"verify_sst_unique_id_in_manifest", "true"}, + {"async_wal_precreate", "true"}, {"fast_sst_open", "true"}, {"max_open_files", "32"}, {"max_total_wal_size", "33"}, @@ -332,6 +333,7 @@ TEST_F(OptionsTest, GetOptionsFromMapTest) { ASSERT_EQ(new_db_opt.track_and_verify_wals_in_manifest, true); ASSERT_EQ(new_db_opt.track_and_verify_wals, true); ASSERT_EQ(new_db_opt.verify_sst_unique_id_in_manifest, true); + ASSERT_EQ(new_db_opt.async_wal_precreate, true); ASSERT_EQ(new_db_opt.fast_sst_open, true); ASSERT_EQ(new_db_opt.max_open_files, 32); ASSERT_EQ(new_db_opt.max_total_wal_size, static_cast(33)); @@ -906,6 +908,7 @@ TEST_F(OptionsTest, OldInterfaceTest) { {"track_and_verify_wals_in_manifest", "true"}, {"track_and_verify_wals", "true"}, {"verify_sst_unique_id_in_manifest", "true"}, + {"async_wal_precreate", "true"}, {"fast_sst_open", "true"}, {"max_open_files", "32"}, {"daily_offpeak_time_utc", "06:30-23:30"}, @@ -923,6 +926,7 @@ TEST_F(OptionsTest, OldInterfaceTest) { ASSERT_EQ(new_db_opt.track_and_verify_wals_in_manifest, true); ASSERT_EQ(new_db_opt.track_and_verify_wals, true); ASSERT_EQ(new_db_opt.verify_sst_unique_id_in_manifest, true); + ASSERT_EQ(new_db_opt.async_wal_precreate, true); ASSERT_EQ(new_db_opt.fast_sst_open, true); ASSERT_EQ(new_db_opt.max_open_files, 32); db_options_map["unknown_option"] = "1"; @@ -2489,6 +2493,7 @@ TEST_F(OptionsOldApiTest, GetOptionsFromMapTest) { {"track_and_verify_wals_in_manifest", "true"}, {"track_and_verify_wals", "true"}, {"verify_sst_unique_id_in_manifest", "true"}, + {"async_wal_precreate", "true"}, {"fast_sst_open", "true"}, {"max_open_files", "32"}, {"max_total_wal_size", "33"}, @@ -2678,6 +2683,7 @@ TEST_F(OptionsOldApiTest, GetOptionsFromMapTest) { ASSERT_EQ(new_db_opt.paranoid_checks, true); ASSERT_EQ(new_db_opt.track_and_verify_wals_in_manifest, true); ASSERT_EQ(new_db_opt.track_and_verify_wals, true); + ASSERT_EQ(new_db_opt.async_wal_precreate, true); ASSERT_EQ(new_db_opt.max_open_files, 32); ASSERT_EQ(new_db_opt.max_total_wal_size, static_cast(33)); ASSERT_EQ(new_db_opt.use_fsync, true); diff --git a/test_util/testutil.cc b/test_util/testutil.cc index f9f9e0bf68..cd6ce58afa 100644 --- a/test_util/testutil.cc +++ b/test_util/testutil.cc @@ -315,6 +315,7 @@ void RandomInitDBOptions(DBOptions* db_opt, Random* rnd) { db_opt->use_adaptive_mutex = rnd->Uniform(2); db_opt->use_fsync = rnd->Uniform(2); db_opt->recycle_log_file_num = rnd->Uniform(2); + db_opt->async_wal_precreate = rnd->Uniform(2); db_opt->avoid_flush_during_recovery = rnd->Uniform(2); db_opt->avoid_flush_during_shutdown = rnd->Uniform(2); db_opt->enforce_single_del_contracts = rnd->Uniform(2); diff --git a/tools/db_bench_tool.cc b/tools/db_bench_tool.cc index b60f51bd84..f6047a0704 100644 --- a/tools/db_bench_tool.cc +++ b/tools/db_bench_tool.cc @@ -1916,6 +1916,8 @@ DEFINE_bool(track_and_verify_wals_in_manifest, false, DEFINE_bool(track_and_verify_wals, false, "See Options.track_and_verify_wals"); +DEFINE_bool(async_wal_precreate, false, "See Options.async_wal_precreate"); + DEFINE_int32(same_value_percentage, 0, "Percentage of time value will be same i.e good for compression " "of the block"); @@ -5063,6 +5065,7 @@ class Benchmark { options.track_and_verify_wals_in_manifest = FLAGS_track_and_verify_wals_in_manifest; options.track_and_verify_wals = FLAGS_track_and_verify_wals; + options.async_wal_precreate = FLAGS_async_wal_precreate; // Integrated BlobDB options.enable_blob_files = FLAGS_enable_blob_files; diff --git a/tools/db_crashtest.py b/tools/db_crashtest.py index 6f8214d2fb..36d8f797a3 100644 --- a/tools/db_crashtest.py +++ b/tools/db_crashtest.py @@ -228,6 +228,7 @@ default_params = { "nooverwritepercent": 1, "open_files": lambda: random.choice([-1, -1, 100, 500000]), "open_files_async": lambda: random.choice([0, 1]), + "async_wal_precreate": lambda: random.choice([0, 1]), "optimize_filters_for_memory": lambda: random.randint(0, 1), "partition_filters": lambda: random.randint(0, 1), "partition_pinning": lambda: random.randint(0, 3), diff --git a/unreleased_history/behavior_changes/error_if_wal_file_exists_empty_wals.md b/unreleased_history/behavior_changes/error_if_wal_file_exists_empty_wals.md new file mode 100644 index 0000000000..2bb4f7afd3 --- /dev/null +++ b/unreleased_history/behavior_changes/error_if_wal_file_exists_empty_wals.md @@ -0,0 +1 @@ +Read-only open with `error_if_wal_file_exists=true` now tolerates empty WAL files so empty precreated WALs do not prevent inspection. diff --git a/unreleased_history/new_features/async_wal_precreate.md b/unreleased_history/new_features/async_wal_precreate.md new file mode 100644 index 0000000000..533b74efda --- /dev/null +++ b/unreleased_history/new_features/async_wal_precreate.md @@ -0,0 +1 @@ +Add experimental DB option `async_wal_precreate` to precreate the next WAL file in a background thread and reduce foreground WAL rotation latency. The option is sanitized to false when WAL recycling is enabled.