mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 02:52:35 -05:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa616f0e79 | ||
|
|
96db15c6e6 | ||
|
|
912596031a | ||
|
|
f4fe40845a | ||
|
|
40a5f27f6e | ||
|
|
9676ce2f84 | ||
|
|
e506aa7154 | ||
|
|
6d4c2c3297 | ||
|
|
2ceb3ef4d2 | ||
|
|
edc76202a2 | ||
|
|
b44c70adcf | ||
|
|
71eb5a1646 | ||
|
|
63a256d768 | ||
|
|
87d4b0d579 | ||
|
|
09bc20b16e | ||
|
|
dd9a806edd |
No files matched your search
@@ -219,6 +219,10 @@ if(MSVC)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi /nologo /EHsc /GS /Gd /GR /GF /fp:precise /Zc:wchar_t /Zc:forScope /errorReport:queue")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FC /d2Zi+ /W4 /wd4127 /wd4996 /wd4100 /wd4324")
|
||||
endif()
|
||||
if(CMAKE_BUILD_TYPE STREQUAL "Release")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DNDEBUG")
|
||||
message(STATUS "Setting /DNDEBUG as CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
|
||||
endif()
|
||||
else()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W -Wextra -Wall -pthread")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-compare -Wshadow -Wno-unused-parameter -Wno-unused-variable -Woverloaded-virtual -Wnon-virtual-dtor -Wno-missing-field-initializers -Wno-strict-aliasing -Wno-invalid-offsetof")
|
||||
|
||||
+21
@@ -1,6 +1,27 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 10.11.2 (02/25/2026)
|
||||
### Bug Fixes
|
||||
* Fix longstanding failures that can arise from reading and/or compacting old DB dirs with range deletions (likely from version < 5.19.0) in many newer versions.
|
||||
|
||||
## 10.11.1 (02/06/2026)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in PosixFS AbortIO that could cause it to ignore an IO completion and a subsequent Poll to hang
|
||||
|
||||
## 10.11.0 (01/23/2026)
|
||||
### Public API Changes
|
||||
* New SetOptions API that allows setting options for multiple CFs, avoiding the need to reserialize OPTIONS file for each CF
|
||||
* Remove remaining pieces of Lua integration
|
||||
|
||||
### Behavior Changes
|
||||
* The new default for `BlockBasedTableOptions::format_version` is 7, which has been supported since RocksDB 10.4.0 and is required in order to use CompressionManagers supporting custom compression types.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a small performance bug with `format_version=7` when decompressing formats other than Snappy and ZSTD.
|
||||
* Fixed an infinite compaction loop bug with User-Defined Timestamps (UDT) where bottommost files were repeatedly marked for compaction even though their timestamp could not be collapsed.
|
||||
* Bugfix for persisted UDT record sequence number zeroing logic.
|
||||
|
||||
## 10.10.0 (12/16/2025)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in best-efforts recovery that causes use-after-free crashes when accessing SST files that were cached during the recovery.
|
||||
|
||||
@@ -34,6 +34,7 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
|
||||
whitebox_crash_test_with_txn whitebox_crash_test_with_ts \
|
||||
whitebox_crash_test_with_optimistic_txn \
|
||||
whitebox_crash_test_with_tiered_storage \
|
||||
crash_test_db_cleanup \
|
||||
|
||||
crash_test: $(DB_STRESS_CMD)
|
||||
# Do not parallelize
|
||||
@@ -161,6 +162,9 @@ whitebox_crash_test_with_optimistic_txn: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --optimistic_txn whitebox --random_kill_odd \
|
||||
$(CRASH_TEST_KILL_ODD) $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
crash_test_db_cleanup: $(DB_STRESS_CMD)
|
||||
$(DB_STRESS_CMD) --delete_dir_and_exit=$(TEST_TMPDIR)
|
||||
|
||||
# Old names DEPRECATED
|
||||
crash_test_with_txn: crash_test_with_wc_txn
|
||||
whitebox_crash_test_with_txn: whitebox_crash_test_with_wc_txn
|
||||
|
||||
@@ -2438,6 +2438,32 @@ bool CompactionJob::UpdateInternalStatsFromInputFiles(
|
||||
bool has_error = false;
|
||||
const ReadOptions read_options(Env::IOActivity::kCompaction);
|
||||
const auto& input_table_properties = compaction->GetInputTableProperties();
|
||||
|
||||
// Check all input files for old block-based SST format_version. Why? Old
|
||||
// block-based SST files from roughly version 5.0 to 5.18 could produce
|
||||
// inaccurate num_entries counts due to the evolution of its handling along
|
||||
// with num_range_deletions. We have to disable some paranoid checks when
|
||||
// compacting files from such an old release. However, we don't have great
|
||||
// information to identify those files, so we heuristically over-approximate
|
||||
// that set of files using
|
||||
// (a) format_version < 5, which will be true for any files from RocksDB <
|
||||
// 6.6.0 and should not be true for any recent production files
|
||||
// (b) to avoid including non-block-based SST files (which still use older
|
||||
// format_version markers, and do not support DeleteRange), we also require
|
||||
// the presence of the user property "rocksdb.block.based.table.index.type",
|
||||
// which was added in RocksDB 2.8 and is always present in block-based tables.
|
||||
for (const auto& tp_pair : input_table_properties) {
|
||||
if (tp_pair.second && tp_pair.second->format_version < 5) {
|
||||
// Check for block-based table by looking for its index type property
|
||||
const auto& user_props = tp_pair.second->user_collected_properties;
|
||||
if (user_props.find(BlockBasedTablePropertyNames::kIndexType) !=
|
||||
user_props.end()) {
|
||||
job_stats_->has_accurate_num_input_records = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int input_level = 0;
|
||||
input_level < static_cast<int>(compaction->num_input_levels());
|
||||
++input_level) {
|
||||
@@ -2682,7 +2708,10 @@ Status CompactionJob::ReadTablePropertiesDirectly(
|
||||
std::shared_ptr<const TableProperties>* tp) {
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
std::string file_name = GetTableFileName(file_meta->fd.GetNumber());
|
||||
Status s = ioptions.fs->NewRandomAccessFile(file_name, file_options_, &file,
|
||||
FileOptions fopts = file_options_;
|
||||
fopts.file_checksum = file_meta->file_checksum;
|
||||
fopts.file_checksum_func_name = file_meta->file_checksum_func_name;
|
||||
Status s = ioptions.fs->NewRandomAccessFile(file_name, fopts, &file,
|
||||
nullptr /* dbg */);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
|
||||
@@ -250,6 +250,10 @@ class CompactionJob {
|
||||
// @param num_input_range_del if non-null, will be set to the number of range
|
||||
// deletion entries in this compaction input.
|
||||
//
|
||||
// If any input file has potentially unreliable num_entries count (old SST
|
||||
// files - details in implementation),
|
||||
// job_stats_->has_accurate_num_input_records is set to false.
|
||||
//
|
||||
// Returns true iff internal_stats_.output_level_stats.num_input_records and
|
||||
// num_input_range_del are calculated successfully.
|
||||
//
|
||||
|
||||
+4
-4
@@ -65,7 +65,7 @@ Status VerifySstFileChecksum(const Options& options,
|
||||
}
|
||||
|
||||
Status VerifySstFileChecksumInternal(const Options& options,
|
||||
const EnvOptions& env_options,
|
||||
const FileOptions& file_options,
|
||||
const ReadOptions& read_options,
|
||||
const std::string& file_path,
|
||||
const SequenceNumber& largest_seqno) {
|
||||
@@ -74,8 +74,8 @@ Status VerifySstFileChecksumInternal(const Options& options,
|
||||
InternalKeyComparator internal_comparator(options.comparator);
|
||||
ImmutableOptions ioptions(options);
|
||||
|
||||
Status s = ioptions.fs->NewRandomAccessFile(
|
||||
file_path, FileOptions(env_options), &file, nullptr);
|
||||
Status s =
|
||||
ioptions.fs->NewRandomAccessFile(file_path, file_options, &file, nullptr);
|
||||
if (s.ok()) {
|
||||
s = ioptions.fs->GetFileSize(file_path, IOOptions(), &file_size, nullptr);
|
||||
} else {
|
||||
@@ -94,7 +94,7 @@ Status VerifySstFileChecksumInternal(const Options& options,
|
||||
const bool kImmortal = true;
|
||||
auto reader_options = TableReaderOptions(
|
||||
ioptions, options.prefix_extractor, options.compression_manager.get(),
|
||||
env_options, internal_comparator, options.block_protection_bytes_per_key,
|
||||
file_options, internal_comparator, options.block_protection_bytes_per_key,
|
||||
false /* skip_filters */, !kImmortal, false /* force_direct_prefetch */,
|
||||
-1 /* level */);
|
||||
reader_options.largest_seqno = largest_seqno;
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
#pragma once
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
Status VerifySstFileChecksumInternal(const Options& options,
|
||||
const EnvOptions& env_options,
|
||||
const FileOptions& file_options,
|
||||
const ReadOptions& read_options,
|
||||
const std::string& file_path,
|
||||
const SequenceNumber& largest_seqno = 0);
|
||||
|
||||
@@ -242,6 +242,43 @@ TEST_F(DBBasicTest, ReadOnlyDB) {
|
||||
Status::Code::kNotSupported);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ReadOnlyDBFlushWAL) {
|
||||
// Test that FlushWAL returns NotSupported on read-only DB, and that
|
||||
// GetLiveFilesStorageInfo works correctly even with manual_wal_flush=true.
|
||||
// This is a regression test for a bug where GetLiveFilesStorageInfo would
|
||||
// crash on read-only DBs with manual_wal_flush=true because FlushWAL
|
||||
// accessed logs_.back() on an empty deque.
|
||||
auto options = CurrentOptions();
|
||||
options.manual_wal_flush = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("foo", "v1"));
|
||||
ASSERT_OK(Put("bar", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("baz", "v3")); // Unflushed data in WAL
|
||||
Close();
|
||||
|
||||
// Reopen as read-only
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
ASSERT_EQ("v1", Get("foo"));
|
||||
ASSERT_EQ("v2", Get("bar"));
|
||||
ASSERT_EQ("v3", Get("baz"));
|
||||
|
||||
// FlushWAL should return NotSupported (not crash)
|
||||
ASSERT_EQ(db_->FlushWAL(/*sync=*/false).code(), Status::Code::kNotSupported);
|
||||
ASSERT_EQ(db_->FlushWAL(/*sync=*/true).code(), Status::Code::kNotSupported);
|
||||
|
||||
// GetLiveFilesStorageInfo should succeed (previously crashed with
|
||||
// manual_wal_flush=true because it called FlushWAL which accessed
|
||||
// logs_.back() on empty deque)
|
||||
LiveFilesStorageInfoOptions lfsi_opts;
|
||||
lfsi_opts.wal_size_for_flush = 0;
|
||||
std::vector<LiveFileStorageInfo> files;
|
||||
ASSERT_OK(db_->GetLiveFilesStorageInfo(lfsi_opts, &files));
|
||||
ASSERT_GT(files.size(), 0);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ReadOnlyDBWithWriteDBIdToManifestSet) {
|
||||
auto options = CurrentOptions();
|
||||
options.write_dbid_to_manifest = false;
|
||||
@@ -5437,6 +5474,94 @@ INSTANTIATE_TEST_CASE_P(DBBasicTestDeadline, DBBasicTestDeadline,
|
||||
::testing::Values(std::make_tuple(true, false),
|
||||
std::make_tuple(false, true),
|
||||
std::make_tuple(true, true)));
|
||||
|
||||
// FileSystemWrapper that captures FileOptions passed to NewRandomAccessFile
|
||||
// for .sst files, so we can verify file_checksum fields are populated.
|
||||
class ChecksumCapturingFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit ChecksumCapturingFS(const std::shared_ptr<FileSystem>& base)
|
||||
: FileSystemWrapper(base) {}
|
||||
|
||||
static const char* kClassName() { return "ChecksumCapturingFS"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus NewRandomAccessFile(const std::string& fname,
|
||||
const FileOptions& opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
if (fname.find(".sst") != std::string::npos) {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
captured_file_checksum_ = opts.file_checksum;
|
||||
captured_file_checksum_func_name_ = opts.file_checksum_func_name;
|
||||
capture_count_++;
|
||||
}
|
||||
return target()->NewRandomAccessFile(fname, opts, result, dbg);
|
||||
}
|
||||
|
||||
std::string GetCapturedFileChecksum() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return captured_file_checksum_;
|
||||
}
|
||||
|
||||
std::string GetCapturedFileChecksumFuncName() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return captured_file_checksum_func_name_;
|
||||
}
|
||||
|
||||
int GetCaptureCount() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
return capture_count_;
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
captured_file_checksum_.clear();
|
||||
captured_file_checksum_func_name_.clear();
|
||||
capture_count_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex mu_;
|
||||
std::string captured_file_checksum_;
|
||||
std::string captured_file_checksum_func_name_;
|
||||
int capture_count_ = 0;
|
||||
};
|
||||
|
||||
TEST_F(DBBasicTest, FileChecksumInFileOptions) {
|
||||
// Verify that file_checksum and file_checksum_func_name from FileMetaData
|
||||
// are propagated through FileOptions when opening SST files.
|
||||
auto capturing_fs =
|
||||
std::make_shared<ChecksumCapturingFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, capturing_fs));
|
||||
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.env = env.get();
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write data and flush to create an SST with a file checksum.
|
||||
ASSERT_OK(Put("key1", "value1"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Reset captures, then reopen to trigger TableCache SST open.
|
||||
capturing_fs->Reset();
|
||||
Reopen(options);
|
||||
|
||||
// Read to trigger SST open through TableCache::GetTableReader.
|
||||
ASSERT_EQ("value1", Get("key1"));
|
||||
|
||||
// Verify that checksum fields were populated.
|
||||
ASSERT_GT(capturing_fs->GetCaptureCount(), 0);
|
||||
ASSERT_FALSE(capturing_fs->GetCapturedFileChecksum().empty());
|
||||
ASSERT_NE(capturing_fs->GetCapturedFileChecksumFuncName(),
|
||||
capturing_fs->GetCapturedFileChecksum());
|
||||
ASSERT_EQ(capturing_fs->GetCapturedFileChecksumFuncName(),
|
||||
"FileChecksumCrc32c");
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -6471,8 +6471,11 @@ Status DBImpl::VerifyChecksumInternal(const ReadOptions& read_options,
|
||||
fmeta->file_checksum_func_name, fname,
|
||||
read_options);
|
||||
} else {
|
||||
FileOptions fopts = file_options_;
|
||||
fopts.file_checksum = fmeta->file_checksum;
|
||||
fopts.file_checksum_func_name = fmeta->file_checksum_func_name;
|
||||
s = ROCKSDB_NAMESPACE::VerifySstFileChecksumInternal(
|
||||
opts, file_options_, read_options, fname, fd.largest_seqno);
|
||||
opts, fopts, read_options, fname, fd.largest_seqno);
|
||||
}
|
||||
RecordTick(stats_, VERIFY_CHECKSUM_READ_BYTES,
|
||||
IOSTATS(bytes_read) - prev_bytes_read);
|
||||
@@ -6540,12 +6543,15 @@ Status DBImpl::VerifyFullFileChecksum(const std::string& file_checksum_expected,
|
||||
}
|
||||
std::string file_checksum;
|
||||
std::string func_name;
|
||||
FileOptions fopts;
|
||||
fopts.file_checksum = file_checksum_expected;
|
||||
fopts.file_checksum_func_name = func_name_expected;
|
||||
s = ROCKSDB_NAMESPACE::GenerateOneFileChecksum(
|
||||
fs_.get(), fname, immutable_db_options_.file_checksum_gen_factory.get(),
|
||||
func_name_expected, &file_checksum, &func_name,
|
||||
read_options.readahead_size, immutable_db_options_.allow_mmap_reads,
|
||||
io_tracer_, immutable_db_options_.rate_limiter.get(), read_options,
|
||||
immutable_db_options_.stats, immutable_db_options_.clock);
|
||||
immutable_db_options_.stats, immutable_db_options_.clock, fopts);
|
||||
if (s.ok()) {
|
||||
assert(func_name_expected == func_name);
|
||||
if (file_checksum != file_checksum_expected) {
|
||||
|
||||
@@ -121,6 +121,11 @@ class DBImplReadOnly : public DBImpl {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
using DBImpl::FlushWAL;
|
||||
Status FlushWAL(const FlushWALOptions& /*options*/) override {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
using DB::IngestExternalFile;
|
||||
Status IngestExternalFile(
|
||||
ColumnFamilyHandle* /*column_family*/,
|
||||
|
||||
@@ -300,6 +300,18 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
// ingestion.
|
||||
// TODO: plumb Env::IOActivity
|
||||
ReadOptions ro;
|
||||
// Pass user-provided checksums through FileOptions when available.
|
||||
// The caller may not have provided checksums at all (empty vectors),
|
||||
// so we guard with a bounds check.
|
||||
FileOptions fopts;
|
||||
if (i < files_checksums.size()) {
|
||||
fopts.file_checksum = files_checksums[i];
|
||||
}
|
||||
if (i < files_checksum_func_names.size()) {
|
||||
fopts.file_checksum_func_name = files_checksum_func_names[i];
|
||||
} else {
|
||||
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
}
|
||||
IOStatus io_s = GenerateOneFileChecksum(
|
||||
fs_.get(), files_to_ingest_[i].internal_file_path,
|
||||
db_options_.file_checksum_gen_factory.get(),
|
||||
@@ -308,7 +320,7 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
ingestion_options_.verify_checksums_readahead_size,
|
||||
db_options_.allow_mmap_reads, io_tracer_,
|
||||
db_options_.rate_limiter.get(), ro, db_options_.stats,
|
||||
db_options_.clock);
|
||||
db_options_.clock, fopts);
|
||||
if (!io_s.ok()) {
|
||||
status = io_s;
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
@@ -1482,13 +1494,15 @@ IOStatus ExternalSstFileIngestionJob::GenerateChecksumForIngestedFile(
|
||||
// TODO: rate limit file reads for checksum calculation during file ingestion.
|
||||
// TODO: plumb Env::IOActivity
|
||||
ReadOptions ro;
|
||||
FileOptions gen_fopts;
|
||||
gen_fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
IOStatus io_s = GenerateOneFileChecksum(
|
||||
fs_.get(), file_to_ingest->internal_file_path,
|
||||
db_options_.file_checksum_gen_factory.get(), requested_checksum_func_name,
|
||||
&file_checksum, &file_checksum_func_name,
|
||||
ingestion_options_.verify_checksums_readahead_size,
|
||||
db_options_.allow_mmap_reads, io_tracer_, db_options_.rate_limiter.get(),
|
||||
ro, db_options_.stats, db_options_.clock);
|
||||
ro, db_options_.stats, db_options_.clock, gen_fopts);
|
||||
if (!io_s.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options_.info_log, "Failed to generate checksum for %s: %s",
|
||||
|
||||
@@ -310,8 +310,10 @@ Status ImportColumnFamilyJob::GetIngestedFileInfo(
|
||||
std::unique_ptr<FSRandomAccessFile> sst_file;
|
||||
std::unique_ptr<RandomAccessFileReader> sst_file_reader;
|
||||
|
||||
status =
|
||||
fs_->NewRandomAccessFile(external_file, env_options_, &sst_file, nullptr);
|
||||
FileOptions fo{env_options_};
|
||||
fo.file_checksum = file_meta.file_checksum;
|
||||
fo.file_checksum_func_name = file_meta.file_checksum_func_name;
|
||||
status = fs_->NewRandomAccessFile(external_file, fo, &sst_file, nullptr);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
|
||||
+3
-2
@@ -98,6 +98,8 @@ Status TableCache::GetTableReader(
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
FileOptions fopts = file_options;
|
||||
fopts.temperature = file_temperature;
|
||||
fopts.file_checksum = file_meta.file_checksum;
|
||||
fopts.file_checksum_func_name = file_meta.file_checksum_func_name;
|
||||
Status s = PrepareIOFromReadOptions(ro, ioptions_.clock, fopts.io_options);
|
||||
TEST_SYNC_POINT_CALLBACK("TableCache::GetTableReader:BeforeOpenFile",
|
||||
const_cast<Status*>(&s));
|
||||
@@ -113,8 +115,7 @@ Status TableCache::GetTableReader(
|
||||
Status temp_s =
|
||||
PrepareIOFromReadOptions(ro, ioptions_.clock, fopts.io_options);
|
||||
if (temp_s.ok()) {
|
||||
temp_s = ioptions_.fs->NewRandomAccessFile(fname, file_options, &file,
|
||||
nullptr);
|
||||
temp_s = ioptions_.fs->NewRandomAccessFile(fname, fopts, &file, nullptr);
|
||||
}
|
||||
if (temp_s.ok()) {
|
||||
RecordTick(ioptions_.stats, NO_FILE_OPENS);
|
||||
|
||||
+5
-3
@@ -1820,8 +1820,10 @@ Status Version::GetTableProperties(const ReadOptions& read_options,
|
||||
file_name = TableFileName(ioptions.cf_paths, file_meta->fd.GetNumber(),
|
||||
file_meta->fd.GetPathId());
|
||||
}
|
||||
s = ioptions.fs->NewRandomAccessFile(file_name, file_options_, &file,
|
||||
nullptr);
|
||||
FileOptions fopts = file_options_;
|
||||
fopts.file_checksum = file_meta->file_checksum;
|
||||
fopts.file_checksum_func_name = file_meta->file_checksum_func_name;
|
||||
s = ioptions.fs->NewRandomAccessFile(file_name, fopts, &file, nullptr);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -3444,7 +3446,7 @@ bool Version::MaybeInitializeFileMetaData(const ReadOptions& read_options,
|
||||
// Ensure new invariants on old files
|
||||
file_meta->num_deletions =
|
||||
std::max(tp->num_deletions, tp->num_range_deletions);
|
||||
file_meta->num_entries = std::max(tp->num_entries, tp->num_deletions);
|
||||
file_meta->num_entries = std::max(tp->num_entries, file_meta->num_deletions);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ DECLARE_bool(verify_before_write);
|
||||
DECLARE_bool(histogram);
|
||||
DECLARE_bool(destroy_db_initially);
|
||||
DECLARE_bool(destroy_db_and_exit);
|
||||
DECLARE_string(delete_dir_and_exit);
|
||||
DECLARE_bool(verbose);
|
||||
DECLARE_bool(progress_reports);
|
||||
DECLARE_uint64(db_write_buffer_size);
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
|
||||
#ifdef GFLAGS
|
||||
#pragma once
|
||||
|
||||
#include "db_stress_tool/db_stress_common.h"
|
||||
#include "file/filename.h"
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "rocksdb/file_checksum.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace {
|
||||
@@ -173,6 +176,35 @@ class DbStressFSWrapper : public FileSystemWrapper {
|
||||
const FileOptions& file_opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* r,
|
||||
IODebugContext* dbg) override {
|
||||
// verify that file checksums are propagated through FileOptions
|
||||
// for SST file opens.
|
||||
|
||||
std::string basename = f.substr(f.rfind('/') + 1);
|
||||
uint64_t file_number;
|
||||
FileType file_type;
|
||||
if (ParseFileName(basename, &file_number, &file_type) &&
|
||||
file_type == kTableFile) {
|
||||
// file_checksum_func_name must always be populated to be sure each call
|
||||
// site within RocksDB is intentional about populating the fields with the
|
||||
// best available information:
|
||||
// - kNoFileChecksumFuncName: no checksum context available
|
||||
// (e.g., SstFileDumper, SstFileReader, checksum generation),
|
||||
// always paired with empty checksum
|
||||
// - kUnknownFileChecksumFuncName: file created without a
|
||||
// checksum factory (from MANIFEST), always paired with
|
||||
// empty checksum
|
||||
// - a real name (e.g., "FileChecksumCrc32c"): checksum exists
|
||||
assert(!file_opts.file_checksum_func_name.empty());
|
||||
if (file_opts.file_checksum_func_name == kUnknownFileChecksumFuncName ||
|
||||
file_opts.file_checksum_func_name == kNoFileChecksumFuncName) {
|
||||
// No checksum available — checksum value must be empty
|
||||
assert(file_opts.file_checksum.empty());
|
||||
} else {
|
||||
// A real checksum function — checksum value must be present
|
||||
assert(!file_opts.file_checksum.empty());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
IOStatus s = target()->NewRandomAccessFile(f, file_opts, &file, dbg);
|
||||
if (s.ok()) {
|
||||
|
||||
@@ -139,6 +139,10 @@ DEFINE_bool(destroy_db_and_exit, false,
|
||||
"Destroys the database dir and exits. Useful for cleanup without "
|
||||
"running stress test. Other options are mostly ignored.");
|
||||
|
||||
DEFINE_string(delete_dir_and_exit, "",
|
||||
"Recursively deletes the specified directory and exits. "
|
||||
"Useful for cleaning up TEST_TMPDIR after crash tests.");
|
||||
|
||||
DEFINE_bool(verbose, false, "Verbose");
|
||||
|
||||
DEFINE_bool(progress_reports, true,
|
||||
|
||||
@@ -94,8 +94,10 @@ int db_stress_tool(int argc, char** argv) {
|
||||
raw_env = fault_env_guard.get();
|
||||
}
|
||||
|
||||
env_wrapper_guard = std::make_shared<CompositeEnvWrapper>(
|
||||
raw_env, std::make_shared<DbStressFSWrapper>(raw_env->GetFileSystem()));
|
||||
auto db_stress_fs =
|
||||
std::make_shared<DbStressFSWrapper>(raw_env->GetFileSystem());
|
||||
env_wrapper_guard =
|
||||
std::make_shared<CompositeEnvWrapper>(raw_env, db_stress_fs);
|
||||
db_stress_env = env_wrapper_guard.get();
|
||||
|
||||
// Handle --destroy_db_and_exit early, before other option validation
|
||||
@@ -111,6 +113,20 @@ int db_stress_tool(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle --delete_dir_and_exit early, before other option validation
|
||||
if (!FLAGS_delete_dir_and_exit.empty()) {
|
||||
s = DestroyDir(raw_env, FLAGS_delete_dir_and_exit);
|
||||
if (s.ok()) {
|
||||
fprintf(stdout, "Successfully deleted directory %s\n",
|
||||
FLAGS_delete_dir_and_exit.c_str());
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Failed to delete directory %s: %s\n",
|
||||
FLAGS_delete_dir_and_exit.c_str(), s.ToString().c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
FLAGS_rep_factory = StringToRepFactory(FLAGS_memtablerep.c_str());
|
||||
|
||||
// The number of background threads should be at least as much the
|
||||
|
||||
Vendored
+193
-24
@@ -41,6 +41,9 @@
|
||||
#include "env/env_chroot.h"
|
||||
#include "env/env_encryption_ctr.h"
|
||||
#include "env/fs_readonly.h"
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
#include "env/io_posix.h"
|
||||
#endif
|
||||
#include "env/mock_env.h"
|
||||
#include "env/unique_id_gen.h"
|
||||
#include "logging/log_buffer.h"
|
||||
@@ -3741,32 +3744,35 @@ void TestAbortIOWithRequests(
|
||||
std::shared_ptr<FileSystem> fs = env->GetFileSystem();
|
||||
std::string fname = test::PerThreadDBPath(env, "testfile_abortio");
|
||||
|
||||
constexpr size_t kSectorSize = 4096;
|
||||
// 1. Create test file once (content doesn't change between iterations)
|
||||
{
|
||||
std::unique_ptr<FSWritableFile> wfile;
|
||||
FileOptions file_opts;
|
||||
file_opts.use_direct_writes = true;
|
||||
ASSERT_OK(fs->NewWritableFile(fname, file_opts, &wfile, nullptr));
|
||||
|
||||
for (int iter = 0; iter < iterations; iter++) {
|
||||
// 1. Create test file of specified size using direct IO
|
||||
{
|
||||
std::unique_ptr<FSWritableFile> wfile;
|
||||
FileOptions file_opts;
|
||||
file_opts.use_direct_writes = true;
|
||||
ASSERT_OK(fs->NewWritableFile(fname, file_opts, &wfile, nullptr));
|
||||
// Query the file's required buffer alignment (logical block size)
|
||||
// instead of hardcoding 4096, to support devices with different
|
||||
// sector sizes.
|
||||
size_t sector_size = wfile->GetRequiredBufferAlignment();
|
||||
|
||||
// Round up to full sectors for direct IO writes
|
||||
size_t num_sectors = (file_size + kSectorSize - 1) / kSectorSize;
|
||||
for (size_t i = 0; i < num_sectors; ++i) {
|
||||
auto data = NewAligned(kSectorSize, static_cast<char>(i + 1));
|
||||
Slice slice(data.get(), kSectorSize);
|
||||
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
// Truncate to exact file size if not aligned to sector boundary
|
||||
if (file_size % kSectorSize != 0) {
|
||||
ASSERT_OK(wfile->Truncate(file_size, IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
|
||||
// Round up to full sectors for direct IO writes
|
||||
size_t num_sectors = (file_size + sector_size - 1) / sector_size;
|
||||
for (size_t i = 0; i < num_sectors; ++i) {
|
||||
auto data = NewAligned(sector_size, static_cast<char>(i + 1));
|
||||
Slice slice(data.get(), sector_size);
|
||||
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
// Truncate to exact file size if not aligned to sector boundary
|
||||
if (file_size % sector_size != 0) {
|
||||
ASSERT_OK(wfile->Truncate(file_size, IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
for (int iter = 0; iter < iterations; iter++) {
|
||||
// 2. Submit ReadAsync requests and immediately abort
|
||||
{
|
||||
FileOptions file_opts;
|
||||
@@ -3781,6 +3787,7 @@ void TestAbortIOWithRequests(
|
||||
std::vector<std::unique_ptr<char, Deleter>> data;
|
||||
std::vector<size_t> vals;
|
||||
IOHandleDeleter del_fn;
|
||||
std::atomic<int> callbacks_invoked{0};
|
||||
|
||||
// Initialize read requests from specs
|
||||
for (size_t i = 0; i < num_reads; i++) {
|
||||
@@ -3796,6 +3803,7 @@ void TestAbortIOWithRequests(
|
||||
[&](FSReadRequest& req, void* cb_arg) {
|
||||
size_t i = *(reinterpret_cast<size_t*>(cb_arg));
|
||||
reqs[i].status = req.status;
|
||||
callbacks_invoked++;
|
||||
};
|
||||
|
||||
// Submit all ReadAsync requests
|
||||
@@ -3822,6 +3830,15 @@ void TestAbortIOWithRequests(
|
||||
// Immediately call AbortIO - this should NOT hang
|
||||
ASSERT_OK(fs->AbortIO(io_handles));
|
||||
|
||||
// Verify all handles are finished and all callbacks were invoked.
|
||||
// Since all handles are passed to AbortIO, every handle is guaranteed
|
||||
// to be finalized (either completed or cancelled).
|
||||
for (size_t i = 0; i < num_reads; i++) {
|
||||
Posix_IOHandle* h = static_cast<Posix_IOHandle*>(io_handles[i]);
|
||||
ASSERT_TRUE(h->is_finished);
|
||||
}
|
||||
ASSERT_EQ(callbacks_invoked.load(), static_cast<int>(num_reads));
|
||||
|
||||
// Clean up handles
|
||||
for (size_t i = 0; i < num_reads; i++) {
|
||||
if (io_handles[i]) {
|
||||
@@ -3829,10 +3846,10 @@ void TestAbortIOWithRequests(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
|
||||
|
||||
fprintf(stderr, "TestAbortIOWithRequests: completed %d iterations\n",
|
||||
iterations);
|
||||
#else
|
||||
@@ -3919,6 +3936,158 @@ TEST_F(TestAsyncRead, AbortIOReversedHandles) {
|
||||
/*use_direct_io=*/true, /*iterations=*/100);
|
||||
}
|
||||
|
||||
// Test for bug fix: AbortIO with partial handles should correctly handle
|
||||
// completions for non-aborted handles.
|
||||
//
|
||||
// Previously, AbortIO would consume completions for non-aborted handles but
|
||||
// not set is_finished (since it expected req_count==2 for all handles).
|
||||
// This caused subsequent Poll calls to hang forever.
|
||||
//
|
||||
// The fix correctly detects handles not in the abort set and finalizes them
|
||||
// immediately when their completion arrives (at req_count==1).
|
||||
TEST_F(TestAsyncRead, AbortIOPartialHandlesBug) {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
std::shared_ptr<FileSystem> fs = env_->GetFileSystem();
|
||||
std::string fname = test::PerThreadDBPath(env_, "testfile_abortio_partial");
|
||||
|
||||
constexpr size_t kSectorSize = 4096;
|
||||
constexpr size_t kFileSize = 2 * 1024 * 1024; // 2MB
|
||||
|
||||
// 1. Create test file with direct I/O
|
||||
{
|
||||
std::unique_ptr<FSWritableFile> wfile;
|
||||
FileOptions file_opts;
|
||||
file_opts.use_direct_writes = true;
|
||||
ASSERT_OK(fs->NewWritableFile(fname, file_opts, &wfile, nullptr));
|
||||
|
||||
size_t num_sectors = kFileSize / kSectorSize;
|
||||
for (size_t i = 0; i < num_sectors; ++i) {
|
||||
auto data = NewAligned(kSectorSize, static_cast<char>(i + 1));
|
||||
Slice slice(data.get(), kSectorSize);
|
||||
ASSERT_OK(wfile->Append(slice, IOOptions(), nullptr));
|
||||
}
|
||||
ASSERT_OK(wfile->Close(IOOptions(), nullptr));
|
||||
}
|
||||
|
||||
// 2. Submit 3 ReadAsync requests, abort only the first one, then Poll the
|
||||
// rest
|
||||
{
|
||||
FileOptions file_opts;
|
||||
file_opts.use_direct_reads = true;
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
ASSERT_OK(fs->NewRandomAccessFile(fname, file_opts, &file, nullptr));
|
||||
|
||||
IOOptions opts;
|
||||
constexpr size_t kNumReads = 3;
|
||||
std::vector<void*> io_handles(kNumReads);
|
||||
std::vector<FSReadRequest> reqs(kNumReads);
|
||||
std::vector<std::unique_ptr<char, Deleter>> data;
|
||||
std::vector<size_t> vals;
|
||||
IOHandleDeleter del_fn;
|
||||
std::atomic<int> callbacks_invoked{0};
|
||||
|
||||
// H0: 1MB read, H1: 4KB read, H2: 4KB read
|
||||
std::vector<std::pair<uint64_t, size_t>> read_specs = {
|
||||
{0, 1024 * 1024}, // H0: 1MB at offset 0
|
||||
{1024 * 1024, 4096}, // H1: 4KB at offset 1MB
|
||||
{1024 * 1024 + 4096, 4096}, // H2: 4KB at offset 1MB+4KB
|
||||
};
|
||||
|
||||
for (size_t i = 0; i < kNumReads; i++) {
|
||||
reqs[i].offset = read_specs[i].first;
|
||||
reqs[i].len = read_specs[i].second;
|
||||
data.emplace_back(NewAligned(reqs[i].len, 0));
|
||||
reqs[i].scratch = data.back().get();
|
||||
vals.push_back(i);
|
||||
}
|
||||
|
||||
std::function<void(FSReadRequest&, void*)> callback =
|
||||
[&](FSReadRequest& req, void* cb_arg) {
|
||||
size_t i = *(reinterpret_cast<size_t*>(cb_arg));
|
||||
reqs[i].status = req.status;
|
||||
callbacks_invoked++;
|
||||
};
|
||||
|
||||
// Submit all ReadAsync requests
|
||||
for (size_t i = 0; i < kNumReads; i++) {
|
||||
void* cb_arg = static_cast<void*>(&(vals[i]));
|
||||
IOStatus s = file->ReadAsync(reqs[i], opts, callback, cb_arg,
|
||||
&(io_handles[i]), &del_fn, nullptr);
|
||||
if (s.IsNotSupported()) {
|
||||
// io_uring not supported, clean up and skip
|
||||
for (size_t j = 0; j < i; j++) {
|
||||
if (io_handles[j]) {
|
||||
del_fn(io_handles[j]);
|
||||
}
|
||||
}
|
||||
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Wait for reads to complete in io_uring (completions in queue but not
|
||||
// consumed). 5 seconds should be plenty for direct I/O reads to complete.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(5));
|
||||
|
||||
// Abort ONLY H0 - this will consume all completions but should correctly
|
||||
// finalize H1 and H2 (since they're not in the abort set).
|
||||
std::vector<void*> abort_handles = {io_handles[0]};
|
||||
ASSERT_OK(fs->AbortIO(abort_handles));
|
||||
|
||||
// Verify H0 is finished (aborted)
|
||||
Posix_IOHandle* h0 = static_cast<Posix_IOHandle*>(io_handles[0]);
|
||||
ASSERT_TRUE(h0->is_finished);
|
||||
ASSERT_EQ(h0->req_count, 2u); // original + cancel
|
||||
|
||||
// Note: H1 and H2 may or may not be finished at this point. AbortIO
|
||||
// finalizes non-aborted handles whose CQEs arrive while waiting for
|
||||
// aborted handles, but CQE ordering is non-deterministic. If H0's
|
||||
// completions arrived first, H1/H2's CQEs are still in the queue.
|
||||
// Poll handles either case correctly.
|
||||
|
||||
// Poll on H1, H2 - completes them if not already finalized by AbortIO
|
||||
std::vector<void*> poll_handles = {io_handles[1], io_handles[2]};
|
||||
|
||||
// Use a watchdog to detect hang (regression test for the original bug
|
||||
// where AbortIO consumed non-aborted CQEs without finalizing them)
|
||||
std::atomic<bool> poll_completed{false};
|
||||
std::thread watchdog([&]() {
|
||||
for (int i = 0; i < 500; i++) { // 5 seconds timeout
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
if (poll_completed) return;
|
||||
}
|
||||
// Bug regression: Poll hung
|
||||
_exit(1);
|
||||
});
|
||||
|
||||
fs->Poll(poll_handles, poll_handles.size());
|
||||
poll_completed = true;
|
||||
watchdog.join();
|
||||
|
||||
// After Poll, H1 and H2 must be finished
|
||||
Posix_IOHandle* h1 = static_cast<Posix_IOHandle*>(io_handles[1]);
|
||||
Posix_IOHandle* h2 = static_cast<Posix_IOHandle*>(io_handles[2]);
|
||||
ASSERT_TRUE(h1->is_finished);
|
||||
ASSERT_TRUE(h2->is_finished);
|
||||
|
||||
// Verify all callbacks were invoked
|
||||
ASSERT_EQ(callbacks_invoked.load(), 3);
|
||||
|
||||
// Clean up handles
|
||||
for (size_t i = 0; i < kNumReads; i++) {
|
||||
if (io_handles[i]) {
|
||||
del_fn(io_handles[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_OK(fs->DeleteFile(fname, IOOptions(), nullptr));
|
||||
#else
|
||||
(void)env_; // Suppress unused variable warning
|
||||
#endif
|
||||
}
|
||||
|
||||
struct StaticDestructionTester {
|
||||
bool activated = false;
|
||||
~StaticDestructionTester() {
|
||||
|
||||
Vendored
+20
-25
@@ -1129,25 +1129,7 @@ class PosixFileSystem : public FileSystem {
|
||||
// Reset cqe data to catch any stray reuse of it
|
||||
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
|
||||
|
||||
FSReadRequest req;
|
||||
req.scratch = posix_handle->scratch;
|
||||
req.offset = posix_handle->offset;
|
||||
req.len = posix_handle->len;
|
||||
|
||||
size_t finished_len = 0;
|
||||
size_t bytes_read = 0;
|
||||
bool read_again = false;
|
||||
UpdateResult(cqe, "", req.len, posix_handle->iov.iov_len,
|
||||
true /*async_read*/, posix_handle->use_direct_io,
|
||||
posix_handle->alignment, finished_len, &req, bytes_read,
|
||||
read_again);
|
||||
posix_handle->is_finished = true;
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
posix_handle->cb(req, posix_handle->cb_arg);
|
||||
|
||||
(void)finished_len;
|
||||
(void)bytes_read;
|
||||
(void)read_again;
|
||||
FinalizeAsyncRead(iu, cqe, posix_handle);
|
||||
|
||||
if (static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
|
||||
break;
|
||||
@@ -1188,6 +1170,11 @@ class PosixFileSystem : public FileSystem {
|
||||
return IOStatus::IOError("");
|
||||
}
|
||||
|
||||
// Mark this handle as being aborted. This is used when processing
|
||||
// completions to distinguish between aborted handles (expect 2
|
||||
// completions: original + cancel) and non-aborted handles (expect 1).
|
||||
posix_handle->is_being_aborted = true;
|
||||
|
||||
// Prepare the cancel request.
|
||||
struct io_uring_sqe* sqe;
|
||||
sqe = io_uring_get_sqe(iu);
|
||||
@@ -1234,6 +1221,14 @@ class PosixFileSystem : public FileSystem {
|
||||
}
|
||||
posix_handle->req_count++;
|
||||
|
||||
if (!posix_handle->is_being_aborted) {
|
||||
// This is a completion for a handle NOT being aborted.
|
||||
// It only has 1 outstanding request (the original read), so we
|
||||
// should finalize it now.
|
||||
FinalizeAsyncRead(iu, cqe, posix_handle);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset cqe data to catch any stray reuse of it
|
||||
static_cast<struct io_uring_cqe*>(cqe)->user_data = 0xd5d5d5d5d5d5d5d5;
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
@@ -1247,8 +1242,9 @@ class PosixFileSystem : public FileSystem {
|
||||
// - And finally, if the request to cancel wasn't
|
||||
// found, the cancel request is completed with -ENOENT.
|
||||
//
|
||||
// Every handle has to wait for 2 requests completion: original one and
|
||||
// the cancel request which is tracked by PosixHandle::req_count.
|
||||
// Every handle being aborted has to wait for 2 requests completion:
|
||||
// original one and the cancel request which is tracked by
|
||||
// PosixHandle::req_count.
|
||||
// Note: We must mark is_finished and invoke the callback for ANY handle
|
||||
// that reaches req_count == 2, not just the one we're currently waiting
|
||||
// for (io_handles[i]). Otherwise, if completions arrive out of order,
|
||||
@@ -1278,7 +1274,7 @@ class PosixFileSystem : public FileSystem {
|
||||
void SupportedOps(int64_t& supported_ops) override {
|
||||
supported_ops = 0;
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
if (IsIOUringEnabled()) {
|
||||
if (IsIOUringEnabled() && thread_local_async_read_io_urings_) {
|
||||
// Underlying FS supports async_io
|
||||
supported_ops |= (1 << FSSupportedOps::kAsyncIO);
|
||||
}
|
||||
@@ -1344,9 +1340,8 @@ PosixFileSystem::PosixFileSystem()
|
||||
page_size_(getpagesize()),
|
||||
allow_non_owner_access_(true) {
|
||||
#if defined(ROCKSDB_IOURING_PRESENT)
|
||||
// Test whether IOUring is supported, and if it does, create a managing
|
||||
// object for thread local point so that in the future thread-local
|
||||
// io_uring can be created.
|
||||
// Test whether IOUring is supported with the same flags that ReadAsync and
|
||||
// MultiRead will use at runtime.
|
||||
struct io_uring* new_io_uring = CreateIOUring();
|
||||
if (new_io_uring != nullptr) {
|
||||
thread_local_async_read_io_urings_.reset(new ThreadLocalPtr(DeleteIOUring));
|
||||
|
||||
Vendored
+2
-8
@@ -755,10 +755,7 @@ IOStatus PosixRandomAccessFile::MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_multi_read_io_urings_->Get());
|
||||
if (iu == nullptr) {
|
||||
unsigned int flags = 0;
|
||||
flags |= IORING_SETUP_SINGLE_ISSUER;
|
||||
flags |= IORING_SETUP_DEFER_TASKRUN;
|
||||
iu = CreateIOUring(flags);
|
||||
iu = CreateIOUring();
|
||||
if (iu != nullptr) {
|
||||
thread_local_multi_read_io_urings_->Reset(iu);
|
||||
}
|
||||
@@ -1090,10 +1087,7 @@ IOStatus PosixRandomAccessFile::ReadAsync(
|
||||
iu = static_cast<struct io_uring*>(
|
||||
thread_local_async_read_io_urings_->Get());
|
||||
if (iu == nullptr) {
|
||||
unsigned int flags = 0;
|
||||
flags |= IORING_SETUP_SINGLE_ISSUER;
|
||||
flags |= IORING_SETUP_DEFER_TASKRUN;
|
||||
iu = CreateIOUring(flags);
|
||||
iu = CreateIOUring();
|
||||
if (iu != nullptr) {
|
||||
thread_local_async_read_io_urings_->Reset(iu);
|
||||
}
|
||||
|
||||
Vendored
+30
-1
@@ -127,6 +127,7 @@ struct Posix_IOHandle {
|
||||
use_direct_io(_use_direct_io),
|
||||
alignment(_alignment),
|
||||
is_finished(false),
|
||||
is_being_aborted(false),
|
||||
req_count(0) {}
|
||||
|
||||
struct iovec iov;
|
||||
@@ -139,6 +140,10 @@ struct Posix_IOHandle {
|
||||
bool use_direct_io;
|
||||
size_t alignment;
|
||||
bool is_finished;
|
||||
// is_being_aborted is set by AbortIO when a cancel request is submitted.
|
||||
// Used to distinguish between aborted handles (expect 2 completions) and
|
||||
// non-aborted handles (expect 1 completion) when processing completions.
|
||||
bool is_being_aborted;
|
||||
// req_count is used by AbortIO API to keep track of number of requests.
|
||||
uint32_t req_count;
|
||||
};
|
||||
@@ -197,6 +202,27 @@ inline void UpdateResult(struct io_uring_cqe* cqe, const std::string& file_name,
|
||||
(void)len;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Finalize a completed async read request.
|
||||
// Processes the CQE result, marks the handle as finished, and invokes the
|
||||
// callback. This is shared between Poll and AbortIO (for non-aborted handles).
|
||||
inline void FinalizeAsyncRead(struct io_uring* iu, struct io_uring_cqe* cqe,
|
||||
Posix_IOHandle* posix_handle) {
|
||||
FSReadRequest req;
|
||||
req.scratch = posix_handle->scratch;
|
||||
req.offset = posix_handle->offset;
|
||||
req.len = posix_handle->len;
|
||||
|
||||
size_t finished_len = 0;
|
||||
size_t bytes_read = 0;
|
||||
bool read_again = false;
|
||||
UpdateResult(cqe, "", req.len, posix_handle->iov.iov_len, true /*async_read*/,
|
||||
posix_handle->use_direct_io, posix_handle->alignment,
|
||||
finished_len, &req, bytes_read, read_again);
|
||||
posix_handle->is_finished = true;
|
||||
io_uring_cqe_seen(iu, cqe);
|
||||
posix_handle->cb(req, posix_handle->cb_arg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef OS_LINUX
|
||||
@@ -307,8 +333,11 @@ inline void DeleteIOUring(void* p) {
|
||||
delete iu;
|
||||
}
|
||||
|
||||
inline struct io_uring* CreateIOUring(unsigned int flags = 0) {
|
||||
inline struct io_uring* CreateIOUring() {
|
||||
struct io_uring* new_io_uring = new struct io_uring;
|
||||
unsigned int flags = 0;
|
||||
flags |= IORING_SETUP_SINGLE_ISSUER;
|
||||
flags |= IORING_SETUP_DEFER_TASKRUN;
|
||||
int ret = io_uring_queue_init(kIoUringDepth, new_io_uring, flags);
|
||||
if (ret) {
|
||||
delete new_io_uring;
|
||||
|
||||
@@ -351,7 +351,7 @@ void FilePrefetchBuffer::ClearOutdatedData(uint64_t offset, size_t length) {
|
||||
assert(IsBufferQueueEmpty() || buf->IsOffsetInBuffer(offset));
|
||||
}
|
||||
|
||||
void FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
|
||||
Status FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
|
||||
BufferInfo* buf = GetFirstBuffer();
|
||||
|
||||
if (buf->async_read_in_progress_ && fs_ != nullptr) {
|
||||
@@ -362,7 +362,16 @@ void FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
|
||||
std::vector<void*> handles;
|
||||
handles.emplace_back(buf->io_handle_);
|
||||
StopWatch sw(clock_, stats_, POLL_WAIT_MICROS);
|
||||
fs_->Poll(handles, 1).PermitUncheckedError();
|
||||
IOStatus io_s = fs_->Poll(handles, 1);
|
||||
// Allow tests to inject Poll errors
|
||||
TEST_SYNC_POINT_CALLBACK("FilePrefetchBuffer::PollIfNeeded:IOStatus",
|
||||
&io_s);
|
||||
if (!io_s.ok()) {
|
||||
// On Poll failure, clean up the handle and abort.
|
||||
// DestroyAndClearIOHandle also sets async_read_in_progress_ to false.
|
||||
DestroyAndClearIOHandle(buf);
|
||||
return io_s;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset and Release io_handle after the Poll API as request has been
|
||||
@@ -373,6 +382,7 @@ void FilePrefetchBuffer::PollIfNeeded(uint64_t offset, size_t length) {
|
||||
// Always call outdated data after Poll as Buffers might be out of sync w.r.t
|
||||
// offset and length.
|
||||
ClearOutdatedData(offset, length);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// ReadAheadSizeTuning API calls readaheadsize_cb_
|
||||
@@ -511,7 +521,10 @@ Status FilePrefetchBuffer::HandleOverlappingAsyncData(
|
||||
// by Seek, but the next access is at another offset.
|
||||
if (buf->async_read_in_progress_ &&
|
||||
buf->IsOffsetInBufferWithAsyncProgress(offset)) {
|
||||
PollIfNeeded(offset, length);
|
||||
Status poll_status = PollIfNeeded(offset, length);
|
||||
if (!poll_status.ok()) {
|
||||
return poll_status;
|
||||
}
|
||||
}
|
||||
|
||||
if (IsBufferQueueEmpty() || NumBuffersAllocated() == 1) {
|
||||
@@ -646,7 +659,10 @@ Status FilePrefetchBuffer::PrefetchInternal(const IOOptions& opts,
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
PollIfNeeded(tmp_offset, tmp_length);
|
||||
Status poll_status = PollIfNeeded(tmp_offset, tmp_length);
|
||||
if (!poll_status.ok()) {
|
||||
return poll_status;
|
||||
}
|
||||
}
|
||||
|
||||
AllocateBufferIfEmpty();
|
||||
|
||||
@@ -431,7 +431,7 @@ class FilePrefetchBuffer {
|
||||
void ClearOutdatedData(uint64_t offset, size_t len);
|
||||
|
||||
// It calls Poll API to check for any pending asynchronous request.
|
||||
void PollIfNeeded(uint64_t offset, size_t len);
|
||||
Status PollIfNeeded(uint64_t offset, size_t len);
|
||||
|
||||
Status PrefetchInternal(const IOOptions& opts, RandomAccessFileReader* reader,
|
||||
uint64_t offset, size_t length, size_t readahead_size,
|
||||
|
||||
+8
-2
@@ -178,7 +178,8 @@ IOStatus GenerateOneFileChecksum(
|
||||
std::string* file_checksum_func_name,
|
||||
size_t verify_checksums_readahead_size, bool /*allow_mmap_reads*/,
|
||||
std::shared_ptr<IOTracer>& io_tracer, RateLimiter* rate_limiter,
|
||||
const ReadOptions& read_options, Statistics* stats, SystemClock* clock) {
|
||||
const ReadOptions& read_options, Statistics* stats, SystemClock* clock,
|
||||
const FileOptions& file_options) {
|
||||
if (checksum_factory == nullptr) {
|
||||
return IOStatus::InvalidArgument("Checksum factory is invalid");
|
||||
}
|
||||
@@ -218,7 +219,12 @@ IOStatus GenerateOneFileChecksum(
|
||||
std::unique_ptr<RandomAccessFileReader> reader;
|
||||
{
|
||||
std::unique_ptr<FSRandomAccessFile> r_file;
|
||||
io_s = fs->NewRandomAccessFile(file_path, FileOptions(), &r_file, nullptr);
|
||||
FileOptions fopts = file_options;
|
||||
if (fopts.file_checksum.empty()) {
|
||||
// No expected checksum is known — this is a from-scratch computation.
|
||||
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
}
|
||||
io_s = fs->NewRandomAccessFile(file_path, fopts, &r_file, nullptr);
|
||||
if (!io_s.ok()) {
|
||||
return io_s;
|
||||
}
|
||||
|
||||
+2
-1
@@ -83,7 +83,8 @@ IOStatus GenerateOneFileChecksum(
|
||||
std::string* file_checksum_func_name,
|
||||
size_t verify_checksums_readahead_size, bool allow_mmap_reads,
|
||||
std::shared_ptr<IOTracer>& io_tracer, RateLimiter* rate_limiter,
|
||||
const ReadOptions& read_options, Statistics* stats, SystemClock* clock);
|
||||
const ReadOptions& read_options, Statistics* stats, SystemClock* clock,
|
||||
const FileOptions& file_options);
|
||||
|
||||
inline IOStatus PrepareIOFromReadOptions(const ReadOptions& ro,
|
||||
SystemClock* clock, IOOptions& opts,
|
||||
|
||||
@@ -2517,6 +2517,187 @@ TEST_P(PrefetchTest1, SeekParallelizationTest) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(PrefetchTest1, PollErrorRecoveryDuringIteration) {
|
||||
// This end-to-end test verifies that Poll() errors during async prefetching
|
||||
// are properly propagated to the iterator. When Poll() fails, the iterator
|
||||
// should stop and return an IOError status.
|
||||
//
|
||||
// With error injection on the 3rd Poll call, the iterator reads ~231 keys
|
||||
// (out of 500) before encountering the error.
|
||||
|
||||
if (mem_env_ || encrypted_env_) {
|
||||
ROCKSDB_GTEST_SKIP("Test requires non-mem or non-encrypted environment");
|
||||
return;
|
||||
}
|
||||
|
||||
const int kNumKeys = 500;
|
||||
std::shared_ptr<MockFS> fs = std::make_shared<MockFS>(
|
||||
FileSystem::Default(), /*support_prefetch=*/false);
|
||||
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, fs));
|
||||
|
||||
bool use_direct_io = GetParam();
|
||||
Options options;
|
||||
SetGenericOptions(env.get(), use_direct_io, options);
|
||||
options.statistics = CreateDBStatistics();
|
||||
BlockBasedTableOptions table_options;
|
||||
SetBlockBasedTableOptions(table_options);
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
|
||||
Status s = TryReopen(options);
|
||||
if (use_direct_io && (s.IsNotSupported() || s.IsInvalidArgument())) {
|
||||
ROCKSDB_GTEST_SKIP("Direct IO not supported");
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(s);
|
||||
|
||||
// Write keys with known values so we can verify correctness
|
||||
std::map<std::string, std::string> expected_data;
|
||||
{
|
||||
WriteBatch batch;
|
||||
for (int i = 0; i < kNumKeys; i++) {
|
||||
std::string key = BuildKey(i);
|
||||
std::string value = "value_" + std::to_string(i) + "_" +
|
||||
std::string(100, 'x'); // Make values ~110 bytes
|
||||
ASSERT_OK(batch.Put(key, value));
|
||||
expected_data[key] = value;
|
||||
}
|
||||
ASSERT_OK(db_->Write(WriteOptions(), &batch));
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
std::string start_key = BuildKey(0);
|
||||
std::string end_key = BuildKey(kNumKeys - 1);
|
||||
Slice least(start_key.data(), start_key.size());
|
||||
Slice greatest(end_key.data(), end_key.size());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &least, &greatest));
|
||||
|
||||
// Set up callbacks to track async IO and inject Poll errors
|
||||
std::atomic<int> poll_call_count{0};
|
||||
std::atomic<int> poll_error_injected_count{0};
|
||||
bool read_async_called = false;
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FilePrefetchBuffer::PollIfNeeded:IOStatus", [&](void* arg) {
|
||||
poll_call_count++;
|
||||
int current_count = poll_call_count.load();
|
||||
|
||||
// Inject error on the third Poll call to allow some keys to be read
|
||||
// first
|
||||
if (current_count == 3) {
|
||||
IOStatus* io_s = static_cast<IOStatus*>(arg);
|
||||
*io_s = IOStatus::IOError("Injected Poll error for e2e testing");
|
||||
poll_error_injected_count++;
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Injected error on "
|
||||
"Poll call #"
|
||||
<< current_count << std::endl;
|
||||
}
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"UpdateResults::io_uring_result",
|
||||
[&](void* /*arg*/) { read_async_called = true; });
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Iterate through all keys with async IO enabled
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.adaptive_readahead = true;
|
||||
|
||||
int keys_read = 0;
|
||||
int data_mismatches = 0;
|
||||
Status iter_status;
|
||||
{
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
std::string key = iter->key().ToString();
|
||||
std::string value = iter->value().ToString();
|
||||
|
||||
auto it = expected_data.find(key);
|
||||
if (it == expected_data.end()) {
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Unexpected key: " << key
|
||||
<< std::endl;
|
||||
data_mismatches++;
|
||||
} else if (it->second != value) {
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Value mismatch for key "
|
||||
<< key << std::endl;
|
||||
data_mismatches++;
|
||||
}
|
||||
keys_read++;
|
||||
}
|
||||
iter_status = iter->status();
|
||||
}
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Log results
|
||||
std::cout << "PollErrorRecoveryDuringIteration: " << "read_async_called="
|
||||
<< read_async_called << ", poll_calls=" << poll_call_count.load()
|
||||
<< ", poll_errors_injected=" << poll_error_injected_count.load()
|
||||
<< ", keys_read=" << keys_read << ", expected_keys=" << kNumKeys
|
||||
<< ", data_mismatches=" << data_mismatches
|
||||
<< ", iter_status=" << iter_status.ToString() << std::endl;
|
||||
|
||||
// Verify no data mismatches occurred for keys that were read
|
||||
ASSERT_EQ(data_mismatches, 0)
|
||||
<< "Found " << data_mismatches << " data mismatches";
|
||||
|
||||
if (read_async_called) {
|
||||
// Async IO was used - verify Poll error was injected and propagated
|
||||
ASSERT_EQ(poll_call_count.load(), 3)
|
||||
<< "Expected exactly 3 Poll calls when error injected on 3rd call";
|
||||
ASSERT_EQ(poll_error_injected_count.load(), 1)
|
||||
<< "Expected exactly 1 Poll error to be injected";
|
||||
|
||||
// The iterator should have stopped with an error status
|
||||
ASSERT_TRUE(iter_status.IsIOError())
|
||||
<< "Expected iterator to report IOError after Poll failure, got: "
|
||||
<< iter_status.ToString();
|
||||
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Successfully verified "
|
||||
"Poll error was injected and propagated to iterator"
|
||||
<< std::endl;
|
||||
} else {
|
||||
// Async IO not supported - iterator should complete successfully
|
||||
ASSERT_OK(iter_status);
|
||||
ASSERT_EQ(keys_read, kNumKeys);
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Async IO (io_uring) not "
|
||||
"supported on this platform, verified data correctness"
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
// Retry iteration without error injection - verify all data is still readable
|
||||
// This confirms the Poll error didn't corrupt state
|
||||
{
|
||||
int retry_keys_read = 0;
|
||||
int retry_data_mismatches = 0;
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
std::string key = iter->key().ToString();
|
||||
std::string value = iter->value().ToString();
|
||||
|
||||
auto it = expected_data.find(key);
|
||||
if (it == expected_data.end()) {
|
||||
retry_data_mismatches++;
|
||||
} else if (it->second != value) {
|
||||
retry_data_mismatches++;
|
||||
}
|
||||
retry_keys_read++;
|
||||
}
|
||||
ASSERT_OK(iter->status())
|
||||
<< "Retry iteration failed: " << iter->status().ToString();
|
||||
ASSERT_EQ(retry_keys_read, kNumKeys)
|
||||
<< "Retry should read all " << kNumKeys << " keys";
|
||||
ASSERT_EQ(retry_data_mismatches, 0)
|
||||
<< "Retry found " << retry_data_mismatches << " data mismatches";
|
||||
std::cout << "PollErrorRecoveryDuringIteration: Retry succeeded, read all "
|
||||
<< retry_keys_read << " keys correctly" << std::endl;
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
namespace {
|
||||
#ifdef GFLAGS
|
||||
const int kMaxArgCount = 100;
|
||||
@@ -3376,6 +3557,76 @@ TEST_F(FilePrefetchBufferTest, ForCompaction) {
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(FilePrefetchBufferTest, PollErrorPropagation) {
|
||||
// This test verifies that Poll() errors in PollIfNeeded are properly
|
||||
// propagated rather than being silently ignored.
|
||||
|
||||
std::string fname = "poll-error-test";
|
||||
Random rand(0);
|
||||
std::string content = rand.RandomString(32768);
|
||||
Write(fname, content);
|
||||
|
||||
FileOptions opts;
|
||||
std::unique_ptr<RandomAccessFileReader> r;
|
||||
Read(fname, opts, &r);
|
||||
|
||||
// Set up readahead params for async prefetching
|
||||
ReadaheadParams readahead_params;
|
||||
readahead_params.initial_readahead_size = 16384;
|
||||
readahead_params.max_readahead_size = 16384;
|
||||
|
||||
FilePrefetchBuffer fpb(readahead_params, /*enable=*/true,
|
||||
/*track_min_offset=*/false, fs());
|
||||
|
||||
Slice result;
|
||||
// Start an async prefetch to set up async_read_in_progress_ state
|
||||
Status s = fpb.PrefetchAsync(IOOptions(), r.get(), 0, 4096, &result);
|
||||
|
||||
// Skip test on platforms that don't support async IO
|
||||
if (s.IsNotSupported()) {
|
||||
ROCKSDB_GTEST_SKIP("Async IO not supported on this platform");
|
||||
return;
|
||||
}
|
||||
ASSERT_TRUE(s.IsTryAgain());
|
||||
std::cout << "PollErrorPropagation: Async IO supported, proceeding with test"
|
||||
<< std::endl;
|
||||
|
||||
// Set up SyncPoint to inject Poll error
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FilePrefetchBuffer::PollIfNeeded:IOStatus", [&](void* arg) {
|
||||
IOStatus* io_s = static_cast<IOStatus*>(arg);
|
||||
*io_s = IOStatus::IOError("Injected Poll error for testing");
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// TryReadFromCache will call PollIfNeeded to complete the async read
|
||||
IOOptions io_opts;
|
||||
io_opts.rate_limiter_priority = Env::IOPriority::IO_LOW;
|
||||
Status read_status;
|
||||
bool found =
|
||||
fpb.TryReadFromCache(io_opts, r.get(), 0, 4096, &result, &read_status);
|
||||
|
||||
// When PollIfNeeded fails:
|
||||
// 1. PrefetchInternal returns the error status
|
||||
// 2. TryReadFromCacheUntracked sets *status to the error and returns false
|
||||
// Therefore: found should be false, and read_status should contain the error
|
||||
ASSERT_FALSE(found) << "Expected TryReadFromCache to return false on Poll "
|
||||
"error, but it returned true";
|
||||
ASSERT_TRUE(read_status.IsIOError())
|
||||
<< "Expected IOError status, got: " << read_status.ToString();
|
||||
ASSERT_TRUE(read_status.ToString().find("Injected Poll error") !=
|
||||
std::string::npos)
|
||||
<< "Expected error message to contain 'Injected Poll error', got: "
|
||||
<< read_status.ToString();
|
||||
|
||||
std::cout << "PollErrorPropagation: Poll error correctly propagated - "
|
||||
<< "found=" << found << ", status=" << read_status.ToString()
|
||||
<< std::endl;
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
class FSBufferPrefetchTest
|
||||
: public testing::Test,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
|
||||
@@ -22,7 +22,12 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// The unknown file checksum.
|
||||
constexpr char kUnknownFileChecksum[] = "";
|
||||
// The unknown sst file checksum function name.
|
||||
// Indicates that the file metadata says that no checksum factory was configured
|
||||
// when the file was written.
|
||||
constexpr char kUnknownFileChecksumFuncName[] = "Unknown";
|
||||
// Used when opening a file and there is no file checksum metadata to propagate
|
||||
// at all.
|
||||
constexpr char kNoFileChecksumFuncName[] = "Unavailable";
|
||||
// The standard DB file checksum function name.
|
||||
// This is the name of the checksum function returned by
|
||||
// GetFileChecksumGenCrc32cFactory();
|
||||
|
||||
@@ -201,6 +201,19 @@ struct FileOptions : EnvOptions {
|
||||
// FSWritableFile object creation.
|
||||
Env::WriteLifeTimeHint write_hint = Env::WLTH_NOT_SET;
|
||||
|
||||
// File checksum of the file being opened. Empty string if no checksum is
|
||||
// available.
|
||||
std::string file_checksum;
|
||||
|
||||
// Name of the checksum function used to compute file_checksum. Set to
|
||||
// kUnknownFileChecksumFuncName when file was created without a checksum
|
||||
// factory. Set to kNoFileChecksumFuncName when no checksum metadata is
|
||||
// available.
|
||||
// Production FileSystems will accept empty values for both
|
||||
// file_checksum and file_checksum_func_name, but internally within RocksDB
|
||||
// that is forbidden for checking/auditing purposes.
|
||||
std::string file_checksum_func_name;
|
||||
|
||||
FileOptions() : EnvOptions(), handoff_checksum_type(ChecksumType::kCRC32c) {}
|
||||
|
||||
FileOptions(const DBOptions& opts)
|
||||
@@ -216,7 +229,9 @@ struct FileOptions : EnvOptions {
|
||||
io_options(opts.io_options),
|
||||
temperature(opts.temperature),
|
||||
handoff_checksum_type(opts.handoff_checksum_type),
|
||||
write_hint(opts.write_hint) {}
|
||||
write_hint(opts.write_hint),
|
||||
file_checksum(opts.file_checksum),
|
||||
file_checksum_func_name(opts.file_checksum_func_name) {}
|
||||
|
||||
FileOptions& operator=(const FileOptions&) = default;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 10
|
||||
#define ROCKSDB_MINOR 11
|
||||
#define ROCKSDB_PATCH 0
|
||||
#define ROCKSDB_PATCH 2
|
||||
|
||||
// Make it easy to do conditional compilation based on version checks, i.e.
|
||||
// #if ROCKSDB_VERSION_GE(4, 5, 6)
|
||||
|
||||
@@ -66,7 +66,7 @@ IndexBuilder* IndexBuilder::CreateIndexBuilder(
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assert(!"Do not recognize the index type ");
|
||||
assert(false && "Do not recognize the index type ");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/file_checksum.h"
|
||||
#include "rocksdb/iterator.h"
|
||||
#include "rocksdb/slice_transform.h"
|
||||
#include "rocksdb/status.h"
|
||||
@@ -85,6 +86,7 @@ Status SstFileDumper::GetTableReader(const std::string& file_path) {
|
||||
uint64_t file_size = 0;
|
||||
FileOptions fopts = soptions_;
|
||||
fopts.temperature = file_temp_;
|
||||
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
Status s = fs->NewRandomAccessFile(file_path, fopts, &file, nullptr);
|
||||
if (s.ok()) {
|
||||
// check empty file
|
||||
@@ -129,6 +131,7 @@ Status SstFileDumper::GetTableReader(const std::string& file_path) {
|
||||
if (magic_number == kCuckooTableMagicNumber) {
|
||||
fopts = soptions_;
|
||||
fopts.temperature = file_temp_;
|
||||
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
}
|
||||
|
||||
fs->NewRandomAccessFile(file_path, fopts, &file, nullptr);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "options/cf_options.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/file_checksum.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "table/get_context.h"
|
||||
#include "table/table_builder.h"
|
||||
@@ -51,6 +52,7 @@ Status SstFileReader::Open(const std::string& file_path) {
|
||||
std::unique_ptr<FSRandomAccessFile> file;
|
||||
std::unique_ptr<RandomAccessFileReader> file_reader;
|
||||
FileOptions fopts(r->soptions);
|
||||
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
const auto& fs = r->options.env->GetFileSystem();
|
||||
|
||||
s = fs->GetFileSize(file_path, fopts.io_options, &file_size, nullptr);
|
||||
|
||||
@@ -223,6 +223,17 @@ compare_db()
|
||||
set -e
|
||||
}
|
||||
|
||||
compact_db()
|
||||
{
|
||||
set +e
|
||||
[ "$SANITY_CHECK" ] || bash "$script_copy_dir"/compact_db.sh "$1" "$2" "$3"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ==== Error compacting DB at $1 ====
|
||||
exit 1
|
||||
fi
|
||||
set -e
|
||||
}
|
||||
|
||||
write_external_sst()
|
||||
{
|
||||
set +e
|
||||
@@ -387,10 +398,16 @@ DISABLE_WARNING_AS_ERROR=1 invoke_make ldb -j$J
|
||||
|
||||
for checkout_ref in "${checkout_refs[@]}"
|
||||
do
|
||||
# We currently assume DB backward compatibility for every branch listed
|
||||
# We assume DB backward compatibility for every branch listed
|
||||
echo "== Use $current_checkout_name to open DB generated using $checkout_ref..."
|
||||
compare_db $db_test_dir/$checkout_ref $current_db_test_dir db_dump.txt 1 0
|
||||
|
||||
echo "== Use $current_checkout_name to compact DB generated using $checkout_ref..."
|
||||
compact_db $db_test_dir/$checkout_ref 1 0
|
||||
|
||||
echo "== After compaction, re-verify DB originally from $checkout_ref..."
|
||||
compare_db $db_test_dir/$checkout_ref $current_db_test_dir db_dump_after_compact.txt 1 0
|
||||
|
||||
if member_of_array "$checkout_ref" "${ext_backward_only_refs[@]}" ||
|
||||
member_of_array "$checkout_ref" "${ext_forward_refs[@]}"
|
||||
then
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
|
||||
#
|
||||
# A shell script to compact DB generated by generate_random_db.sh.
|
||||
# ./ldb needs to be available to be executed.
|
||||
#
|
||||
# Usage: <SCRIPT> <DB Path> [if_try_load_options] [if_ignore_unknown_options]
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "usage: $BASH_SOURCE <db_directory> [if_try_load_options] [if_ignore_unknown_options]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
db_dir=$1
|
||||
try_load_options=${2:-"1"}
|
||||
ignore_unknown_options=${3:-"0"}
|
||||
extra_params=
|
||||
|
||||
if [ "$try_load_options" = "0" ]; then
|
||||
extra_params=" --try_load_options=false"
|
||||
elif [ "$try_load_options" = "1" ]; then
|
||||
extra_params=" --try_load_options=true"
|
||||
fi
|
||||
|
||||
if [ "$ignore_unknown_options" = "1" ]; then
|
||||
extra_params="$extra_params --ignore_unknown_options"
|
||||
fi
|
||||
|
||||
set -e
|
||||
echo == Compacting DB at $db_dir
|
||||
./ldb compact --db=$db_dir $extra_params
|
||||
@@ -29,6 +29,12 @@ if ./ldb --version 2>/dev/null >/dev/null; then
|
||||
rm -rf $db_dir
|
||||
fi
|
||||
|
||||
# Check if deleterange command is supported by grepping ldb --help
|
||||
deleterange_support=
|
||||
if ./ldb --help 2>&1 | grep -q deleterange; then
|
||||
deleterange_support=1
|
||||
fi
|
||||
|
||||
echo == Loading data from $input_data_dir to $db_dir
|
||||
|
||||
declare -a compression_opts=("no" "snappy" "zlib" "bzip2")
|
||||
@@ -65,5 +71,33 @@ do
|
||||
fi
|
||||
./ldb load --db=$db_dir --compression_type=$c $d_arg --bloom_bits=10 \
|
||||
--auto_compaction=false --create_if_missing < $input_data_dir/$f
|
||||
|
||||
# Use md5sum of file to deterministically decide whether to add a range
|
||||
# tombstone (approximately 1/4 of files) and which key to delete
|
||||
file_path=$input_data_dir/$f
|
||||
hash=$(md5sum "$file_path" | cut -c1-8)
|
||||
hash_int=$((16#$hash))
|
||||
|
||||
if [ $((hash_int % 4)) -eq 0 ]; then
|
||||
# Pick a key from this file based on the hash
|
||||
line_count=$(wc -l < "$file_path")
|
||||
if [ "$line_count" -gt 0 ]; then
|
||||
line_num=$((hash_int % line_count + 1))
|
||||
key=$(sed -n "${line_num}p" "$file_path" | cut -d' ' -f1)
|
||||
if [ -n "$key" ]; then
|
||||
# Create end key by appending a character to make a small range
|
||||
end_key="${key}0"
|
||||
if [ "$deleterange_support" == "1" ]; then
|
||||
echo "== Deleting range [$key, $end_key) from $f"
|
||||
./ldb deleterange --db=$db_dir "$key" "$end_key"
|
||||
else
|
||||
# Fall back to point delete for equivalent logical contents
|
||||
echo "== Deleting key $key from $f"
|
||||
./ldb delete --db=$db_dir "$key"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
let "n = n + 1"
|
||||
done
|
||||
@@ -1 +0,0 @@
|
||||
* The new default for `BlockBasedTableOptions::format_version` is 7, which has been supported since RocksDB 10.4.0 and is required in order to use CompressionManagers supporting custom compression types.
|
||||
@@ -1 +0,0 @@
|
||||
* Fixed a small performance bug with `format_version=7` when decompressing formats other than Snappy and ZSTD.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed an infinite compaction loop bug with User-Defined Timestamps (UDT) where bottommost files were repeatedly marked for compaction even though their timestamp could not be collapsed.
|
||||
@@ -1 +0,0 @@
|
||||
Bugfix for persisted UDT record sequence number zeroing logic.
|
||||
@@ -1 +0,0 @@
|
||||
* Remove remaining pieces of Lua integration
|
||||
@@ -1 +0,0 @@
|
||||
New SetOptions API that allows setting options for multiple CFs, avoiding the need to reserialize OPTIONS file for each CF
|
||||
@@ -254,7 +254,7 @@ bool RandomAccessCacheFile::ParseRec(const LBA& lba, Slice* key, Slice* val,
|
||||
|
||||
CacheRecord rec;
|
||||
if (!rec.Deserialize(data)) {
|
||||
assert(!"Error deserializing data");
|
||||
assert(false && "Error deserializing data");
|
||||
Error(log_, "Error de-serializing record from file %s off %d",
|
||||
Path().c_str(), lba.off_);
|
||||
return false;
|
||||
@@ -339,7 +339,7 @@ bool WriteableCacheFile::Append(const Slice& key, const Slice& val, LBA* lba) {
|
||||
CacheRecord rec(key, val);
|
||||
if (!rec.Serialize(&bufs_, &buf_woff_)) {
|
||||
// unexpected error: unable to serialize the data
|
||||
assert(!"Error serializing record");
|
||||
assert(false && "Error serializing record");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -101,14 +101,14 @@ class BlockCacheFile : public LRUElement<BlockCacheFile> {
|
||||
// append key/value to file and return LBA locator to user
|
||||
virtual bool Append(const Slice& /*key*/, const Slice& /*val*/,
|
||||
LBA* const /*lba*/) {
|
||||
assert(!"not implemented");
|
||||
assert(false && "not implemented");
|
||||
return false;
|
||||
}
|
||||
|
||||
// read from the record locator (LBA) and return key, value and status
|
||||
virtual bool Read(const LBA& /*lba*/, Slice* /*key*/, Slice* /*block*/,
|
||||
char* /*scratch*/) {
|
||||
assert(!"not implemented");
|
||||
assert(false && "not implemented");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ Status VolatileCacheTier::Lookup(const Slice& page_key,
|
||||
}
|
||||
|
||||
bool VolatileCacheTier::Erase(const Slice& /*key*/) {
|
||||
assert(!"not supported");
|
||||
assert(false && "not supported");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user